diff options
author | Andrea Barberio <insomniac@slackware.it> | 2017-11-29 21:25:48 +0000 |
---|---|---|
committer | Andrea Barberio <insomniac@slackware.it> | 2017-12-05 23:32:27 +0000 |
commit | 1403bbe04ce275148b601c32e9551b2281110347 (patch) | |
tree | 6eacd224c2ea6b6e3865cb774970726809363d46 /dhcpv6 |
Initial commit
Diffstat (limited to 'dhcpv6')
-rw-r--r-- | dhcpv6/client.go | 122 | ||||
-rw-r--r-- | dhcpv6/defaults.go | 6 | ||||
-rw-r--r-- | dhcpv6/dhcpv6.go | 215 | ||||
-rw-r--r-- | dhcpv6/dhcpv6_test.go | 145 | ||||
-rw-r--r-- | dhcpv6/iputils.go | 30 | ||||
-rw-r--r-- | dhcpv6/options/clientid.go | 57 | ||||
-rw-r--r-- | dhcpv6/options/dnsrecursivenameserver.go | 59 | ||||
-rw-r--r-- | dhcpv6/options/domainsearchlist.go | 57 | ||||
-rw-r--r-- | dhcpv6/options/duid.go | 100 | ||||
-rw-r--r-- | dhcpv6/options/elapsedtime.go | 52 | ||||
-rw-r--r-- | dhcpv6/options/elapsedtime_test.go | 44 | ||||
-rw-r--r-- | dhcpv6/options/iaaddress.go | 86 | ||||
-rw-r--r-- | dhcpv6/options/iaprefix.go | 98 | ||||
-rw-r--r-- | dhcpv6/options/nontemporaryaddress.go | 86 | ||||
-rw-r--r-- | dhcpv6/options/options.go | 125 | ||||
-rw-r--r-- | dhcpv6/options/prefixdelegation.go | 86 | ||||
-rw-r--r-- | dhcpv6/options/requestedoption.go | 72 | ||||
-rw-r--r-- | dhcpv6/options/rfc1035label.go | 54 | ||||
-rw-r--r-- | dhcpv6/options/rfc1035label_test.go | 66 | ||||
-rw-r--r-- | dhcpv6/options/serverid.go | 57 | ||||
-rw-r--r-- | dhcpv6/options/statuscode.go | 63 | ||||
-rw-r--r-- | dhcpv6/options/types.go | 152 | ||||
-rw-r--r-- | dhcpv6/server.go | 56 | ||||
-rw-r--r-- | dhcpv6/types.go | 46 |
24 files changed, 1934 insertions, 0 deletions
diff --git a/dhcpv6/client.go b/dhcpv6/client.go new file mode 100644 index 0000000..787439d --- /dev/null +++ b/dhcpv6/client.go @@ -0,0 +1,122 @@ +package dhcpv6 + +import ( + "fmt" + "net" + "time" +) + +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 +) + +var AllDHCPRelayAgentsAndServers = net.ParseIP("ff02::1:2") +var AllDHCPServers = net.ParseIP("ff05::1:3") + +type Client struct { + Dialer *net.Dialer + ReadTimeout *time.Duration + WriteTimeout *time.Duration + LocalAddr net.Addr + RemoteAddr net.Addr +} + +// Make a stateful DHCPv6 request +func (c *Client) Exchange(ifname string, d *DHCPv6) ([]DHCPv6, error) { + conversation := make([]DHCPv6, 1) + var err error + + // Solicit + if d == nil { + d, err = NewSolicitForInterface(ifname) + if err != nil { + return conversation, err + } + } + conversation[0] = *d + advertise, err := c.ExchangeSolicitAdvertise(ifname, d) + if err != nil { + return conversation, err + } + conversation = append(conversation, *advertise) + + // TODO request/reply + return conversation, nil +} + +func (c *Client) ExchangeSolicitAdvertise(ifname string, d *DHCPv6) (*DHCPv6, error) { + // if no LocalAddr is specified, get the interface's link-local address + var laddr net.UDPAddr + if c.LocalAddr == nil { + llAddr, err := GetLinkLocalAddr(ifname) + if err != nil { + return nil, err + } + laddr = net.UDPAddr{IP: *llAddr, Port: 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: 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() + + // set WriteTimeout to DefaultWriteTimeout if no other timeout is specified + var wtimeout time.Duration + if c.WriteTimeout == nil { + wtimeout = DefaultWriteTimeout + } else { + wtimeout = *c.WriteTimeout + } + conn.SetWriteDeadline(time.Now().Add(wtimeout)) + + // send the SOLICIT packet out + _, err = conn.WriteTo(d.ToBytes(), &raddr) + if err != nil { + return nil, err + } + + // set ReadTimeout to DefaultReadTimeout if no other timeout is specified + var rtimeout time.Duration + if c.ReadTimeout == nil { + rtimeout = DefaultReadTimeout + } else { + rtimeout = *c.ReadTimeout + } + conn.SetReadDeadline(time.Now().Add(rtimeout)) + + // wait for an ADVERTISE response + buf := make([]byte, maxUDPReceivedPacketSize) + oobdata := []byte{} // ignoring oob data + n, _, _, _, err := conn.ReadMsgUDP(buf, oobdata) + if err != nil { + return nil, err + } + adv, err := FromBytes(buf[:n]) + if err != nil { + return nil, err + } + return adv, nil +} diff --git a/dhcpv6/defaults.go b/dhcpv6/defaults.go new file mode 100644 index 0000000..ae14bba --- /dev/null +++ b/dhcpv6/defaults.go @@ -0,0 +1,6 @@ +package dhcpv6 + +const ( + DefaultClientPort = 546 + DefaultServerPort = 547 +) diff --git a/dhcpv6/dhcpv6.go b/dhcpv6/dhcpv6.go new file mode 100644 index 0000000..6b664d2 --- /dev/null +++ b/dhcpv6/dhcpv6.go @@ -0,0 +1,215 @@ +package dhcpv6 + +import ( + "crypto/rand" + "encoding/binary" + "fmt" + "github.com/insomniacslk/dhcp/dhcpv6/options" + "github.com/insomniacslk/dhcp/iana" + "log" + "net" + "time" +) + +const HeaderSize = 4 + +type DHCPv6 struct { + message MessageType + transactionID uint32 // only 24 bits are used though + options []options.Option +} + +func BytesToTransactionID(data []byte) (*uint32, error) { + // return a uint32 from a sequence of bytes, representing a transaction ID. + // Transaction IDs are three-bytes long. If the provided data is shorter than + // 3 bytes, it return an error. If longer, will use the first three bytes + // only. + if len(data) < 3 { + return nil, fmt.Errorf("Invalid transaction ID: less than 3 bytes") + } + buf := make([]byte, 4) + copy(buf[1:4], data[:3]) + tid := binary.BigEndian.Uint32(buf) + return &tid, nil +} + +func GenerateTransactionID() (*uint32, error) { + var tid *uint32 + for { + tidBytes := make([]byte, 4) + n, err := rand.Read(tidBytes) + if n != 4 { + return nil, fmt.Errorf("Invalid random sequence: shorter than 4 bytes") + } + tid, err = BytesToTransactionID(tidBytes) + if err != nil { + return nil, err + } + if tid == nil { + return nil, fmt.Errorf("Error: got a nil Transaction ID") + } + // retry until != 0 + // TODO add retry limit + if *tid != 0 { + break + } + } + return tid, nil +} + +func FromBytes(data []byte) (*DHCPv6, error) { + if len(data) < HeaderSize { + return nil, fmt.Errorf("Invalid DHCPv6 header: shorter than %v bytes", HeaderSize) + } + tid, err := BytesToTransactionID(data[1:4]) + if err != nil { + return nil, err + } + d := DHCPv6{ + message: MessageType(data[0]), + transactionID: *tid, + } + options, err := options.FromBytes(data[4:]) + if err != nil { + return nil, err + } + d.options = options + return &d, nil +} + +func New() (*DHCPv6, error) { + tid, err := GenerateTransactionID() + if err != nil { + return nil, err + } + d := DHCPv6{ + message: SOLICIT, + transactionID: *tid, + } + return &d, nil +} + +// Return a time integer suitable for DUID-LLT, i.e. the current time counted in +// seconds since January 1st, 2000, midnight UTC, modulo 2^32 +func GetTime() uint32 { + now := time.Since(time.Date(2000, time.January, 1, 0, 0, 0, 0, time.UTC)) + return uint32((now.Nanoseconds() / 1000000000) % 0xffffffff) +} + +// Create a new SOLICIT message with DUID-LLT, using the given network +// interface's hardware address and current time +func NewSolicitForInterface(ifname string) (*DHCPv6, error) { + d, err := New() + if err != nil { + return nil, err + } + d.SetMessage(SOLICIT) + iface, err := net.InterfaceByName(ifname) + if err != nil { + return nil, err + } + cid := options.OptClientId{} + cid.SetClientID(options.Duid{ + Type: options.DUID_LLT, + HwType: iana.HwTypeEthernet, + Time: GetTime(), + LinkLayerAddr: iface.HardwareAddr, + }) + + d.AddOption(&cid) + oro := options.OptRequestedOption{} + oro.SetRequestedOptions([]options.OptionCode{ + options.DNS_RECURSIVE_NAME_SERVER, + options.DOMAIN_SEARCH_LIST, + }) + d.AddOption(&oro) + d.AddOption(&options.OptElapsedTime{}) + // FIXME use real values for IA_NA + iaNa := options.OptIANA{} + iaNa.SetIAID([4]byte{0x27, 0xfe, 0x8f, 0x95}) + iaNa.SetT1(0xe10) + iaNa.SetT2(0x1518) + d.AddOption(&iaNa) + return d, nil +} + +func (d *DHCPv6) Message() MessageType { + return d.message +} + +func (d *DHCPv6) SetMessage(message MessageType) { + if MessageToString[message] == "" { + log.Printf("Warning: unknown DHCPv6 message: %v", message) + } + d.message = message +} + +func (d *DHCPv6) MessageToString() string { + if m := MessageToString[d.message]; m != "" { + return m + } + return "Invalid" +} + +func (d *DHCPv6) TransactionID() uint32 { + return d.transactionID +} + +func (d *DHCPv6) SetTransactionID(tid uint32) { + ttid := tid & 0x00ffffff + if ttid != tid { + log.Printf("Warning: truncating transaction ID that is longer than 24 bits: %v", tid) + } + d.transactionID = ttid +} + +func (d *DHCPv6) Options() []options.Option { + return d.options +} + +func (d *DHCPv6) SetOptions(options []options.Option) { + d.options = options +} + +func (d *DHCPv6) AddOption(option options.Option) { + d.options = append(d.options, option) +} + +func (d *DHCPv6) String() string { + return fmt.Sprintf("DHCPv6(message=%v transactionID=0x%06x, %d options)", + d.MessageToString(), d.TransactionID(), len(d.options), + ) +} + +func (d *DHCPv6) Summary() string { + ret := fmt.Sprintf( + "DHCPv6\n"+ + " message=%v\n"+ + " transactionid=0x%06x\n", + d.MessageToString(), + d.TransactionID(), + ) + ret += " options=[" + if len(d.options) > 0 { + ret += "\n" + } + for _, opt := range d.options { + ret += fmt.Sprintf(" %v\n", opt.String()) + } + ret += " ]\n" + return ret +} + +// Convert a DHCPv6 structure into its binary representation, suitable for being +// sent over the network +func (d *DHCPv6) ToBytes() []byte { + var ret []byte + ret = append(ret, byte(d.message)) + tidBytes := make([]byte, 4) + binary.BigEndian.PutUint32(tidBytes, d.transactionID) + ret = append(ret, tidBytes[1:4]...) // discard the first byte + for _, opt := range d.options { + ret = append(ret, opt.ToBytes()...) + } + return ret +} diff --git a/dhcpv6/dhcpv6_test.go b/dhcpv6/dhcpv6_test.go new file mode 100644 index 0000000..084a8b4 --- /dev/null +++ b/dhcpv6/dhcpv6_test.go @@ -0,0 +1,145 @@ +package dhcpv6 + +import ( + "bytes" + "github.com/insomniacslk/dhcp/dhcpv6/options" + "testing" +) + +func TestBytesToTransactionID(t *testing.T) { + // only the first three bytes should be used + tid, err := BytesToTransactionID([]byte{0x11, 0x22, 0x33, 0xaa}) + if err != nil { + t.Fatal(err) + } + if tid == nil { + t.Fatal("Invalid Transaction ID. Should not be nil") + } + if *tid != 0x112233 { + t.Fatalf("Invalid Transaction ID. Expected 0x%x, got 0x%x", 0x112233, *tid) + } +} + +func TestBytesToTransactionIDShortData(t *testing.T) { + // short sequence, less than three bytes + tid, err := BytesToTransactionID([]byte{0x11, 0x22}) + if err == nil { + t.Fatal("Expected non-nil error, got nil instead") + } + if tid != nil { + t.Errorf("Expected nil Transaction ID, got %v instead", *tid) + } +} + +func TestGenerateTransactionID(t *testing.T) { + tid, err := GenerateTransactionID() + if err != nil { + t.Fatal(err) + } + if tid == nil { + t.Fatal("Expected non-nil Transaction ID, got nil instead") + } + if *tid > 0xffffff { + // TODO this should be better tested by mocking the random generator + t.Fatalf("Invalid Transaction ID: should be smaller than 0xffffff. Got 0x%x instead", *tid) + } +} + +func TestNew(t *testing.T) { + d, err := New() + if err != nil { + t.Fatal(err) + } + if d == nil { + t.Fatal("Expected non-nil DHCPv6, got nil instead") + } + if d.message != SOLICIT { + t.Fatalf("Invalid message type. Expected %v, got %v", SOLICIT, d.message) + } + if d.transactionID == 0 { + t.Fatal("Invalid Transaction ID, expected non-zero, got zero") + } + if len(d.options) != 0 { + t.Fatalf("Invalid options: expected none, got %v", len(d.options)) + } +} + +func TestSettersAndGetters(t *testing.T) { + d := DHCPv6{} + // Message + d.SetMessage(SOLICIT) + msg := d.Message() + if msg != SOLICIT { + t.Fatalf("Invalid Message. Expected %v, got %v", SOLICIT, msg) + } + d.SetMessage(ADVERTISE) + msg = d.Message() + if msg != ADVERTISE { + t.Fatalf("Invalid Message. Expected %v, got %v", ADVERTISE, msg) + } + // TransactionID + d.SetTransactionID(12345) + tid := d.TransactionID() + if tid != 12345 { + t.Fatalf("Invalid Transaction ID. Expected %v, got %v", 12345, tid) + } + // Options + opts := d.Options() + if len(opts) != 0 { + t.Fatalf("Invalid Options. Expected empty array, got %v", opts) + } + opt := options.OptionGeneric{OptionCode: 0, OptionData: []byte{}} + d.SetOptions([]options.Option{&opt}) + opts = d.Options() + if len(opts) != 1 { + t.Fatalf("Invalid Options. Expected one-element array, got %v", len(opts)) + } + if _, ok := opts[0].(*options.OptionGeneric); !ok { + t.Fatalf("Invalid Options. Expected one OptionGeneric, got %v", opts[0]) + } +} + +func TestAddOption(t *testing.T) { + d := DHCPv6{} + opts := d.Options() + if len(opts) != 0 { + t.Fatalf("Invalid Options. Expected empty array, got %v", opts) + } + opt := options.OptionGeneric{OptionCode: 0, OptionData: []byte{}} + d.AddOption(&opt) + opts = d.Options() + if len(opts) != 1 { + t.Fatalf("Invalid Options. Expected one-element array, got %v", len(opts)) + } + if _, ok := opts[0].(*options.OptionGeneric); !ok { + t.Fatalf("Invalid Options. Expected one OptionGeneric, got %v", opts[0]) + } +} + +func TestToBytes(t *testing.T) { + d := DHCPv6{} + d.SetMessage(SOLICIT) + d.SetTransactionID(0xabcdef) + opt := options.OptionGeneric{OptionCode: 0, OptionData: []byte{}} + d.AddOption(&opt) + toBytes := d.ToBytes() + expected := []byte{01, 0xab, 0xcd, 0xef, 0x00, 0x00, 0x00, 0x00} + if !bytes.Equal(toBytes, expected) { + t.Fatalf("Invalid ToBytes result. Expected %v, got %v", expected, toBytes) + } +} + +func TestFromAndToBytes(t *testing.T) { + expected := []byte{01, 0xab, 0xcd, 0xef, 0x00, 0x00, 0x00, 0x00} + d, err := FromBytes(expected) + if err != nil { + t.Fatal(err) + } + toBytes := d.ToBytes() + if !bytes.Equal(toBytes, expected) { + t.Fatalf("Invalid ToBytes result. Expected %v, got %v", expected, toBytes) + } +} + +// TODO test NewSolicit +// test String and Summary diff --git a/dhcpv6/iputils.go b/dhcpv6/iputils.go new file mode 100644 index 0000000..c3ac3aa --- /dev/null +++ b/dhcpv6/iputils.go @@ -0,0 +1,30 @@ +package dhcpv6 + +import ( + "fmt" + "net" +) + +func GetLinkLocalAddr(ifname string) (*net.IP, error) { + ifaces, err := net.Interfaces() + if err != nil { + return nil, err + } + for _, iface := range ifaces { + if iface.Name != ifname { + continue + } + ifaddrs, err := iface.Addrs() + if err != nil { + return nil, err + } + for _, ifaddr := range ifaddrs { + if ifaddr, ok := ifaddr.(*net.IPNet); ok { + if ifaddr.IP.To4() == nil && ifaddr.IP.IsLinkLocalUnicast() { + return &ifaddr.IP, nil + } + } + } + } + return nil, fmt.Errorf("No link-local address found for interface %v", ifname) +} diff --git a/dhcpv6/options/clientid.go b/dhcpv6/options/clientid.go new file mode 100644 index 0000000..71422d7 --- /dev/null +++ b/dhcpv6/options/clientid.go @@ -0,0 +1,57 @@ +package options + +// This module defines the OptClientId and DUID structures. +// https://www.ietf.org/rfc/rfc3315.txt + +import ( + "encoding/binary" + "fmt" +) + +type OptClientId struct { + cid Duid +} + +func (op *OptClientId) Code() OptionCode { + return OPTION_CLIENTID +} + +func (op *OptClientId) ToBytes() []byte { + buf := make([]byte, 4) + binary.BigEndian.PutUint16(buf[0:2], uint16(OPTION_CLIENTID)) + binary.BigEndian.PutUint16(buf[2:4], uint16(op.Length())) + buf = append(buf, op.cid.ToBytes()...) + return buf +} + +func (op *OptClientId) ClientID() Duid { + return op.cid +} + +func (op *OptClientId) SetClientID(cid Duid) { + op.cid = cid +} + +func (op *OptClientId) Length() int { + return op.cid.Length() +} + +func (op *OptClientId) String() string { + return fmt.Sprintf("OptClientId{cid=%v}", op.cid.String()) +} + +// build an OptClientId structure from a sequence of bytes. +// The input data does not include option code and length bytes. +func ParseOptClientId(data []byte) (*OptClientId, error) { + if len(data) < 2 { + // at least the DUID type is necessary to continue + return nil, fmt.Errorf("Invalid OptClientId data: shorter than 2 bytes") + } + opt := OptClientId{} + cid, err := DuidFromBytes(data) + if err != nil { + return nil, err + } + opt.cid = *cid + return &opt, nil +} diff --git a/dhcpv6/options/dnsrecursivenameserver.go b/dhcpv6/options/dnsrecursivenameserver.go new file mode 100644 index 0000000..a28fd1d --- /dev/null +++ b/dhcpv6/options/dnsrecursivenameserver.go @@ -0,0 +1,59 @@ +package options + +// This module defines the OptDNSRecursiveNameServer structure. +// https://www.ietf.org/rfc/rfc3646.txt + +import ( + "encoding/binary" + "fmt" + "net" +) + +type OptDNSRecursiveNameServer struct { + nameServers []net.IP +} + +func (op *OptDNSRecursiveNameServer) Code() OptionCode { + return DNS_RECURSIVE_NAME_SERVER +} + +func (op *OptDNSRecursiveNameServer) ToBytes() []byte { + buf := make([]byte, 4) + binary.BigEndian.PutUint16(buf[0:2], uint16(DNS_RECURSIVE_NAME_SERVER)) + binary.BigEndian.PutUint16(buf[2:4], uint16(op.Length())) + for _, ns := range op.nameServers { + buf = append(buf, ns...) + } + return buf +} + +func (op *OptDNSRecursiveNameServer) NameServers() []net.IP { + return op.nameServers +} + +func (op *OptDNSRecursiveNameServer) SetNameServers(ns []net.IP) { + op.nameServers = ns +} + +func (op *OptDNSRecursiveNameServer) Length() int { + return len(op.nameServers) * net.IPv6len +} + +func (op *OptDNSRecursiveNameServer) String() string { + return fmt.Sprintf("OptDNSRecursiveNameServer{nameservers=%v}", op.nameServers) +} + +// build an OptDNSRecursiveNameServer structure from a sequence of bytes. +// The input data does not include option code and length bytes. +func ParseOptDNSRecursiveNameServer(data []byte) (*OptDNSRecursiveNameServer, error) { + if len(data)%2 != 0 { + return nil, fmt.Errorf("Invalid OptDNSRecursiveNameServer data: length is not a multiple of 2") + } + opt := OptDNSRecursiveNameServer{} + var nameServers []net.IP + for i := 0; i < len(data); i += net.IPv6len { + nameServers = append(nameServers, data[i:i+net.IPv6len]) + } + opt.nameServers = nameServers + return &opt, nil +} diff --git a/dhcpv6/options/domainsearchlist.go b/dhcpv6/options/domainsearchlist.go new file mode 100644 index 0000000..048384f --- /dev/null +++ b/dhcpv6/options/domainsearchlist.go @@ -0,0 +1,57 @@ +package options + +// This module defines the OptDomainSearchList structure. +// https://www.ietf.org/rfc/rfc3646.txt + +import ( + "encoding/binary" + "fmt" +) + +type OptDomainSearchList struct { + domainSearchList []string +} + +func (op *OptDomainSearchList) Code() OptionCode { + return DOMAIN_SEARCH_LIST +} + +func (op *OptDomainSearchList) ToBytes() []byte { + buf := make([]byte, 4) + binary.BigEndian.PutUint16(buf[0:2], uint16(DOMAIN_SEARCH_LIST)) + binary.BigEndian.PutUint16(buf[2:4], uint16(op.Length())) + buf = append(buf, LabelsToBytes(op.domainSearchList)...) + return buf +} + +func (op *OptDomainSearchList) DomainSearchList() []string { + return op.domainSearchList +} + +func (op *OptDomainSearchList) SetDomainSearchList(dsList []string) { + op.domainSearchList = dsList +} + +func (op *OptDomainSearchList) Length() int { + var length int + for _, label := range op.domainSearchList { + length += len(label) + 2 // add the first and the last length bytes + } + return length +} + +func (op *OptDomainSearchList) String() string { + return fmt.Sprintf("OptDomainSearchList{searchlist=%v}", op.domainSearchList) +} + +// build an OptDomainSearchList structure from a sequence of bytes. +// The input data does not include option code and length bytes. +func ParseOptDomainSearchList(data []byte) (*OptDomainSearchList, error) { + opt := OptDomainSearchList{} + var err error + opt.domainSearchList, err = LabelsFromBytes(data) + if err != nil { + return nil, err + } + return &opt, nil +} diff --git a/dhcpv6/options/duid.go b/dhcpv6/options/duid.go new file mode 100644 index 0000000..0fc3e33 --- /dev/null +++ b/dhcpv6/options/duid.go @@ -0,0 +1,100 @@ +package options + +import ( + "encoding/binary" + "fmt" + "github.com/insomniacslk/dhcp/iana" +) + +type DuidType uint16 + +const ( + DUID_LL DuidType = iota + DUID_LLT + DUID_EN +) + +var DuidTypeToString = map[DuidType]string{ + DUID_LL: "DUID-LL", + DUID_LLT: "DUID-LLT", + DUID_EN: "DUID-EN", +} + +type Duid struct { + Type DuidType + HwType iana.HwTypeType // for DUID-LLT and DUID-LL. Ignored otherwise. RFC 826 + Time uint32 // for DUID-LLT. Ignored otherwise + LinkLayerAddr []byte + EnterpriseNumber uint32 // for DUID-EN. Ignored otherwise + EnterpriseIdentifier []byte // for DUID-EN. Ignored otherwise +} + +func (d *Duid) Length() int { + if d.Type == DUID_LLT || d.Type == DUID_LL { + return 8 + len(d.LinkLayerAddr) + } + if d.Type == DUID_EN { + return 6 + len(d.EnterpriseIdentifier) + } + return 0 // should never happen +} + +func (d *Duid) ToBytes() []byte { + if d.Type == DUID_LLT || d.Type == DUID_LL { + buf := make([]byte, 8) + binary.BigEndian.PutUint16(buf[0:2], uint16(d.Type)) + binary.BigEndian.PutUint16(buf[2:4], uint16(d.HwType)) + binary.BigEndian.PutUint32(buf[4:8], d.Time) + return append(buf, d.LinkLayerAddr...) + } else if d.Type == DUID_EN { + buf := make([]byte, 6) + binary.BigEndian.PutUint16(buf[0:2], uint16(d.Type)) + binary.BigEndian.PutUint32(buf[2:6], d.EnterpriseNumber) + return append(buf, d.EnterpriseIdentifier...) + } + return []byte{} // should never happen +} + +func (d *Duid) String() string { + dtype := DuidTypeToString[d.Type] + if dtype == "" { + dtype = "Unknown" + } + hwtype := iana.HwTypeToString[d.HwType] + if hwtype == "" { + hwtype = "Unknown" + } + var hwaddr string + if d.HwType == iana.HwTypeEthernet { + for _, b := range d.LinkLayerAddr { + hwaddr += fmt.Sprintf("%02x:", b) + } + if len(hwaddr) > 0 && hwaddr[len(hwaddr)-1] == ':' { + hwaddr = hwaddr[:len(hwaddr)-1] + } + } + return fmt.Sprintf("DUID{type=%v hwtype=%v hwaddr=%v}", dtype, hwtype, hwaddr) +} + +func DuidFromBytes(data []byte) (*Duid, error) { + if len(data) < 2 { + return nil, fmt.Errorf("Invalid DUID: shorter than 2 bytes") + } + d := Duid{} + d.Type = DuidType(binary.BigEndian.Uint16(data[0:2])) + if d.Type == DUID_LLT || d.Type == DUID_LL { + if len(data) < 8 { + return nil, fmt.Errorf("Invalid DUID-LL/LLT: shorter than 8 bytes") + } + d.HwType = iana.HwTypeType(binary.BigEndian.Uint16(data[2:4])) + d.Time = binary.BigEndian.Uint32(data[4:8]) + d.LinkLayerAddr = data[8:] + } else if d.Type == DUID_EN { + if len(data) < 6 { + return nil, fmt.Errorf("Invalid DUID-EN: shorter than 6 bytes") + } + d.EnterpriseNumber = binary.BigEndian.Uint32(data[2:6]) + d.EnterpriseIdentifier = data[6:] + } + return &d, nil +} diff --git a/dhcpv6/options/elapsedtime.go b/dhcpv6/options/elapsedtime.go new file mode 100644 index 0000000..e95185d --- /dev/null +++ b/dhcpv6/options/elapsedtime.go @@ -0,0 +1,52 @@ +package options + +// This module defines the OptElapsedTime structure. +// https://www.ietf.org/rfc/rfc3315.txt + +import ( + "encoding/binary" + "fmt" +) + +type OptElapsedTime struct { + elapsedTime uint16 +} + +func (op *OptElapsedTime) Code() OptionCode { + return OPTION_ELAPSED_TIME +} + +func (op *OptElapsedTime) ToBytes() []byte { + buf := make([]byte, 6) + binary.BigEndian.PutUint16(buf[0:2], uint16(OPTION_ELAPSED_TIME)) + binary.BigEndian.PutUint16(buf[2:4], 2) + binary.BigEndian.PutUint16(buf[4:6], uint16(op.elapsedTime)) + return buf +} + +func (op *OptElapsedTime) ElapsedTime() uint16 { + return op.elapsedTime +} + +func (op *OptElapsedTime) SetElapsedTime(elapsedTime uint16) { + op.elapsedTime = elapsedTime +} + +func (op *OptElapsedTime) Length() int { + return 2 +} + +func (op *OptElapsedTime) String() string { + return fmt.Sprintf("OptElapsedTime{elapsedtime=%v}", op.elapsedTime) +} + +// build an OptElapsedTime structure from a sequence of bytes. +// The input data does not include option code and length bytes. +func ParseOptElapsedTime(data []byte) (*OptElapsedTime, error) { + opt := OptElapsedTime{} + if len(data) != 2 { + return nil, fmt.Errorf("Invalid elapsed time data length. Expected 2 bytes, got %v", len(data)) + } + opt.elapsedTime = binary.BigEndian.Uint16(data) + return &opt, nil +} diff --git a/dhcpv6/options/elapsedtime_test.go b/dhcpv6/options/elapsedtime_test.go new file mode 100644 index 0000000..402d62b --- /dev/null +++ b/dhcpv6/options/elapsedtime_test.go @@ -0,0 +1,44 @@ +package options + +import ( + "bytes" + "testing" +) + +func TestOptElapsedTime(t *testing.T) { + opt, err := ParseOptElapsedTime([]byte{0xaa, 0xbb}) + if err != nil { + t.Fatal(err) + } + if optLen := opt.Length(); optLen != 2 { + t.Fatalf("Invalid length. Expected 2, got %v", optLen) + } + if elapsedTime := opt.ElapsedTime(); elapsedTime != 0xaabb { + t.Fatalf("Invalid elapsed time. Expected 0xaabb, got %v", elapsedTime) + } +} + +func TestOptElapsedTimeToBytes(t *testing.T) { + opt := OptElapsedTime{} + expected := []byte{0, 8, 0, 2, 0, 0} + if toBytes := opt.ToBytes(); !bytes.Equal(expected, toBytes) { + t.Fatalf("Invalid ToBytes output. Expected %v, got %v", expected, toBytes) + } +} + +func TestOptElapsedTimeSetGetElapsedTime(t *testing.T) { + opt := OptElapsedTime{} + opt.SetElapsedTime(10) + if elapsedTime := opt.ElapsedTime(); elapsedTime != 10 { + t.Fatalf("Invalid elapsed time. Expected 10, got %v", elapsedTime) + } +} + +func TestOptElapsedTimeString(t *testing.T) { + opt := OptElapsedTime{} + opt.SetElapsedTime(10) + expected := "OptElapsedTime{elapsedtime=10}" + if optString := opt.String(); optString != expected { + t.Fatalf("Invalid elapsed time string. Expected %v, got %v", expected, optString) + } +} diff --git a/dhcpv6/options/iaaddress.go b/dhcpv6/options/iaaddress.go new file mode 100644 index 0000000..24f28ad --- /dev/null +++ b/dhcpv6/options/iaaddress.go @@ -0,0 +1,86 @@ +package options + +// This module defines the OptIAAddress structure. +// https://www.ietf.org/rfc/rfc3633.txt + +import ( + "encoding/binary" + "fmt" + "net" +) + +type OptIAAddress struct { + ipv6Addr [16]byte + preferredLifetime uint32 + validLifetime uint32 + options []byte +} + +func (op *OptIAAddress) Code() OptionCode { + return OPTION_IAADDR +} + +func (op *OptIAAddress) ToBytes() []byte { + buf := make([]byte, 28) + binary.BigEndian.PutUint16(buf[0:2], uint16(OPTION_IAADDR)) + binary.BigEndian.PutUint16(buf[2:4], uint16(op.Length())) + copy(buf[4:20], op.ipv6Addr[:]) + binary.BigEndian.PutUint32(buf[20:24], op.preferredLifetime) + binary.BigEndian.PutUint32(buf[24:28], op.validLifetime) + buf = append(buf, op.options...) + return buf +} + +func (op *OptIAAddress) IPv6Addr() []byte { + return op.ipv6Addr[:] +} + +func (op *OptIAAddress) SetIPv6Addr(addr [16]byte) { + op.ipv6Addr = addr +} + +func (op *OptIAAddress) PreferredLifetime() uint32 { + return op.preferredLifetime +} + +func (op *OptIAAddress) SetPreferredLifetime(pl uint32) { + op.preferredLifetime = pl +} + +func (op *OptIAAddress) ValidLifetime() uint32 { + return op.validLifetime +} + +func (op *OptIAAddress) SetValidLifetime(vl uint32) { + op.validLifetime = vl +} +func (op *OptIAAddress) Options() []byte { + return op.options +} + +func (op *OptIAAddress) SetOptions(options []byte) { + op.options = options +} + +func (op *OptIAAddress) Length() int { + return 24 + len(op.options) +} + +func (op *OptIAAddress) String() string { + return fmt.Sprintf("OptIAAddress{ipv6addr=%v, preferredlifetime=%v, validlifetime=%v, options=%v}", + net.IP(op.ipv6Addr[:]), op.preferredLifetime, op.validLifetime, op.options) +} + +// build an OptIAAddress structure from a sequence of bytes. +// The input data does not include option code and length bytes. +func ParseOptIAAddress(data []byte) (*OptIAAddress, error) { + opt := OptIAAddress{} + if len(data) < 24 { + return nil, fmt.Errorf("Invalid IA Address data length. Expected at least 24 bytes, got %v", len(data)) + } + copy(opt.ipv6Addr[:], data[:16]) + opt.preferredLifetime = binary.BigEndian.Uint32(data[16:20]) + opt.validLifetime = binary.BigEndian.Uint32(data[20:24]) + copy(opt.options, data[24:]) + return &opt, nil +} diff --git a/dhcpv6/options/iaprefix.go b/dhcpv6/options/iaprefix.go new file mode 100644 index 0000000..d91df2a --- /dev/null +++ b/dhcpv6/options/iaprefix.go @@ -0,0 +1,98 @@ +package options + +// This module defines the OptIAPrefix structure. +// https://www.ietf.org/rfc/rfc3633.txt + +import ( + "encoding/binary" + "fmt" + "net" +) + +type OptIAPrefix struct { + preferredLifetime uint32 + validLifetime uint32 + prefixLength byte + ipv6Prefix [16]byte + options []byte +} + +func (op *OptIAPrefix) Code() OptionCode { + return OPTION_IAPREFIX +} + +func (op *OptIAPrefix) ToBytes() []byte { + buf := make([]byte, 25) + binary.BigEndian.PutUint16(buf[0:2], uint16(OPTION_IAPREFIX)) + binary.BigEndian.PutUint16(buf[2:4], uint16(op.Length())) + binary.BigEndian.PutUint32(buf[4:8], op.preferredLifetime) + binary.BigEndian.PutUint32(buf[8:12], op.validLifetime) + buf = append(buf, op.prefixLength) + buf = append(buf, op.ipv6Prefix[:]...) + buf = append(buf, op.options...) + return buf +} + +func (op *OptIAPrefix) PreferredLifetime() uint32 { + return op.preferredLifetime +} + +func (op *OptIAPrefix) SetPreferredLifetime(pl uint32) { + op.preferredLifetime = pl +} + +func (op *OptIAPrefix) ValidLifetime() uint32 { + return op.validLifetime +} + +func (op *OptIAPrefix) SetValidLifetime(vl uint32) { + op.validLifetime = vl +} + +func (op *OptIAPrefix) PrefixLength() byte { + return op.prefixLength +} + +func (op *OptIAPrefix) SetPrefixLength(pl byte) { + op.prefixLength = pl +} + +func (op *OptIAPrefix) IPv6Prefix() []byte { + return op.ipv6Prefix[:] +} + +func (op *OptIAPrefix) SetIPv6Prefix(p [16]byte) { + op.ipv6Prefix = p +} + +func (op *OptIAPrefix) Options() []byte { + return op.options +} + +func (op *OptIAPrefix) SetOptions(options []byte) { + op.options = options +} + +func (op *OptIAPrefix) Length() int { + return 25 + len(op.options) +} + +func (op *OptIAPrefix) String() string { + return fmt.Sprintf("OptIAPrefix{preferredlifetime=%v, validlifetime=%v, prefixlength=%v, ipv6prefix=%v, options=%v}", + op.preferredLifetime, op.validLifetime, op.prefixLength, net.IP(op.ipv6Prefix[:]), op.options) +} + +// build an OptIAPrefix structure from a sequence of bytes. +// The input data does not include option code and length bytes. +func ParseOptIAPrefix(data []byte) (*OptIAPrefix, error) { + opt := OptIAPrefix{} + if len(data) < 12 { + return nil, fmt.Errorf("Invalid IA for Prefix Delegation data length. Expected at least 12 bytes, got %v", len(data)) + } + opt.preferredLifetime = binary.BigEndian.Uint32(data[:4]) + opt.validLifetime = binary.BigEndian.Uint32(data[4:8]) + opt.prefixLength = data[9] + copy(opt.ipv6Prefix[:], data[9:17]) + copy(opt.options, data[17:]) + return &opt, nil +} diff --git a/dhcpv6/options/nontemporaryaddress.go b/dhcpv6/options/nontemporaryaddress.go new file mode 100644 index 0000000..703b8cc --- /dev/null +++ b/dhcpv6/options/nontemporaryaddress.go @@ -0,0 +1,86 @@ +package options + +// This module defines the OptIANA structure. +// https://www.ietf.org/rfc/rfc3633.txt + +import ( + "encoding/binary" + "fmt" +) + +type OptIANA struct { + iaId [4]byte + t1 uint32 + t2 uint32 + options []byte +} + +func (op *OptIANA) Code() OptionCode { + return OPTION_IA_NA +} + +func (op *OptIANA) ToBytes() []byte { + buf := make([]byte, 16) + binary.BigEndian.PutUint16(buf[0:2], uint16(OPTION_IA_NA)) + binary.BigEndian.PutUint16(buf[2:4], uint16(op.Length())) + copy(buf[4:8], op.iaId[:]) + binary.BigEndian.PutUint32(buf[8:12], op.t1) + binary.BigEndian.PutUint32(buf[12:16], op.t2) + buf = append(buf, op.options...) + return buf +} + +func (op *OptIANA) IAID() []byte { + return op.iaId[:] +} + +func (op *OptIANA) SetIAID(iaId [4]byte) { + op.iaId = iaId +} + +func (op *OptIANA) T1() uint32 { + return op.t1 +} + +func (op *OptIANA) SetT1(t1 uint32) { + op.t1 = t1 +} + +func (op *OptIANA) T2() uint32 { + return op.t2 +} + +func (op *OptIANA) SetT2(t2 uint32) { + op.t2 = t2 +} + +func (op *OptIANA) Options() []byte { + return op.options +} + +func (op *OptIANA) SetOptions(options []byte) { + op.options = options +} + +func (op *OptIANA) Length() int { + return 12 + len(op.options) +} + +func (op *OptIANA) String() string { + return fmt.Sprintf("OptIANA{IAID=%v, t1=%v, t2=%v, options=%v}", + op.iaId, op.t1, op.t2, op.options) +} + +// build an OptIANA structure from a sequence of bytes. +// The input data does not include option code and length bytes. +func ParseOptIANA(data []byte) (*OptIANA, error) { + opt := OptIANA{} + if len(data) < 12 { + return nil, fmt.Errorf("Invalid IA for Non-temporary Addresses data length. Expected at least 12 bytes, got %v", len(data)) + } + copy(opt.iaId[:], data[:4]) + opt.t1 = binary.BigEndian.Uint32(data[4:8]) + opt.t2 = binary.BigEndian.Uint32(data[8:12]) + copy(opt.options, data[12:]) + return &opt, nil +} diff --git a/dhcpv6/options/options.go b/dhcpv6/options/options.go new file mode 100644 index 0000000..6b587dd --- /dev/null +++ b/dhcpv6/options/options.go @@ -0,0 +1,125 @@ +package options + +import ( + "encoding/binary" + "fmt" +) + +type OptionCode uint16 + +type Option interface { + Code() OptionCode + ToBytes() []byte + Length() int + String() string +} + +type OptionGeneric struct { + OptionCode OptionCode + OptionData []byte +} + +func (og *OptionGeneric) Code() OptionCode { + return og.OptionCode +} + +func (og *OptionGeneric) ToBytes() []byte { + var ret []byte + codeBytes := make([]byte, 2) + binary.BigEndian.PutUint16(codeBytes, uint16(og.OptionCode)) + ret = append(ret, codeBytes...) + lengthBytes := make([]byte, 2) + binary.BigEndian.PutUint16(lengthBytes, uint16(len(og.OptionData))) + ret = append(ret, lengthBytes...) + ret = append(ret, og.OptionData...) + return ret +} + +func (og *OptionGeneric) String() string { + code, ok := OptionCodeToString[og.OptionCode] + if !ok { + code = "UnknownOption" + } + return fmt.Sprintf("%v -> %v", code, og.OptionData) +} + +func (og *OptionGeneric) Length() int { + return len(og.OptionData) +} + +func ParseOption(dataStart []byte) (Option, error) { + // Parse a sequence of bytes as a single DHCPv6 option. + // Returns the option structure, or an error if any. + if len(dataStart) < 4 { + return nil, fmt.Errorf("Invalid DHCPv6 option: less than 4 bytes") + } + code := OptionCode(binary.BigEndian.Uint16(dataStart[:2])) + length := int(binary.BigEndian.Uint16(dataStart[2:4])) + if len(dataStart) < length+4 { + return nil, fmt.Errorf("Invalid option length. Declared %v, actual %v", + length, len(dataStart)-4, + ) + } + var ( + err error + opt Option + ) + optData := dataStart[4 : 4+length] + switch code { + case OPTION_CLIENTID: + opt, err = ParseOptClientId(optData) + case OPTION_SERVERID: + opt, err = ParseOptServerId(optData) + case OPTION_ELAPSED_TIME: + opt, err = ParseOptElapsedTime(optData) + case OPTION_ORO: + opt, err = ParseOptRequestedOption(optData) + case DNS_RECURSIVE_NAME_SERVER: + opt, err = ParseOptDNSRecursiveNameServer(optData) + case DOMAIN_SEARCH_LIST: + opt, err = ParseOptDomainSearchList(optData) + case OPTION_IA_NA: + opt, err = ParseOptIANA(optData) + case OPTION_IA_PD: + opt, err = ParseOptIAForPrefixDelegation(optData) + case OPTION_IAADDR: + opt, err = ParseOptIAAddress(optData) + case OPTION_IAPREFIX: + opt, err = ParseOptIAPrefix(optData) + case OPTION_STATUS_CODE: + opt, err = ParseOptStatusCode(optData) + default: + opt = &OptionGeneric{OptionCode: code, OptionData: optData} + } + if err != nil { + return nil, err + } + return opt, nil +} + +func FromBytes(data []byte) ([]Option, error) { + // Parse a sequence of bytes until the end and build a list of options from + // it. Returns an error if any invalid option or length is found. + if len(data) < 4 { + // cannot be shorter than option code (2 bytes) + length (2 bytes) + return nil, fmt.Errorf("Invalid options: shorter than 4 bytes") + } + options := make([]Option, 0, 10) + idx := 0 + for { + if idx == len(data) { + break + } + if idx > len(data) { + // this should never happen + return nil, fmt.Errorf("Error: reading past the end of options") + } + opt, err := ParseOption(data[idx:]) + if err != nil { + return nil, err + } + options = append(options, opt) + idx += opt.Length() + 4 // 4 bytes for type + length + } + return options, nil +} diff --git a/dhcpv6/options/prefixdelegation.go b/dhcpv6/options/prefixdelegation.go new file mode 100644 index 0000000..1c02433 --- /dev/null +++ b/dhcpv6/options/prefixdelegation.go @@ -0,0 +1,86 @@ +package options + +// This module defines the OptIAForPrefixDelegation structure. +// https://www.ietf.org/rfc/rfc3633.txt + +import ( + "encoding/binary" + "fmt" +) + +type OptIAForPrefixDelegation struct { + iaId [4]byte + t1 uint32 + t2 uint32 + options []byte +} + +func (op *OptIAForPrefixDelegation) Code() OptionCode { + return OPTION_IA_PD +} + +func (op *OptIAForPrefixDelegation) ToBytes() []byte { + buf := make([]byte, 16) + binary.BigEndian.PutUint16(buf[0:2], uint16(OPTION_IA_PD)) + binary.BigEndian.PutUint16(buf[2:4], uint16(op.Length())) + copy(buf[4:8], op.iaId[:]) + binary.BigEndian.PutUint32(buf[8:12], op.t1) + binary.BigEndian.PutUint32(buf[12:16], op.t2) + buf = append(buf, op.options...) + return buf +} + +func (op *OptIAForPrefixDelegation) IAID() []byte { + return op.iaId[:] +} + +func (op *OptIAForPrefixDelegation) SetIAID(iaId [4]byte) { + op.iaId = iaId +} + +func (op *OptIAForPrefixDelegation) T1() uint32 { + return op.t1 +} + +func (op *OptIAForPrefixDelegation) SetT1(t1 uint32) { + op.t1 = t1 +} + +func (op *OptIAForPrefixDelegation) T2() uint32 { + return op.t2 +} + +func (op *OptIAForPrefixDelegation) SetT2(t2 uint32) { + op.t2 = t2 +} + +func (op *OptIAForPrefixDelegation) Options() []byte { + return op.options +} + +func (op *OptIAForPrefixDelegation) SetOptions(options []byte) { + op.options = options +} + +func (op *OptIAForPrefixDelegation) Length() int { + return 12 + len(op.options) +} + +func (op *OptIAForPrefixDelegation) String() string { + return fmt.Sprintf("OptIAForPrefixDelegation{IAID=%v, t1=%v, t2=%v, options=%v}", + op.iaId, op.t1, op.t2, op.options) +} + +// build an OptIAForPrefixDelegation structure from a sequence of bytes. +// The input data does not include option code and length bytes. +func ParseOptIAForPrefixDelegation(data []byte) (*OptIAForPrefixDelegation, error) { + opt := OptIAForPrefixDelegation{} + if len(data) < 12 { + return nil, fmt.Errorf("Invalid IA for Prefix Delegation data length. Expected at least 12 bytes, got %v", len(data)) + } + copy(opt.iaId[:], data[:4]) + opt.t1 = binary.BigEndian.Uint32(data[4:8]) + opt.t2 = binary.BigEndian.Uint32(data[8:12]) + opt.options = append(opt.options, data[12:]...) + return &opt, nil +} diff --git a/dhcpv6/options/requestedoption.go b/dhcpv6/options/requestedoption.go new file mode 100644 index 0000000..88e9ff3 --- /dev/null +++ b/dhcpv6/options/requestedoption.go @@ -0,0 +1,72 @@ +package options + +// This module defines the OptRequestedOption structure. +// https://www.ietf.org/rfc/rfc3315.txt + +import ( + "encoding/binary" + "fmt" +) + +type OptRequestedOption struct { + requestedOptions []OptionCode +} + +func (op *OptRequestedOption) Code() OptionCode { + return OPTION_ORO +} + +func (op *OptRequestedOption) ToBytes() []byte { + buf := make([]byte, 4) + roBytes := make([]byte, 2) + binary.BigEndian.PutUint16(buf[0:2], uint16(OPTION_ORO)) + binary.BigEndian.PutUint16(buf[2:4], uint16(op.Length())) + for _, ro := range op.requestedOptions { + binary.BigEndian.PutUint16(roBytes, uint16(ro)) + buf = append(buf, roBytes...) + } + return buf +} + +func (op *OptRequestedOption) RequestedOptions() []OptionCode { + return op.requestedOptions +} + +func (op *OptRequestedOption) SetRequestedOptions(opts []OptionCode) { + op.requestedOptions = opts +} + +func (op *OptRequestedOption) Length() int { + return len(op.requestedOptions) * 2 +} + +func (op *OptRequestedOption) String() string { + roString := "[" + for idx, code := range op.requestedOptions { + if name, ok := OptionCodeToString[OptionCode(code)]; ok { + roString += name + } else { + roString += "Unknown" + } + if idx < len(op.requestedOptions)-1 { + roString += ", " + } + } + roString += "]" + return fmt.Sprintf("OptRequestedOption{options=%v}", roString) +} + +// build an OptRequestedOption structure from a sequence of bytes. +// The input data does not include option code and length bytes. +func ParseOptRequestedOption(data []byte) (*OptRequestedOption, error) { + if len(data)%2 != 0 { + return nil, fmt.Errorf("Invalid OptRequestedOption data: length is not a multiple of 2") + } + opt := OptRequestedOption{} + var rOpts []OptionCode + for i := 0; i < len(data); i += 2 { + rOpts = append(rOpts, OptionCode(binary.BigEndian.Uint16(data[i:i+2]))) + } + opt.requestedOptions = rOpts + return &opt, nil +} diff --git a/dhcpv6/options/rfc1035label.go b/dhcpv6/options/rfc1035label.go new file mode 100644 index 0000000..0d2cff3 --- /dev/null +++ b/dhcpv6/options/rfc1035label.go @@ -0,0 +1,54 @@ +package options + +import ( + "fmt" + "strings" +) + +func LabelsFromBytes(buf []byte) ([]string, error) { + var ( + pos = 0 + domains = make([]string, 0) + label = "" + ) + for { + if pos >= len(buf) { + return domains, nil + } + length := int(buf[pos]) + pos++ + if length == 0 { + domains = append(domains, label) + label = "" + } + if len(buf)-pos < length { + return nil, fmt.Errorf("DomainNamesFromBytes: invalid short label length") + } + if label != "" { + label += "." + } + label += string(buf[pos : pos+length]) + pos += length + } + return domains, nil +} + +func LabelToBytes(label string) []byte { + var encodedLabel []byte + if len(label) == 0 { + return []byte{0} + } + for _, part := range strings.Split(label, ".") { + encodedLabel = append(encodedLabel, byte(len(part))) + encodedLabel = append(encodedLabel, []byte(part)...) + } + return append(encodedLabel, 0) +} + +func LabelsToBytes(labels []string) []byte { + var encodedLabels []byte + for _, label := range labels { + encodedLabels = append(encodedLabels, LabelToBytes(label)...) + } + return encodedLabels +} diff --git a/dhcpv6/options/rfc1035label_test.go b/dhcpv6/options/rfc1035label_test.go new file mode 100644 index 0000000..4e1debb --- /dev/null +++ b/dhcpv6/options/rfc1035label_test.go @@ -0,0 +1,66 @@ +package options + +import ( + "bytes" + "testing" +) + +func TestLabelsFromBytes(t *testing.T) { + labels, err := LabelsFromBytes([]byte{ + 0x9, 's', 'l', 'a', 'c', 'k', 'w', 'a', 'r', 'e', + 0x2, 'i', 't', + 0x0, + }) + if err != nil { + t.Fatal(err) + } + if len(labels) != 1 { + t.Fatalf("Invalid labels length. Expected: 1, got: %v", len(labels)) + } + if labels[0] != "slackware.it" { + t.Fatalf("Invalid label. Expected: %v, got: %v'", "slackware.it", labels[0]) + } +} + +func TestLabelsFromBytesZeroLength(t *testing.T) { + labels, err := LabelsFromBytes([]byte{}) + if err != nil { + t.Fatal(err) + } + if len(labels) != 0 { + t.Fatalf("Invalid labels length. Expected: 0, got: %v", len(labels)) + } +} + +func TestLabelsFromBytesInvalidLength(t *testing.T) { + labels, err := LabelsFromBytes([]byte{0x3, 0xaa, 0xbb}) // short length + if err == nil { + t.Fatal("Expected error, got nil") + } + if len(labels) != 0 { + t.Fatalf("Invalid labels length. Expected: 0, got: %v", len(labels)) + } + if labels != nil { + t.Fatalf("Invalid label. Expected nil, got %v", labels) + } +} + +func TestLabelToBytes(t *testing.T) { + encodedLabel := LabelToBytes("slackware.it") + expected := []byte{ + 0x9, 's', 'l', 'a', 'c', 'k', 'w', 'a', 'r', 'e', + 0x2, 'i', 't', + 0x0, + } + if !bytes.Equal(encodedLabel, expected) { + t.Fatalf("Invalid label. Expected: %v, got: %v", expected, encodedLabel) + } +} + +func TestLabelToBytesZeroLength(t *testing.T) { + encodedLabel := LabelToBytes("") + expected := []byte{0} + if !bytes.Equal(encodedLabel, expected) { + t.Fatalf("Invalid label. Expected: %v, got: %v", expected, encodedLabel) + } +} diff --git a/dhcpv6/options/serverid.go b/dhcpv6/options/serverid.go new file mode 100644 index 0000000..a66f4b4 --- /dev/null +++ b/dhcpv6/options/serverid.go @@ -0,0 +1,57 @@ +package options + +// This module defines the OptServerId and DUID structures. +// https://www.ietf.org/rfc/rfc3315.txt + +import ( + "encoding/binary" + "fmt" +) + +type OptServerId struct { + sid Duid +} + +func (op *OptServerId) Code() OptionCode { + return OPTION_SERVERID +} + +func (op *OptServerId) ToBytes() []byte { + buf := make([]byte, 4) + binary.BigEndian.PutUint16(buf[0:2], uint16(OPTION_SERVERID)) + binary.BigEndian.PutUint16(buf[2:4], uint16(op.Length())) + buf = append(buf, op.sid.ToBytes()...) + return buf +} + +func (op *OptServerId) ServerID() Duid { + return op.sid +} + +func (op *OptServerId) SetServerID(sid Duid) { + op.sid = sid +} + +func (op *OptServerId) Length() int { + return op.sid.Length() +} + +func (op *OptServerId) String() string { + return fmt.Sprintf("OptServerId{sid=%v}", op.sid.String()) +} + +// build an OptServerId structure from a sequence of bytes. +// The input data does not include option code and length bytes. +func ParseOptServerId(data []byte) (*OptServerId, error) { + if len(data) < 2 { + // at least the DUID type is necessary to continue + return nil, fmt.Errorf("Invalid OptServerId data: shorter than 2 bytes") + } + opt := OptServerId{} + sid, err := DuidFromBytes(data) + if err != nil { + return nil, err + } + opt.sid = *sid + return &opt, nil +} diff --git a/dhcpv6/options/statuscode.go b/dhcpv6/options/statuscode.go new file mode 100644 index 0000000..60f6f71 --- /dev/null +++ b/dhcpv6/options/statuscode.go @@ -0,0 +1,63 @@ +package options + +// This module defines the OptStatusCode structure. +// https://www.ietf.org/rfc/rfc3315.txt + +import ( + "encoding/binary" + "fmt" +) + +type OptStatusCode struct { + statusCode uint16 + statusMessage []byte +} + +func (op *OptStatusCode) Code() OptionCode { + return OPTION_STATUS_CODE +} + +func (op *OptStatusCode) ToBytes() []byte { + buf := make([]byte, 6) + binary.BigEndian.PutUint16(buf[0:2], uint16(OPTION_STATUS_CODE)) + binary.BigEndian.PutUint16(buf[2:4], uint16(op.Length())) + binary.BigEndian.PutUint16(buf[4:6], op.statusCode) + buf = append(buf, op.statusMessage...) + return buf +} + +func (op *OptStatusCode) StatusCode() uint16 { + return op.statusCode +} + +func (op *OptStatusCode) SetStatusCode(code uint16) { + op.statusCode = code +} + +func (op *OptStatusCode) StatusMessage() uint16 { + return op.statusCode +} + +func (op *OptStatusCode) SetStatusMessage(message []byte) { + op.statusMessage = message +} + +func (op *OptStatusCode) Length() int { + return 2 + len(op.statusMessage) +} + +func (op *OptStatusCode) String() string { + return fmt.Sprintf("OptStatusCode{code=%v, message=%v}", op.statusCode, string(op.statusMessage)) +} + +// build an OptStatusCode structure from a sequence of bytes. +// The input data does not include option code and length bytes. +func ParseOptStatusCode(data []byte) (*OptStatusCode, error) { + if len(data) < 2 { + return nil, fmt.Errorf("Invalid OptStatusCode data: length is shorter than 2") + } + opt := OptStatusCode{} + opt.statusCode = binary.BigEndian.Uint16(data[0:2]) + opt.statusMessage = append(opt.statusMessage, data[2:]...) + return &opt, nil +} diff --git a/dhcpv6/options/types.go b/dhcpv6/options/types.go new file mode 100644 index 0000000..48f1003 --- /dev/null +++ b/dhcpv6/options/types.go @@ -0,0 +1,152 @@ +package options + +const ( + _ OptionCode = iota // skip 0 + OPTION_CLIENTID + OPTION_SERVERID + OPTION_IA_NA + OPTION_IA_TA + OPTION_IAADDR + OPTION_ORO + OPTION_PREFERENCE + OPTION_ELAPSED_TIME + OPTION_RELAY_MSG + _ // skip 10 + OPTION_AUTH + OPTION_UNICAST + OPTION_STATUS_CODE + OPTION_RAPID_COMMIT + OPTION_USER_CLASS + OPTION_VENDOR_CLASS + OPTION_VENDOR_OPTS + OPTION_INTERFACE_ID + OPTION_RECONF_MSG + OPTION_RECONF_ACCEPT + SIP_SERVERS_DOMAIN_NAME_LIST + SIP_SERVERS_IPV6_ADDRESS_LIST + DNS_RECURSIVE_NAME_SERVER + DOMAIN_SEARCH_LIST + OPTION_IA_PD + OPTION_IAPREFIX + OPTION_NIS_SERVERS + OPTION_NISP_SERVERS + OPTION_NIS_DOMAIN_NAME + OPTION_NISP_DOMAIN_NAME + SNTP_SERVER_LIST + INFORMATION_REFRESH_TIME + BCMCS_CONTROLLER_DOMAIN_NAME_LIST + BCMCS_CONTROLLER_IPV6_ADDRESS_LIST + _ // skip 35 + OPTION_GEOCONF_CIVIC + OPTION_REMOTE_ID + RELAY_AGENT_SUBSCRIBER_ID + FQDN + PANA_AUTHENTICATION_AGENT + OPTION_NEW_POSIX_TIMEZONE + OPTION_NEW_TZDB_TIMEZONE + ECHO_REQUEST + OPTION_LQ_QUERY + OPTION_CLIENT_DATA + OPTION_CLT_TIME + OPTION_LQ_RELAY_DATA + OPTION_LQ_CLIENT_LINK + MIPV6_HOME_NETWORK_ID_FQDN + MIPV6_VISITED_HOME_NETWORK_INFORMATION + LOST_SERVER + CAPWAP_ACCESS_CONTROLLER_ADDRESSES + RELAY_ID + OPTION_IPV6_ADDRESS_MOS + OPTION_IPV6_FQDN_MOS + OPTION_NTP_SERVER + OPTION_V6_ACCESS_DOMAIN + OPTION_SIP_UA_CS_LIST + OPT_BOOTFILE_URL + OPT_BOOTFILE_PARAM + OPTION_CLIENT_ARCH_TYPE + OPTION_NII + OPTION_GEOLOCATION + OPTION_AFTR_NAME + OPTION_ERP_LOCAL_DOMAIN_NAME + OPTION_RSOO + OPTION_PD_EXCLUDE + VIRTUAL_SUBNET_SELECTION + MIPV6_IDENTIFIED_HOME_NETWORK_INFORMATION + MIPV6_UNRESTRICTED_HOME_NETWORK_INFORMATION + MIPV6_HOME_NETWORK_PREFIX + MIPV6_HOME_AGENT_ADDRESS + MIPV6_HOME_AGENT_FQDN +) + +var OptionCodeToString = map[OptionCode]string{ + OPTION_CLIENTID: "OPTION_CLIENTID", + OPTION_SERVERID: "OPTION_SERVERID", + OPTION_IA_NA: "OPTION_IA_NA", + OPTION_IA_TA: "OPTION_IA_TA", + OPTION_IAADDR: "OPTION_IAADDR", + OPTION_ORO: "OPTION_ORO", + OPTION_PREFERENCE: "OPTION_PREFERENCE", + OPTION_ELAPSED_TIME: "OPTION_ELAPSED_TIME", + OPTION_RELAY_MSG: "OPTION_RELAY_MSG", + OPTION_AUTH: "OPTION_AUTH", + OPTION_UNICAST: "OPTION_UNICAST", + OPTION_STATUS_CODE: "OPTION_STATUS_CODE", + OPTION_RAPID_COMMIT: "OPTION_RAPID_COMMIT", + OPTION_USER_CLASS: "OPTION_USER_CLASS", + OPTION_VENDOR_CLASS: "OPTION_VENDOR_CLASS", + OPTION_VENDOR_OPTS: "OPTION_VENDOR_OPTS", + OPTION_INTERFACE_ID: "OPTION_INTERFACE_ID", + OPTION_RECONF_MSG: "OPTION_RECONF_MSG", + OPTION_RECONF_ACCEPT: "OPTION_RECONF_ACCEPT", + SIP_SERVERS_DOMAIN_NAME_LIST: "SIP Servers Domain Name List", + SIP_SERVERS_IPV6_ADDRESS_LIST: "SIP Servers IPv6 Address List", + DNS_RECURSIVE_NAME_SERVER: "DNS Recursive Name Server", + DOMAIN_SEARCH_LIST: "Domain Search List", + OPTION_IA_PD: "OPTION_IA_PD", + OPTION_IAPREFIX: "OPTION_IAPREFIX", + OPTION_NIS_SERVERS: "OPTION_NIS_SERVERS", + OPTION_NISP_SERVERS: "OPTION_NISP_SERVERS", + OPTION_NIS_DOMAIN_NAME: "OPTION_NIS_DOMAIN_NAME", + OPTION_NISP_DOMAIN_NAME: "OPTION_NISP_DOMAIN_NAME", + SNTP_SERVER_LIST: "SNTP Server List", + INFORMATION_REFRESH_TIME: "Information Refresh Time", + BCMCS_CONTROLLER_DOMAIN_NAME_LIST: "BCMCS Controller Domain Name List", + BCMCS_CONTROLLER_IPV6_ADDRESS_LIST: "BCMCS Controller IPv6 Address List", + OPTION_GEOCONF_CIVIC: "OPTION_GEOCONF", + OPTION_REMOTE_ID: "OPTION_REMOTE_ID", + RELAY_AGENT_SUBSCRIBER_ID: "Relay-Agent Subscriber ID", + FQDN: "FQDN", + PANA_AUTHENTICATION_AGENT: "PANA Authentication Agent", + OPTION_NEW_POSIX_TIMEZONE: "OPTION_NEW_POSIX_TIME_ZONE", + OPTION_NEW_TZDB_TIMEZONE: "OPTION_NEW_TZDB_TIMEZONE", + ECHO_REQUEST: "Echo Request", + OPTION_LQ_QUERY: "OPTION_LQ_QUERY", + OPTION_CLIENT_DATA: "OPTION_CLIENT_DATA", + OPTION_CLT_TIME: "OPTION_CLT_TIME", + OPTION_LQ_RELAY_DATA: "OPTION_LQ_RELAY_DATA", + OPTION_LQ_CLIENT_LINK: "OPTION_LQ_CLIENT_LINK", + MIPV6_HOME_NETWORK_ID_FQDN: "MIPv6 Home Network ID FQDN", + MIPV6_VISITED_HOME_NETWORK_INFORMATION: "MIPv6 Visited Home Network Information", + LOST_SERVER: "LoST Server", + CAPWAP_ACCESS_CONTROLLER_ADDRESSES: "CAPWAP Access Controller Addresses", + RELAY_ID: "RELAY_ID", + OPTION_IPV6_ADDRESS_MOS: "OPTION-IPv6_Address-MoS", + OPTION_IPV6_FQDN_MOS: "OPTION-IPv6-FQDN-MoS", + OPTION_NTP_SERVER: "OPTION_NTP_SERVER", + OPTION_V6_ACCESS_DOMAIN: "OPTION_V6_ACCESS_DOMAIN", + OPTION_SIP_UA_CS_LIST: "OPTION_SIP_UA_CS_LIST", + OPT_BOOTFILE_URL: "OPT_BOOTFILE_URL", + OPT_BOOTFILE_PARAM: "OPT_BOOTFILE_PARAM", + OPTION_CLIENT_ARCH_TYPE: "OPTION_CLIENT_ARCH_TYPE", + OPTION_NII: "OPTION_NII", + OPTION_GEOLOCATION: "OPTION_GEOLOCATION", + OPTION_AFTR_NAME: "OPTION_AFTR_NAME", + OPTION_ERP_LOCAL_DOMAIN_NAME: "OPTION_ERP_LOCAL_DOMAIN_NAME", + OPTION_RSOO: "OPTION_RSOO", + OPTION_PD_EXCLUDE: "OPTION_PD_EXCLUDE", + VIRTUAL_SUBNET_SELECTION: "Virtual Subnet Selection", + MIPV6_IDENTIFIED_HOME_NETWORK_INFORMATION: "MIPv6 Identified Home Network Information", + MIPV6_UNRESTRICTED_HOME_NETWORK_INFORMATION: "MIPv6 Unrestricted Home Network Information", + MIPV6_HOME_NETWORK_PREFIX: "MIPv6 Home Network Prefix", + MIPV6_HOME_AGENT_ADDRESS: "MIPv6 Home Agent Address", + MIPV6_HOME_AGENT_FQDN: "MIPv6 Home Agent FQDN", +} diff --git a/dhcpv6/server.go b/dhcpv6/server.go new file mode 100644 index 0000000..e12136a --- /dev/null +++ b/dhcpv6/server.go @@ -0,0 +1,56 @@ +package dhcpv6 + +import ( + "fmt" + "log" + "net" +) + +type ResponseWriter interface { + LocalAddr() net.Addr + RemoteAddr() net.Addr + WriteMsg(DHCPv6) error + Write([]byte) (int, error) + Close() error +} + +type Handler interface { + ServeDHCP(w ResponseWriter, m *DHCPv6) +} + +type Server struct { + PacketConn net.PacketConn + Handler Handler +} + +func (s *Server) ActivateAndServe() error { + if s.PacketConn == nil { + return fmt.Errorf("Error: no packet connection specified") + } + var pc *net.UDPConn + var ok bool + if pc, ok = s.PacketConn.(*net.UDPConn); !ok { + return fmt.Errorf("Error: not an UDPConn") + } + if pc == nil { + return fmt.Errorf("ActivateAndServe: Invalid nil PacketConn") + } + log.Print("Handling requests") + for { + rbuf := make([]byte, 1024) // FIXME this is bad + n, peer, err := pc.ReadFrom(rbuf) + if err != nil { + log.Printf("Error reading from packet conn: %v", err) + continue + } + log.Printf("Handling request from %v", peer) + m, err := FromBytes(rbuf[:n]) + if err != nil { + log.Printf("Error parsing DHCPv6 request: %v", err) + continue + } + log.Print(m.Summary()) + // FIXME use s.Handler + } + return nil +} diff --git a/dhcpv6/types.go b/dhcpv6/types.go new file mode 100644 index 0000000..370243e --- /dev/null +++ b/dhcpv6/types.go @@ -0,0 +1,46 @@ +package dhcpv6 + +// from http://www.networksorcery.com/enp/protocol/dhcpv6.htm + +type MessageType uint8 + +const ( + _ MessageType = iota // skip 0 + SOLICIT + ADVERTISE + REQUEST + CONFIRM + RENEW + REBIND + REPLY + RELEASE + DECLINE + RECONFIGURE + INFORMATION_REQUEST + RELAY_FORW + RELAY_REPL + LEASEQUERY + LEASEQUERY_REPLY + LEASEQUERY_DONE + LEASEQUERY_DATA +) + +var MessageToString = map[MessageType]string{ + SOLICIT: "SOLICIT", + ADVERTISE: "ADVERTISE", + REQUEST: "REQUEST", + CONFIRM: "CONFIRM", + RENEW: "RENEW", + REBIND: "REBIND", + REPLY: "REPLY", + RELEASE: "RELEASE", + DECLINE: "DECLINE", + RECONFIGURE: "RECONFIGURE", + INFORMATION_REQUEST: "INFORMATION-REQUEST", + RELAY_FORW: "RELAY-FORW", + RELAY_REPL: "RELAY-REPL", + LEASEQUERY: "LEASEQUERY", + LEASEQUERY_REPLY: "LEASEQUERY-REPLY", + LEASEQUERY_DONE: "LEASEQUERY-DONE", + LEASEQUERY_DATA: "LEASEQUERY-DATA", +} |