// Copyright 2018 the u-root Authors and Andrea Barberio. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package nclient6 is a minimum-functionality client for DHCPv6. package nclient6 import ( "context" "errors" "fmt" "log" "net" "os" "strings" "sync" "sync/atomic" "time" "github.com/insomniacslk/dhcp/dhcpv6" ) // Broadcast destination IP addresses as defined by RFC 3315 var ( AllDHCPRelayAgentsAndServers = &net.UDPAddr{ IP: net.ParseIP("ff02::1:2"), Port: dhcpv6.DefaultServerPort, } AllDHCPServers = &net.UDPAddr{ IP: net.ParseIP("ff05::1:3"), Port: dhcpv6.DefaultServerPort, } ) var ( // ErrNoResponse is returned when no response packet is received. ErrNoResponse = errors.New("no matching response packet received") ) // pendingCh is a channel associated with a pending TransactionID. type pendingCh struct { // SendAndRead closes done to indicate that it wishes for no more // messages for this particular XID. done <-chan struct{} // ch is used by the receive loop to distribute DHCP messages. ch chan<- *dhcpv6.Message } // Client is a DHCPv6 client. type Client struct { ifaceHWAddr net.HardwareAddr conn net.PacketConn timeout time.Duration retry int logger logger // bufferCap is the channel capacity for each TransactionID. bufferCap int // serverAddr is the UDP address to send all packets to. // // This may be an actual broadcast address, or a unicast address. serverAddr *net.UDPAddr // closed is an atomic bool set to 1 when done is closed. closed uint32 // done is closed to unblock the receive loop. done chan struct{} // wg protects the receiveLoop. wg sync.WaitGroup // printDropped logs dropped packets to logger if true. printDropped bool pendingMu sync.Mutex // pending stores the distribution channels for each pending // TransactionID. receiveLoop uses this map to determine which channel // to send a new DHCP message to. pending map[dhcpv6.TransactionID]*pendingCh } type logger interface { Printf(format string, v ...interface{}) PrintMessage(prefix string, message *dhcpv6.Message) } type emptyLogger struct{} func (e emptyLogger) Printf(format string, v ...interface{}) {} func (e emptyLogger) PrintMessage(prefix string, message *dhcpv6.Message) {} type shortSummaryLogger struct { *log.Logger } func (s shortSummaryLogger) Printf(format string, v ...interface{}) { s.Logger.Printf(format, v...) } func (s shortSummaryLogger) PrintMessage(prefix string, message *dhcpv6.Message) { s.Printf("%s: %s", prefix, message) } type debugLogger struct { *log.Logger } func (d debugLogger) Printf(format string, v ...interface{}) { d.Logger.Printf(format, v...) } func (d debugLogger) PrintMessage(prefix string, message *dhcpv6.Message) { d.Printf("%s: %s", prefix, message.Summary()) } // NewIPv6UDPConn returns a UDP connection bound to both the interface and port // given based on a IPv6 DGRAM socket. func NewIPv6UDPConn(iface string, port int) (net.PacketConn, error) { ip, err := dhcpv6.GetLinkLocalAddr(iface) if err != nil { return nil, err } return net.ListenUDP("udp6", &net.UDPAddr{ IP: ip, Port: port, Zone: iface, }) } // New returns a new DHCPv6 client for the given network interface. func New(iface string, opts ...ClientOpt) (*Client, error) { c, err := NewIPv6UDPConn(iface, dhcpv6.DefaultClientPort) if err != nil { return nil, err } i, err := net.InterfaceByName(iface) if err != nil { return nil, err } return NewWithConn(c, i.HardwareAddr, opts...) } // NewWithConn creates a new DHCP client that sends and receives packets on the // given interface. func NewWithConn(conn net.PacketConn, ifaceHWAddr net.HardwareAddr, opts ...ClientOpt) (*Client, error) { c := &Client{ ifaceHWAddr: ifaceHWAddr, timeout: 5 * time.Second, retry: 3, serverAddr: AllDHCPRelayAgentsAndServers, bufferCap: 5, conn: conn, logger: emptyLogger{}, done: make(chan struct{}), pending: make(map[dhcpv6.TransactionID]*pendingCh), } for _, opt := range opts { opt(c) } if c.conn == nil { return nil, fmt.Errorf("require a connection") } c.receiveLoop() return c, nil } // Close closes the underlying connection. func (c *Client) Close() error { // Make sure not to close done twice. if !atomic.CompareAndSwapUint32(&c.closed, 0, 1) { return nil } err := c.conn.Close() // Closing c.done sets off a chain reaction: // // Any SendAndRead unblocks trying to receive more messages, which // means rem() gets called. // // rem() should be unblocking receiveLoop if it is blocked. // // receiveLoop should then exit gracefully. close(c.done) // Wait for receiveLoop to stop. c.wg.Wait() return err } func isErrClosing(err error) bool { // Unfortunately, the epoll-connection-closed error is internal to the // net library. return strings.Contains(err.Error(), "use of closed network connection") } func (c *Client) receiveLoop() { c.wg.Add(1) go func() { defer c.wg.Done() for { // TODO: Clients can send a "max packet size" option in their // packets, IIRC. Choose a reasonable size and set it. b := make([]byte, 1500) n, _, err := c.conn.ReadFrom(b) if err != nil { if !isErrClosing(err) { c.logger.Printf("error reading from UDP connection: %v", err) } return } msg, err := dhcpv6.MessageFromBytes(b[:n]) if err != nil { // Not a valid DHCP packet; keep listening. if c.printDropped { if len(b) > 12 { b = b[:12] } c.logger.Printf("Invalid DHCPv6 message received (len %d bytes), first 12 bytes: %#x", n, b) } continue } c.pendingMu.Lock() p, ok := c.pending[msg.TransactionID] if ok { select { case <-p.done: close(p.ch) delete(c.pending, msg.TransactionID) // This send may block. case p.ch <- msg: } } else if c.printDropped { // The Stringer will print the transaction ID. c.logger.Printf("No client waiting for msg with this XID: %s", msg) } c.pendingMu.Unlock() } }() } // ClientOpt is a function that configures the Client. type ClientOpt func(*Client) // WithTimeout configures the retransmission timeout. // // Default is 5 seconds. func WithTimeout(d time.Duration) ClientOpt { return func(c *Client) { c.timeout = d } } // WithLogDroppedPackets logs a short message for dropped packets. func WithLogDroppedPackets() ClientOpt { return func(c *Client) { c.printDropped = true } } // WithRetry configures the number of retransmissions to attempt. // // Default is 3. func WithRetry(r int) ClientOpt { return func(c *Client) { c.retry = r } } // WithConn configures the packet connection to use. func WithConn(conn net.PacketConn) ClientOpt { return func(c *Client) { c.conn = conn } } // WithBroadcastAddr configures the address to broadcast to. func WithBroadcastAddr(n *net.UDPAddr) ClientOpt { return func(c *Client) { c.serverAddr = n } } // WithSummaryLogger logs one-line DHCPv6 message summarys when sent & received. func WithSummaryLogger() ClientOpt { return func(c *Client) { c.logger = shortSummaryLogger{ Logger: log.New(os.Stderr, "[dhcpv6] ", log.LstdFlags), } } } // WithDebugLogger logs multi-line full DHCPv6 messages when sent & received. func WithDebugLogger() ClientOpt { return func(c *Client) { c.logger = debugLogger{ Logger: log.New(os.Stderr, "[dhcpv6] ", log.LstdFlags), } } } // Matcher matches DHCP packets. type Matcher func(*dhcpv6.Message) bool // IsMessageType returns a matcher that checks for the message type. func IsMessageType(t dhcpv6.MessageType, tt ...dhcpv6.MessageType) Matcher { return func(p *dhcpv6.Message) bool { if p.MessageType == t { return true } for _, mt := range tt { if p.MessageType == mt { return true } } return false } } // RemoteAddr is the default DHCP server address this client sends messages to. func (c *Client) RemoteAddr() *net.UDPAddr { // Make a copy so the caller cannot modify the address once the client // is running. cop := *c.serverAddr return &cop } // InterfaceAddr returns the MAC address of the client's interface. func (c *Client) InterfaceAddr() net.HardwareAddr { b := make(net.HardwareAddr, len(c.ifaceHWAddr)) copy(b, c.ifaceHWAddr) return b } // RapidSolicit sends a solicitation message with the RapidCommit option and // returns the first valid reply received. func (c *Client) RapidSolicit(ctx context.Context, modifiers ...dhcpv6.Modifier) (*dhcpv6.Message, error) { solicit, err := dhcpv6.NewSolicit(c.ifaceHWAddr, append(modifiers, dhcpv6.WithRapidCommit)...) if err != nil { return nil, err } msg, err := c.SendAndRead(ctx, c.serverAddr, solicit, IsMessageType(dhcpv6.MessageTypeReply, dhcpv6.MessageTypeAdvertise)) if err != nil { return nil, err } switch msg.MessageType { case dhcpv6.MessageTypeReply: // We got RapidCommitted. return msg, nil case dhcpv6.MessageTypeAdvertise: // We didn't get RapidCommitted. Request regular lease. return c.Request(ctx, msg, modifiers...) default: return nil, fmt.Errorf("invalid message type: cannot happen") } } // Solicit sends a solicitation message and returns the first valid // advertisement received. func (c *Client) Solicit(ctx context.Context, modifiers ...dhcpv6.Modifier) (*dhcpv6.Message, error) { solicit, err := dhcpv6.NewSolicit(c.ifaceHWAddr, modifiers...) if err != nil { return nil, err } msg, err := c.SendAndRead(ctx, c.serverAddr, solicit, IsMessageType(dhcpv6.MessageTypeAdvertise)) if err != nil { return nil, err } return msg, nil } // Request requests an IP Assignment from peer given an advertise message. func (c *Client) Request(ctx context.Context, advertise *dhcpv6.Message, modifiers ...dhcpv6.Modifier) (*dhcpv6.Message, error) { request, err := dhcpv6.NewRequestFromAdvertise(advertise, modifiers...) if err != nil { return nil, err } return c.SendAndRead(ctx, c.serverAddr, request, nil) } // send sends p to destination and returns a response channel. // // The returned function must be called after all desired responses have been // received. // // Responses will be matched by transaction ID. func (c *Client) send(dest net.Addr, msg *dhcpv6.Message) (<-chan *dhcpv6.Message, func(), error) { c.pendingMu.Lock() if _, ok := c.pending[msg.TransactionID]; ok { c.pendingMu.Unlock() return nil, nil, fmt.Errorf("transaction ID %s already in use", msg.TransactionID) } ch := make(chan *dhcpv6.Message, c.bufferCap) done := make(chan struct{}) c.pending[msg.TransactionID] = &pendingCh{done: done, ch: ch} c.pendingMu.Unlock() cancel := func() { // Why can't we just close ch here? // // Because receiveLoop may potentially be blocked trying to // send on ch. We gotta unblock it first, so it'll unlock the // lock, and then we can take the lock and remove the XID from // the pending transaction map. close(done) c.pendingMu.Lock() if p, ok := c.pending[msg.TransactionID]; ok { close(p.ch) delete(c.pending, msg.TransactionID) } c.pendingMu.Unlock() } if _, err := c.conn.WriteTo(msg.ToBytes(), dest); err != nil { cancel() return nil, nil, fmt.Errorf("error writing packet to connection: %v", err) } return ch, cancel, nil } // This should never be visible to a user. var errDeadlineExceeded = errors.New("INTERNAL ERROR: deadline exceeded") // SendAndRead sends a packet p to a destination dest and waits for the first // response matching `match` as well as its Transaction ID. // // If match is nil, the first packet matching the Transaction ID is returned. func (c *Client) SendAndRead(ctx context.Context, dest *net.UDPAddr, msg *dhcpv6.Message, match Matcher) (*dhcpv6.Message, error) { var response *dhcpv6.Message err := c.retryFn(func(timeout time.Duration) error { ch, rem, err := c.send(dest, msg) if err != nil { return err } c.logger.PrintMessage("sent message", msg) defer rem() for { select { case <-c.done: return ErrNoResponse case <-time.After(timeout): return errDeadlineExceeded case <-ctx.Done(): return ctx.Err() case packet := <-ch: if match == nil || match(packet) { c.logger.PrintMessage("received message", packet) response = packet return nil } } } }) if err == errDeadlineExceeded { return nil, ErrNoResponse } if err != nil { return nil, err } return response, nil } func (c *Client) retryFn(fn func(timeout time.Duration) error) error { timeout := c.timeout // Each retry takes the amount of timeout at worst. for i := 0; i < c.retry || c.retry < 0; i++ { switch err := fn(timeout); err { case nil: // Got it! return nil case errDeadlineExceeded: // Double timeout, then retry. timeout *= 2 default: return err } } return errDeadlineExceeded }