github.com/system-transparency/u-root@v6.0.1-0.20190919065413-ed07a650de4c+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 provides a unified interface for interfacing with both
     6  // DHCPv4 and DHCPv6 clients.
     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  	"os"
    19  	"strings"
    20  	"sync"
    21  	"time"
    22  
    23  	"github.com/insomniacslk/dhcp/dhcpv4"
    24  	"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
    25  	"github.com/insomniacslk/dhcp/dhcpv6"
    26  	"github.com/insomniacslk/dhcp/dhcpv6/nclient6"
    27  	"github.com/vishvananda/netlink"
    28  	"golang.org/x/sys/unix"
    29  )
    30  
    31  const linkUpAttempt = 30 * time.Second
    32  
    33  // isIpv6LinkReady returns true iff the interface has a link-local address
    34  // which is not tentative.
    35  func isIpv6LinkReady(l netlink.Link) (bool, error) {
    36  	addrs, err := netlink.AddrList(l, netlink.FAMILY_V6)
    37  	if err != nil {
    38  		return false, err
    39  	}
    40  	for _, addr := range addrs {
    41  		if addr.IP.IsLinkLocalUnicast() && (addr.Flags&unix.IFA_F_TENTATIVE == 0) {
    42  			if addr.Flags&unix.IFA_F_DADFAILED != 0 {
    43  				log.Printf("DADFAILED for %v, continuing anyhow", addr.IP)
    44  			}
    45  			return true, nil
    46  		}
    47  	}
    48  	return false, nil
    49  }
    50  
    51  // IfUp ensures the given network interface is up and returns the link object.
    52  func IfUp(ifname string) (netlink.Link, error) {
    53  	start := time.Now()
    54  	for time.Since(start) < linkUpAttempt {
    55  		// Note that it may seem odd to keep trying the LinkByName
    56  		// operation, but consider that a hotplug device such as USB
    57  		// ethernet can just vanish.
    58  		iface, err := netlink.LinkByName(ifname)
    59  		if err != nil {
    60  			return nil, fmt.Errorf("cannot get interface %q by name: %v", ifname, err)
    61  		}
    62  
    63  		if iface.Attrs().Flags&net.FlagUp == net.FlagUp {
    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 %d seconds", ifname, linkUpAttempt)
    74  }
    75  
    76  // Configure4 adds IP addresses, routes, and DNS servers to the system.
    77  func Configure4(iface netlink.Link, packet *dhcpv4.DHCPv4) error {
    78  	p := NewPacket4(iface, packet)
    79  	return p.Configure()
    80  }
    81  
    82  // Configure6 adds IPv6 addresses, routes, and DNS servers to the system.
    83  func Configure6(iface netlink.Link, packet *dhcpv6.Message) error {
    84  	p := NewPacket6(iface, packet)
    85  
    86  	l := p.Lease()
    87  	if l == nil {
    88  		return fmt.Errorf("no lease returned")
    89  	}
    90  
    91  	// Add the address to the iface.
    92  	dst := &netlink.Addr{
    93  		IPNet: &net.IPNet{
    94  			IP:   l.IPv6Addr,
    95  			Mask: net.IPMask(net.ParseIP("ffff:ffff:ffff:ffff::")),
    96  		},
    97  		PreferedLft: int(l.PreferredLifetime),
    98  		ValidLft:    int(l.ValidLifetime),
    99  		// Optimistic DAD (Duplicate Address Detection) means we can
   100  		// use the address before DAD is complete. The DHCP server's
   101  		// job was to give us a unique IP so there is little risk of a
   102  		// collision.
   103  		Flags: unix.IFA_F_OPTIMISTIC,
   104  	}
   105  	if err := netlink.AddrReplace(iface, dst); err != nil {
   106  		if os.IsExist(err) {
   107  			return fmt.Errorf("add/replace %s to %v: %v", dst, iface, err)
   108  		}
   109  	}
   110  
   111  	if ips := p.DNS(); ips != nil {
   112  		if err := WriteDNSSettings(ips, nil, ""); err != nil {
   113  			return err
   114  		}
   115  	}
   116  	return nil
   117  }
   118  
   119  // WriteDNSSettings writes the given nameservers, search list, and domain to resolv.conf.
   120  func WriteDNSSettings(ns []net.IP, sl []string, domain string) error {
   121  	rc := &bytes.Buffer{}
   122  	if domain != "" {
   123  		rc.WriteString(fmt.Sprintf("domain %s\n", domain))
   124  	}
   125  	if ns != nil {
   126  		for _, ip := range ns {
   127  			rc.WriteString(fmt.Sprintf("nameserver %s\n", ip))
   128  		}
   129  	}
   130  	if sl != nil {
   131  		rc.WriteString("search ")
   132  		rc.WriteString(strings.Join(sl, " "))
   133  		rc.WriteString("\n")
   134  	}
   135  	return ioutil.WriteFile("/etc/resolv.conf", rc.Bytes(), 0644)
   136  }
   137  
   138  // Lease is a network configuration obtained by DHCP.
   139  type Lease interface {
   140  	fmt.Stringer
   141  
   142  	// Configure configures the associated interface with the network
   143  	// configuration.
   144  	Configure() error
   145  
   146  	// Boot is a URL to obtain booting information from that was part of
   147  	// the network config.
   148  	Boot() (*url.URL, error)
   149  
   150  	// ISCSIBoot returns the target address and volume name to boot from if
   151  	// they were part of the DHCP message.
   152  	ISCSIBoot() (*net.TCPAddr, string, error)
   153  
   154  	// Link is the interface the configuration is for.
   155  	Link() netlink.Link
   156  }
   157  
   158  // LogLevel is the amount of information to log.
   159  type LogLevel uint8
   160  
   161  // LogLevel are the levels.
   162  const (
   163  	LogInfo    LogLevel = 0
   164  	LogSummary LogLevel = 1
   165  	LogDebug   LogLevel = 2
   166  )
   167  
   168  // Config is a DHCP client configuration.
   169  type Config struct {
   170  	// Timeout is the timeout for one DHCP request attempt.
   171  	Timeout time.Duration
   172  
   173  	// Retries is how many times to retry DHCP attempts.
   174  	Retries int
   175  
   176  	// LogLevel determines the amount of information printed for each
   177  	// attempt. The highest log level should print each entire packet sent
   178  	// and received.
   179  	LogLevel LogLevel
   180  }
   181  
   182  func lease4(ctx context.Context, iface netlink.Link, c Config) (Lease, error) {
   183  	mods := []nclient4.ClientOpt{
   184  		nclient4.WithTimeout(c.Timeout),
   185  		nclient4.WithRetry(c.Retries),
   186  	}
   187  	switch c.LogLevel {
   188  	case LogSummary:
   189  		mods = append(mods, nclient4.WithSummaryLogger())
   190  	case LogDebug:
   191  		mods = append(mods, nclient4.WithDebugLogger())
   192  	}
   193  	client, err := nclient4.New(iface.Attrs().Name, mods...)
   194  	if err != nil {
   195  		return nil, err
   196  	}
   197  
   198  	log.Printf("Attempting to get DHCPv4 lease on %s", iface.Attrs().Name)
   199  	_, p, err := client.Request(ctx, dhcpv4.WithNetboot,
   200  		dhcpv4.WithOption(dhcpv4.OptClassIdentifier("PXE UROOT")),
   201  		dhcpv4.WithRequestedOptions(dhcpv4.OptionSubnetMask))
   202  	if err != nil {
   203  		return nil, err
   204  	}
   205  
   206  	packet := NewPacket4(iface, p)
   207  	log.Printf("Got DHCPv4 lease on %s: %v", iface.Attrs().Name, p.Summary())
   208  	return packet, nil
   209  }
   210  
   211  func lease6(ctx context.Context, iface netlink.Link, c Config) (Lease, error) {
   212  	// For ipv6, we cannot bind to the port until Duplicate Address
   213  	// Detection (DAD) is complete which is indicated by the link being no
   214  	// longer marked as "tentative". This usually takes about a second.
   215  	for {
   216  		if ready, err := isIpv6LinkReady(iface); err != nil {
   217  			return nil, err
   218  		} else if ready {
   219  			break
   220  		}
   221  		select {
   222  		case <-time.After(100 * time.Millisecond):
   223  			continue
   224  		case <-ctx.Done():
   225  			return nil, errors.New("timeout after waiting for a non-tentative IPv6 address")
   226  		}
   227  	}
   228  
   229  	client, err := nclient6.New(iface.Attrs().Name,
   230  		nclient6.WithTimeout(c.Timeout),
   231  		nclient6.WithRetry(c.Retries))
   232  	if err != nil {
   233  		return nil, err
   234  	}
   235  
   236  	log.Printf("Attempting to get DHCPv6 lease on %s", iface.Attrs().Name)
   237  	p, err := client.RapidSolicit(ctx, dhcpv6.WithNetboot)
   238  	if err != nil {
   239  		return nil, err
   240  	}
   241  
   242  	packet := NewPacket6(iface, p)
   243  	log.Printf("Got DHCPv6 lease on %s: %v", iface.Attrs().Name, p.Summary())
   244  	return packet, nil
   245  }
   246  
   247  // Result is the result of a particular DHCP attempt.
   248  type Result struct {
   249  	// Interface is the network interface the attempt was sent on.
   250  	Interface netlink.Link
   251  
   252  	// Lease is the DHCP configuration returned.
   253  	//
   254  	// If Lease is set, Err is nil.
   255  	Lease Lease
   256  
   257  	// Err is an error that occured during the DHCP attempt.
   258  	Err error
   259  }
   260  
   261  // SendRequests coordinates soliciting DHCP configuration on all ifs.
   262  //
   263  // ipv4 and ipv6 determine whether to send DHCPv4 and DHCPv6 requests,
   264  // respectively.
   265  //
   266  // The *Result channel will be closed when all requests have completed.
   267  func SendRequests(ctx context.Context, ifs []netlink.Link, ipv4, ipv6 bool, c Config) chan *Result {
   268  	// Yeah, this is a hack, until we can cancel all leases in progress.
   269  	r := make(chan *Result, 3*len(ifs))
   270  
   271  	var wg sync.WaitGroup
   272  	for _, iface := range ifs {
   273  		wg.Add(1)
   274  		go func(iface netlink.Link) {
   275  			defer wg.Done()
   276  
   277  			log.Printf("Bringing up interface %s...", iface.Attrs().Name)
   278  			if _, err := IfUp(iface.Attrs().Name); err != nil {
   279  				log.Printf("Could not bring up interface %s: %v", iface.Attrs().Name, err)
   280  				return
   281  			}
   282  
   283  			if ipv4 {
   284  				wg.Add(1)
   285  				go func(iface netlink.Link) {
   286  					defer wg.Done()
   287  					lease, err := lease4(ctx, iface, c)
   288  					r <- &Result{iface, lease, err}
   289  				}(iface)
   290  			}
   291  
   292  			if ipv6 {
   293  				wg.Add(1)
   294  				go func(iface netlink.Link) {
   295  					defer wg.Done()
   296  					lease, err := lease6(ctx, iface, c)
   297  					r <- &Result{iface, lease, err}
   298  				}(iface)
   299  			}
   300  		}(iface)
   301  	}
   302  
   303  	go func() {
   304  		wg.Wait()
   305  		close(r)
   306  	}()
   307  	return r
   308  }