summaryrefslogtreecommitdiffhomepage
path: root/dhcpv6/client6
diff options
context:
space:
mode:
Diffstat (limited to 'dhcpv6/client6')
-rw-r--r--dhcpv6/client6/client.go218
-rw-r--r--dhcpv6/client6/client_test.go14
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)
+}