package client6 import ( "errors" "fmt" "net" "time" "github.com/insomniacslk/dhcp/dhcpv6" ) // Client constants const ( DefaultWriteTimeout = 3 * time.Second // time to wait for write calls DefaultReadTimeout = 3 * time.Second // time to wait for read calls DefaultInterfaceUpTimeout = 3 * time.Second // time to wait before a network interface goes up MaxUDPReceivedPacketSize = 8192 // arbitrary size. Theoretically could be up to 65kb ) // Client implements a DHCPv6 client type Client struct { ReadTimeout time.Duration WriteTimeout time.Duration LocalAddr net.Addr RemoteAddr net.Addr SimulateRelay bool RelayOptions dhcpv6.Options // These options will be added to relay message if SimulateRelay is true } // NewClient returns a Client with default settings func NewClient() *Client { return &Client{ ReadTimeout: DefaultReadTimeout, WriteTimeout: DefaultWriteTimeout, } } // Exchange executes a 4-way DHCPv6 request (Solicit, Advertise, Request, // Reply). The modifiers will be applied to the Solicit and Request packets. // A common use is to make sure that the Solicit packet has the right options, // see modifiers.go func (c *Client) Exchange(ifname string, modifiers ...dhcpv6.Modifier) ([]dhcpv6.DHCPv6, error) { conversation := make([]dhcpv6.DHCPv6, 0) var err error // Solicit solicit, advertise, err := c.Solicit(ifname, modifiers...) if solicit != nil { conversation = append(conversation, solicit) } if err != nil { return conversation, err } conversation = append(conversation, advertise) // Decapsulate advertise if it's relayed before passing it to Request if advertise.IsRelay() { advertiseRelay := advertise.(*dhcpv6.RelayMessage) advertise, err = advertiseRelay.GetInnerMessage() if err != nil { return conversation, err } } request, reply, err := c.Request(ifname, advertise.(*dhcpv6.Message), modifiers...) if request != nil { conversation = append(conversation, request) } if err != nil { return conversation, err } conversation = append(conversation, reply) return conversation, nil } func (c *Client) sendReceive(ifname string, packet dhcpv6.DHCPv6, expectedType dhcpv6.MessageType) (dhcpv6.DHCPv6, error) { if packet == nil { return nil, fmt.Errorf("Packet to send cannot be nil") } // if no LocalAddr is specified, get the interface's link-local address var laddr net.UDPAddr if c.LocalAddr == nil { llAddr, err := dhcpv6.GetLinkLocalAddr(ifname) if err != nil { return nil, err } laddr = net.UDPAddr{IP: llAddr, Port: dhcpv6.DefaultClientPort, Zone: ifname} } else { if addr, ok := c.LocalAddr.(*net.UDPAddr); ok { laddr = *addr } else { return nil, fmt.Errorf("Invalid local address: not a net.UDPAddr: %v", c.LocalAddr) } } if c.SimulateRelay { var err error packet, err = dhcpv6.EncapsulateRelay(packet, dhcpv6.MessageTypeRelayForward, net.IPv6zero, laddr.IP) if err != nil { return nil, err } // Add Relay Options to ecapsulated Packet for _, opt := range c.RelayOptions { packet.UpdateOption(opt) } } if expectedType == dhcpv6.MessageTypeNone { // infer the expected type from the packet being sent if packet.Type() == dhcpv6.MessageTypeSolicit { expectedType = dhcpv6.MessageTypeAdvertise } else if packet.Type() == dhcpv6.MessageTypeRequest { expectedType = dhcpv6.MessageTypeReply } else if packet.Type() == dhcpv6.MessageTypeRelayForward { expectedType = dhcpv6.MessageTypeRelayReply } else if packet.Type() == dhcpv6.MessageTypeLeaseQuery { expectedType = dhcpv6.MessageTypeLeaseQueryReply } // and probably more } // if no RemoteAddr is specified, use AllDHCPRelayAgentsAndServers var raddr net.UDPAddr if c.RemoteAddr == nil { raddr = net.UDPAddr{IP: dhcpv6.AllDHCPRelayAgentsAndServers, Port: dhcpv6.DefaultServerPort} } else { if addr, ok := c.RemoteAddr.(*net.UDPAddr); ok { raddr = *addr } else { return nil, fmt.Errorf("Invalid remote address: not a net.UDPAddr: %v", c.RemoteAddr) } } // prepare the socket to listen on for replies conn, err := net.ListenUDP("udp6", &laddr) if err != nil { return nil, err } defer conn.Close() // wait for the listener to be ready, fail if it takes too much time deadline := time.Now().Add(time.Second) for { if now := time.Now(); now.After(deadline) { return nil, errors.New("Timed out waiting for listener to be ready") } if conn.LocalAddr() != nil { break } time.Sleep(10 * time.Millisecond) } // send the packet out if err := conn.SetWriteDeadline(time.Now().Add(c.WriteTimeout)); err != nil { return nil, err } _, err = conn.WriteTo(packet.ToBytes(), &raddr) if err != nil { return nil, err } // wait for a reply oobdata := []byte{} // ignoring oob data if err := conn.SetReadDeadline(time.Now().Add(c.ReadTimeout)); err != nil { return nil, err } var ( adv dhcpv6.DHCPv6 isMessage bool ) defer conn.Close() msg, ok := packet.(*dhcpv6.Message) if ok { isMessage = true } for { buf := make([]byte, MaxUDPReceivedPacketSize) n, _, _, _, err := conn.ReadMsgUDP(buf, oobdata) if err != nil { return nil, err } adv, err = dhcpv6.FromBytes(buf[:n]) if err != nil { // skip non-DHCP packets // // TODO: It also skips DHCP packets with any errors (for example // if bootfile params are encoded incorrectly). We need to // log such cases instead of silently skip them. continue } if recvMsg, ok := adv.(*dhcpv6.Message); ok && isMessage { // if a regular message, check the transaction ID first // XXX should this unpack relay messages and check the XID of the // inner packet too? if msg.TransactionID != recvMsg.TransactionID { // different XID, we don't want this packet for sure continue } } if expectedType == dhcpv6.MessageTypeNone { // just take whatever arrived break } else if adv.Type() == expectedType { break } } return adv, nil } // Solicit sends a Solicit, returns the Solicit, an Advertise (if not nil), and // an error if any. The modifiers will be applied to the Solicit before sending // it, see modifiers.go func (c *Client) Solicit(ifname string, modifiers ...dhcpv6.Modifier) (dhcpv6.DHCPv6, dhcpv6.DHCPv6, error) { iface, err := net.InterfaceByName(ifname) if err != nil { return nil, nil, err } solicit, err := dhcpv6.NewSolicit(iface.HardwareAddr) if err != nil { return nil, nil, err } for _, mod := range modifiers { mod(solicit) } advertise, err := c.sendReceive(ifname, solicit, dhcpv6.MessageTypeNone) return solicit, advertise, err } // Request sends a Request built from an Advertise. It returns the Request, a // Reply (if not nil), and an error if any. The modifiers will be applied to // the Request before sending it, see modifiers.go func (c *Client) Request(ifname string, advertise *dhcpv6.Message, modifiers ...dhcpv6.Modifier) (dhcpv6.DHCPv6, dhcpv6.DHCPv6, error) { request, err := dhcpv6.NewRequestFromAdvertise(advertise) if err != nil { return nil, nil, err } for _, mod := range modifiers { mod(request) } reply, err := c.sendReceive(ifname, request, dhcpv6.MessageTypeNone) return request, reply, err }