github.com/u-root/u-root@v7.0.1-0.20200915234505-ad7babab0a8e+incompatible/cmds/boot/fbnetboot/main.go (about) 1 // Copyright 2017-2019 the u-root Authors. All rights reserved 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "crypto/tls" 9 "crypto/x509" 10 "errors" 11 "flag" 12 "fmt" 13 "io/ioutil" 14 "log" 15 "net" 16 "net/http" 17 "net/url" 18 "os" 19 "os/exec" 20 "path/filepath" 21 "strings" 22 "time" 23 24 "github.com/insomniacslk/dhcp/dhcpv4" 25 "github.com/insomniacslk/dhcp/dhcpv6" 26 "github.com/insomniacslk/dhcp/iana" 27 "github.com/insomniacslk/dhcp/interfaces" 28 "github.com/insomniacslk/dhcp/netboot" 29 "github.com/u-root/u-root/pkg/boot/kexec" 30 "github.com/u-root/u-root/pkg/crypto" 31 ) 32 33 var ( 34 useV4 = flag.Bool("4", false, "Get a DHCPv4 lease") 35 useV6 = flag.Bool("6", true, "Get a DHCPv6 lease") 36 ifname = flag.String("i", "", "Interface to send packets through") 37 dryRun = flag.Bool("dryrun", false, "Do everything except assigning IP addresses, changing DNS, and kexec") 38 doDebug = flag.Bool("d", false, "Print debug output") 39 skipDHCP = flag.Bool("skip-dhcp", false, "Skip DHCP and rely on SLAAC for network configuration. This requires -netboot-url") 40 overrideNetbootURL = flag.String("netboot-url", "", "Override the netboot URL normally obtained via DHCP") 41 readTimeout = flag.Int("timeout", 3, "Read timeout in seconds") 42 dhcpRetries = flag.Int("retries", 3, "Number of times a DHCP request is retried") 43 userClass = flag.String("userclass", "", "Override DHCP User Class option") 44 caCertFile = flag.String("cacerts", "/etc/cacerts.pem", "CA cert file") 45 skipCertVerify = flag.Bool("skip-cert-verify", false, "Don't authenticate https certs") 46 doFix = flag.Bool("fix", false, "Try to run fixmynetboot if netboot fails") 47 ) 48 49 const ( 50 interfaceUpTimeout = 10 * time.Second 51 maxHTTPAttempts = 3 52 retryInterval = time.Second 53 ) 54 55 var banner = ` 56 57 _________________________________ 58 < Net booting is so hot right now > 59 --------------------------------- 60 \ ^__^ 61 \ (oo)\_______ 62 (__)\ )\/\ 63 ||----w | 64 || || 65 66 ` 67 var debug = func(string, ...interface{}) {} 68 69 func main() { 70 flag.Parse() 71 if *skipDHCP && *overrideNetbootURL == "" { 72 log.Fatal("-skip-dhcp requires -netboot-url") 73 } 74 if *doDebug { 75 debug = log.Printf 76 } 77 log.Print(banner) 78 79 if !*useV6 && !*useV4 { 80 log.Fatal("At least one of DHCPv6 and DHCPv4 is required") 81 } 82 83 iflist := []net.Interface{} 84 if *ifname != "" { 85 var iface *net.Interface 86 var err error 87 if iface, err = net.InterfaceByName(*ifname); err != nil { 88 log.Fatalf("Could not find interface %s: %v", *ifname, err) 89 } 90 iflist = append(iflist, *iface) 91 } else { 92 var err error 93 if iflist, err = interfaces.GetNonLoopbackInterfaces(); err != nil { 94 log.Fatalf("Could not obtain the list of network interfaces: %v", err) 95 } 96 } 97 98 for _, iface := range iflist { 99 log.Printf("Waiting for network interface %s to come up", iface.Name) 100 start := time.Now() 101 _, err := netboot.IfUp(iface.Name, interfaceUpTimeout) 102 if err != nil { 103 log.Printf("IfUp failed: %v", err) 104 continue 105 } 106 debug("Interface %s is up after %v", iface.Name, time.Since(start)) 107 108 var dhcp []dhcpFunc 109 if *useV6 { 110 dhcp = append(dhcp, dhcp6) 111 } 112 if *useV4 { 113 dhcp = append(dhcp, dhcp4) 114 } 115 for _, d := range dhcp { 116 if err := boot(iface.Name, d); err != nil { 117 if *doFix { 118 cmd := exec.Command("fixmynetboot", iface.Name) 119 log.Printf("Running %s", strings.Join(cmd.Args, " ")) 120 cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr 121 if err := cmd.Run(); err != nil { 122 log.Printf("Error calling fixmynetboot: %v", err) 123 log.Print("fixmynetboot failed. Check the above output to manually debug the issue.") 124 os.Exit(1) 125 } 126 } 127 log.Printf("Could not boot from %s: %v", iface.Name, err) 128 } 129 } 130 } 131 132 log.Fatalln("Could not boot from any interfaces") 133 } 134 135 func retryableNetError(err error) bool { 136 if err == nil { 137 return false 138 } 139 switch err := err.(type) { 140 case net.Error: 141 if err.Timeout() { 142 return true 143 } 144 } 145 return false 146 } 147 148 func retryableHTTPError(resp *http.Response) bool { 149 if resp == nil { 150 return false 151 } 152 if resp.StatusCode == 500 || resp.StatusCode == 502 { 153 return true 154 } 155 return false 156 } 157 158 func boot(ifname string, dhcp dhcpFunc) error { 159 var ( 160 bootconf *netboot.BootConf 161 err error 162 ) 163 if *skipDHCP { 164 log.Print("Skipping DHCP") 165 } else { 166 // send a netboot request via DHCP 167 bootconf, err = dhcp(ifname) 168 if err != nil { 169 return fmt.Errorf("DHCPv6: netboot request for interface %s failed: %v", ifname, err) 170 } 171 debug("DHCP: network configuration: %+v", bootconf.NetConf) 172 if !*dryRun { 173 log.Printf("DHCP: configuring network interface %s with %v", ifname, bootconf.NetConf) 174 if err = netboot.ConfigureInterface(ifname, &bootconf.NetConf); err != nil { 175 return fmt.Errorf("DHCP: cannot configure interface %s: %v", ifname, err) 176 } 177 } 178 if *overrideNetbootURL != "" { 179 bootconf.BootfileURL = *overrideNetbootURL 180 } 181 log.Printf("DHCP: boot file for interface %s is %s", ifname, bootconf.BootfileURL) 182 } 183 if *overrideNetbootURL != "" { 184 bootconf.BootfileURL = *overrideNetbootURL 185 } 186 debug("DHCP: boot file URL is %s", bootconf.BootfileURL) 187 // check for supported schemes 188 scheme, err := getScheme(bootconf.BootfileURL) 189 if err != nil { 190 return fmt.Errorf("DHCP: cannot get scheme from URL: %v", err) 191 } 192 if scheme == "" { 193 return errors.New("DHCP: no valid scheme found in URL") 194 } 195 196 client, err := getClientForBootfile(bootconf.BootfileURL) 197 if err != nil { 198 return fmt.Errorf("DHCP: cannot get client for %s: %v", bootconf.BootfileURL, err) 199 } 200 log.Printf("DHCP: fetching boot file URL: %s", bootconf.BootfileURL) 201 202 var resp *http.Response 203 for attempt := 0; attempt < maxHTTPAttempts; attempt++ { 204 log.Printf("netboot: attempt %d for http.Get", attempt+1) 205 req, err := http.NewRequest(http.MethodGet, bootconf.BootfileURL, nil) 206 if err != nil { 207 return fmt.Errorf("could not build request for %s: %v", bootconf.BootfileURL, err) 208 } 209 resp, err = client.Do(req) 210 if err != nil && retryableNetError(err) || retryableHTTPError(resp) { 211 time.Sleep(retryInterval) 212 continue 213 } 214 if err == nil { 215 break 216 } 217 return fmt.Errorf("DHCP: http.Get of %s failed: %v", bootconf.BootfileURL, err) 218 } 219 // FIXME this will not be called if something fails after this point 220 defer resp.Body.Close() 221 if resp.StatusCode != 200 { 222 return fmt.Errorf("status code is not 200 OK: %d", resp.StatusCode) 223 } 224 body, err := ioutil.ReadAll(resp.Body) 225 if err != nil { 226 return fmt.Errorf("DHCP: cannot read boot file from the network: %v", err) 227 } 228 crypto.TryMeasureData(crypto.BootConfigPCR, body, bootconf.BootfileURL) 229 u, err := url.Parse(bootconf.BootfileURL) 230 if err != nil { 231 return fmt.Errorf("DHCP: cannot parse URL %s: %v", bootconf.BootfileURL, err) 232 } 233 // extract file name component 234 if strings.HasSuffix(u.Path, "/") { 235 return fmt.Errorf("invalid file path, cannot end with '/': %s", u.Path) 236 } 237 filename := filepath.Base(u.Path) 238 if filename == "." || filename == "" { 239 return fmt.Errorf("invalid empty file name extracted from file path %s", u.Path) 240 } 241 if err = ioutil.WriteFile(filename, body, 0400); err != nil { 242 return fmt.Errorf("DHCP: cannot write to file %s: %v", filename, err) 243 } 244 debug("DHCP: saved boot file to %s", filename) 245 246 cmdline := strings.Join(bootconf.BootfileParam, " ") 247 if !*dryRun { 248 log.Printf("DHCP: kexec'ing into %s (with arguments: \"%s\")", filename, cmdline) 249 kernel, err := os.OpenFile(filename, os.O_RDONLY, 0) 250 if err != nil { 251 return fmt.Errorf("DHCP: cannot open file %s: %v", filename, err) 252 } 253 if err = kexec.FileLoad(kernel, nil /* ramfs */, cmdline); err != nil { 254 return fmt.Errorf("DHCP: kexec.FileLoad failed: %v", err) 255 } 256 if err = kexec.Reboot(); err != nil { 257 return fmt.Errorf("DHCP: kexec.Reboot failed: %v", err) 258 } 259 } else { 260 log.Printf("DHCP: I would've kexec into %s (with arguments: \"%s\") now unless the dry mode", filename, cmdline) 261 } 262 return nil 263 } 264 265 func getScheme(urlstring string) (string, error) { 266 u, err := url.Parse(urlstring) 267 if err != nil { 268 return "", err 269 } 270 scheme := strings.ToLower(u.Scheme) 271 if scheme != "http" && scheme != "https" { 272 return "", fmt.Errorf("URL scheme '%s' must be http or https", scheme) 273 } 274 return scheme, nil 275 } 276 277 func loadCaCerts() (*x509.CertPool, error) { 278 rootCAs, err := x509.SystemCertPool() 279 if err != nil { 280 return nil, err 281 } 282 if rootCAs == nil { 283 debug("certs: rootCAs == nil") 284 rootCAs = x509.NewCertPool() 285 } 286 caCerts, err := ioutil.ReadFile(*caCertFile) 287 if err != nil { 288 return nil, fmt.Errorf("could not find cert file '%v' - %v", *caCertFile, err) 289 } 290 // TODO: Decide if this should also support compressed certs 291 // Might be better to have a generic compressed config API 292 if ok := rootCAs.AppendCertsFromPEM(caCerts); !ok { 293 debug("Failed to append CA Certs from %s, using system certs only", *caCertFile) 294 } else { 295 debug("CA certs appended from PEM") 296 } 297 return rootCAs, nil 298 299 } 300 301 func getClientForBootfile(bootfile string) (*http.Client, error) { 302 var client *http.Client 303 scheme, err := getScheme(bootfile) 304 if err != nil { 305 return nil, err 306 } 307 308 switch scheme { 309 case "https": 310 var config *tls.Config 311 if *skipCertVerify { 312 config = &tls.Config{ 313 InsecureSkipVerify: true, 314 } 315 } else if *caCertFile != "" { 316 rootCAs, err := loadCaCerts() 317 if err != nil { 318 return nil, err 319 } 320 config = &tls.Config{ 321 RootCAs: rootCAs, 322 } 323 } 324 tr := &http.Transport{TLSClientConfig: config} 325 client = &http.Client{Transport: tr} 326 debug("https client setup (use certs from VPD: %t, skipCertVerify %t)", 327 *skipCertVerify, *caCertFile != "") 328 case "http": 329 client = &http.Client{} 330 debug("http client setup") 331 default: 332 return nil, fmt.Errorf("Scheme %s is unsupported", scheme) 333 } 334 return client, nil 335 } 336 337 type dhcpFunc func(string) (bootconf *netboot.BootConf, err error) 338 339 func dhcp6(ifname string) (*netboot.BootConf, error) { 340 log.Printf("Trying to obtain a DHCPv6 lease on %s", ifname) 341 modifiers := []dhcpv6.Modifier{ 342 dhcpv6.WithArchType(iana.EFI_X86_64), 343 } 344 if *userClass != "" { 345 modifiers = append(modifiers, dhcpv6.WithUserClass([]byte(*userClass))) 346 } 347 conversation, err := netboot.RequestNetbootv6(ifname, time.Duration(*readTimeout)*time.Second, *dhcpRetries, modifiers...) 348 for _, m := range conversation { 349 debug(m.Summary()) 350 } 351 if err != nil { 352 return nil, fmt.Errorf("DHCPv6: netboot request for interface %s failed: %v", ifname, err) 353 } 354 return netboot.ConversationToNetconf(conversation) 355 } 356 357 func dhcp4(ifname string) (*netboot.BootConf, error) { 358 log.Printf("Trying to obtain a DHCPv4 lease on %s", ifname) 359 var modifiers []dhcpv4.Modifier 360 if *userClass != "" { 361 modifiers = append(modifiers, dhcpv4.WithUserClass(*userClass, false)) 362 } 363 conversation, err := netboot.RequestNetbootv4(ifname, time.Duration(*readTimeout)*time.Second, *dhcpRetries, modifiers...) 364 for _, m := range conversation { 365 debug(m.Summary()) 366 } 367 if err != nil { 368 return nil, fmt.Errorf("DHCPv4: netboot request for interface %s failed: %v", ifname, err) 369 } 370 return netboot.ConversationToNetconfv4(conversation) 371 }