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  }