github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/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  	"log"
    15  	"net"
    16  	"net/url"
    17  	"os"
    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 os.WriteFile("/etc/resolv.conf", rc.Bytes(), 0o644)
    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  	// V6ClientPort is the port that is used to send and receive DHCPv6
   154  	// messages.
   155  	//
   156  	// If not set, it will default to dhcpv6's default (546).
   157  	V6ClientPort *int
   158  
   159  	// V4ServerAddr can be a unicast or broadcast destination for IPv4 DHCP
   160  	// messages.
   161  	//
   162  	// If not set, it will default to nclient4's default (DHCP broadcast
   163  	// address).
   164  	V4ServerAddr *net.UDPAddr
   165  
   166  	// If true, add Client Identifier (61) option to the IPv4 request.
   167  	V4ClientIdentifier bool
   168  }
   169  
   170  func lease4(ctx context.Context, iface netlink.Link, c Config) (Lease, error) {
   171  	mods := []nclient4.ClientOpt{
   172  		nclient4.WithTimeout(c.Timeout),
   173  		nclient4.WithRetry(c.Retries),
   174  	}
   175  	switch c.LogLevel {
   176  	case LogSummary:
   177  		mods = append(mods, nclient4.WithSummaryLogger())
   178  	case LogDebug:
   179  		mods = append(mods, nclient4.WithDebugLogger())
   180  	}
   181  	if c.V4ServerAddr != nil {
   182  		mods = append(mods, nclient4.WithServerAddr(c.V4ServerAddr))
   183  	}
   184  	client, err := nclient4.New(iface.Attrs().Name, mods...)
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  	defer client.Close()
   189  
   190  	// Prepend modifiers with default options, so they can be overriden.
   191  	reqmods := append(
   192  		[]dhcpv4.Modifier{
   193  			dhcpv4.WithOption(dhcpv4.OptClassIdentifier("PXE UROOT")),
   194  			dhcpv4.WithRequestedOptions(dhcpv4.OptionSubnetMask),
   195  			dhcpv4.WithNetboot,
   196  		},
   197  		c.Modifiers4...)
   198  
   199  	if c.V4ClientIdentifier {
   200  		// Client Id is hardware type + mac per RFC 2132 9.14.
   201  		ident := []byte{0x01} // Type ethernet
   202  		ident = append(ident, iface.Attrs().HardwareAddr...)
   203  		reqmods = append(reqmods, dhcpv4.WithOption(dhcpv4.OptClientIdentifier(ident)))
   204  	}
   205  
   206  	log.Printf("Attempting to get DHCPv4 lease on %s", iface.Attrs().Name)
   207  	lease, err := client.Request(ctx, reqmods...)
   208  	if err != nil {
   209  		return nil, err
   210  	}
   211  
   212  	packet := NewPacket4(iface, lease.ACK)
   213  	log.Printf("Got DHCPv4 lease on %s: %v", iface.Attrs().Name, lease.ACK.Summary())
   214  	return packet, nil
   215  }
   216  
   217  func lease6(ctx context.Context, iface netlink.Link, c Config, linkUpTimeout time.Duration) (Lease, error) {
   218  	clientPort := dhcpv6.DefaultClientPort
   219  	if c.V6ClientPort != nil {
   220  		clientPort = *c.V6ClientPort
   221  	}
   222  
   223  	// For ipv6, we cannot bind to the port until Duplicate Address
   224  	// Detection (DAD) is complete which is indicated by the link being no
   225  	// longer marked as "tentative". This usually takes about a second.
   226  
   227  	// If the link is never going to be ready, don't wait forever.
   228  	// (The user may not have configured a ctx with a timeout.)
   229  	//
   230  	// Hardcode the timeout to 30s for now.
   231  	linkTimeout := time.After(linkUpTimeout)
   232  	for {
   233  		if ready, err := isIpv6LinkReady(iface); err != nil {
   234  			return nil, err
   235  		} else if ready {
   236  			break
   237  		}
   238  		select {
   239  		case <-time.After(100 * time.Millisecond):
   240  			continue
   241  		case <-linkTimeout:
   242  			return nil, errors.New("timeout after waiting for a non-tentative IPv6 address")
   243  		case <-ctx.Done():
   244  			return nil, errors.New("timeout after waiting for a non-tentative IPv6 address")
   245  		}
   246  	}
   247  
   248  	mods := []nclient6.ClientOpt{
   249  		nclient6.WithTimeout(c.Timeout),
   250  		nclient6.WithRetry(c.Retries),
   251  	}
   252  	switch c.LogLevel {
   253  	case LogSummary:
   254  		mods = append(mods, nclient6.WithSummaryLogger())
   255  	case LogDebug:
   256  		mods = append(mods, nclient6.WithDebugLogger())
   257  	}
   258  	if c.V6ServerAddr != nil {
   259  		mods = append(mods, nclient6.WithBroadcastAddr(c.V6ServerAddr))
   260  	}
   261  	conn, err := nclient6.NewIPv6UDPConn(iface.Attrs().Name, clientPort)
   262  	if err != nil {
   263  		return nil, err
   264  	}
   265  	i, err := net.InterfaceByName(iface.Attrs().Name)
   266  	if err != nil {
   267  		return nil, err
   268  	}
   269  	client, err := nclient6.NewWithConn(conn, i.HardwareAddr, mods...)
   270  	if err != nil {
   271  		return nil, err
   272  	}
   273  	defer client.Close()
   274  
   275  	// Prepend modifiers with default options, so they can be overriden.
   276  	reqmods := append(
   277  		[]dhcpv6.Modifier{
   278  			dhcpv6.WithNetboot,
   279  		},
   280  		c.Modifiers6...)
   281  
   282  	log.Printf("Attempting to get DHCPv6 lease on %s", iface.Attrs().Name)
   283  	p, err := client.RapidSolicit(ctx, reqmods...)
   284  	if err != nil {
   285  		return nil, err
   286  	}
   287  
   288  	packet := NewPacket6(iface, p)
   289  	log.Printf("Got DHCPv6 lease on %s: %v", iface.Attrs().Name, p.Summary())
   290  	return packet, nil
   291  }
   292  
   293  // NetworkProtocol is either IPv4 or IPv6.
   294  type NetworkProtocol int
   295  
   296  // Possible network protocols; either IPv4, IPv6, or both.
   297  const (
   298  	NetIPv4 NetworkProtocol = 1
   299  	NetIPv6 NetworkProtocol = 2
   300  	NetBoth NetworkProtocol = 3
   301  )
   302  
   303  func (n NetworkProtocol) String() string {
   304  	switch n {
   305  	case NetIPv4:
   306  		return "IPv4"
   307  	case NetIPv6:
   308  		return "IPv6"
   309  	case NetBoth:
   310  		return "IPv4+IPv6"
   311  	}
   312  	return fmt.Sprintf("unknown network protocol (%#x)", n)
   313  }
   314  
   315  // Result is the result of a particular DHCP attempt.
   316  type Result struct {
   317  	// Protocol is the IP protocol that we tried to configure.
   318  	Protocol NetworkProtocol
   319  
   320  	// Interface is the network interface the attempt was sent on.
   321  	Interface netlink.Link
   322  
   323  	// Lease is the DHCP configuration returned.
   324  	//
   325  	// If Lease is set, Err is nil.
   326  	Lease Lease
   327  
   328  	// Err is an error that occured during the DHCP attempt.
   329  	Err error
   330  }
   331  
   332  // SendRequests coordinates soliciting DHCP configuration on all ifs.
   333  //
   334  // ipv4 and ipv6 determine whether to send DHCPv4 and DHCPv6 requests,
   335  // respectively.
   336  //
   337  // The *Result channel will be closed when all requests have completed.
   338  func SendRequests(ctx context.Context, ifs []netlink.Link, ipv4, ipv6 bool, c Config, linkUpTimeout time.Duration) chan *Result {
   339  	// Yeah, this is a hack, until we can cancel all leases in progress.
   340  	r := make(chan *Result, 3*len(ifs))
   341  
   342  	var wg sync.WaitGroup
   343  	for _, iface := range ifs {
   344  		wg.Add(1)
   345  		go func(iface netlink.Link) {
   346  			defer wg.Done()
   347  
   348  			log.Printf("Bringing up interface %s...", iface.Attrs().Name)
   349  			if _, err := IfUp(iface.Attrs().Name, linkUpTimeout); err != nil {
   350  				log.Printf("Could not bring up interface %s: %v", iface.Attrs().Name, err)
   351  				return
   352  			}
   353  
   354  			if ipv4 {
   355  				wg.Add(1)
   356  				go func(iface netlink.Link) {
   357  					defer wg.Done()
   358  					lease, err := lease4(ctx, iface, c)
   359  					r <- &Result{NetIPv4, iface, lease, err}
   360  				}(iface)
   361  			}
   362  
   363  			if ipv6 {
   364  				wg.Add(1)
   365  				go func(iface netlink.Link) {
   366  					defer wg.Done()
   367  					lease, err := lease6(ctx, iface, c, linkUpTimeout)
   368  					r <- &Result{NetIPv6, iface, lease, err}
   369  				}(iface)
   370  			}
   371  		}(iface)
   372  	}
   373  
   374  	go func() {
   375  		wg.Wait()
   376  		close(r)
   377  	}()
   378  	return r
   379  }