summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--dhcpv4/client.go146
-rw-r--r--dhcpv4/defaults.go6
-rw-r--r--dhcpv4/dhcpv4.go514
-rw-r--r--dhcpv4/dhcpv4_test.go333
-rw-r--r--dhcpv4/options.go102
-rw-r--r--dhcpv4/options_test.go148
-rw-r--r--dhcpv4/types.go345
-rw-r--r--dhcpv6/client.go122
-rw-r--r--dhcpv6/defaults.go6
-rw-r--r--dhcpv6/dhcpv6.go215
-rw-r--r--dhcpv6/dhcpv6_test.go145
-rw-r--r--dhcpv6/iputils.go30
-rw-r--r--dhcpv6/options/clientid.go57
-rw-r--r--dhcpv6/options/dnsrecursivenameserver.go59
-rw-r--r--dhcpv6/options/domainsearchlist.go57
-rw-r--r--dhcpv6/options/duid.go100
-rw-r--r--dhcpv6/options/elapsedtime.go52
-rw-r--r--dhcpv6/options/elapsedtime_test.go44
-rw-r--r--dhcpv6/options/iaaddress.go86
-rw-r--r--dhcpv6/options/iaprefix.go98
-rw-r--r--dhcpv6/options/nontemporaryaddress.go86
-rw-r--r--dhcpv6/options/options.go125
-rw-r--r--dhcpv6/options/prefixdelegation.go86
-rw-r--r--dhcpv6/options/requestedoption.go72
-rw-r--r--dhcpv6/options/rfc1035label.go54
-rw-r--r--dhcpv6/options/rfc1035label_test.go66
-rw-r--r--dhcpv6/options/serverid.go57
-rw-r--r--dhcpv6/options/statuscode.go63
-rw-r--r--dhcpv6/options/types.go152
-rw-r--r--dhcpv6/server.go56
-rw-r--r--dhcpv6/types.go46
-rw-r--r--iana/hwtypes.go80
32 files changed, 3608 insertions, 0 deletions
diff --git a/dhcpv4/client.go b/dhcpv4/client.go
new file mode 100644
index 0000000..34724e4
--- /dev/null
+++ b/dhcpv4/client.go
@@ -0,0 +1,146 @@
+package dhcpv4
+
+import (
+ "encoding/binary"
+ "golang.org/x/net/ipv4"
+ "net"
+ "syscall"
+ "time"
+)
+
+const (
+ maxUDPReceivedPacketSize = 8192 // arbitrary size. Theoretically could be up to 65kb
+)
+
+type Client struct {
+ Network string
+ Dialer *net.Dialer
+ Timeout time.Duration
+}
+
+func makeRawBroadcastPacket(payload []byte) ([]byte, error) {
+ udp := make([]byte, 8)
+ binary.BigEndian.PutUint16(udp[:2], ClientPort)
+ binary.BigEndian.PutUint16(udp[2:4], ServerPort)
+ binary.BigEndian.PutUint16(udp[4:6], uint16(8+len(payload)))
+ binary.BigEndian.PutUint16(udp[6:8], 0) // try to offload the checksum
+
+ h := ipv4.Header{
+ Version: 4,
+ Len: 20,
+ TotalLen: 20 + len(udp) + len(payload),
+ TTL: 64,
+ Protocol: 17, // UDP
+ Dst: net.IPv4bcast,
+ Src: net.IPv4zero,
+ }
+ ret, err := h.Marshal()
+ if err != nil {
+ return nil, err
+ }
+ ret = append(ret, udp...)
+ ret = append(ret, payload...)
+ return ret, nil
+}
+
+// Run a full DORA transaction: Discovery, Offer, Request, Acknowledge, over
+// UDP. Does not retry in case of failures.
+// Returns a list of DHCPv4 structures representing the exchange. It can contain
+// up to four elements, ordered as Discovery, Offer, Request and Acknowledge.
+// In case of errors, an error is returned, and the list of DHCPv4 objects will
+// be shorted than 4, containing all the sent and received DHCPv4 messages.
+func (c *Client) Exchange(d *DHCPv4, ifname string) ([]DHCPv4, error) {
+ conversation := make([]DHCPv4, 1)
+ var err error
+
+ // Discovery
+ if d == nil {
+ d, err = NewDiscovery()
+ if err != nil {
+ return conversation, err
+ }
+ iface, err := net.InterfaceByName(ifname)
+ if err != nil {
+ return conversation, err
+ }
+ d.SetClientHwAddr(iface.HardwareAddr)
+ }
+ conversation[0] = *d
+
+ fd, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_RAW, syscall.IPPROTO_RAW)
+ if err != nil {
+ return conversation, err
+ }
+ err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)
+ if err != nil {
+ return conversation, err
+ }
+ err = syscall.SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_HDRINCL, 1)
+ if err != nil {
+ return conversation, err
+ }
+ err = syscall.SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_BROADCAST, 1)
+ if err != nil {
+ return conversation, err
+ }
+ err = syscall.BindToDevice(fd, ifname)
+ if err != nil {
+ return conversation, err
+ }
+
+ daddr := syscall.SockaddrInet4{Port: ClientPort, Addr: [4]byte{255, 255, 255, 255}}
+ packet, err := makeRawBroadcastPacket(d.ToBytes())
+ if err != nil {
+ return conversation, err
+ }
+ err = syscall.Sendto(fd, packet, 0, &daddr)
+ if err != nil {
+ return conversation, err
+ }
+
+ // Offer
+ conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4(0, 0, 0, 0), Port: ClientPort})
+ if err != nil {
+ return conversation, err
+ }
+ defer conn.Close()
+
+ buf := make([]byte, maxUDPReceivedPacketSize)
+ oobdata := []byte{} // ignoring oob data
+ n, _, _, _, err := conn.ReadMsgUDP(buf, oobdata)
+ offer, err := FromBytes(buf[:n])
+ if err != nil {
+ return conversation, err
+ }
+ // TODO match the packet content
+ // TODO check that the peer address matches the declared server IP and port
+ conversation = append(conversation, *offer)
+
+ // Request
+ request, err := RequestFromOffer(*offer)
+ if err != nil {
+ return conversation, err
+ }
+ conversation = append(conversation, *request)
+ packet, err = makeRawBroadcastPacket(request.ToBytes())
+ if err != nil {
+ return conversation, err
+ }
+ err = syscall.Sendto(fd, packet, 0, &daddr)
+ if err != nil {
+ return conversation, err
+ }
+
+ // Acknowledge
+ buf = make([]byte, maxUDPReceivedPacketSize)
+ n, _, _, _, err = conn.ReadMsgUDP(buf, oobdata)
+ acknowledge, err := FromBytes(buf[:n])
+ if err != nil {
+ return conversation, err
+ }
+ // TODO match the packet content
+ // TODO check that the peer address matches the declared server IP and port
+ conversation = append(conversation, *acknowledge)
+
+ return conversation, nil
+}
diff --git a/dhcpv4/defaults.go b/dhcpv4/defaults.go
new file mode 100644
index 0000000..4faec2c
--- /dev/null
+++ b/dhcpv4/defaults.go
@@ -0,0 +1,6 @@
+package dhcpv4
+
+const (
+ ServerPort = 67
+ ClientPort = 68
+)
diff --git a/dhcpv4/dhcpv4.go b/dhcpv4/dhcpv4.go
new file mode 100644
index 0000000..7cf1da3
--- /dev/null
+++ b/dhcpv4/dhcpv4.go
@@ -0,0 +1,514 @@
+package dhcpv4
+
+import (
+ "crypto/rand"
+ "encoding/binary"
+ "errors"
+ "fmt"
+ "github.com/insomniacslk/dhcp/iana"
+ "log"
+ "net"
+ "strings"
+)
+
+const HeaderSize = 236
+const MaxMessageSize = 576
+
+type DHCPv4 struct {
+ opcode OpcodeType
+ hwType iana.HwTypeType
+ hwAddrLen uint8
+ hopCount uint8
+ transactionID uint32
+ numSeconds uint16
+ flags uint16
+ clientIPAddr net.IP
+ yourIPAddr net.IP
+ serverIPAddr net.IP
+ gatewayIPAddr net.IP
+ clientHwAddr [16]byte
+ serverHostName [64]byte
+ bootFileName [128]byte
+ options []Option
+}
+
+// Generate a random 32-bits number suitable as TransactionID
+func GenerateTransactionID() (*uint32, error) {
+ b := make([]byte, 4)
+ n, err := rand.Read(b)
+ if n != 4 {
+ return nil, errors.New("Invalid random sequence: smaller than 32 bits")
+ }
+ if err != nil {
+ return nil, err
+ }
+ tid := binary.LittleEndian.Uint32(b)
+ return &tid, nil
+}
+
+// Create a new DHCPv4 structure and fill it up with default values. It won't be
+// a valid DHCPv4 message so you will need to adjust its fields.
+// See also NewDiscovery, NewOffer, NewRequest, NewAcknowledge, NewInform and
+// NewRelease .
+func New() (*DHCPv4, error) {
+ tid, err := GenerateTransactionID()
+ if err != nil {
+ return nil, err
+ }
+ d := DHCPv4{
+ opcode: OpcodeBootRequest,
+ hwType: iana.HwTypeEthernet,
+ hwAddrLen: 6,
+ hopCount: 0,
+ transactionID: *tid,
+ numSeconds: 0,
+ flags: 0,
+ clientIPAddr: net.IPv4zero,
+ yourIPAddr: net.IPv4zero,
+ serverIPAddr: net.IPv4zero,
+ gatewayIPAddr: net.IPv4zero,
+ }
+ copy(d.clientHwAddr[:], []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
+ copy(d.serverHostName[:], []byte{})
+ copy(d.bootFileName[:], []byte{})
+ options, err := OptionsFromBytes(MagicCookie)
+ if err != nil {
+ return nil, err
+ }
+ d.options = options
+ return &d, nil
+}
+
+// Will build a new DHCPv4 Discovery message, with a default Ethernet HW type
+// and a null hardware address. The caller needs to fill the remaining fields up
+func NewDiscovery() (*DHCPv4, error) {
+ d, err := New()
+ if err != nil {
+ return nil, err
+ }
+ d.SetOpcode(OpcodeBootRequest)
+ d.SetHwType(iana.HwTypeEthernet)
+ d.SetHwAddrLen(6)
+ d.SetBroadcast()
+ d.AddOption(Option{
+ Code: OptionDHCPMessageType,
+ Data: []byte{1},
+ })
+ d.AddOption(Option{
+ Code: OptionParameterRequestList,
+ Data: []byte{OptionSubnetMask, OptionRouter, OptionDomainName, OptionDomainNameServer},
+ })
+ // the End option has to be added explicitly
+ d.AddOption(Option{Code: OptionEnd})
+ return d, nil
+}
+
+// Build a DHCPv4 request from an offer
+func RequestFromOffer(offer DHCPv4) (*DHCPv4, error) {
+ d, err := New()
+ if err != nil {
+ return nil, err
+ }
+ d.SetOpcode(OpcodeBootRequest)
+ d.SetHwType(offer.HwType())
+ d.SetHwAddrLen(offer.HwAddrLen())
+ hwaddr := offer.ClientHwAddr()
+ d.SetClientHwAddr(hwaddr[:])
+ d.SetTransactionID(offer.TransactionID())
+ if offer.IsBroadcast() {
+ d.SetBroadcast()
+ } else {
+ d.SetUnicast()
+ }
+ // find server IP address
+ var serverIP []byte
+ for _, opt := range offer.options {
+ if opt.Code == OptionServerIdentifier {
+ serverIP = make([]byte, 4)
+ copy(serverIP, opt.Data)
+ }
+ }
+ if serverIP == nil {
+ return nil, errors.New("Missing Server IP Address in DHCP Offer")
+ }
+ d.SetServerIPAddr(serverIP)
+ d.AddOption(Option{
+ Code: OptionDHCPMessageType,
+ Data: []byte{3},
+ })
+ d.AddOption(Option{
+ Code: OptionRequestedIPAddress,
+ Data: offer.YourIPAddr(),
+ })
+ d.AddOption(Option{
+ Code: OptionServerIdentifier,
+ Data: serverIP,
+ })
+ // the End option has to be added explicitly
+ d.AddOption(Option{Code: OptionEnd})
+ return d, nil
+}
+
+func FromBytes(data []byte) (*DHCPv4, error) {
+ if len(data) < HeaderSize {
+ return nil, errors.New(fmt.Sprintf("Invalid DHCPv4 header: shorter than %v bytes", HeaderSize))
+ }
+ d := DHCPv4{
+ opcode: OpcodeType(data[0]),
+ hwType: iana.HwTypeType(data[1]),
+ hwAddrLen: data[2],
+ hopCount: data[3],
+ transactionID: binary.BigEndian.Uint32(data[4:8]),
+ numSeconds: binary.BigEndian.Uint16(data[8:10]),
+ flags: binary.BigEndian.Uint16(data[10:12]),
+ clientIPAddr: net.IP(data[12:16]),
+ yourIPAddr: net.IP(data[16:20]),
+ serverIPAddr: net.IP(data[20:24]),
+ gatewayIPAddr: net.IP(data[24:28]),
+ }
+ copy(d.clientHwAddr[:], data[28:44])
+ copy(d.serverHostName[:], data[44:108])
+ copy(d.bootFileName[:], data[108:236])
+ options, err := OptionsFromBytes(data[236:])
+ if err != nil {
+ return nil, err
+ }
+ d.options = options
+ return &d, nil
+}
+
+func (d *DHCPv4) Opcode() OpcodeType {
+ return d.opcode
+}
+
+func (d *DHCPv4) OpcodeToString() string {
+ opcode := OpcodeToString[d.opcode]
+ if opcode == "" {
+ opcode = "Invalid"
+ }
+ return opcode
+}
+
+func (d *DHCPv4) SetOpcode(opcode OpcodeType) {
+ if OpcodeToString[opcode] == "" {
+ log.Printf("Warning: unknown DHCPv4 opcode: %v", opcode)
+ }
+ d.opcode = opcode
+}
+
+func (d *DHCPv4) HwType() iana.HwTypeType {
+ return d.hwType
+}
+
+func (d *DHCPv4) HwTypeToString() string {
+ hwtype, ok := iana.HwTypeToString[d.hwType]
+ if !ok {
+ hwtype = "Invalid"
+ }
+ return hwtype
+}
+
+func (d *DHCPv4) SetHwType(hwType iana.HwTypeType) {
+ if _, ok := iana.HwTypeToString[hwType]; !ok {
+ log.Printf("Warning: Invalid DHCPv4 hwtype: %v", hwType)
+ }
+ d.hwType = hwType
+}
+
+func (d *DHCPv4) HwAddrLen() uint8 {
+ return d.hwAddrLen
+}
+
+func (d *DHCPv4) SetHwAddrLen(hwAddrLen uint8) {
+ if hwAddrLen > 16 {
+ log.Printf("Warning: invalid HwAddrLen: %v > 16, using 16 instead", hwAddrLen)
+ hwAddrLen = 16
+ }
+ d.hwAddrLen = hwAddrLen
+}
+
+func (d *DHCPv4) HopCount() uint8 {
+ return d.hopCount
+}
+
+func (d *DHCPv4) SetHopCount(hopCount uint8) {
+ d.hopCount = hopCount
+}
+
+func (d *DHCPv4) TransactionID() uint32 {
+ return d.transactionID
+}
+
+func (d *DHCPv4) SetTransactionID(transactionID uint32) {
+ d.transactionID = transactionID
+}
+
+func (d *DHCPv4) NumSeconds() uint16 {
+ return d.numSeconds
+}
+
+func (d *DHCPv4) SetNumSeconds(numSeconds uint16) {
+ d.numSeconds = numSeconds
+}
+
+func (d *DHCPv4) Flags() uint16 {
+ return d.flags
+}
+
+func (d *DHCPv4) SetFlags(flags uint16) {
+ d.flags = flags
+}
+
+func (d *DHCPv4) FlagsToString() string {
+ flags := ""
+ if d.IsBroadcast() {
+ flags += "Broadcast"
+ } else {
+ flags += "Unicast"
+ }
+ if d.flags&0xfe != 0 {
+ flags += " (reserved bits not zeroed)"
+ }
+ return flags
+}
+
+func (d *DHCPv4) IsBroadcast() bool {
+ return d.flags&0x8000 == 0x8000
+}
+
+func (d *DHCPv4) SetBroadcast() {
+ d.flags |= 0x8000
+}
+
+func (d *DHCPv4) IsUnicast() bool {
+ return d.flags&0x8000 == 0
+}
+
+func (d *DHCPv4) SetUnicast() {
+ d.flags &= ^uint16(0x8000)
+}
+
+func (d *DHCPv4) ClientIPAddr() net.IP {
+ return d.clientIPAddr
+}
+
+func (d *DHCPv4) SetClientIPAddr(clientIPAddr net.IP) {
+ d.clientIPAddr = clientIPAddr
+}
+
+func (d *DHCPv4) YourIPAddr() net.IP {
+ return d.yourIPAddr
+}
+
+func (d *DHCPv4) SetYourIPAddr(yourIPAddr net.IP) {
+ d.yourIPAddr = yourIPAddr
+}
+
+func (d *DHCPv4) ServerIPAddr() net.IP {
+ return d.serverIPAddr
+}
+
+func (d *DHCPv4) SetServerIPAddr(serverIPAddr net.IP) {
+ d.serverIPAddr = serverIPAddr
+}
+
+func (d *DHCPv4) GatewayIPAddr() net.IP {
+ return d.gatewayIPAddr
+}
+
+func (d *DHCPv4) SetGatewayIPAddr(gatewayIPAddr net.IP) {
+ d.gatewayIPAddr = gatewayIPAddr
+}
+
+func (d *DHCPv4) ClientHwAddr() [16]byte {
+ return d.clientHwAddr
+}
+
+func (d *DHCPv4) ClientHwAddrToString() string {
+ var ret string
+ for _, b := range d.clientHwAddr[:d.hwAddrLen] {
+ ret += fmt.Sprintf("%02x:", b)
+ }
+ return ret[:len(ret)-1] // remove trailing `:`
+}
+
+func (d *DHCPv4) SetClientHwAddr(clientHwAddr []byte) {
+ if len(clientHwAddr) > 16 {
+ log.Printf("Warning: too long HW Address (%d bytes), truncating to 16 bytes", len(clientHwAddr))
+ clientHwAddr = clientHwAddr[:16]
+ }
+ copy(d.clientHwAddr[:len(clientHwAddr)], clientHwAddr)
+ // pad the remaining bytes, if any
+ for i := len(clientHwAddr); i < 16; i++ {
+ d.clientHwAddr[i] = 0
+ }
+}
+
+func (d *DHCPv4) ServerHostName() [64]byte {
+ return d.serverHostName
+}
+
+func (d *DHCPv4) ServerHostNameToString() string {
+ return strings.TrimRight(string(d.serverHostName[:]), "\x00")
+}
+
+func (d *DHCPv4) SetServerHostName(serverHostName []byte) {
+ if len(serverHostName) > 64 {
+ serverHostName = serverHostName[:64]
+ } else if len(serverHostName) < 64 {
+ for i := len(serverHostName) - 1; i < 64; i++ {
+ serverHostName = append(serverHostName, 0)
+ }
+ }
+ // need an array, not a slice, so let's copy it
+ var newServerHostName [64]byte
+ copy(newServerHostName[:], serverHostName)
+ d.serverHostName = newServerHostName
+}
+
+func (d *DHCPv4) BootFileName() [128]byte {
+ return d.bootFileName
+}
+
+func (d *DHCPv4) BootFileNameToString() string {
+ return strings.TrimRight(string(d.bootFileName[:]), "\x00")
+}
+
+func (d *DHCPv4) SetBootFileName(bootFileName []byte) {
+ if len(bootFileName) > 128 {
+ bootFileName = bootFileName[:128]
+ } else if len(bootFileName) < 128 {
+ for i := len(bootFileName) - 1; i < 128; i++ {
+ bootFileName = append(bootFileName, 0)
+ }
+ }
+ // need an array, not a slice, so let's copy it
+ var newBootFileName [128]byte
+ copy(newBootFileName[:], bootFileName)
+ d.bootFileName = newBootFileName
+}
+
+func (d *DHCPv4) Options() []Option {
+ return d.options
+}
+
+func (d *DHCPv4) StrippedOptions() []Option {
+ // differently from Options() this function strips away anything coming
+ // after the End option (normally just Pad options).
+ strippedOptions := []Option{}
+ for _, opt := range d.options {
+ strippedOptions = append(strippedOptions, opt)
+ if opt.Code == OptionEnd {
+ break
+ }
+ }
+ return strippedOptions
+}
+
+func (d *DHCPv4) SetOptions(options []Option) {
+ d.options = options
+}
+
+func (d *DHCPv4) AddOption(option Option) {
+ d.options = append(d.options, option)
+}
+
+func (d *DHCPv4) String() string {
+ return fmt.Sprintf("DHCPv4(opcode=%v hwtype=%v hwaddr=%v)",
+ d.OpcodeToString(), d.HwTypeToString(), d.ClientHwAddr())
+}
+
+func (d *DHCPv4) Summary() string {
+ ret := fmt.Sprintf(
+ "DHCPv4\n"+
+ " opcode=%v\n"+
+ " hwtype=%v\n"+
+ " hwaddrlen=%v\n"+
+ " hopcount=%v\n"+
+ " transactionid=0x%08x\n"+
+ " numseconds=%v\n"+
+ " flags=%v (0x%02x)\n"+
+ " clientipaddr=%v\n"+
+ " youripaddr=%v\n"+
+ " serveripaddr=%v\n"+
+ " gatewayipaddr=%v\n"+
+ " clienthwaddr=%v\n"+
+ " serverhostname=%v\n"+
+ " bootfilename=%v\n",
+ d.OpcodeToString(),
+ d.HwTypeToString(),
+ d.HwAddrLen(),
+ d.HopCount(),
+ d.TransactionID(),
+ d.NumSeconds(),
+ d.FlagsToString(),
+ d.Flags(),
+ d.ClientIPAddr(),
+ d.YourIPAddr(),
+ d.ServerIPAddr(),
+ d.GatewayIPAddr(),
+ d.ClientHwAddrToString(),
+ d.ServerHostNameToString(),
+ d.BootFileNameToString(),
+ )
+ ret += " options=\n"
+ for _, opt := range d.options {
+ ret += fmt.Sprintf(" %v\n", opt.String())
+ if opt.Code == OptionEnd {
+ break
+ }
+ }
+ return ret
+}
+
+func (d *DHCPv4) ValidateOptions() {
+ // TODO find duplicate options
+ foundOptionEnd := false
+ for _, opt := range d.options {
+ if foundOptionEnd {
+ if opt.Code == OptionEnd {
+ log.Print("Warning: found duplicate End option")
+ }
+ if opt.Code != OptionEnd && opt.Code != OptionPad {
+ name := OptionCodeToString[opt.Code]
+ log.Printf("Warning: found option %v (%v) after End option", opt.Code, name)
+ }
+ }
+ if opt.Code == OptionEnd {
+ foundOptionEnd = true
+ }
+ }
+ if !foundOptionEnd {
+ log.Print("Warning: no End option found")
+ }
+}
+
+// Convert a DHCPv4 structure into its binary representation, suitable for being
+// sent over the network
+func (d *DHCPv4) ToBytes() []byte {
+ // This won't check if the End option is present, you've been warned
+ var ret []byte
+ u32 := make([]byte, 4)
+ u16 := make([]byte, 2)
+
+ ret = append(ret, byte(d.opcode))
+ ret = append(ret, byte(d.hwType))
+ ret = append(ret, byte(d.hwAddrLen))
+ ret = append(ret, byte(d.hopCount))
+ binary.BigEndian.PutUint32(u32, d.transactionID)
+ ret = append(ret, u32...)
+ binary.BigEndian.PutUint16(u16, d.numSeconds)
+ ret = append(ret, u16...)
+ binary.BigEndian.PutUint16(u16, d.flags)
+ ret = append(ret, u16...)
+ ret = append(ret, d.clientIPAddr[:4]...)
+ ret = append(ret, d.yourIPAddr[:4]...)
+ ret = append(ret, d.serverIPAddr[:4]...)
+ ret = append(ret, d.gatewayIPAddr[:4]...)
+ ret = append(ret, d.clientHwAddr[:16]...)
+ ret = append(ret, d.serverHostName[:64]...)
+ ret = append(ret, d.bootFileName[:128]...)
+ d.ValidateOptions() // print warnings about broken options, if any
+ ret = append(ret, OptionsToBytes(d.options)...)
+ return ret
+}
diff --git a/dhcpv4/dhcpv4_test.go b/dhcpv4/dhcpv4_test.go
new file mode 100644
index 0000000..5a12207
--- /dev/null
+++ b/dhcpv4/dhcpv4_test.go
@@ -0,0 +1,333 @@
+package dhcpv4
+
+import (
+ "bytes"
+ "github.com/insomniacslk/dhcp/iana"
+ "net"
+ "testing"
+)
+
+// NOTE: if one of the following Assert* fails where expected and got values are
+// the same, you probably have to cast one of them to match the other one's
+// type, e.g. comparing int and byte, even the same value, will fail.
+func AssertEqual(t *testing.T, a, b interface{}, what string) {
+ if a != b {
+ t.Fatalf("Invalid %s. %v != %v", what, a, b)
+ }
+}
+
+func AssertEqualBytes(t *testing.T, a, b []byte, what string) {
+ if !bytes.Equal(a, b) {
+ t.Fatalf("Invalid %s. %v != %v", what, a, b)
+ }
+}
+
+func AssertEqualIPAddr(t *testing.T, a, b net.IP, what string) {
+ if !net.IP.Equal(a, b) {
+ t.Fatalf("Invalid %s. %v != %v", what, a, b)
+ }
+}
+
+func TestFromBytes(t *testing.T) {
+ data := []byte{
+ 1, // dhcp request
+ 1, // ethernet hw type
+ 6, // hw addr length
+ 3, // hop count
+ 0xaa, 0xbb, 0xcc, 0xdd, // transaction ID, big endian (network)
+ 0, 3, // number of seconds
+ 0, 1, // broadcast
+ 0, 0, 0, 0, // client IP address
+ 0, 0, 0, 0, // your IP address
+ 0, 0, 0, 0, // server IP address
+ 0, 0, 0, 0, // gateway IP address
+ 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // client MAC address + padding
+ }
+ // server host name
+ expectedHostname := []byte{}
+ for i := 0; i < 64; i++ {
+ expectedHostname = append(expectedHostname, 0)
+ }
+ data = append(data, expectedHostname...)
+ // boot file name
+ expectedBootfilename := []byte{}
+ for i := 0; i < 128; i++ {
+ expectedBootfilename = append(expectedBootfilename, 0)
+ }
+ data = append(data, expectedBootfilename...)
+ // magic cookie, then no options
+ data = append(data, []byte{99, 130, 83, 99}...)
+
+ d, err := FromBytes(data)
+ if err != nil {
+ t.Fatal(err)
+ }
+ AssertEqual(t, d.Opcode(), OpcodeBootRequest, "opcode")
+ AssertEqual(t, d.HwType(), iana.HwTypeEthernet, "hardware type")
+ AssertEqual(t, d.HwAddrLen(), byte(6), "hardware address length")
+ AssertEqual(t, d.HopCount(), byte(3), "hop count")
+ AssertEqual(t, d.TransactionID(), uint32(0xaabbccdd), "transaction ID")
+ AssertEqual(t, d.NumSeconds(), uint16(3), "number of seconds")
+ AssertEqual(t, d.Flags(), uint16(1), "flags")
+ AssertEqualIPAddr(t, d.ClientIPAddr(), net.IPv4zero, "client IP address")
+ AssertEqualIPAddr(t, d.YourIPAddr(), net.IPv4zero, "your IP address")
+ AssertEqualIPAddr(t, d.ServerIPAddr(), net.IPv4zero, "server IP address")
+ AssertEqualIPAddr(t, d.GatewayIPAddr(), net.IPv4zero, "gateway IP address")
+ hwaddr := d.ClientHwAddr()
+ AssertEqualBytes(t, hwaddr[:], []byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, "flags")
+ hostname := d.ServerHostName()
+ AssertEqualBytes(t, hostname[:], expectedHostname, "server host name")
+ bootfilename := d.BootFileName()
+ AssertEqualBytes(t, bootfilename[:], expectedBootfilename, "boot file name")
+ // no need to check Magic Cookie as it is already validated in FromBytes
+ // above
+}
+
+func TestFromBytesZeroLength(t *testing.T) {
+ data := []byte{}
+ _, err := FromBytes(data)
+ if err == nil {
+ t.Fatal("Expected error, got nil")
+ }
+}
+
+func TestFromBytesShortLength(t *testing.T) {
+ data := []byte{1, 1, 6, 0}
+ _, err := FromBytes(data)
+ if err == nil {
+ t.Fatal("Expected error, got nil")
+ }
+}
+
+func TestFromBytesInvalidOptions(t *testing.T) {
+ data := []byte{
+ 1, // dhcp request
+ 1, // ethernet hw type
+ 6, // hw addr length
+ 0, // hop count
+ 0xaa, 0xbb, 0xcc, 0xdd, // transaction ID
+ 3, 0, // number of seconds
+ 1, 0, // broadcast
+ 0, 0, 0, 0, // client IP address
+ 0, 0, 0, 0, // your IP address
+ 0, 0, 0, 0, // server IP address
+ 0, 0, 0, 0, // gateway IP address
+ 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // client MAC address + padding
+ }
+ // server host name
+ for i := 0; i < 64; i++ {
+ data = append(data, 0)
+ }
+ // boot file name
+ for i := 0; i < 128; i++ {
+ data = append(data, 0)
+ }
+ // invalid magic cookie, forcing option parsing to fail
+ data = append(data, []byte{99, 130, 83, 98}...)
+ _, err := FromBytes(data)
+ if err == nil {
+ t.Fatal("Expected error, got nil")
+ }
+}
+
+func TestSettersAndGetters(t *testing.T) {
+ data := []byte{
+ 1, // dhcp request
+ 1, // ethernet hw type
+ 6, // hw addr length
+ 3, // hop count
+ 0xaa, 0xbb, 0xcc, 0xdd, // transaction ID, big endian (network)
+ 0, 3, // number of seconds
+ 0, 1, // broadcast
+ 1, 2, 3, 4, // client IP address
+ 5, 6, 7, 8, // your IP address
+ 9, 10, 11, 12, // server IP address
+ 13, 14, 15, 16, // gateway IP address
+ 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // client MAC address + padding
+ }
+ // server host name
+ expectedHostname := []byte{}
+ for i := 0; i < 64; i++ {
+ expectedHostname = append(expectedHostname, 0)
+ }
+ data = append(data, expectedHostname...)
+ // boot file name
+ expectedBootfilename := []byte{}
+ for i := 0; i < 128; i++ {
+ expectedBootfilename = append(expectedBootfilename, 0)
+ }
+ data = append(data, expectedBootfilename...)
+ // magic cookie, then no options
+ data = append(data, []byte{99, 130, 83, 99}...)
+ d, err := FromBytes(data)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // getter/setter for Opcode
+ AssertEqual(t, d.Opcode(), OpcodeBootRequest, "opcode")
+ d.SetOpcode(OpcodeBootReply)
+ AssertEqual(t, d.Opcode(), OpcodeBootReply, "opcode")
+
+ // getter/setter for HwType
+ AssertEqual(t, d.HwType(), iana.HwTypeEthernet, "hardware type")
+ d.SetHwType(iana.HwTypeARCNET)
+ AssertEqual(t, d.HwType(), iana.HwTypeARCNET, "hardware type")
+
+ // getter/setter for HwAddrLen
+ AssertEqual(t, d.HwAddrLen(), uint8(6), "hardware address length")
+ d.SetHwAddrLen(12)
+ AssertEqual(t, d.HwAddrLen(), uint8(12), "hardware address length")
+
+ // getter/setter for HopCount
+ AssertEqual(t, d.HopCount(), uint8(3), "hop count")
+ d.SetHopCount(1)
+ AssertEqual(t, d.HopCount(), uint8(1), "hop count")
+
+ // getter/setter for TransactionID
+ AssertEqual(t, d.TransactionID(), uint32(0xaabbccdd), "transaction ID")
+ d.SetTransactionID(0xeeff0011)
+ AssertEqual(t, d.TransactionID(), uint32(0xeeff0011), "transaction ID")
+
+ // getter/setter for TransactionID
+ AssertEqual(t, d.NumSeconds(), uint16(3), "number of seconds")
+ d.SetNumSeconds(15)
+ AssertEqual(t, d.NumSeconds(), uint16(15), "number of seconds")
+
+ // getter/setter for Flags
+ AssertEqual(t, d.Flags(), uint16(1), "flags")
+ d.SetFlags(0)
+ AssertEqual(t, d.Flags(), uint16(0), "flags")
+
+ // getter/setter for ClientIPAddr
+ AssertEqualIPAddr(t, d.ClientIPAddr(), net.IPv4(1, 2, 3, 4), "client IP address")
+ d.SetClientIPAddr(net.IPv4(4, 3, 2, 1))
+ AssertEqualIPAddr(t, d.ClientIPAddr(), net.IPv4(4, 3, 2, 1), "client IP address")
+
+ // getter/setter for YourIPAddr
+ AssertEqualIPAddr(t, d.YourIPAddr(), net.IPv4(5, 6, 7, 8), "your IP address")
+ d.SetYourIPAddr(net.IPv4(8, 7, 6, 5))
+ AssertEqualIPAddr(t, d.YourIPAddr(), net.IPv4(8, 7, 6, 5), "your IP address")
+
+ // getter/setter for ServerIPAddr
+ AssertEqualIPAddr(t, d.ServerIPAddr(), net.IPv4(9, 10, 11, 12), "server IP address")
+ d.SetServerIPAddr(net.IPv4(12, 11, 10, 9))
+ AssertEqualIPAddr(t, d.ServerIPAddr(), net.IPv4(12, 11, 10, 9), "server IP address")
+
+ // getter/setter for GatewayIPAddr
+ AssertEqualIPAddr(t, d.GatewayIPAddr(), net.IPv4(13, 14, 15, 16), "gateway IP address")
+ d.SetGatewayIPAddr(net.IPv4(16, 15, 14, 13))
+ AssertEqualIPAddr(t, d.GatewayIPAddr(), net.IPv4(16, 15, 14, 13), "gateway IP address")
+
+ // getter/setter for ClientHwAddr
+ hwaddr := d.ClientHwAddr()
+ AssertEqualBytes(t, hwaddr[:], []byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, "client hardware address")
+ d.SetFlags(0)
+
+ // getter/setter for ServerHostName
+ serverhostname := d.ServerHostName()
+ AssertEqualBytes(t, serverhostname[:], expectedHostname, "server host name")
+ newHostname := []byte{'t', 'e', 's', 't'}
+ for i := 0; i < 60; i++ {
+ newHostname = append(newHostname, 0)
+ }
+ d.SetServerHostName(newHostname)
+ serverhostname = d.ServerHostName()
+ AssertEqualBytes(t, serverhostname[:], newHostname, "server host name")
+
+ // getter/setter for BootFileName
+ bootfilename := d.BootFileName()
+ AssertEqualBytes(t, bootfilename[:], expectedBootfilename, "boot file name")
+ newBootfilename := []byte{'t', 'e', 's', 't'}
+ for i := 0; i < 124; i++ {
+ newBootfilename = append(newBootfilename, 0)
+ }
+ d.SetBootFileName(newBootfilename)
+ bootfilename = d.BootFileName()
+ AssertEqualBytes(t, bootfilename[:], newBootfilename, "boot file name")
+}
+
+func TestToStringMethods(t *testing.T) {
+ d, err := New()
+ if err != nil {
+ t.Fatal(err)
+ }
+ // OpcodeToString
+ d.SetOpcode(OpcodeBootRequest)
+ AssertEqual(t, d.OpcodeToString(), "BootRequest", "OpcodeToString")
+ d.SetOpcode(OpcodeBootReply)
+ AssertEqual(t, d.OpcodeToString(), "BootReply", "OpcodeToString")
+ d.SetOpcode(OpcodeType(0))
+ AssertEqual(t, d.OpcodeToString(), "Invalid", "OpcodeToString")
+
+ // HwTypeToString
+ d.SetHwType(iana.HwTypeEthernet)
+ AssertEqual(t, d.HwTypeToString(), "Ethernet", "HwTypeToString")
+ d.SetHwType(iana.HwTypeARCNET)
+ AssertEqual(t, d.HwTypeToString(), "ARCNET", "HwTypeToString")
+
+ // FlagsToString
+ d.SetUnicast()
+ AssertEqual(t, d.FlagsToString(), "Unicast", "FlagsToString")
+ d.SetBroadcast()
+ AssertEqual(t, d.FlagsToString(), "Broadcast", "FlagsToString")
+ d.SetFlags(0xffff)
+ AssertEqual(t, d.FlagsToString(), "Broadcast (reserved bits not zeroed)", "FlagsToString")
+
+ // ClientHwAddrToString
+ d.SetHwAddrLen(6)
+ d.SetClientHwAddr([]byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
+ AssertEqual(t, d.ClientHwAddrToString(), "aa:bb:cc:dd:ee:ff", "ClientHwAddrToString")
+
+ // ServerHostNameToString
+ d.SetServerHostName([]byte("my.host.local"))
+ AssertEqual(t, d.ServerHostNameToString(), "my.host.local", "ServerHostNameToString")
+
+ // BootFileNameToString
+ d.SetBootFileName([]byte("/my/boot/file"))
+ AssertEqual(t, d.BootFileNameToString(), "/my/boot/file", "BootFileNameToString")
+}
+
+func TestToBytes(t *testing.T) {
+ // the following bytes match what dhcpv4.New would create. Keep them in
+ // sync!
+ expected := []byte{
+ 1, // Opcode BootRequest
+ 1, // HwType Ethernet
+ 6, // HwAddrLen
+ 0, // HopCount
+ 0x11, 0x22, 0x33, 0x44, // TransactionID
+ 0, 0, // NumSeconds
+ 0, 0, // Flags
+ 0, 0, 0, 0, // ClientIPAddr
+ 0, 0, 0, 0, // YourIPAddr
+ 0, 0, 0, 0, // ServerIPAddr
+ 0, 0, 0, 0, // GatewayIPAddr
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ClientHwAddr
+ }
+ // ServerHostName
+ for i := 0; i < 64; i++ {
+ expected = append(expected, 0)
+ }
+ // BootFileName
+ for i := 0; i < 128; i++ {
+ expected = append(expected, 0)
+ }
+ // Magic Cookie
+ expected = append(expected, MagicCookie...)
+
+ d, err := New()
+ if err != nil {
+ t.Fatal(err)
+ }
+ // fix TransactionID to match the expected one, since it's randomly
+ // generated in New()
+ d.SetTransactionID(0x11223344)
+ got := d.ToBytes()
+ AssertEqualBytes(t, expected, got, "ToBytes")
+}
+
+// TODO
+// test broadcast/unicast flags
+// test Options setter/getter
+// test Summary() and String()
diff --git a/dhcpv4/options.go b/dhcpv4/options.go
new file mode 100644
index 0000000..59f2c83
--- /dev/null
+++ b/dhcpv4/options.go
@@ -0,0 +1,102 @@
+package dhcpv4
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+)
+
+type OptionCode byte
+
+var MagicCookie = []byte{99, 130, 83, 99}
+
+type Option struct {
+ Code OptionCode
+ Data []byte
+}
+
+func ParseOption(dataStart []byte) (*Option, error) {
+ // Parse a sequence of bytes as a single DHCPv4 option.
+ // Returns the option code, its data, and an error if any.
+ if len(dataStart) == 0 {
+ return nil, errors.New("Invalid zero-length DHCPv4 option")
+ }
+ opt := OptionCode(dataStart[0])
+ switch opt {
+ case OptionPad, OptionEnd:
+ return &Option{Code: opt, Data: []byte{}}, nil
+ default:
+ length := int(dataStart[1])
+ if len(dataStart) < length+2 {
+ return nil, errors.New(
+ fmt.Sprintf("Invalid data length. Declared %v, actual %v",
+ length, len(dataStart),
+ ))
+ }
+ data := dataStart[2 : 2+length]
+ return &Option{Code: opt, Data: data}, nil
+ }
+}
+
+func OptionsFromBytes(data []byte) ([]Option, error) {
+ // Parse a sequence of bytes until the end and build a list of options from
+ // it. The sequence must contain the Magic Cookie.
+ // Returns an error if any invalid option or length is found.
+ if len(data) < 4 {
+ return nil, errors.New("Invalid options: shorter than 4 bytes")
+ }
+ if !bytes.Equal(data[:4], MagicCookie) {
+ return nil, errors.New(fmt.Sprintf("Invalid Magic Cookie: %v", data[:4]))
+ }
+ options := make([]Option, 0, 10)
+ idx := 4
+ for {
+ if idx == len(data) {
+ break
+ }
+ if idx > len(data) {
+ // this should never happen
+ return nil, errors.New("Error: Reading past the end of options")
+ }
+ opt, err := ParseOption(data[idx:])
+ idx++
+ if err != nil {
+ return nil, err
+ }
+ options = append(options, *opt)
+ if len(opt.Data) > 0 {
+ // options with zero length have no length byte, so here we handle the ones with
+ // nonzero length
+ idx++
+ }
+ idx += len(opt.Data)
+ }
+ return options, nil
+}
+
+func OptionsToBytes(options []Option) []byte {
+ // Convert a list of options to a wire-format representation. This will
+ // include the Magic Cookie
+ ret := MagicCookie
+ for _, opt := range options {
+ ret = append(ret, opt.ToBytes()...)
+ }
+ return ret
+}
+
+func (o *Option) String() string {
+ code, ok := OptionCodeToString[o.Code]
+ if !ok {
+ code = "Unknown"
+ }
+ return fmt.Sprintf("%v -> %v", code, o.Data)
+}
+
+func (o *Option) ToBytes() []byte {
+ // Convert a single option to its wire-format representation
+ ret := []byte{byte(o.Code)}
+ if o.Code != OptionPad && o.Code != OptionEnd {
+ ret = append(ret, byte(len(o.Data)))
+ }
+ return append(ret, o.Data...)
+}
diff --git a/dhcpv4/options_test.go b/dhcpv4/options_test.go
new file mode 100644
index 0000000..07be9bf
--- /dev/null
+++ b/dhcpv4/options_test.go
@@ -0,0 +1,148 @@
+package dhcpv4
+
+import (
+ "bytes"
+ "testing"
+)
+
+func TestParseOption(t *testing.T) {
+ option := []byte{5, 4, 192, 168, 1, 254} // DNS option
+ opt, err := ParseOption(option)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if opt.Code != OptionNameServer {
+ t.Fatalf("Invalid option code. Expected 5, got %v", opt.Code)
+ }
+ if !bytes.Equal(opt.Data, option[2:]) {
+ t.Fatalf("Invalid option data. Expected %v, got %v", option[2:], opt.Data)
+ }
+}
+
+func TestParseOptionPad(t *testing.T) {
+ option := []byte{0}
+ opt, err := ParseOption(option)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if opt.Code != OptionPad {
+ t.Fatalf("Invalid option code. Expected %v, got %v", OptionPad, opt.Code)
+ }
+ if len(opt.Data) != 0 {
+ t.Fatalf("Invalid option data. Expected empty slice, got %v", opt.Data)
+ }
+}
+
+func TestParseOptionZeroLength(t *testing.T) {
+ option := []byte{}
+ _, err := ParseOption(option)
+ if err == nil {
+ t.Fatal("Expected an error, got none")
+ }
+}
+
+func TestParseOptionShortOption(t *testing.T) {
+ option := []byte{53, 1}
+ _, err := ParseOption(option)
+ if err == nil {
+ t.Fatal(err)
+ }
+}
+
+func TestOptionsFromBytes(t *testing.T) {
+ options := []byte{
+ 99, 130, 83, 99, // Magic Cookie
+ 5, 4, 192, 168, 1, 1, // DNS
+ 255, // end
+ 0, 0, 0, //padding
+ }
+ opts, err := OptionsFromBytes(options)
+ if err != nil {
+ t.Fatal(err)
+ }
+ // each padding byte counts as an option. Magic Cookie doesn't add up
+ if len(opts) != 5 {
+ t.Fatal("Invalid options length. Expected 5, got %v", len(opts))
+ }
+ if opts[0].Code != OptionNameServer {
+ t.Fatal("Invalid option code. Expected %v, got %v", OptionNameServer, opts[0].Code)
+ }
+ if !bytes.Equal(opts[0].Data, options[6:10]) {
+ t.Fatal("Invalid option data. Expected %v, got %v", options[6:10], opts[0].Data)
+ }
+ if opts[1].Code != OptionEnd {
+ t.Fatalf("Invalid option code. Expected %v, got %v", OptionEnd, opts[1].Code)
+ }
+ if opts[2].Code != OptionPad {
+ t.Fatalf("Invalid option code. Expected %v, got %v", OptionPad, opts[2].Code)
+ }
+}
+
+func TestOptionsFromBytesZeroLength(t *testing.T) {
+ options := []byte{}
+ _, err := OptionsFromBytes(options)
+ if err == nil {
+ t.Fatal("Expected an error, got none")
+ }
+}
+
+func TestOptionsFromBytesBadMagicCookie(t *testing.T) {
+ options := []byte{1, 2, 3, 4}
+ _, err := OptionsFromBytes(options)
+ if err == nil {
+ t.Fatal("Expected an error, got none")
+ }
+}
+
+func TestOptionsToBytes(t *testing.T) {
+ originalOptions := []byte{
+ 99, 130, 83, 99, // Magic Cookie
+ 5, 4, 192, 168, 1, 1, // DNS
+ 255, // end
+ 0, 0, 0, //padding
+ }
+ options, err := OptionsFromBytes(originalOptions)
+ if err != nil {
+ t.Fatal(err)
+ }
+ finalOptions := OptionsToBytes(options)
+ if !bytes.Equal(originalOptions, finalOptions) {
+ t.Fatalf("Invalid options. Expected %v, got %v", originalOptions, finalOptions)
+ }
+}
+
+func TestOptionsToBytesEmpty(t *testing.T) {
+ originalOptions := []byte{99, 130, 83, 99}
+ options, err := OptionsFromBytes(originalOptions)
+ if err != nil {
+ t.Fatal(err)
+ }
+ finalOptions := OptionsToBytes(options)
+ if !bytes.Equal(originalOptions, finalOptions) {
+ t.Fatalf("Invalid options. Expected %v, got %v", originalOptions, finalOptions)
+ }
+}
+
+func TestOptionsToStringPad(t *testing.T) {
+ option := []byte{0}
+ opt, err := ParseOption(option)
+ if err != nil {
+ t.Fatal(err)
+ }
+ stropt := opt.String()
+ if stropt != "Pad -> []" {
+ t.Fatalf("Invalid string representation: %v", stropt)
+ }
+}
+
+func TestOptionsToStringDHCPMessageType(t *testing.T) {
+ option := []byte{53, 1, 5}
+ opt, err := ParseOption(option)
+ if err != nil {
+ t.Fatal(err)
+ }
+ stropt := opt.String()
+ if stropt != "DHCP Message Type -> [5]" {
+ t.Fatalf("Invalid string representation: %v", stropt)
+ }
+}
diff --git a/dhcpv4/types.go b/dhcpv4/types.go
new file mode 100644
index 0000000..c0fcfe1
--- /dev/null
+++ b/dhcpv4/types.go
@@ -0,0 +1,345 @@
+package dhcpv4
+
+// values from http://www.networksorcery.com/enp/protocol/dhcp.htm and
+// http://www.networksorcery.com/enp/protocol/bootp/options.htm
+
+type OpcodeType uint8
+
+const (
+ _ OpcodeType = iota // skip 0
+ OpcodeBootRequest
+ OpcodeBootReply
+)
+
+var OpcodeToString = map[OpcodeType]string{
+ OpcodeBootRequest: "BootRequest",
+ OpcodeBootReply: "BootReply",
+}
+
+// DHCPv4 Options
+const (
+ OptionPad OptionCode = 0
+ OptionSubnetMask = 1
+ OptionTimeOffset = 2
+ OptionRouter = 3
+ OptionTimeServer = 4
+ OptionNameServer = 5
+ OptionDomainNameServer = 6
+ OptionLogServer = 7
+ OptionQuoteServer = 8
+ OptionLPRServer = 9
+ OptionImpressServer = 10
+ OptionResourceLocationServer = 11
+ OptionHostName = 12
+ OptionBootFileSize = 13
+ OptionMeritDumpFile = 14
+ OptionDomainName = 15
+ OptionSwapServer = 16
+ OptionRootPath = 17
+ OptionExtensionsPath = 18
+ OptionIPForwarding = 19
+ OptionNonLocalSourceRouting = 20
+ OptionPolicyFilter = 21
+ OptionMaximumDatagramAssemblySize = 22
+ OptionDefaultIPTTL = 23
+ OptionPathMTUAgingTimeout = 24
+ OptionPathMTUPlateauTable = 25
+ OptionInterfaceMTU = 26
+ OptionAllSubnetsAreLocal = 27
+ OptionBroadcastAddress = 28
+ OptionPerformMaskDiscovery = 29
+ OptionMaskSupplier = 30
+ OptionPerformRouterDiscovery = 31
+ OptionRouterSolicitationAddress = 32
+ OptionStaticRoutingTable = 33
+ OptionTrailerEncapsulation = 34
+ OptionArpCacheTimeout = 35
+ OptionEthernetEncapsulation = 36
+ OptionDefaulTCPTTL = 37
+ OptionTCPKeepaliveInterval = 38
+ OptionTCPKeepaliveGarbage = 39
+ OptionNetworkInformationServiceDomain = 40
+ OptionNetworkInformationServers = 41
+ OptionNTPServers = 42
+ OptionVendorSpecificInformation = 43
+ OptionNetBIOSOverTCPIPNameServer = 44
+ OptionNetBIOSOverTCPIPDatagramDistributionServer = 45
+ OptionNetBIOSOverTCPIPNodeType = 46
+ OptionNetBIOSOverTCPIPScope = 47
+ OptionXWindowSystemFontServer = 48
+ OptionXWindowSystemDisplayManger = 49
+ OptionRequestedIPAddress = 50
+ OptionIPAddressLeaseTime = 51
+ OptionOptionOverload = 52
+ OptionDHCPMessageType = 53
+ OptionServerIdentifier = 54
+ OptionParameterRequestList = 55
+ OptionMessage = 56
+ OptionMaximumDHCPMessageSize = 57
+ OptionRenewTimeValue = 58
+ OptionRebindingTimeValue = 59
+ OptionClassIdentifier = 60
+ OptionClientIdentifier = 61
+ OptionNetWareIPDomainName = 62
+ OptionNetWareIPInformation = 63
+ OptionNetworkInformationServicePlusDomain = 64
+ OptionNetworkInformationServicePlusServers = 65
+ OptionTFTPServerName = 66
+ OptionBootfileName = 67
+ OptionMobileIPHomeAgent = 68
+ OptionSimpleMailTransportProtocolServer = 69
+ OptionPostOfficeProtocolServer = 70
+ OptionNetworkNewsTransportProtocolServer = 71
+ OptionDefaultWorldWideWebServer = 72
+ OptionDefaultFingerServer = 73
+ OptionDefaultInternetRelayChatServer = 74
+ OptionStreetTalkServer = 75
+ OptionStreetTalkDirectoryAssistanceServer = 76
+ OptionUserClassInformation = 77
+ OptionSLPDirectoryAgent = 78
+ OptionSLPServiceScope = 79
+ OptionRapidCommit = 80
+ OptionFQDN = 81
+ OptionRelayAgentInformation = 82
+ OptionInternetStorageNameService = 83
+ // Option 84 returned in RFC 3679
+ OptionNDSServers = 85
+ OptionNDSTreeName = 86
+ OptionNDSContext = 87
+ OptionBCMCSControllerDomainNameList = 88
+ OptionBCMCSControllerIPv4AddressList = 89
+ OptionAuthentication = 90
+ OptionClientLastTransactionTime = 91
+ OptionAssociatedIP = 92
+ OptionClientSystemArchitectureType = 93
+ OptionClientNetworkInterfaceIdentifier = 94
+ OptionLDAP = 95
+ // Option 96 returned in RFC 3679
+ OptionClientMachineIdentifier = 97
+ OptionOpenGroupUserAuthentication = 98
+ OptionGeoConfCivic = 99
+ OptionIEEE10031TZString = 100
+ OptionReferenceToTZDatabase = 101
+ // Options 102-111 returned in RFC 3679
+ OptionNetInfoParentServerAddress = 112
+ OptionNetInfoParentServerTag = 113
+ OptionURL = 114
+ // Option 115 returned in RFC 3679
+ OptionAutoConfigure = 116
+ OptionNameServiceSearch = 117
+ OptionSubnetSelection = 118
+ OptionDNSDomainSearchList = 119
+ OptionSIPServersDHCPOption = 120
+ OptionClasslessStaticRouteOption = 121
+ OptionCCC = 122
+ OptionGeoConf = 123
+ OptionVendorIdentifyingVendorClass = 124
+ OptionVendorIdentifyingVendorSpecific = 125
+ // Options 126-127 returned in RFC 3679
+ OptionTFTPServerIPAddress = 128
+ OptionCallServerIPAddress = 129
+ OptionDiscriminationString = 130
+ OptionRemoteStatisticsServerIPAddress = 131
+ Option8021PVLANID = 132
+ Option8021QL2Priority = 133
+ OptionDiffservCodePoint = 134
+ OptionHTTPProxyForPhoneSpecificApplications = 135
+ OptionPANAAuthenticationAgent = 136
+ OptionLoSTServer = 137
+ OptionCAPWAPAccessControllerAddresses = 138
+ OptionOPTIONIPv4AddressMoS = 139
+ OptionOPTIONIPv4FQDNMoS = 140
+ OptionSIPUAConfigurationServiceDomains = 141
+ OptionOPTIONIPv4AddressANDSF = 142
+ OptionOPTIONIPv6AddressANDSF = 143
+ // Options 144-149 returned in RFC 3679
+ OptionTFTPServerAddress = 150
+ OptionStatusCode = 151
+ OptionBaseTime = 152
+ OptionStartTimeOfState = 153
+ OptionQueryStartTime = 154
+ OptionQueryEndTime = 155
+ OptionDHCPState = 156
+ OptionDataSource = 157
+ // Options 158-174 returned in RFC 3679
+ OptionEtherboot = 175
+ OptionIPTelephone = 176
+ OptionEtherbootPacketCableAndCableHome = 177
+ // Options 178-207 returned in RFC 3679
+ OptionPXELinuxMagicString = 208
+ OptionPXELinuxConfigFile = 209
+ OptionPXELinuxPathPrefix = 210
+ OptionPXELinuxRebootTime = 211
+ OptionOPTION6RD = 212
+ OptionOPTIONv4AccessDomain = 213
+ // Options 214-219 returned in RFC 3679
+ OptionSubnetAllocation = 220
+ OptionVirtualSubnetAllocation = 221
+ // Options 222-223 returned in RFC 3679
+ // Options 224-254 are reserved for private use
+ OptionEnd = 255
+)
+
+var OptionCodeToString = map[OptionCode]string{
+ OptionPad: "Pad",
+ OptionSubnetMask: "Subnet Mask",
+ OptionTimeOffset: "Time Offset",
+ OptionRouter: "Router",
+ OptionTimeServer: "Time Server",
+ OptionNameServer: "Name Server",
+ OptionDomainNameServer: "Domain Name Server",
+ OptionLogServer: "Log Server",
+ OptionQuoteServer: "Quote Server",
+ OptionLPRServer: "LPR Server",
+ OptionImpressServer: "Impress Server",
+ OptionResourceLocationServer: "Resource Location Server",
+ OptionHostName: "Host Name",
+ OptionBootFileSize: "Boot File Size",
+ OptionMeritDumpFile: "Merit Dump File",
+ OptionDomainName: "Domain Name",
+ OptionSwapServer: "Swap Server",
+ OptionRootPath: "Root Path",
+ OptionExtensionsPath: "Extensions Path",
+ OptionIPForwarding: "IP Forwarding enable/disable",
+ OptionNonLocalSourceRouting: "Non-local Source Routing enable/disable",
+ OptionPolicyFilter: "Policy Filter",
+ OptionMaximumDatagramAssemblySize: "Maximum Datagram Reassembly Size",
+ OptionDefaultIPTTL: "Default IP Time-to-live",
+ OptionPathMTUAgingTimeout: "Path MTU Aging Timeout",
+ OptionPathMTUPlateauTable: "Path MTU Plateau Table",
+ OptionInterfaceMTU: "Interface MTU",
+ OptionAllSubnetsAreLocal: "All Subnets Are Local",
+ OptionBroadcastAddress: "Broadcast Address",
+ OptionPerformMaskDiscovery: "Perform Mask Discovery",
+ OptionMaskSupplier: "Mask Supplier",
+ OptionPerformRouterDiscovery: "Perform Router Discovery",
+ OptionRouterSolicitationAddress: "Router Solicitation Address",
+ OptionStaticRoutingTable: "Static Routing Table",
+ OptionTrailerEncapsulation: "Trailer Encapsulation",
+ OptionArpCacheTimeout: "ARP Cache Timeout",
+ OptionEthernetEncapsulation: "Ethernet Encapsulation",
+ OptionDefaulTCPTTL: "Default TCP TTL",
+ OptionTCPKeepaliveInterval: "TCP Keepalive Interval",
+ OptionTCPKeepaliveGarbage: "TCP Keepalive Garbage",
+ OptionNetworkInformationServiceDomain: "Network Information Service Domain",
+ OptionNetworkInformationServers: "Network Information Servers",
+ OptionNTPServers: "NTP Servers",
+ OptionVendorSpecificInformation: "Vendor Specific Information",
+ OptionNetBIOSOverTCPIPNameServer: "NetBIOS over TCP/IP Name Server",
+ OptionNetBIOSOverTCPIPDatagramDistributionServer: "NetBIOS over TCP/IP Datagram Distribution Server",
+ OptionNetBIOSOverTCPIPNodeType: "NetBIOS over TCP/IP Node Type",
+ OptionNetBIOSOverTCPIPScope: "NetBIOS over TCP/IP Scope",
+ OptionXWindowSystemFontServer: "X Window System Font Server",
+ OptionXWindowSystemDisplayManger: "X Window System Display Manager",
+ OptionRequestedIPAddress: "Requested IP Address",
+ OptionIPAddressLeaseTime: "IP Addresses Lease Time",
+ OptionOptionOverload: "Option Overload",
+ OptionDHCPMessageType: "DHCP Message Type",
+ OptionServerIdentifier: "Server Identifier",
+ OptionParameterRequestList: "Parameter Request List",
+ OptionMessage: "Message",
+ OptionMaximumDHCPMessageSize: "Maximum DHCP Message Size",
+ OptionRenewTimeValue: "Renew Time Value",
+ OptionRebindingTimeValue: "Rebinding Time Value",
+ OptionClassIdentifier: "Class Identifier",
+ OptionClientIdentifier: "Client identifier",
+ OptionNetWareIPDomainName: "NetWare/IP Domain Name",
+ OptionNetWareIPInformation: "NetWare/IP Information",
+ OptionNetworkInformationServicePlusDomain: "Network Information Service+ Domain",
+ OptionNetworkInformationServicePlusServers: "Network Information Service+ Servers",
+ OptionTFTPServerName: "TFTP Server Name",
+ OptionBootfileName: "Bootfile Name",
+ OptionMobileIPHomeAgent: "Mobile IP Home Agent",
+ OptionSimpleMailTransportProtocolServer: "SMTP Server",
+ OptionPostOfficeProtocolServer: "POP Server",
+ OptionNetworkNewsTransportProtocolServer: "NNTP Server",
+ OptionDefaultWorldWideWebServer: "Default WWW Server",
+ OptionDefaultFingerServer: "Default Finger Server",
+ OptionDefaultInternetRelayChatServer: "Default IRC Server",
+ OptionStreetTalkServer: "StreetTalk Server",
+ OptionStreetTalkDirectoryAssistanceServer: "StreetTalk Directory Assistance Server",
+ OptionUserClassInformation: "User Class Information",
+ OptionSLPDirectoryAgent: "SLP DIrectory Agent",
+ OptionSLPServiceScope: "SLP Service Scope",
+ OptionRapidCommit: "Rapid Commit",
+ OptionFQDN: "FQDN",
+ OptionRelayAgentInformation: "Relay Agent Information",
+ OptionInternetStorageNameService: "Internet Storage Name Service",
+ // Option 84 returned in RFC 3679
+ OptionNDSServers: "NDS Servers",
+ OptionNDSTreeName: "NDS Tree Name",
+ OptionNDSContext: "NDS Context",
+ OptionBCMCSControllerDomainNameList: "BCMCS Controller Domain Name List",
+ OptionBCMCSControllerIPv4AddressList: "BCMCS Controller IPv4 Address List",
+ OptionAuthentication: "Authentication",
+ OptionClientLastTransactionTime: "Client Last Transaction Time",
+ OptionAssociatedIP: "Associated IP",
+ OptionClientSystemArchitectureType: "Client System Architecture Type",
+ OptionClientNetworkInterfaceIdentifier: "Client Network Interface Identifier",
+ OptionLDAP: "LDAP",
+ // Option 96 returned in RFC 3679
+ OptionClientMachineIdentifier: "Client Machine Identifier",
+ OptionOpenGroupUserAuthentication: "OpenGroup's User Authentication",
+ OptionGeoConfCivic: "GEOCONF_CIVIC",
+ OptionIEEE10031TZString: "IEEE 1003.1 TZ String",
+ OptionReferenceToTZDatabase: "Reference to the TZ Database",
+ // Options 102-111 returned in RFC 3679
+ OptionNetInfoParentServerAddress: "NetInfo Parent Server Address",
+ OptionNetInfoParentServerTag: "NetInfo Parent Server Tag",
+ OptionURL: "URL",
+ // Option 115 returned in RFC 3679
+ OptionAutoConfigure: "Auto-Configure",
+ OptionNameServiceSearch: "Name Service Search",
+ OptionSubnetSelection: "Subnet Selection",
+ OptionDNSDomainSearchList: "DNS Domain Search List",
+ OptionSIPServersDHCPOption: "SIP Servers DHCP Option",
+ OptionClasslessStaticRouteOption: "Classless Static Route Option",
+ OptionCCC: "CCC, CableLabs Client Configuration",
+ OptionGeoConf: "GeoConf",
+ OptionVendorIdentifyingVendorClass: "Vendor-Identifying Vendor Class",
+ OptionVendorIdentifyingVendorSpecific: "Vendor-Identifying Vendor-Specific",
+ // Options 126-127 returned in RFC 3679
+ OptionTFTPServerIPAddress: "TFTP Server IP Address",
+ OptionCallServerIPAddress: "Call Server IP Address",
+ OptionDiscriminationString: "Discrimination String",
+ OptionRemoteStatisticsServerIPAddress: "RemoteStatistics Server IP Address",
+ Option8021PVLANID: "802.1P VLAN ID",
+ Option8021QL2Priority: "802.1Q L2 Priority",
+ OptionDiffservCodePoint: "Diffserv Code Point",
+ OptionHTTPProxyForPhoneSpecificApplications: "HTTP Proxy for phone-specific applications",
+ OptionPANAAuthenticationAgent: "PANA Authentication Agent",
+ OptionLoSTServer: "LoST Server",
+ OptionCAPWAPAccessControllerAddresses: "CAPWAP Access Controller Addresses",
+ OptionOPTIONIPv4AddressMoS: "OPTION-IPv4_Address-MoS",
+ OptionOPTIONIPv4FQDNMoS: "OPTION-IPv4_FQDN-MoS",
+ OptionSIPUAConfigurationServiceDomains: "SIP UA Configuration Service Domains",
+ OptionOPTIONIPv4AddressANDSF: "OPTION-IPv4_Address-ANDSF",
+ OptionOPTIONIPv6AddressANDSF: "OPTION-IPv6_Address-ANDSF",
+ // Options 144-149 returned in RFC 3679
+ OptionTFTPServerAddress: "TFTP Server Address",
+ OptionStatusCode: "Status Code",
+ OptionBaseTime: "Base Time",
+ OptionStartTimeOfState: "Start Time of State",
+ OptionQueryStartTime: "Query Start Time",
+ OptionQueryEndTime: "Query End Time",
+ OptionDHCPState: "DHCP Staet",
+ OptionDataSource: "Data Source",
+ // Options 158-174 returned in RFC 3679
+ OptionEtherboot: "Etherboot",
+ OptionIPTelephone: "IP Telephone",
+ OptionEtherbootPacketCableAndCableHome: "Etherboot / PacketCable and CableHome",
+ // Options 178-207 returned in RFC 3679
+ OptionPXELinuxMagicString: "PXELinux Magic String",
+ OptionPXELinuxConfigFile: "PXELinux Config File",
+ OptionPXELinuxPathPrefix: "PXELinux Path Prefix",
+ OptionPXELinuxRebootTime: "PXELinux Reboot Time",
+ OptionOPTION6RD: "OPTION_6RD",
+ OptionOPTIONv4AccessDomain: "OPTION_V4_ACCESS_DOMAIN",
+ // Options 214-219 returned in RFC 3679
+ OptionSubnetAllocation: "Subnet Allocation",
+ OptionVirtualSubnetAllocation: "Virtual Subnet Selection",
+ // Options 222-223 returned in RFC 3679
+ // Options 224-254 are reserved for private use
+
+ OptionEnd: "End",
+}
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",
+}
diff --git a/iana/hwtypes.go b/iana/hwtypes.go
new file mode 100644
index 0000000..7467a78
--- /dev/null
+++ b/iana/hwtypes.go
@@ -0,0 +1,80 @@
+package iana
+
+type HwTypeType uint8
+
+const (
+ _ HwTypeType = iota // skip 0
+ HwTypeEthernet
+ HwTypeExperimentalEthernet
+ HwTypeAmateurRadioAX25
+ HwTypeProteonTokenRing
+ HwTypeChaos
+ HwTypeIEEE802
+ HwTypeARCNET
+ HwTypeHyperchannel
+ HwTypeLanstar
+ HwTypeAutonet
+ HwTypeLocalTalk
+ HwTypeLocalNet
+ HwTypeUltraLink
+ HwTypeSMDS
+ HwTypeFrameRelay
+ HwTypeATM
+ HwTypeHDLC
+ HwTypeFibreChannel
+ HwTypeATM2
+ HwTypeSerialLine
+ HwTypeATM3
+ HwTypeMILSTD188220
+ HwTypeMetricom
+ HwTypeIEEE1394
+ HwTypeMAPOS
+ HwTypeTwinaxial
+ HwTypeEUI64
+ HwTypeHIPARP
+ HwTypeISO7816
+ HwTypeARPSec
+ HwTypeIPsec
+ HwTypeInfiniband
+ HwTypeCAI
+ HwTypeWiegandInterface
+ HwTypePureIP
+)
+
+var HwTypeToString = map[HwTypeType]string{
+ HwTypeEthernet: "Ethernet",
+ HwTypeExperimentalEthernet: "Experimental Ethernet",
+ HwTypeAmateurRadioAX25: "Amateur Radio AX.25",
+ HwTypeProteonTokenRing: "Proteon ProNET Token Ring",
+ HwTypeChaos: "Chaos",
+ HwTypeIEEE802: "IEEE 802",
+ HwTypeARCNET: "ARCNET",
+ HwTypeHyperchannel: "Hyperchannel",
+ HwTypeLanstar: "Lanstar",
+ HwTypeAutonet: "Autonet Short Address",
+ HwTypeLocalTalk: "LocalTalk",
+ HwTypeLocalNet: "LocalNet",
+ HwTypeUltraLink: "Ultra link",
+ HwTypeSMDS: "SMDS",
+ HwTypeFrameRelay: "Frame Relay",
+ HwTypeATM: "ATM",
+ HwTypeHDLC: "HDLC",
+ HwTypeFibreChannel: "Fibre Channel",
+ HwTypeATM2: "ATM 2",
+ HwTypeSerialLine: "Serial Line",
+ HwTypeATM3: "ATM 3",
+ HwTypeMILSTD188220: "MIL-STD-188-220",
+ HwTypeMetricom: "Metricom",
+ HwTypeIEEE1394: "IEEE 1394.1995",
+ HwTypeMAPOS: "MAPOS",
+ HwTypeTwinaxial: "Twinaxial",
+ HwTypeEUI64: "EUI-64",
+ HwTypeHIPARP: "HIPARP",
+ HwTypeISO7816: "IP and ARP over ISO 7816-3",
+ HwTypeARPSec: "ARPSec",
+ HwTypeIPsec: "IPsec tunnel",
+ HwTypeInfiniband: "Infiniband",
+ HwTypeCAI: "CAI, TIA-102 Project 125 Common Air Interface",
+ HwTypeWiegandInterface: "Wiegand Interface",
+ HwTypePureIP: "Pure IP",
+}