github.com/u-root/u-root@v7.0.1-0.20200915234505-ad7babab0a8e+incompatible/pkg/dhclient/dhclient.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 dhclient allows for getting both DHCPv4 and DHCPv6 leases on
     6  // multiple network interfaces in parallel.
     7  package dhclient
     8  
     9  import (
    10  	"bytes"
    11  	"context"
    12  	"errors"
    13  	"fmt"
    14  	"io/ioutil"
    15  	"log"
    16  	"net"
    17  	"net/url"
    18  	"strings"
    19  	"sync"
    20  	"time"
    21  
    22  	"github.com/insomniacslk/dhcp/dhcpv4"
    23  	"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
    24  	"github.com/insomniacslk/dhcp/dhcpv6"
    25  	"github.com/insomniacslk/dhcp/dhcpv6/nclient6"
    26  	"github.com/vishvananda/netlink"
    27  	"golang.org/x/sys/unix"
    28  )
    29  
    30  // isIpv6LinkReady returns true if the interface has a link-local address
    31  // which is not tentative.
    32  func isIpv6LinkReady(l netlink.Link) (bool, error) {
    33  	addrs, err := netlink.AddrList(l, netlink.FAMILY_V6)
    34  	if err != nil {
    35  		return false, err
    36  	}
    37  	for _, addr := range addrs {
    38  		if addr.IP.IsLinkLocalUnicast() && (addr.Flags&unix.IFA_F_TENTATIVE == 0) {
    39  			if addr.Flags&unix.IFA_F_DADFAILED != 0 {
    40  				log.Printf("DADFAILED for %v, continuing anyhow", addr.IP)
    41  			}
    42  			return true, nil
    43  		}
    44  	}
    45  	return false, nil
    46  }
    47  
    48  // IfUp ensures the given network interface is up and returns the link object.
    49  func IfUp(ifname string, linkUpTimeout time.Duration) (netlink.Link, error) {
    50  	start := time.Now()
    51  	for time.Since(start) < linkUpTimeout {
    52  		// Note that it may seem odd to keep trying the LinkByName
    53  		// operation, but consider that a hotplug device such as USB
    54  		// ethernet can just vanish.
    55  		iface, err := netlink.LinkByName(ifname)
    56  		if err != nil {
    57  			return nil, fmt.Errorf("cannot get interface %q by name: %v", ifname, err)
    58  		}
    59  
    60  		// Check if link is actually operational.
    61  		// https://www.kernel.org/doc/Documentation/networking/operstates.txt states that we should check
    62  		// for OperUp and OperUnknown.
    63  		if o := iface.Attrs().OperState; o == netlink.OperUp || o == netlink.OperUnknown {
    64  			return iface, nil
    65  		}
    66  
    67  		if err := netlink.LinkSetUp(iface); err != nil {
    68  			return nil, fmt.Errorf("interface %q: %v can't make it up: %v", ifname, iface, err)
    69  		}
    70  		time.Sleep(100 * time.Millisecond)
    71  	}
    72  
    73  	return nil, fmt.Errorf("link %q still down after %v seconds", ifname, linkUpTimeout.Seconds())
    74  }
    75  
    76  // WriteDNSSettings writes the given nameservers, search list, and domain to resolv.conf.
    77  func WriteDNSSettings(ns []net.IP, sl []string, domain string) error {
    78  	rc := &bytes.Buffer{}
    79  	if domain != "" {
    80  		rc.WriteString(fmt.Sprintf("domain %s\n", domain))
    81  	}
    82  	for _, ip := range ns {
    83  		rc.WriteString(fmt.Sprintf("nameserver %s\n", ip))
    84  	}
    85  	if sl != nil {
    86  		rc.WriteString("search ")
    87  		rc.WriteString(strings.Join(sl, " "))
    88  		rc.WriteString("\n")
    89  	}
    90  	return ioutil.WriteFile("/etc/resolv.conf", rc.Bytes(), 0644)
    91  }
    92  
    93  // Lease is a network configuration obtained by DHCP.
    94  type Lease interface {
    95  	fmt.Stringer
    96  
    97  	// Configure configures the associated interface with the network
    98  	// configuration.
    99  	Configure() error
   100  
   101  	// Boot is a URL to obtain booting information from that was part of
   102  	// the network config.
   103  	Boot() (*url.URL, error)
   104  
   105  	// ISCSIBoot returns the target address and volume name to boot from if
   106  	// they were part of the DHCP message.
   107  	ISCSIBoot() (*net.TCPAddr, string, error)
   108  
   109  	// Link is the interface the configuration is for.
   110  	Link() netlink.Link
   111  
   112  	// Return the full DHCP response, this is either a *dhcpv4.DHCPv4 or a
   113  	// *dhcpv6.Message.
   114  	Message() (*dhcpv4.DHCPv4, *dhcpv6.Message)
   115  }
   116  
   117  // LogLevel is the amount of information to log.
   118  type LogLevel uint8
   119  
   120  // LogLevel are the levels.
   121  const (
   122  	LogInfo    LogLevel = 0
   123  	LogSummary LogLevel = 1
   124  	LogDebug   LogLevel = 2
   125  )
   126  
   127  // Config is a DHCP client configuration.
   128  type Config struct {
   129  	// Timeout is the timeout for one DHCP request attempt.
   130  	Timeout time.Duration
   131  
   132  	// Retries is how many times to retry DHCP attempts.
   133  	Retries int
   134  
   135  	// LogLevel determines the amount of information printed for each
   136  	// attempt. The highest log level should print each entire packet sent
   137  	// and received.
   138  	LogLevel LogLevel
   139  
   140  	// Modifiers4 allows modifications to the IPv4 DHCP request.
   141  	Modifiers4 []dhcpv4.Modifier
   142  
   143  	// Modifiers6 allows modifications to the IPv6 DHCP request.
   144  	Modifiers6 []dhcpv6.Modifier
   145  
   146  	// V6ServerAddr can be a unicast or broadcast destination for DHCPv6
   147  	// messages.
   148  	//
   149  	// If not set, it will default to nclient6's default (all servers &
   150  	// relay agents).
   151  	V6ServerAddr *net.UDPAddr
   152  
   153  	// V4ServerAddr can be a unicast or broadcast destination for IPv4 DHCP
   154  	// messages.
   155  	//
   156  	// If not set, it will default to nclient4's default (DHCP broadcast
   157  	// address).
   158  	V4ServerAddr *net.UDPAddr
   159  }
   160  
   161  func lease4(ctx context.Context, iface netlink.Link, c Config) (Lease, error) {
   162  	mods := []nclient4.ClientOpt{
   163  		nclient4.WithTimeout(c.Timeout),
   164  		nclient4.WithRetry(c.Retries),
   165  	}
   166  	switch c.LogLevel {
   167  	case LogSummary:
   168  		mods = append(mods, nclient4.WithSummaryLogger())
   169  	case LogDebug:
   170  		mods = append(mods, nclient4.WithDebugLogger())
   171  	}
   172  	if c.V4ServerAddr != nil {
   173  		mods = append(mods, nclient4.WithServerAddr(c.V4ServerAddr))
   174  	}
   175  	client, err := nclient4.New(iface.Attrs().Name, mods...)
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  	defer client.Close()
   180  
   181  	// Prepend modifiers with default options, so they can be overriden.
   182  	reqmods := append(
   183  		[]dhcpv4.Modifier{
   184  			dhcpv4.WithOption(dhcpv4.OptClassIdentifier("PXE UROOT")),
   185  			dhcpv4.WithRequestedOptions(dhcpv4.OptionSubnetMask),
   186  			dhcpv4.WithNetboot,
   187  		},
   188  		c.Modifiers4...)
   189  
   190  	log.Printf("Attempting to get DHCPv4 lease on %s", iface.Attrs().Name)
   191  	_, p, err := client.Request(ctx, reqmods...)
   192  	if err != nil {
   193  		return nil, err
   194  	}
   195  
   196  	packet := NewPacket4(iface, p)
   197  	log.Printf("Got DHCPv4 lease on %s: %v", iface.Attrs().Name, p.Summary())
   198  	return packet, nil
   199  }
   200  
   201  func lease6(ctx context.Context, iface netlink.Link, c Config, linkUpTimeout time.Duration) (Lease, error) {
   202  	// For ipv6, we cannot bind to the port until Duplicate Address
   203  	// Detection (DAD) is complete which is indicated by the link being no
   204  	// longer marked as "tentative". This usually takes about a second.
   205  
   206  	// If the link is never going to be ready, don't wait forever.
   207  	// (The user may not have configured a ctx with a timeout.)
   208  	//
   209  	// Hardcode the timeout to 30s for now.
   210  	linkTimeout := time.After(linkUpTimeout)
   211  	for {
   212  		if ready, err := isIpv6LinkReady(iface); err != nil {
   213  			return nil, err
   214  		} else if ready {
   215  			break
   216  		}
   217  		select {
   218  		case <-time.After(100 * time.Millisecond):
   219  			continue
   220  		case <-linkTimeout:
   221  			return nil, errors.New("timeout after waiting for a non-tentative IPv6 address")
   222  		case <-ctx.Done():
   223  			return nil, errors.New("timeout after waiting for a non-tentative IPv6 address")
   224  		}
   225  	}
   226  
   227  	mods := []nclient6.ClientOpt{
   228  		nclient6.WithTimeout(c.Timeout),
   229  		nclient6.WithRetry(c.Retries),
   230  	}
   231  	switch c.LogLevel {
   232  	case LogSummary:
   233  		mods = append(mods, nclient6.WithSummaryLogger())
   234  	case LogDebug:
   235  		mods = append(mods, nclient6.WithDebugLogger())
   236  	}
   237  	if c.V6ServerAddr != nil {
   238  		mods = append(mods, nclient6.WithBroadcastAddr(c.V6ServerAddr))
   239  	}
   240  	client, err := nclient6.New(iface.Attrs().Name, mods...)
   241  	if err != nil {
   242  		return nil, err
   243  	}
   244  	defer client.Close()
   245  
   246  	// Prepend modifiers with default options, so they can be overriden.
   247  	reqmods := append(
   248  		[]dhcpv6.Modifier{
   249  			dhcpv6.WithNetboot,
   250  		},
   251  		c.Modifiers6...)
   252  
   253  	log.Printf("Attempting to get DHCPv6 lease on %s", iface.Attrs().Name)
   254  	p, err := client.RapidSolicit(ctx, reqmods...)
   255  	if err != nil {
   256  		return nil, err
   257  	}
   258  
   259  	packet := NewPacket6(iface, p)
   260  	log.Printf("Got DHCPv6 lease on %s: %v", iface.Attrs().Name, p.Summary())
   261  	return packet, nil
   262  }
   263  
   264  // NetworkProtocol is either IPv4 or IPv6.
   265  type NetworkProtocol int
   266  
   267  // Possible network protocols; either IPv4, IPv6, or both.
   268  const (
   269  	NetIPv4 NetworkProtocol = 1
   270  	NetIPv6 NetworkProtocol = 2
   271  	NetBoth NetworkProtocol = 3
   272  )
   273  
   274  func (n NetworkProtocol) String() string {
   275  	switch n {
   276  	case NetIPv4:
   277  		return "IPv4"
   278  	case NetIPv6:
   279  		return "IPv6"
   280  	case NetBoth:
   281  		return "IPv4+IPv6"
   282  	}
   283  	return fmt.Sprintf("unknown network protocol (%#x)", n)
   284  }
   285  
   286  // Result is the result of a particular DHCP attempt.
   287  type Result struct {
   288  	// Protocol is the IP protocol that we tried to configure.
   289  	Protocol NetworkProtocol
   290  
   291  	// Interface is the network interface the attempt was sent on.
   292  	Interface netlink.Link
   293  
   294  	// Lease is the DHCP configuration returned.
   295  	//
   296  	// If Lease is set, Err is nil.
   297  	Lease Lease
   298  
   299  	// Err is an error that occured during the DHCP attempt.
   300  	Err error
   301  }
   302  
   303  // SendRequests coordinates soliciting DHCP configuration on all ifs.
   304  //
   305  // ipv4 and ipv6 determine whether to send DHCPv4 and DHCPv6 requests,
   306  // respectively.
   307  //
   308  // The *Result channel will be closed when all requests have completed.
   309  func SendRequests(ctx context.Context, ifs []netlink.Link, ipv4, ipv6 bool, c Config, linkUpTimeout time.Duration) chan *Result {
   310  	// Yeah, this is a hack, until we can cancel all leases in progress.
   311  	r := make(chan *Result, 3*len(ifs))
   312  
   313  	var wg sync.WaitGroup
   314  	for _, iface := range ifs {
   315  		wg.Add(1)
   316  		go func(iface netlink.Link) {
   317  			defer wg.Done()
   318  
   319  			log.Printf("Bringing up interface %s...", iface.Attrs().Name)
   320  			if _, err := IfUp(iface.Attrs().Name, linkUpTimeout); err != nil {
   321  				log.Printf("Could not bring up interface %s: %v", iface.Attrs().Name, err)
   322  				return
   323  			}
   324  
   325  			if ipv4 {
   326  				wg.Add(1)
   327  				go func(iface netlink.Link) {
   328  					defer wg.Done()
   329  					lease, err := lease4(ctx, iface, c)
   330  					r <- &Result{NetIPv4, iface, lease, err}
   331  				}(iface)
   332  			}
   333  
   334  			if ipv6 {
   335  				wg.Add(1)
   336  				go func(iface netlink.Link) {
   337  					defer wg.Done()
   338  					lease, err := lease6(ctx, iface, c, linkUpTimeout)
   339  					r <- &Result{NetIPv6, iface, lease, err}
   340  				}(iface)
   341  			}
   342  		}(iface)
   343  	}
   344  
   345  	go func() {
   346  		wg.Wait()
   347  		close(r)
   348  	}()
   349  	return r
   350  }