diff options
Diffstat (limited to 'dhcpv6/client6')
-rw-r--r-- | dhcpv6/client6/client.go | 218 | ||||
-rw-r--r-- | dhcpv6/client6/client_test.go | 14 |
2 files changed, 232 insertions, 0 deletions
diff --git a/dhcpv6/client6/client.go b/dhcpv6/client6/client.go new file mode 100644 index 0000000..4075bde --- /dev/null +++ b/dhcpv6/client6/client.go @@ -0,0 +1,218 @@ +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 +) + +// Broadcast destination IP addresses as defined by RFC 3315 +var ( + AllDHCPRelayAgentsAndServers = net.ParseIP("ff02::1:2") + AllDHCPServers = net.ParseIP("ff05::1:3") +) + +// Client implements a DHCPv6 client +type Client struct { + ReadTimeout time.Duration + WriteTimeout time.Duration + LocalAddr net.Addr + RemoteAddr net.Addr +} + +// 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.DHCPv6Relay) + advertise, err = advertiseRelay.GetInnerMessage() + if err != nil { + return conversation, err + } + } + request, reply, err := c.Request(ifname, advertise, 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 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 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 no RemoteAddr is specified, use AllDHCPRelayAgentsAndServers + var raddr net.UDPAddr + if c.RemoteAddr == nil { + raddr = net.UDPAddr{IP: 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 + conn.SetWriteDeadline(time.Now().Add(c.WriteTimeout)) + _, err = conn.WriteTo(packet.ToBytes(), &raddr) + if err != nil { + return nil, err + } + + // wait for a reply + oobdata := []byte{} // ignoring oob data + conn.SetReadDeadline(time.Now().Add(c.ReadTimeout)) + var ( + adv dhcpv6.DHCPv6 + isMessage bool + ) + defer conn.Close() + msg, ok := packet.(*dhcpv6.DHCPv6Message) + 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 + continue + } + if recvMsg, ok := adv.(*dhcpv6.DHCPv6Message); 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) { + solicit, err := dhcpv6.NewSolicitForInterface(ifname) + if err != nil { + return nil, nil, err + } + for _, mod := range modifiers { + solicit = 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.DHCPv6, modifiers ...dhcpv6.Modifier) (dhcpv6.DHCPv6, dhcpv6.DHCPv6, error) { + request, err := dhcpv6.NewRequestFromAdvertise(advertise) + if err != nil { + return nil, nil, err + } + for _, mod := range modifiers { + request = mod(request) + } + reply, err := c.sendReceive(ifname, request, dhcpv6.MessageTypeNone) + return request, reply, err +} diff --git a/dhcpv6/client6/client_test.go b/dhcpv6/client6/client_test.go new file mode 100644 index 0000000..1e05a62 --- /dev/null +++ b/dhcpv6/client6/client_test.go @@ -0,0 +1,14 @@ +package client6 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestNewClient(t *testing.T) { + c := NewClient() + require.NotNil(t, c) + require.Equal(t, DefaultReadTimeout, c.ReadTimeout) + require.Equal(t, DefaultWriteTimeout, c.WriteTimeout) +} |