github.com/hugelgupf/u-root@v0.0.0-20191023214958-4807c632154c/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  
   112  // LogLevel is the amount of information to log.
   113  type LogLevel uint8
   114  
   115  // LogLevel are the levels.
   116  const (
   117  	LogInfo    LogLevel = 0
   118  	LogSummary LogLevel = 1
   119  	LogDebug   LogLevel = 2
   120  )
   121  
   122  // Config is a DHCP client configuration.
   123  type Config struct {
   124  	// Timeout is the timeout for one DHCP request attempt.
   125  	Timeout time.Duration
   126  
   127  	// Retries is how many times to retry DHCP attempts.
   128  	Retries int
   129  
   130  	// LogLevel determines the amount of information printed for each
   131  	// attempt. The highest log level should print each entire packet sent
   132  	// and received.
   133  	LogLevel LogLevel
   134  }
   135  
   136  func lease4(ctx context.Context, iface netlink.Link, c Config) (Lease, error) {
   137  	mods := []nclient4.ClientOpt{
   138  		nclient4.WithTimeout(c.Timeout),
   139  		nclient4.WithRetry(c.Retries),
   140  	}
   141  	switch c.LogLevel {
   142  	case LogSummary:
   143  		mods = append(mods, nclient4.WithSummaryLogger())
   144  	case LogDebug:
   145  		mods = append(mods, nclient4.WithDebugLogger())
   146  	}
   147  	client, err := nclient4.New(iface.Attrs().Name, mods...)
   148  	if err != nil {
   149  		return nil, err
   150  	}
   151  
   152  	log.Printf("Attempting to get DHCPv4 lease on %s", iface.Attrs().Name)
   153  	_, p, err := client.Request(ctx, dhcpv4.WithNetboot,
   154  		dhcpv4.WithOption(dhcpv4.OptClassIdentifier("PXE UROOT")),
   155  		dhcpv4.WithRequestedOptions(dhcpv4.OptionSubnetMask))
   156  	if err != nil {
   157  		return nil, err
   158  	}
   159  
   160  	packet := NewPacket4(iface, p)
   161  	log.Printf("Got DHCPv4 lease on %s: %v", iface.Attrs().Name, p.Summary())
   162  	return packet, nil
   163  }
   164  
   165  func lease6(ctx context.Context, iface netlink.Link, c Config) (Lease, error) {
   166  	// For ipv6, we cannot bind to the port until Duplicate Address
   167  	// Detection (DAD) is complete which is indicated by the link being no
   168  	// longer marked as "tentative". This usually takes about a second.
   169  
   170  	// If the link is never going to be ready, don't wait forever.
   171  	// (The user may not have configured a ctx with a timeout.)
   172  	//
   173  	// Hardcode the timeout to 30s for now.
   174  	linkTimeout := time.After(linkUpAttempt)
   175  	for {
   176  		if ready, err := isIpv6LinkReady(iface); err != nil {
   177  			return nil, err
   178  		} else if ready {
   179  			break
   180  		}
   181  		select {
   182  		case <-time.After(100 * time.Millisecond):
   183  			continue
   184  		case <-linkTimeout:
   185  			return nil, errors.New("timeout after waiting for a non-tentative IPv6 address")
   186  		case <-ctx.Done():
   187  			return nil, errors.New("timeout after waiting for a non-tentative IPv6 address")
   188  		}
   189  	}
   190  
   191  	mods := []nclient6.ClientOpt{
   192  		nclient6.WithTimeout(c.Timeout),
   193  		nclient6.WithRetry(c.Retries),
   194  	}
   195  	switch c.LogLevel {
   196  	case LogSummary:
   197  		mods = append(mods, nclient6.WithSummaryLogger())
   198  	case LogDebug:
   199  		mods = append(mods, nclient6.WithDebugLogger())
   200  	}
   201  	client, err := nclient6.New(iface.Attrs().Name, mods...)
   202  	if err != nil {
   203  		return nil, err
   204  	}
   205  
   206  	log.Printf("Attempting to get DHCPv6 lease on %s", iface.Attrs().Name)
   207  	p, err := client.RapidSolicit(ctx, dhcpv6.WithNetboot)
   208  	if err != nil {
   209  		return nil, err
   210  	}
   211  
   212  	packet := NewPacket6(iface, p)
   213  	log.Printf("Got DHCPv6 lease on %s: %v", iface.Attrs().Name, p.Summary())
   214  	return packet, nil
   215  }
   216  
   217  // NetworkProtocol is either IPv4 or IPv6.
   218  type NetworkProtocol int
   219  
   220  // Possible network protocols; either IPv4, IPv6, or both.
   221  const (
   222  	NetIPv4 NetworkProtocol = 1
   223  	NetIPv6 NetworkProtocol = 2
   224  	NetBoth NetworkProtocol = 3
   225  )
   226  
   227  func (n NetworkProtocol) String() string {
   228  	switch n {
   229  	case NetIPv4:
   230  		return "IPv4"
   231  	case NetIPv6:
   232  		return "IPv6"
   233  	case NetBoth:
   234  		return "IPv4+IPv6"
   235  	}
   236  	return fmt.Sprintf("unknown network protocol (%#x)", n)
   237  }
   238  
   239  // Result is the result of a particular DHCP attempt.
   240  type Result struct {
   241  	// Protocol is the IP protocol that we tried to configure.
   242  	Protocol NetworkProtocol
   243  
   244  	// Interface is the network interface the attempt was sent on.
   245  	Interface netlink.Link
   246  
   247  	// Lease is the DHCP configuration returned.
   248  	//
   249  	// If Lease is set, Err is nil.
   250  	Lease Lease
   251  
   252  	// Err is an error that occured during the DHCP attempt.
   253  	Err error
   254  }
   255  
   256  // SendRequests coordinates soliciting DHCP configuration on all ifs.
   257  //
   258  // ipv4 and ipv6 determine whether to send DHCPv4 and DHCPv6 requests,
   259  // respectively.
   260  //
   261  // The *Result channel will be closed when all requests have completed.
   262  func SendRequests(ctx context.Context, ifs []netlink.Link, ipv4, ipv6 bool, c Config) chan *Result {
   263  	// Yeah, this is a hack, until we can cancel all leases in progress.
   264  	r := make(chan *Result, 3*len(ifs))
   265  
   266  	var wg sync.WaitGroup
   267  	for _, iface := range ifs {
   268  		wg.Add(1)
   269  		go func(iface netlink.Link) {
   270  			defer wg.Done()
   271  
   272  			log.Printf("Bringing up interface %s...", iface.Attrs().Name)
   273  			if _, err := IfUp(iface.Attrs().Name); err != nil {
   274  				log.Printf("Could not bring up interface %s: %v", iface.Attrs().Name, err)
   275  				return
   276  			}
   277  
   278  			if ipv4 {
   279  				wg.Add(1)
   280  				go func(iface netlink.Link) {
   281  					defer wg.Done()
   282  					lease, err := lease4(ctx, iface, c)
   283  					r <- &Result{NetIPv4, iface, lease, err}
   284  				}(iface)
   285  			}
   286  
   287  			if ipv6 {
   288  				wg.Add(1)
   289  				go func(iface netlink.Link) {
   290  					defer wg.Done()
   291  					lease, err := lease6(ctx, iface, c)
   292  					r <- &Result{NetIPv6, iface, lease, err}
   293  				}(iface)
   294  			}
   295  		}(iface)
   296  	}
   297  
   298  	go func() {
   299  		wg.Wait()
   300  		close(r)
   301  	}()
   302  	return r
   303  }