diff options
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", +} |