github.com/jlowellwofford/u-root@v1.0.0/pkg/dhcp6client/client.go (about)

     1  // Copyright 2017-2018 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 dhcp6client implements a DHCPv6 client as per RFC 3315.
     6  package dhcp6client
     7  
     8  import (
     9  	"context"
    10  	"fmt"
    11  	"net"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/mdlayher/dhcp6"
    17  	"github.com/mdlayher/dhcp6/dhcp6opts"
    18  	"github.com/mdlayher/eui64"
    19  	"github.com/vishvananda/netlink"
    20  )
    21  
    22  // RFC 3315 Section 5.2.
    23  const (
    24  	// ClientPort is the port clients use to listen for DHCP messages.
    25  	ClientPort = 546
    26  
    27  	// ServerPort is the port servers and relay agents use to listen for
    28  	// DHCP messages.
    29  	ServerPort = 547
    30  )
    31  
    32  var (
    33  	// AllServers is all DHCP servers and relay agents on the local network
    34  	// segment (RFC 3315, Section 5.1.).
    35  	AllServers = net.ParseIP("ff02::1:2")
    36  
    37  	// DefaultServers is the default AllServers IP combined with the
    38  	// ServerPort.
    39  	DefaultServers = &net.UDPAddr{
    40  		IP:   AllServers,
    41  		Port: ServerPort,
    42  	}
    43  )
    44  
    45  // Client is a simple DHCPv6 client implementing RFC 3315.
    46  //
    47  //
    48  // Shortest Example:
    49  //
    50  //  c, err := dhcp6client.New(iface)
    51  //  ...
    52  //  iana, packet, err := c.RapidSolicit()
    53  //  ...
    54  //  // iana now contains the IP assigned in the IAAddr option.
    55  //
    56  //
    57  // Example selecting which advertising server to request from:
    58  //
    59  //   c, err := dhcp6client.New(iface)
    60  //   ...
    61  //   ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
    62  //   defer cancel()
    63  //
    64  //   ads, err := c.Solicit(ctx)
    65  //   ...
    66  //   // Selecting the advertisement of server 3.
    67  //   request, err := dhcp6client.RequestIANAFrom(ads[2])
    68  //   ...
    69  //   iana, packet, err := c.RequestOne(request)
    70  //   ...
    71  //   // iana now contains the IP assigned in the IAAddr option.
    72  type Client struct {
    73  	// The interface to send requests on.
    74  	iface netlink.Link
    75  
    76  	// Packet socket to send on.
    77  	conn net.PacketConn
    78  
    79  	// Max number of attempts to multicast DHCPv6 solicits.
    80  	// -1 means infinity.
    81  	retry int
    82  
    83  	// Timeout for each Solicit try.
    84  	timeout time.Duration
    85  }
    86  
    87  // New returns a new DHCPv6 client based on the given parameters.
    88  func New(iface netlink.Link, opts ...ClientOpt) (*Client, error) {
    89  	haddr := iface.Attrs().HardwareAddr
    90  	ip, err := eui64.ParseMAC(net.ParseIP("fe80::"), haddr)
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  
    95  	conn, err := net.ListenUDP("udp6", &net.UDPAddr{
    96  		IP:   ip,
    97  		Port: ClientPort,
    98  		Zone: iface.Attrs().Name,
    99  	})
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  
   104  	c := &Client{
   105  		iface:   iface,
   106  		conn:    conn,
   107  		timeout: 10 * time.Second,
   108  		retry:   3,
   109  	}
   110  
   111  	for _, opt := range opts {
   112  		if err := opt(c); err != nil {
   113  			return nil, err
   114  		}
   115  	}
   116  	return c, nil
   117  }
   118  
   119  // ClientOpt is a function that configures the client.
   120  type ClientOpt func(*Client) error
   121  
   122  // WithTimeout configures the retransmission timeout.
   123  //
   124  // Default is 10 seconds.
   125  //
   126  // TODO(hugelgupf): Check RFC for retransmission behavior.
   127  func WithTimeout(d time.Duration) ClientOpt {
   128  	return func(c *Client) error {
   129  		c.timeout = d
   130  		return nil
   131  	}
   132  }
   133  
   134  // WithRetry configures the retransmission counts.
   135  //
   136  // Default is 3.
   137  //
   138  // TODO(hugelgupf): Check RFC for retransmission behavior.
   139  func WithRetry(retry int) ClientOpt {
   140  	return func(c *Client) error {
   141  		c.retry = retry
   142  		return nil
   143  	}
   144  }
   145  
   146  // RapidSolicit solicits one non-temporary address assignment by multicasting a
   147  // DHCPv6 solicitation message with the rapid commit option.
   148  //
   149  // RapidSolicit returns the first valid, suitable response by any remote server.
   150  func (c *Client) RapidSolicit() (*dhcp6opts.IANA, *dhcp6.Packet, error) {
   151  	solicit, err := NewRapidSolicit(c.iface.Attrs().HardwareAddr)
   152  	if err != nil {
   153  		return nil, nil, err
   154  	}
   155  	return c.RequestOne(solicit)
   156  }
   157  
   158  // RequestOne multicasts the `request` and returns the first matching IANA and
   159  // its associated Packet returned by any server.
   160  func (c *Client) RequestOne(request *dhcp6.Packet) (*dhcp6opts.IANA, *dhcp6.Packet, error) {
   161  	ianas, pkt, err := c.Request(request)
   162  	if err != nil {
   163  		return nil, nil, err
   164  	}
   165  	if len(ianas) != 1 {
   166  		return nil, nil, fmt.Errorf("got %d IANAs, expected 1", len(ianas))
   167  	}
   168  	return ianas[0], pkt, nil
   169  }
   170  
   171  // Solicit multicasts a Solicit message and collects all Advertise responses
   172  // received before c.timeout expires.
   173  //
   174  // Solicit blocks until either:
   175  // - `ctx` is canceled; or
   176  // - we have exhausted all configured retries and timeouts.
   177  func (c *Client) Solicit(ctx context.Context) ([]*dhcp6.Packet, error) {
   178  	solicit, err := NewSolicitPacket(c.iface.Attrs().HardwareAddr)
   179  	if err != nil {
   180  		return nil, err
   181  	}
   182  
   183  	wg, out, errCh := c.SimpleSendAndRead(ctx, DefaultServers, solicit)
   184  	defer wg.Wait()
   185  
   186  	var ads []*dhcp6.Packet
   187  	// resps is closed by SimpleSendAndRead when done.
   188  	for r := range out {
   189  		if r.Packet.MessageType == dhcp6.MessageTypeAdvertise {
   190  			ads = append(ads, r.Packet)
   191  		}
   192  	}
   193  
   194  	if err, ok := <-errCh; ok && err != nil {
   195  		return nil, err
   196  	}
   197  	return ads, nil
   198  }
   199  
   200  // This name smells.
   201  type errorList []string
   202  
   203  func newManyErrs() *errorList {
   204  	return new(errorList)
   205  }
   206  
   207  func (e *errorList) add(err error) {
   208  	*e = append(*e, err.Error())
   209  }
   210  
   211  func (e errorList) Error() string {
   212  	return strings.Join([]string(e), "; ")
   213  }
   214  
   215  // Request requests non-temporary address assignments by multicasting the given
   216  // message.
   217  //
   218  // This request message may be any DHCPv6 request message type; e.g. a
   219  // Solicit with the Rapid Commit option or a Rebind message.
   220  func (c *Client) Request(request *dhcp6.Packet) ([]*dhcp6opts.IANA, *dhcp6.Packet, error) {
   221  	errs := newManyErrs()
   222  
   223  	// These are the IANAs we are looking for in responses.
   224  	reqIANAs, err := dhcp6opts.GetIANA(request.Options)
   225  	if err != nil {
   226  		return nil, nil, fmt.Errorf("request packet contains no IANAs: %v", err)
   227  	}
   228  
   229  	ctx, cancel := context.WithCancel(context.Background())
   230  	wg, out, errCh := c.SimpleSendAndRead(ctx, DefaultServers, request)
   231  	// Explicitly cancel the goroutine first, then wait.
   232  	defer func() {
   233  		cancel()
   234  		wg.Wait()
   235  	}()
   236  
   237  	for packet := range out {
   238  		if ianas, err := SuitableReply(reqIANAs, packet.Packet); err != nil {
   239  			errs.add(err)
   240  		} else {
   241  			// Guess we found our IANAs! The context will cancel
   242  			// all our problems.
   243  			return ianas, packet.Packet, nil
   244  		}
   245  	}
   246  
   247  	// Check if an error occurred.
   248  	if err, ok := <-errCh; ok && err != nil {
   249  		errs.add(err)
   250  	}
   251  
   252  	errs.add(fmt.Errorf("no suitable responses"))
   253  	return nil, nil, errs
   254  }
   255  
   256  // ClientPacket is a DHCP packet and the interface it corresponds to.
   257  type ClientPacket struct {
   258  	Interface netlink.Link
   259  	Packet    *dhcp6.Packet
   260  }
   261  
   262  // ClientError is an error that occurred on the associated interface.
   263  type ClientError struct {
   264  	Interface netlink.Link
   265  	Err       error
   266  }
   267  
   268  // Error implements error.
   269  func (ce *ClientError) Error() string {
   270  	if ce.Interface != nil {
   271  		return fmt.Sprintf("error on %q: %v", ce.Interface.Attrs().Name, ce.Err)
   272  	}
   273  	return fmt.Sprintf("error without interface: %v", ce.Err)
   274  }
   275  
   276  func (c *Client) newClientErr(err error) *ClientError {
   277  	if err == nil {
   278  		return nil
   279  	}
   280  	return &ClientError{
   281  		Interface: c.iface,
   282  		Err:       err,
   283  	}
   284  }
   285  
   286  // SimpleSendAndRead multicasts a DHCPv6 packet and launches a goroutine to
   287  // read response packets. Those response packets will be sent on the channel
   288  // returned. The sender will close both goroutines when it stops reading
   289  // packets, for example when the context is canceled.
   290  //
   291  // Callers must cancel ctx when they have received the packet they are looking
   292  // for. Otherwise, the spawned goroutine will keep reading until it times out.
   293  // More importantly, if you send another packet, the spawned goroutine may read
   294  // the response faster than the one launched for the other packet.
   295  //
   296  // See Client.Solicit for an example use of SendAndRead.
   297  //
   298  // Callers sending a packet on one interface should use this. Callers intending
   299  // to send packets on many interface at the same time, should look at using
   300  // SendAndRead instead.
   301  //
   302  // Example Usage:
   303  //
   304  //   func sendRequest(someRequest *Packet...) (*Packet, error) {
   305  //     ctx, cancel := context.WithCancel(context.Background())
   306  //     defer cancel()
   307  //
   308  //     out, errCh := c.SimpleSendAndRead(ctx, DefaultServers, someRequest)
   309  //
   310  //     for response := range out {
   311  //       if response == What You Want {
   312  //         // Context cancelation will stop the reading goroutine.
   313  //         return response, ...
   314  //       }
   315  //     }
   316  //
   317  //     if err, ok := <-errCh; ok && err != nil {
   318  //       return nil, err
   319  //     }
   320  //     return nil, fmt.Errorf("got no valid responses")
   321  //   }
   322  //
   323  // TODO(hugelgupf): since the client only has one connection, maybe it should
   324  // just have one dedicated goroutine for reading from the UDP socket, and use a
   325  // request and response queue.
   326  func (c *Client) SimpleSendAndRead(ctx context.Context, dest *net.UDPAddr, p *dhcp6.Packet) (*sync.WaitGroup, <-chan *ClientPacket, <-chan *ClientError) {
   327  	out := make(chan *ClientPacket, 10)
   328  	errOut := make(chan *ClientError, 1)
   329  	var wg sync.WaitGroup
   330  	wg.Add(1)
   331  	go func() {
   332  		c.SendAndRead(ctx, dest, p, out, errOut)
   333  		close(out)
   334  		close(errOut)
   335  		wg.Done()
   336  	}()
   337  	return &wg, out, errOut
   338  }
   339  
   340  // SendAndRead sends the given packet `dest` to `to` and reads
   341  // responses on the UDP connection. Any valid DHCP reply with the correct
   342  // Transaction ID is sent on `out`.
   343  //
   344  // SendAndRead blocks reading response packets until either:
   345  // - `ctx` is canceled; or
   346  // - we have exhausted all configured retries and timeouts.
   347  //
   348  // SendAndRead retries sending the packet and receiving responses according to
   349  // the configured number of c.retry, using a response timeout of c.timeout.
   350  //
   351  // TODO(hugelgupf): SendAndRead should follow RFC 3315 Section 14 for
   352  // retransmission behavior. Also conform to Section 15 for what kind of
   353  // messages must be discarded.
   354  func (c *Client) SendAndRead(ctx context.Context, dest *net.UDPAddr, p *dhcp6.Packet, out chan<- *ClientPacket, errCh chan<- *ClientError) {
   355  	// This ensures that
   356  	// - we send at most one error on errCh; and
   357  	// - we don't forget to send err on errCh in the many return statements
   358  	//   of sendAndRead.
   359  	if err := c.sendAndRead(ctx, dest, p, out); err != nil {
   360  		errCh <- c.newClientErr(err)
   361  	}
   362  }
   363  
   364  func (c *Client) sendAndRead(ctx context.Context, dest *net.UDPAddr, p *dhcp6.Packet, out chan<- *ClientPacket) error {
   365  	pkt, err := p.MarshalBinary()
   366  	if err != nil {
   367  		return err
   368  	}
   369  
   370  	return c.retryFn(func() error {
   371  		if _, err := c.conn.WriteTo(pkt, dest); err != nil {
   372  			return fmt.Errorf("error writing packet to connection: %v", err)
   373  		}
   374  
   375  		var numPackets int
   376  		timeoutCtx, cancel := context.WithTimeout(ctx, c.timeout)
   377  		defer cancel()
   378  		for {
   379  			select {
   380  			case <-timeoutCtx.Done():
   381  				if numPackets > 0 {
   382  					return nil
   383  				}
   384  
   385  				// No packets received. Sadness.
   386  				return timeoutCtx.Err()
   387  			default:
   388  			}
   389  
   390  			// Since a context can be canceled not just because of
   391  			// a deadline, we must check the context every once in
   392  			// a while. Use what is (hopefully) a small part of the
   393  			// context deadline rather than the context's deadline.
   394  			c.conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
   395  
   396  			// TODO: Clients can send a "max packet size" option in
   397  			// their packets, IIRC. Choose a reasonable size and
   398  			// set it.
   399  			b := make([]byte, 1500)
   400  			n, _, err := c.conn.ReadFrom(b)
   401  			if oerr, ok := err.(*net.OpError); ok && oerr.Timeout() {
   402  				// Continue to check ctx.Done() above and
   403  				// return the appropriate error.
   404  				continue
   405  			} else if err != nil {
   406  				return fmt.Errorf("error reading from UDP connection: %v", err)
   407  			}
   408  
   409  			pkt := &dhcp6.Packet{}
   410  			if err := pkt.UnmarshalBinary(b[:n]); err != nil {
   411  				// Not a valid DHCPv6 reply; keep listening.
   412  				continue
   413  			}
   414  
   415  			if pkt.TransactionID != p.TransactionID {
   416  				// Not the right response packet.
   417  				continue
   418  			}
   419  
   420  			numPackets++
   421  
   422  			clientPkt := &ClientPacket{
   423  				Packet:    pkt,
   424  				Interface: c.iface,
   425  			}
   426  
   427  			// Make sure that sending the response has priority.
   428  			select {
   429  			case out <- clientPkt:
   430  				continue
   431  			default:
   432  			}
   433  
   434  			// We deliberately only check the parent context here.
   435  			// c.timeout should only apply to reading from the
   436  			// conn, not sending on out.
   437  			select {
   438  			case <-ctx.Done():
   439  				return ctx.Err()
   440  			case out <- clientPkt:
   441  			}
   442  		}
   443  	})
   444  }
   445  
   446  // SuitableReply validates whether a pkt is a valid Reply message as defined by
   447  // RFC 3315, Section 18.1.8.
   448  //
   449  // It returns all valid IANAs corresponding to requested IANAs.
   450  func SuitableReply(reqIANAs []*dhcp6opts.IANA, pkt *dhcp6.Packet) ([]*dhcp6opts.IANA, error) {
   451  	// RFC 3315, Section 18.1.8.
   452  	// A suitable Reply packet must have:
   453  	//
   454  	// - non-negative status code (or no status), and
   455  	// - an IANA with IAID matching one of the ones we used in our request, and
   456  	// -- a non-negative status code (or no status) in the matching IANA, and
   457  	// -- a non-zero number of IAAddrs in the matching IANA.
   458  	if pkt.MessageType != dhcp6.MessageTypeReply {
   459  		return nil, fmt.Errorf("got DHCP message of type %s, wanted %s", pkt.MessageType, dhcp6.MessageTypeReply)
   460  	}
   461  
   462  	if status, err := dhcp6opts.GetStatusCode(pkt.Options); err == nil && status.Code != dhcp6.StatusSuccess {
   463  		return nil, fmt.Errorf("packet has status %s: %s", status.Code, status.Message)
   464  	}
   465  
   466  	ianas, err := dhcp6opts.GetIANA(pkt.Options)
   467  	if err != nil {
   468  		return nil, fmt.Errorf("successful packet had problem with IANA: %v", err)
   469  	}
   470  
   471  	var returned []*dhcp6opts.IANA
   472  	for _, iana := range ianas {
   473  		for _, reqIANA := range reqIANAs {
   474  			if iana.IAID != reqIANA.IAID {
   475  				continue
   476  			}
   477  
   478  			if status, err := dhcp6opts.GetStatusCode(iana.Options); err == nil && status.Code != dhcp6.StatusSuccess {
   479  				continue
   480  			}
   481  
   482  			iaAddrs, err := dhcp6opts.GetIAAddr(iana.Options)
   483  			if err != nil || len(iaAddrs) == 0 {
   484  				continue
   485  			}
   486  
   487  			returned = append(returned, iana)
   488  		}
   489  	}
   490  
   491  	return returned, nil
   492  }
   493  
   494  func (c *Client) retryFn(fn func() error) error {
   495  	// Each retry takes the amount of timeout at worst.
   496  	for i := 0; i < c.retry || c.retry < 0; i++ {
   497  		switch err := fn(); err {
   498  		case nil:
   499  			// Got it!
   500  			return nil
   501  
   502  		case context.DeadlineExceeded:
   503  			// Just retry.
   504  			// TODO(hugelgupf): Sleep here for some random amount of time.
   505  
   506  		default:
   507  			return err
   508  		}
   509  	}
   510  
   511  	return context.DeadlineExceeded
   512  }