summaryrefslogtreecommitdiffhomepage
path: root/dhcpv4
diff options
context:
space:
mode:
authorSean Karlage <skarlage@get9.io>2018-03-16 08:42:29 -0700
committerinsomniac <insomniacslk@users.noreply.github.com>2018-03-16 15:42:29 +0000
commit79b8450e99efee338596f15fc1f8f88c3e42edc0 (patch)
tree9e62bedb49ca5b6b8a61188f71a9cdfd248edafd /dhcpv4
parent1d020f7d3aba3dfe1c142c5f73d3b3b3992ad2ad (diff)
Add more specific dhcpv4 options (#17)
Added several DHCPv4 options
Diffstat (limited to 'dhcpv4')
-rw-r--r--dhcpv4/bsdp/bsdp.go61
-rw-r--r--dhcpv4/dhcpv4.go42
-rw-r--r--dhcpv4/option_class_identifier.go52
-rw-r--r--dhcpv4/option_class_identifier_test.go41
-rw-r--r--dhcpv4/option_generic.go22
-rw-r--r--dhcpv4/option_generic_test.go6
-rw-r--r--dhcpv4/option_maximum_dhcp_message_size.go57
-rw-r--r--dhcpv4/option_maximum_dhcp_message_size_test.go41
-rw-r--r--dhcpv4/option_message_type.go57
-rw-r--r--dhcpv4/option_message_type_test.go52
-rw-r--r--dhcpv4/option_parameter_request_list.go60
-rw-r--r--dhcpv4/option_parameter_request_list_test.go39
-rw-r--r--dhcpv4/option_requested_ip_address.go57
-rw-r--r--dhcpv4/option_requested_ip_address_test.go44
-rw-r--r--dhcpv4/option_server_identifier.go56
-rw-r--r--dhcpv4/option_server_identifier_test.go44
-rw-r--r--dhcpv4/options.go41
-rw-r--r--dhcpv4/options_test.go49
-rw-r--r--dhcpv4/types.go12
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