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