diff options
author | Sean Karlage <skarlage@get9.io> | 2018-03-16 08:42:29 -0700 |
---|---|---|
committer | insomniac <insomniacslk@users.noreply.github.com> | 2018-03-16 15:42:29 +0000 |
commit | 79b8450e99efee338596f15fc1f8f88c3e42edc0 (patch) | |
tree | 9e62bedb49ca5b6b8a61188f71a9cdfd248edafd /dhcpv4 | |
parent | 1d020f7d3aba3dfe1c142c5f73d3b3b3992ad2ad (diff) |
Add more specific dhcpv4 options (#17)
Added several DHCPv4 options
Diffstat (limited to 'dhcpv4')
-rw-r--r-- | dhcpv4/bsdp/bsdp.go | 61 | ||||
-rw-r--r-- | dhcpv4/dhcpv4.go | 42 | ||||
-rw-r--r-- | dhcpv4/option_class_identifier.go | 52 | ||||
-rw-r--r-- | dhcpv4/option_class_identifier_test.go | 41 | ||||
-rw-r--r-- | dhcpv4/option_generic.go | 22 | ||||
-rw-r--r-- | dhcpv4/option_generic_test.go | 6 | ||||
-rw-r--r-- | dhcpv4/option_maximum_dhcp_message_size.go | 57 | ||||
-rw-r--r-- | dhcpv4/option_maximum_dhcp_message_size_test.go | 41 | ||||
-rw-r--r-- | dhcpv4/option_message_type.go | 57 | ||||
-rw-r--r-- | dhcpv4/option_message_type_test.go | 52 | ||||
-rw-r--r-- | dhcpv4/option_parameter_request_list.go | 60 | ||||
-rw-r--r-- | dhcpv4/option_parameter_request_list_test.go | 39 | ||||
-rw-r--r-- | dhcpv4/option_requested_ip_address.go | 57 | ||||
-rw-r--r-- | dhcpv4/option_requested_ip_address_test.go | 44 | ||||
-rw-r--r-- | dhcpv4/option_server_identifier.go | 56 | ||||
-rw-r--r-- | dhcpv4/option_server_identifier_test.go | 44 | ||||
-rw-r--r-- | dhcpv4/options.go | 41 | ||||
-rw-r--r-- | dhcpv4/options_test.go | 49 | ||||
-rw-r--r-- | dhcpv4/types.go | 12 |
19 files changed, 750 insertions, 83 deletions
diff --git a/dhcpv4/bsdp/bsdp.go b/dhcpv4/bsdp/bsdp.go index f18fdce..e93b654 100644 --- a/dhcpv4/bsdp/bsdp.go +++ b/dhcpv4/bsdp/bsdp.go @@ -213,36 +213,25 @@ func NewInformListForInterface(iface string, replyPort uint16) (*dhcpv4.DHCPv4, for _, opt := range vendorOpts { vendorOptsBytes = append(vendorOptsBytes, opt.ToBytes()...) } - d.AddOption(dhcpv4.OptionGeneric{ + d.AddOption(&dhcpv4.OptionGeneric{ OptionCode: dhcpv4.OptionVendorSpecificInformation, Data: vendorOptsBytes, }) - d.AddOption(dhcpv4.OptionGeneric{ - OptionCode: dhcpv4.OptionParameterRequestList, - Data: []byte{ - byte(dhcpv4.OptionVendorSpecificInformation), - byte(dhcpv4.OptionClassIdentifier), + d.AddOption(&dhcpv4.OptParameterRequestList{ + RequestedOpts: []dhcpv4.OptionCode{ + dhcpv4.OptionVendorSpecificInformation, + dhcpv4.OptionClassIdentifier, }, }) - - u16 := make([]byte, 2) - binary.BigEndian.PutUint16(u16, MaxDHCPMessageSize) - d.AddOption(dhcpv4.OptionGeneric{ - OptionCode: dhcpv4.OptionMaximumDHCPMessageSize, - Data: u16, - }) + d.AddOption(&dhcpv4.OptMaximumDHCPMessageSize{Size: MaxDHCPMessageSize}) vendorClassID, err := makeVendorClassIdentifier() if err != nil { return nil, err } - d.AddOption(dhcpv4.OptionGeneric{ - OptionCode: dhcpv4.OptionClassIdentifier, - Data: []byte(vendorClassID), - }) - - d.AddOption(dhcpv4.OptionGeneric{OptionCode: dhcpv4.OptionEnd}) + d.AddOption(&dhcpv4.OptClassIdentifier{Identifier: vendorClassID}) + d.AddOption(&dhcpv4.OptionGeneric{OptionCode: dhcpv4.OptionEnd}) return d, nil } @@ -296,10 +285,7 @@ func InformSelectForAck(ack dhcpv4.DHCPv4, replyPort uint16, selectedImage BootI if serverIP.To4() == nil { return nil, fmt.Errorf("could not parse server identifier from ACK") } - vendorOpts = append(vendorOpts, dhcpv4.OptionGeneric{ - OptionCode: OptionServerIdentifier, - Data: serverIP, - }) + vendorOpts = append(vendorOpts, &dhcpv4.OptServerIdentifier{ServerID: serverIP}) // Validate replyPort if requested. if needsReplyPort(replyPort) { @@ -313,32 +299,25 @@ func InformSelectForAck(ack dhcpv4.DHCPv4, replyPort uint16, selectedImage BootI if err != nil { return nil, err } - d.AddOption(dhcpv4.OptionGeneric{ - OptionCode: dhcpv4.OptionClassIdentifier, - Data: []byte(vendorClassID), - }) - d.AddOption(dhcpv4.OptionGeneric{ - OptionCode: dhcpv4.OptionParameterRequestList, - Data: []byte{ - byte(dhcpv4.OptionSubnetMask), - byte(dhcpv4.OptionRouter), - byte(dhcpv4.OptionBootfileName), - byte(dhcpv4.OptionVendorSpecificInformation), - byte(dhcpv4.OptionClassIdentifier), + d.AddOption(&dhcpv4.OptClassIdentifier{Identifier: vendorClassID}) + d.AddOption(&dhcpv4.OptParameterRequestList{ + RequestedOpts: []dhcpv4.OptionCode{ + dhcpv4.OptionSubnetMask, + dhcpv4.OptionRouter, + dhcpv4.OptionBootfileName, + dhcpv4.OptionVendorSpecificInformation, + dhcpv4.OptionClassIdentifier, }, }) - d.AddOption(dhcpv4.OptionGeneric{ - OptionCode: dhcpv4.OptionDHCPMessageType, - Data: []byte{byte(dhcpv4.MessageTypeInform)}, - }) + d.AddOption(&dhcpv4.OptMessageType{MessageType: dhcpv4.MessageTypeInform}) var vendorOptsBytes []byte for _, opt := range vendorOpts { vendorOptsBytes = append(vendorOptsBytes, opt.ToBytes()...) } - d.AddOption(dhcpv4.OptionGeneric{ + d.AddOption(&dhcpv4.OptionGeneric{ OptionCode: dhcpv4.OptionVendorSpecificInformation, Data: vendorOptsBytes, }) - d.AddOption(dhcpv4.OptionGeneric{OptionCode: dhcpv4.OptionEnd}) + d.AddOption(&dhcpv4.OptionGeneric{OptionCode: dhcpv4.OptionEnd}) return d, nil } diff --git a/dhcpv4/dhcpv4.go b/dhcpv4/dhcpv4.go index 9ab1908..6fd0e36 100644 --- a/dhcpv4/dhcpv4.go +++ b/dhcpv4/dhcpv4.go @@ -133,17 +133,13 @@ func NewDiscoveryForInterface(ifname string) (*DHCPv4, error) { d.SetHwAddrLen(uint8(len(iface.HardwareAddr))) d.SetClientHwAddr(iface.HardwareAddr) d.SetBroadcast() - d.AddOption(&OptionGeneric{ - OptionCode: OptionDHCPMessageType, - Data: []byte{byte(MessageTypeDiscover)}, - }) - d.AddOption(&OptionGeneric{ - OptionCode: OptionParameterRequestList, - Data: []byte{ - byte(OptionSubnetMask), - byte(OptionRouter), - byte(OptionDomainName), - byte(OptionDomainNameServer), + d.AddOption(&OptMessageType{MessageType: MessageTypeDiscover}) + d.AddOption(&OptParameterRequestList{ + RequestedOpts: []OptionCode{ + OptionSubnetMask, + OptionRouter, + OptionDomainName, + OptionDomainNameServer, }, }) // the End option has to be added explicitly @@ -183,10 +179,7 @@ func NewInformForInterface(ifname string, needsBroadcast bool) (*DHCPv4, error) } d.SetClientIPAddr(localIPs[0]) - d.AddOption(&OptionGeneric{ - OptionCode: OptionDHCPMessageType, - Data: []byte{byte(MessageTypeInform)}, - }) + d.AddOption(&OptMessageType{MessageType: MessageTypeDiscover}) return d, nil } @@ -212,26 +205,16 @@ func RequestFromOffer(offer DHCPv4) (*DHCPv4, error) { var serverIP []byte for _, opt := range offer.options { if opt.Code() == OptionServerIdentifier { - serverIP = make([]byte, 4) - copy(serverIP, opt.(*OptionGeneric).Data) + serverIP = opt.(*OptServerIdentifier).ServerID } } if serverIP == nil { return nil, errors.New("Missing Server IP Address in DHCP Offer") } d.SetServerIPAddr(serverIP) - d.AddOption(&OptionGeneric{ - OptionCode: OptionDHCPMessageType, - Data: []byte{byte(MessageTypeRequest)}, - }) - d.AddOption(&OptionGeneric{ - OptionCode: OptionRequestedIPAddress, - Data: offer.YourIPAddr(), - }) - d.AddOption(&OptionGeneric{ - OptionCode: OptionServerIdentifier, - Data: serverIP, - }) + d.AddOption(&OptMessageType{MessageType: MessageTypeRequest}) + d.AddOption(&OptRequestedIPAddress{RequestedAddr: offer.YourIPAddr()}) + d.AddOption(&OptServerIdentifier{ServerID: serverIP}) // the End option has to be added explicitly d.AddOption(&OptionGeneric{OptionCode: OptionEnd}) return d, nil @@ -448,6 +431,7 @@ func (d *DHCPv4) ClientHwAddr() [16]byte { return d.clientHwAddr } +// ClientHwAddrToString converts the hardware address field to a string. func (d *DHCPv4) ClientHwAddrToString() string { var ret []string for _, b := range d.clientHwAddr[:d.hwAddrLen] { diff --git a/dhcpv4/option_class_identifier.go b/dhcpv4/option_class_identifier.go new file mode 100644 index 0000000..ae5dba2 --- /dev/null +++ b/dhcpv4/option_class_identifier.go @@ -0,0 +1,52 @@ +package dhcpv4 + +import ( + "fmt" +) + +// This option implements the Class Identifier option +// https://tools.ietf.org/html/rfc2132 + +// OptClassIdentifier represents the DHCP message type option. +type OptClassIdentifier struct { + Identifier string +} + +// ParseOptClassIdentifier constructs an OptClassIdentifier struct from a sequence of +// bytes and returns it, or an error. +func ParseOptClassIdentifier(data []byte) (*OptClassIdentifier, error) { + // Should at least have code and length + if len(data) < 2 { + return nil, ErrShortByteStream + } + code := OptionCode(data[0]) + if code != OptionClassIdentifier { + return nil, fmt.Errorf("expected option %v, got %v instead", OptionClassIdentifier, code) + } + length := int(data[1]) + if len(data) < 2+length { + return nil, ErrShortByteStream + } + return &OptClassIdentifier{Identifier: string(data[2 : 2+length])}, nil +} + +// Code returns the option code. +func (o *OptClassIdentifier) Code() OptionCode { + return OptionClassIdentifier +} + +// ToBytes returns a serialized stream of bytes for this option. +func (o *OptClassIdentifier) ToBytes() []byte { + return append([]byte{byte(o.Code()), byte(o.Length())}, []byte(o.Identifier)...) +} + +// String returns a human-readable string for this option. +func (o *OptClassIdentifier) String() string { + return fmt.Sprintf("Class Identifier -> %v", o.Identifier) +} + +// Length returns the length of the data portion (excluding option code and byte +// for length, if any). +func (o *OptClassIdentifier) Length() int { + return len(o.Identifier) +} diff --git a/dhcpv4/option_class_identifier_test.go b/dhcpv4/option_class_identifier_test.go new file mode 100644 index 0000000..a6a8f21 --- /dev/null +++ b/dhcpv4/option_class_identifier_test.go @@ -0,0 +1,41 @@ +package dhcpv4 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOptClassIdentifierInterfaceMethods(t *testing.T) { + o := OptClassIdentifier{Identifier: "foo"} + require.Equal(t, OptionClassIdentifier, o.Code(), "Code") + require.Equal(t, 3, o.Length(), "Length") + require.Equal(t, []byte{60, 3, 'f', 'o', 'o'}, o.ToBytes(), "ToBytes") +} + +func TestParseOptClassIdentifier(t *testing.T) { + data := []byte{60, 4, 't', 'e', 's', 't'} // DISCOVER + o, err := ParseOptClassIdentifier(data) + require.NoError(t, err) + require.Equal(t, &OptClassIdentifier{Identifier: "test"}, o) + + // Short byte stream + data = []byte{60} + _, err = ParseOptClassIdentifier(data) + require.Error(t, err, "should get error from short byte stream") + + // Wrong code + data = []byte{54, 2, 1, 1} + _, err = ParseOptClassIdentifier(data) + require.Error(t, err, "should get error from wrong code") + + // Bad length + data = []byte{60, 6, 1, 1, 1} + _, err = ParseOptClassIdentifier(data) + require.Error(t, err, "should get error from bad length") +} + +func TestOptClassIdentifierString(t *testing.T) { + o := OptClassIdentifier{Identifier: "testy test"} + require.Equal(t, "Class Identifier -> testy test", o.String()) +} diff --git a/dhcpv4/option_generic.go b/dhcpv4/option_generic.go index 0fb57cd..0cecfd6 100644 --- a/dhcpv4/option_generic.go +++ b/dhcpv4/option_generic.go @@ -1,6 +1,7 @@ package dhcpv4 import ( + "errors" "fmt" ) @@ -12,6 +13,27 @@ type OptionGeneric struct { Data []byte } +// ParseOptionGeneric parses a bytestream and creates a new OptionGeneric from +// it, or an error. +func ParseOptionGeneric(data []byte) (*OptionGeneric, error) { + if len(data) == 0 { + return nil, errors.New("invalid zero-length bytestream") + } + var ( + length int + optionData []byte + ) + code := OptionCode(data[0]) + if code != OptionPad && code != OptionEnd { + length = int(data[1]) + if len(data) < length+2 { + return nil, fmt.Errorf("invalid data length: declared %v, actual %v", length, len(data)) + } + optionData = data[2 : length+2] + } + return &OptionGeneric{OptionCode: code, Data: optionData}, nil +} + // Code returns the generic option code. func (o OptionGeneric) Code() OptionCode { return o.OptionCode diff --git a/dhcpv4/option_generic_test.go b/dhcpv4/option_generic_test.go index 33f8481..dbc0fc1 100644 --- a/dhcpv4/option_generic_test.go +++ b/dhcpv4/option_generic_test.go @@ -6,6 +6,12 @@ import ( "github.com/stretchr/testify/require" ) +func TestParseOptionGeneric(t *testing.T) { + // Empty bytestream produces error + _, err := ParseOptionGeneric([]byte{}) + require.Error(t, err, "error from empty bytestream") +} + func TestOptionGenericCode(t *testing.T) { o := OptionGeneric{ OptionCode: OptionDHCPMessageType, diff --git a/dhcpv4/option_maximum_dhcp_message_size.go b/dhcpv4/option_maximum_dhcp_message_size.go new file mode 100644 index 0000000..05186f5 --- /dev/null +++ b/dhcpv4/option_maximum_dhcp_message_size.go @@ -0,0 +1,57 @@ +package dhcpv4 + +import ( + "encoding/binary" + "fmt" +) + +// This option implements the Maximum DHCP Message size option +// https://tools.ietf.org/html/rfc2132 + +// OptMaximumDHCPMessageSize represents the DHCP message type option. +type OptMaximumDHCPMessageSize struct { + Size uint16 +} + +// ParseOptMaximumDHCPMessageSize constructs an OptMaximumDHCPMessageSize struct from a sequence of +// bytes and returns it, or an error. +func ParseOptMaximumDHCPMessageSize(data []byte) (*OptMaximumDHCPMessageSize, error) { + // Should at least have code, length, and message type. + if len(data) < 4 { + return nil, ErrShortByteStream + } + code := OptionCode(data[0]) + if code != OptionMaximumDHCPMessageSize { + return nil, fmt.Errorf("expected option %v, got %v instead", OptionMaximumDHCPMessageSize, code) + } + length := int(data[1]) + if length != 2 { + return nil, fmt.Errorf("expected length 2, got %v instead", length) + } + msgSize := binary.BigEndian.Uint16(data[2:4]) + return &OptMaximumDHCPMessageSize{Size: msgSize}, nil +} + +// Code returns the option code. +func (o *OptMaximumDHCPMessageSize) Code() OptionCode { + return OptionMaximumDHCPMessageSize +} + +// ToBytes returns a serialized stream of bytes for this option. +func (o *OptMaximumDHCPMessageSize) ToBytes() []byte { + serializedSize := make([]byte, 2) + binary.BigEndian.PutUint16(serializedSize, o.Size) + serializedOpt := []byte{byte(o.Code()), byte(o.Length())} + return append(serializedOpt, serializedSize...) +} + +// String returns a human-readable string for this option. +func (o *OptMaximumDHCPMessageSize) String() string { + return fmt.Sprintf("Maximum DHCP Message Size -> %v", o.Size) +} + +// Length returns the length of the data portion (excluding option code and byte +// for length, if any). +func (o *OptMaximumDHCPMessageSize) Length() int { + return 2 +} diff --git a/dhcpv4/option_maximum_dhcp_message_size_test.go b/dhcpv4/option_maximum_dhcp_message_size_test.go new file mode 100644 index 0000000..65a26fc --- /dev/null +++ b/dhcpv4/option_maximum_dhcp_message_size_test.go @@ -0,0 +1,41 @@ +package dhcpv4 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOptMaximumDHCPMessageSizeInterfaceMethods(t *testing.T) { + o := OptMaximumDHCPMessageSize{Size: 1500} + require.Equal(t, OptionMaximumDHCPMessageSize, o.Code(), "Code") + require.Equal(t, 2, o.Length(), "Length") + require.Equal(t, []byte{57, 2, 5, 220}, o.ToBytes(), "ToBytes") +} + +func TestParseOptMaximumDHCPMessageSize(t *testing.T) { + data := []byte{57, 2, 5, 220} // DISCOVER + o, err := ParseOptMaximumDHCPMessageSize(data) + require.NoError(t, err) + require.Equal(t, &OptMaximumDHCPMessageSize{Size: 1500}, o) + + // Short byte stream + data = []byte{57, 2} + _, err = ParseOptMaximumDHCPMessageSize(data) + require.Error(t, err, "should get error from short byte stream") + + // Wrong code + data = []byte{54, 2, 1, 1} + _, err = ParseOptMaximumDHCPMessageSize(data) + require.Error(t, err, "should get error from wrong code") + + // Bad length + data = []byte{57, 3, 1, 1, 1} + _, err = ParseOptMaximumDHCPMessageSize(data) + require.Error(t, err, "should get error from bad length") +} + +func TestOptMaximumDHCPMessageSizeString(t *testing.T) { + o := OptMaximumDHCPMessageSize{Size: 1500} + require.Equal(t, "Maximum DHCP Message Size -> 1500", o.String()) +} diff --git a/dhcpv4/option_message_type.go b/dhcpv4/option_message_type.go new file mode 100644 index 0000000..c47f137 --- /dev/null +++ b/dhcpv4/option_message_type.go @@ -0,0 +1,57 @@ +package dhcpv4 + +import ( + "fmt" +) + +// This option implements the message type option +// https://tools.ietf.org/html/rfc2132 + +// OptMessageType represents the DHCP message type option. +type OptMessageType struct { + MessageType MessageType +} + +// ParseOptMessageType constructs an OptMessageType struct from a sequence of +// bytes and returns it, or an error. +func ParseOptMessageType(data []byte) (*OptMessageType, error) { + // Should at least have code, length, and message type. + if len(data) < 3 { + return nil, ErrShortByteStream + } + code := OptionCode(data[0]) + if code != OptionDHCPMessageType { + return nil, fmt.Errorf("expected option %v, got %v instead", OptionDHCPMessageType, code) + } + length := int(data[1]) + if length != 1 { + return nil, ErrShortByteStream + } + messageType := MessageType(data[2]) + return &OptMessageType{MessageType: messageType}, nil +} + +// Code returns the option code. +func (o *OptMessageType) Code() OptionCode { + return OptionDHCPMessageType +} + +// ToBytes returns a serialized stream of bytes for this option. +func (o *OptMessageType) ToBytes() []byte { + return []byte{byte(o.Code()), byte(o.Length()), byte(o.MessageType)} +} + +// String returns a human-readable string for this option. +func (o *OptMessageType) String() string { + s, ok := MessageTypeToString[o.MessageType] + if !ok { + s = "UNKNOWN" + } + return fmt.Sprintf("DHCP Message Type -> %s", s) +} + +// Length returns the length of the data portion (excluding option code and byte +// for length, if any). +func (o *OptMessageType) Length() int { + return 1 +} diff --git a/dhcpv4/option_message_type_test.go b/dhcpv4/option_message_type_test.go new file mode 100644 index 0000000..a8060d6 --- /dev/null +++ b/dhcpv4/option_message_type_test.go @@ -0,0 +1,52 @@ +package dhcpv4 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOptMessageTypeInterfaceMethods(t *testing.T) { + o := OptMessageType{MessageType: MessageTypeDiscover} + require.Equal(t, OptionDHCPMessageType, o.Code(), "Code") + require.Equal(t, 1, o.Length(), "Length") + require.Equal(t, []byte{53, 1, 1}, o.ToBytes(), "ToBytes") +} + +func TestOptMessageTypeNew(t *testing.T) { + o := OptMessageType{MessageType: MessageTypeDiscover} + require.Equal(t, OptionDHCPMessageType, o.Code()) + require.Equal(t, MessageTypeDiscover, o.MessageType) +} + +func TestParseOptMessageType(t *testing.T) { + data := []byte{53, 1, 1} // DISCOVER + o, err := ParseOptMessageType(data) + require.NoError(t, err) + require.Equal(t, &OptMessageType{MessageType: MessageTypeDiscover}, o) + + // Short byte stream + data = []byte{53, 1} + _, err = ParseOptMessageType(data) + require.Error(t, err, "should get error from short byte stream") + + // Wrong code + data = []byte{54, 1, 1} + _, err = ParseOptMessageType(data) + require.Error(t, err, "should get error from wrong code") + + // Bad length + data = []byte{53, 5, 1} + _, err = ParseOptMessageType(data) + require.Error(t, err, "should get error from bad length") +} + +func TestOptMessageTypeString(t *testing.T) { + // known + o := OptMessageType{MessageType: MessageTypeDiscover} + require.Equal(t, "DHCP Message Type -> DISCOVER", o.String()) + + // unknown + o = OptMessageType{MessageType: 99} + require.Equal(t, "DHCP Message Type -> UNKNOWN", o.String()) +} diff --git a/dhcpv4/option_parameter_request_list.go b/dhcpv4/option_parameter_request_list.go new file mode 100644 index 0000000..f6b0348 --- /dev/null +++ b/dhcpv4/option_parameter_request_list.go @@ -0,0 +1,60 @@ +package dhcpv4 + +import ( + "fmt" +) + +// This option implements the parameter request list option +// https://tools.ietf.org/html/rfc2132 + +// OptParameterRequestList represents the parameter request list option. +type OptParameterRequestList struct { + RequestedOpts []OptionCode +} + +// ParseOptParameterRequestList returns a new OptParameterRequestList from a +// byte stream, or error if any. +func ParseOptParameterRequestList(data []byte) (*OptParameterRequestList, error) { + // Should at least have code + length byte. + if len(data) < 2 { + return nil, ErrShortByteStream + } + code := OptionCode(data[0]) + if code != OptionParameterRequestList { + return nil, fmt.Errorf("expected code %v, got %v", OptionParameterRequestList, code) + } + length := int(data[1]) + if len(data) < length+2 { + return nil, ErrShortByteStream + } + var requestedOpts []OptionCode + for _, opt := range data[2 : length+2] { + requestedOpts = append(requestedOpts, OptionCode(opt)) + } + return &OptParameterRequestList{RequestedOpts: requestedOpts}, nil +} + +// Code returns the option code. +func (o *OptParameterRequestList) Code() OptionCode { + return OptionParameterRequestList +} + +// ToBytes returns a serialized stream of bytes for this option. +func (o *OptParameterRequestList) ToBytes() []byte { + ret := []byte{byte(o.Code()), byte(o.Length())} + for _, req := range o.RequestedOpts { + ret = append(ret, byte(req)) + } + return ret +} + +// String returns a human-readable string for this option. +func (o *OptParameterRequestList) String() string { + return fmt.Sprintf("Parameter Request List -> %v", o.RequestedOpts) +} + +// Length returns the length of the data portion (excluding option code and byte +// for length, if any). +func (o *OptParameterRequestList) Length() int { + return len(o.RequestedOpts) +} diff --git a/dhcpv4/option_parameter_request_list_test.go b/dhcpv4/option_parameter_request_list_test.go new file mode 100644 index 0000000..4441b29 --- /dev/null +++ b/dhcpv4/option_parameter_request_list_test.go @@ -0,0 +1,39 @@ +package dhcpv4 + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOptParameterRequestListInterfaceMethods(t *testing.T) { + requestedOpts := []OptionCode{OptionBootfileName, OptionNameServer} + o := &OptParameterRequestList{RequestedOpts: requestedOpts} + require.Equal(t, OptionParameterRequestList, o.Code(), "Code") + + expectedBytes := []byte{55, 2, 67, 5} + require.Equal(t, expectedBytes, o.ToBytes(), "ToBytes") + + expectedString := "Parameter Request List -> [67 5]" + require.Equal(t, expectedString, o.String(), "String") +} + +func TestParseOptParameterRequestList(t *testing.T) { + var ( + o *OptParameterRequestList + err error + ) + o, err = ParseOptParameterRequestList([]byte{}) + require.Error(t, err, "empty byte stream") + + o, err = ParseOptParameterRequestList([]byte{55, 2}) + require.Error(t, err, "short byte stream") + + o, err = ParseOptParameterRequestList([]byte{53, 2, 1, 1}) + require.Error(t, err, "wrong option code") + + o, err = ParseOptParameterRequestList([]byte{55, 2, 67, 5}) + require.NoError(t, err) + expectedOpts := []OptionCode{OptionBootfileName, OptionNameServer} + require.Equal(t, expectedOpts, o.RequestedOpts) +} diff --git a/dhcpv4/option_requested_ip_address.go b/dhcpv4/option_requested_ip_address.go new file mode 100644 index 0000000..8662263 --- /dev/null +++ b/dhcpv4/option_requested_ip_address.go @@ -0,0 +1,57 @@ +package dhcpv4 + +import ( + "fmt" + "net" +) + +// This option implements the requested IP address option +// https://tools.ietf.org/html/rfc2132 + +// OptRequestedIPAddress represents an option encapsulating the server +// identifier. +type OptRequestedIPAddress struct { + RequestedAddr net.IP +} + +// ParseOptRequestedIPAddress returns a new OptServerIdentifier from a byte +// stream, or error if any. +func ParseOptRequestedIPAddress(data []byte) (*OptRequestedIPAddress, error) { + if len(data) < 2 { + return nil, ErrShortByteStream + } + code := OptionCode(data[0]) + if code != OptionRequestedIPAddress { + return nil, fmt.Errorf("expected code %v, got %v", OptionRequestedIPAddress, code) + } + length := int(data[1]) + if length != 4 { + return nil, fmt.Errorf("unexepcted length: expected 4, got %v", length) + } + if len(data) < 6 { + return nil, ErrShortByteStream + } + return &OptRequestedIPAddress{RequestedAddr: net.IP(data[2 : 2+length])}, nil +} + +// Code returns the option code. +func (o *OptRequestedIPAddress) Code() OptionCode { + return OptionRequestedIPAddress +} + +// ToBytes returns a serialized stream of bytes for this option. +func (o *OptRequestedIPAddress) ToBytes() []byte { + ret := []byte{byte(o.Code()), byte(o.Length())} + return append(ret, o.RequestedAddr.To4()...) +} + +// String returns a human-readable string. +func (o *OptRequestedIPAddress) String() string { + return fmt.Sprintf("Requested IP Address -> %v", o.RequestedAddr.String()) +} + +// Length returns the length of the data portion (excluding option code an byte +// length). +func (o *OptRequestedIPAddress) Length() int { + return len(o.RequestedAddr.To4()) +} diff --git a/dhcpv4/option_requested_ip_address_test.go b/dhcpv4/option_requested_ip_address_test.go new file mode 100644 index 0000000..2951ebb --- /dev/null +++ b/dhcpv4/option_requested_ip_address_test.go @@ -0,0 +1,44 @@ +package dhcpv4 + +import ( + "net" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOptServerIdentifierInterfaceMethods(t *testing.T) { + ip := net.IP{192, 168, 0, 1} + o := OptServerIdentifier{ServerID: ip} + + require.Equal(t, OptionServerIdentifier, o.Code(), "Code") + + expectedBytes := []byte{54, 4, 192, 168, 0, 1} + require.Equal(t, expectedBytes, o.ToBytes(), "ToBytes") + + require.Equal(t, 4, o.Length(), "Length") + + require.Equal(t, "Server Identifier -> 192.168.0.1", o.String(), "String") +} + +func TestParseOptServerIdentifier(t *testing.T) { + var ( + o *OptServerIdentifier + err error + ) + o, err = ParseOptServerIdentifier([]byte{}) + require.Error(t, err, "empty byte stream") + + o, err = ParseOptServerIdentifier([]byte{54, 4, 192}) + require.Error(t, err, "short byte stream") + + o, err = ParseOptServerIdentifier([]byte{54, 3, 192, 168, 0, 1}) + require.Error(t, err, "wrong IP length") + + o, err = ParseOptServerIdentifier([]byte{53, 4, 192, 168, 1}) + require.Error(t, err, "wrong option code") + + o, err = ParseOptServerIdentifier([]byte{54, 4, 192, 168, 0, 1}) + require.NoError(t, err) + require.Equal(t, net.IP{192, 168, 0, 1}, o.ServerID) +} diff --git a/dhcpv4/option_server_identifier.go b/dhcpv4/option_server_identifier.go new file mode 100644 index 0000000..26c21a7 --- /dev/null +++ b/dhcpv4/option_server_identifier.go @@ -0,0 +1,56 @@ +package dhcpv4 + +import ( + "fmt" + "net" +) + +// This option implements the server identifier option +// https://tools.ietf.org/html/rfc2132 + +// OptServerIdentifier represents an option encapsulating the server identifier. +type OptServerIdentifier struct { + ServerID net.IP +} + +// ParseOptServerIdentifier returns a new OptServerIdentifier from a byte +// stream, or error if any. +func ParseOptServerIdentifier(data []byte) (*OptServerIdentifier, error) { + if len(data) < 2 { + return nil, ErrShortByteStream + } + code := OptionCode(data[0]) + if code != OptionServerIdentifier { + return nil, fmt.Errorf("expected code %v, got %v", OptionServerIdentifier, code) + } + length := int(data[1]) + if length != 4 { + return nil, fmt.Errorf("unexepcted length: expected 4, got %v", length) + } + if len(data) < 6 { + return nil, ErrShortByteStream + } + return &OptServerIdentifier{ServerID: net.IP(data[2 : 2+length])}, nil +} + +// Code returns the option code. +func (o *OptServerIdentifier) Code() OptionCode { + return OptionServerIdentifier +} + +// ToBytes returns a serialized stream of bytes for this option. +func (o *OptServerIdentifier) ToBytes() []byte { + ret := []byte{byte(o.Code()), byte(o.Length())} + return append(ret, o.ServerID.To4()...) +} + +// String returns a human-readable string. +func (o *OptServerIdentifier) String() string { + return fmt.Sprintf("Server Identifier -> %v", o.ServerID.String()) +} + +// Length returns the length of the data portion (excluding option code an byte +// length). +func (o *OptServerIdentifier) Length() int { + return len(o.ServerID.To4()) +} diff --git a/dhcpv4/option_server_identifier_test.go b/dhcpv4/option_server_identifier_test.go new file mode 100644 index 0000000..94fb542 --- /dev/null +++ b/dhcpv4/option_server_identifier_test.go @@ -0,0 +1,44 @@ +package dhcpv4 + +import ( + "net" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOptRequestedIPAddressInterfaceMethods(t *testing.T) { + ip := net.IP{192, 168, 0, 1} + o := OptRequestedIPAddress{RequestedAddr: ip} + + require.Equal(t, OptionRequestedIPAddress, o.Code(), "Code") + + expectedBytes := []byte{50, 4, 192, 168, 0, 1} + require.Equal(t, expectedBytes, o.ToBytes(), "ToBytes") + + require.Equal(t, 4, o.Length(), "Length") + + require.Equal(t, "Requested IP Address -> 192.168.0.1", o.String(), "String") +} + +func TestParseOptRequestedIPAddress(t *testing.T) { + var ( + o *OptRequestedIPAddress + err error + ) + o, err = ParseOptRequestedIPAddress([]byte{}) + require.Error(t, err, "empty byte stream") + + o, err = ParseOptRequestedIPAddress([]byte{50, 4, 192}) + require.Error(t, err, "short byte stream") + + o, err = ParseOptRequestedIPAddress([]byte{50, 3, 192, 168, 0, 1}) + require.Error(t, err, "wrong IP length") + + o, err = ParseOptRequestedIPAddress([]byte{53, 4, 192, 168, 1}) + require.Error(t, err, "wrong option code") + + o, err = ParseOptRequestedIPAddress([]byte{50, 4, 192, 168, 0, 1}) + require.NoError(t, err) + require.Equal(t, net.IP{192, 168, 0, 1}, o.RequestedAddr) +} diff --git a/dhcpv4/options.go b/dhcpv4/options.go index e4a2200..a1b5c93 100644 --- a/dhcpv4/options.go +++ b/dhcpv4/options.go @@ -6,6 +6,14 @@ import ( "fmt" ) +// ErrShortByteStream is an error that is thrown any time a short byte stream is +// detected during option parsing. +var ErrShortByteStream = errors.New("short byte stream") + +// ErrZeroLengthByteStream is an error that is thrown any time a zero-length +// byte stream is encountered. +var ErrZeroLengthByteStream = errors.New("zero-length byte stream") + // MagicCookie is the magic 4-byte value at the beginning of the list of options // in a DHCPv4 packet. var MagicCookie = []byte{99, 130, 83, 99} @@ -27,23 +35,30 @@ func ParseOption(data []byte) (Option, error) { if len(data) == 0 { return nil, errors.New("invalid zero-length DHCPv4 option") } - code := OptionCode(data[0]) var ( - length int - optionData []byte + opt Option + err error ) - if code != OptionPad && code != OptionEnd { - length = int(data[1]) - if len(data) < length+2 { - return nil, fmt.Errorf("invalid data length: declared %v, actual %v", length, len(data)) - } - optionData = data[2 : length+2] - } - - switch code { + switch OptionCode(data[0]) { + case OptionDHCPMessageType: + opt, err = ParseOptMessageType(data) + case OptionParameterRequestList: + opt, err = ParseOptParameterRequestList(data) + case OptionRequestedIPAddress: + opt, err = ParseOptRequestedIPAddress(data) + case OptionServerIdentifier: + opt, err = ParseOptServerIdentifier(data) + case OptionMaximumDHCPMessageSize: + opt, err = ParseOptMaximumDHCPMessageSize(data) + case OptionClassIdentifier: + opt, err = ParseOptClassIdentifier(data) default: - return &OptionGeneric{OptionCode: code, Data: optionData}, nil + opt, err = ParseOptionGeneric(data) + } + if err != nil { + return nil, err } + return opt, nil } // OptionsFromBytes parses a sequence of bytes until the end and builds a list diff --git a/dhcpv4/options_test.go b/dhcpv4/options_test.go index 7ee0ebe..7f4b5f8 100644 --- a/dhcpv4/options_test.go +++ b/dhcpv4/options_test.go @@ -7,6 +7,7 @@ import ( ) func TestParseOption(t *testing.T) { + // Generic option := []byte{5, 4, 192, 168, 1, 254} // DNS option opt, err := ParseOption(option) require.NoError(t, err) @@ -15,6 +16,54 @@ func TestParseOption(t *testing.T) { require.Equal(t, []byte{192, 168, 1, 254}, generic.Data) require.Equal(t, 4, generic.Length()) require.Equal(t, "Name Server -> [192 168 1 254]", generic.String()) + + // Message type + option = []byte{53, 1, 1} + opt, err = ParseOption(option) + require.NoError(t, err) + require.Equal(t, OptionDHCPMessageType, opt.Code(), "Code") + require.Equal(t, 1, opt.Length(), "Length") + require.Equal(t, option, opt.ToBytes(), "ToBytes") + + // Parameter request list + option = []byte{55, 3, 5, 53, 61} + opt, err = ParseOption(option) + require.NoError(t, err) + require.Equal(t, OptionParameterRequestList, opt.Code(), "Code") + require.Equal(t, 3, opt.Length(), "Length") + require.Equal(t, option, opt.ToBytes(), "ToBytes") + + // Requested IP address + option = []byte{50, 4, 1, 2, 3, 4} + opt, err = ParseOption(option) + require.NoError(t, err) + require.Equal(t, OptionRequestedIPAddress, opt.Code(), "Code") + require.Equal(t, 4, opt.Length(), "Length") + require.Equal(t, option, opt.ToBytes(), "ToBytes") + + // Option server ID + option = []byte{54, 4, 1, 2, 3, 4} + opt, err = ParseOption(option) + require.NoError(t, err) + require.Equal(t, OptionServerIdentifier, opt.Code(), "Code") + require.Equal(t, 4, opt.Length(), "Length") + require.Equal(t, option, opt.ToBytes(), "ToBytes") + + // Option max message size + option = []byte{57, 2, 1, 2} + opt, err = ParseOption(option) + require.NoError(t, err) + require.Equal(t, OptionMaximumDHCPMessageSize, opt.Code(), "Code") + require.Equal(t, 2, opt.Length(), "Length") + require.Equal(t, option, opt.ToBytes(), "ToBytes") + + // Option class identifier + option = []byte{60, 4, 't', 'e', 's', 't'} + opt, err = ParseOption(option) + require.NoError(t, err) + require.Equal(t, OptionClassIdentifier, opt.Code(), "Code") + require.Equal(t, 4, opt.Length(), "Length") + require.Equal(t, option, opt.ToBytes(), "ToBytes") } func TestParseOptionZeroLength(t *testing.T) { diff --git a/dhcpv4/types.go b/dhcpv4/types.go index 90ea5da..7c22bff 100644 --- a/dhcpv4/types.go +++ b/dhcpv4/types.go @@ -18,6 +18,18 @@ const ( MessageTypeInform MessageType = 8 ) +// MessageTypeToString maps DHCP message types to human-readable strings. +var MessageTypeToString = map[MessageType]string{ + MessageTypeDiscover: "DISCOVER", + MessageTypeOffer: "OFFER", + MessageTypeRequest: "REQUEST", + MessageTypeDecline: "DECLINE", + MessageTypeAck: "ACK", + MessageTypeNak: "NAK", + MessageTypeRelease: "RELEASE", + MessageTypeInform: "INFORM", +} + // OpcodeType represents a DHCPv4 opcode. type OpcodeType uint8 |