summaryrefslogtreecommitdiffhomepage
path: root/dhcpv4
diff options
context:
space:
mode:
Diffstat (limited to 'dhcpv4')
-rw-r--r--dhcpv4/client.go17
-rw-r--r--dhcpv4/dhcpv4.go24
-rw-r--r--dhcpv4/dhcpv4_test.go35
-rw-r--r--dhcpv4/modifiers.go49
-rw-r--r--dhcpv4/modifiers_test.go69
-rw-r--r--dhcpv4/option_archtype.go109
-rw-r--r--dhcpv4/option_archtype_test.go68
-rw-r--r--dhcpv4/option_bootfile_name.go2
-rw-r--r--dhcpv4/option_bootfile_name_test.go5
-rw-r--r--dhcpv4/option_domain_name.go6
-rw-r--r--dhcpv4/option_domain_name_server.go2
-rw-r--r--dhcpv4/option_domain_search.go63
-rw-r--r--dhcpv4/option_domain_search_test.go37
-rw-r--r--dhcpv4/option_host_name.go49
-rw-r--r--dhcpv4/option_host_name_test.go41
-rw-r--r--dhcpv4/option_ip_address_lease_time.go57
-rw-r--r--dhcpv4/option_ip_address_lease_time_test.go41
-rw-r--r--dhcpv4/option_maximum_dhcp_message_size.go4
-rw-r--r--dhcpv4/option_maximum_dhcp_message_size_test.go2
-rw-r--r--dhcpv4/option_ntp_servers.go70
-rw-r--r--dhcpv4/option_ntp_servers_test.go65
-rw-r--r--dhcpv4/option_router.go70
-rw-r--r--dhcpv4/option_router_test.go65
-rw-r--r--dhcpv4/option_subnet_mask.go56
-rw-r--r--dhcpv4/option_subnet_mask_test.go44
-rw-r--r--dhcpv4/option_tftp_server_name.go2
-rw-r--r--dhcpv4/option_tftp_server_name_test.go5
-rw-r--r--dhcpv4/option_userclass.go38
-rw-r--r--dhcpv4/option_userclass_test.go57
-rw-r--r--dhcpv4/options.go38
-rw-r--r--dhcpv4/options_test.go72
31 files changed, 1199 insertions, 63 deletions
diff --git a/dhcpv4/client.go b/dhcpv4/client.go
index 4b10240..8a44338 100644
--- a/dhcpv4/client.go
+++ b/dhcpv4/client.go
@@ -124,8 +124,8 @@ func MakeListeningSocket(ifname string) (int, error) {
// 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(ifname string, discover *DHCPv4) ([]DHCPv4, error) {
- conversation := make([]DHCPv4, 1)
+func (c *Client) Exchange(ifname string, discover *DHCPv4, modifiers ...Modifier) ([]*DHCPv4, error) {
+ conversation := make([]*DHCPv4, 1)
var err error
// Get our file descriptor for the broadcast socket.
@@ -149,28 +149,31 @@ func (c *Client) Exchange(ifname string, discover *DHCPv4) ([]DHCPv4, error) {
return conversation, err
}
}
- conversation[0] = *discover
+ for _, mod := range modifiers {
+ discover = mod(discover)
+ }
+ conversation[0] = discover
// Offer
offer, err := BroadcastSendReceive(sfd, rfd, discover, c.ReadTimeout, c.WriteTimeout, MessageTypeOffer)
if err != nil {
return conversation, err
}
- conversation = append(conversation, *offer)
+ conversation = append(conversation, offer)
// Request
- request, err := RequestFromOffer(*offer)
+ request, err := NewRequestFromOffer(offer, modifiers...)
if err != nil {
return conversation, err
}
- conversation = append(conversation, *request)
+ conversation = append(conversation, request)
// Ack
ack, err := BroadcastSendReceive(sfd, rfd, request, c.ReadTimeout, c.WriteTimeout, MessageTypeAck)
if err != nil {
return conversation, err
}
- conversation = append(conversation, *ack)
+ conversation = append(conversation, ack)
return conversation, nil
}
diff --git a/dhcpv4/dhcpv4.go b/dhcpv4/dhcpv4.go
index eb0f467..d452f6e 100644
--- a/dhcpv4/dhcpv4.go
+++ b/dhcpv4/dhcpv4.go
@@ -38,6 +38,10 @@ type DHCPv4 struct {
options []Option
}
+// Modifier defines the signature for functions that can modify DHCPv4
+// structures. This is used to simplify packet manipulation
+type Modifier func(d *DHCPv4) *DHCPv4
+
// GetExternalIPv4Addrs obtains the currently-configured, non-loopback IPv4
// addresses from `addrs` coming from a particular interface (e.g.
// net.Interface.Addrs).
@@ -157,8 +161,8 @@ func NewInform(hwaddr net.HardwareAddr, localIP net.IP) (*DHCPv4, error) {
return d, nil
}
-// RequestFromOffer builds a DHCPv4 request from an offer.
-func RequestFromOffer(offer DHCPv4) (*DHCPv4, error) {
+// NewRequestFromOffer builds a DHCPv4 request from an offer.
+func NewRequestFromOffer(offer *DHCPv4, modifiers ...Modifier) (*DHCPv4, error) {
d, err := New()
if err != nil {
return nil, err
@@ -188,11 +192,14 @@ func RequestFromOffer(offer DHCPv4) (*DHCPv4, error) {
d.AddOption(&OptMessageType{MessageType: MessageTypeRequest})
d.AddOption(&OptRequestedIPAddress{RequestedAddr: offer.YourIPAddr()})
d.AddOption(&OptServerIdentifier{ServerID: serverIP})
+ for _, mod := range modifiers {
+ d = mod(d)
+ }
return d, nil
}
// NewReplyFromRequest builds a DHCPv4 reply from a request.
-func NewReplyFromRequest(request *DHCPv4) (*DHCPv4, error) {
+func NewReplyFromRequest(request *DHCPv4, modifiers ...Modifier) (*DHCPv4, error) {
reply, err := New()
if err != nil {
return nil, err
@@ -205,6 +212,9 @@ func NewReplyFromRequest(request *DHCPv4) (*DHCPv4, error) {
reply.SetTransactionID(request.TransactionID())
reply.SetFlags(request.Flags())
reply.SetGatewayIPAddr(request.GatewayIPAddr())
+ for _, mod := range modifiers {
+ reply = mod(reply)
+ }
return reply, nil
}
@@ -664,10 +674,10 @@ func (d *DHCPv4) ToBytes() []byte {
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.clientIPAddr.To4()...)
+ ret = append(ret, d.yourIPAddr.To4()...)
+ ret = append(ret, d.serverIPAddr.To4()...)
+ ret = append(ret, d.gatewayIPAddr.To4()...)
ret = append(ret, d.clientHwAddr[:16]...)
ret = append(ret, d.serverHostName[:64]...)
ret = append(ret, d.bootFileName[:128]...)
diff --git a/dhcpv4/dhcpv4_test.go b/dhcpv4/dhcpv4_test.go
index e0afaba..fe9fe0d 100644
--- a/dhcpv4/dhcpv4_test.go
+++ b/dhcpv4/dhcpv4_test.go
@@ -389,19 +389,19 @@ func TestStrippedOptions(t *testing.T) {
}
}
-func TestRequestFromOffer(t *testing.T) {
+func TestDHCPv4NewRequestFromOffer(t *testing.T) {
offer, err := New()
require.NoError(t, err)
offer.SetBroadcast()
offer.AddOption(&OptMessageType{MessageType: MessageTypeOffer})
- req, err := RequestFromOffer(*offer)
+ req, err := NewRequestFromOffer(offer)
require.Error(t, err)
// Now add the option so it doesn't error out.
offer.AddOption(&OptServerIdentifier{ServerID: net.IPv4(192, 168, 0, 1)})
// Broadcast request
- req, err = RequestFromOffer(*offer)
+ req, err = NewRequestFromOffer(offer)
require.NoError(t, err)
require.NotNil(t, req.MessageType())
require.Equal(t, MessageTypeRequest, *req.MessageType())
@@ -410,12 +410,25 @@ func TestRequestFromOffer(t *testing.T) {
// Unicast request
offer.SetUnicast()
- req, err = RequestFromOffer(*offer)
+ req, err = NewRequestFromOffer(offer)
require.NoError(t, err)
require.True(t, req.IsUnicast())
require.False(t, req.IsBroadcast())
}
+func TestDHCPv4NewRequestFromOfferWithModifier(t *testing.T) {
+ offer, err := New()
+ require.NoError(t, err)
+ offer.AddOption(&OptMessageType{MessageType: MessageTypeOffer})
+ offer.AddOption(&OptServerIdentifier{ServerID: net.IPv4(192, 168, 0, 1)})
+ userClass := WithUserClass([]byte("linuxboot"), false)
+ req, err := NewRequestFromOffer(offer, userClass)
+ require.NoError(t, err)
+ require.NotEqual(t, (*MessageType)(nil), *req.MessageType())
+ require.Equal(t, MessageTypeRequest, *req.MessageType())
+ require.Equal(t, "User Class Information -> linuxboot", req.options[3].String())
+}
+
func TestNewReplyFromRequest(t *testing.T) {
discover, err := New()
require.NoError(t, err)
@@ -426,7 +439,19 @@ func TestNewReplyFromRequest(t *testing.T) {
require.Equal(t, discover.GatewayIPAddr(), reply.GatewayIPAddr())
}
-func TestMessageTypeNil(t *testing.T) {
+func TestNewReplyFromRequestWithModifier(t *testing.T) {
+ discover, err := New()
+ require.NoError(t, err)
+ discover.SetGatewayIPAddr(net.IPv4(192, 168, 0, 1))
+ userClass := WithUserClass([]byte("linuxboot"), false)
+ reply, err := NewReplyFromRequest(discover, userClass)
+ require.NoError(t, err)
+ require.Equal(t, discover.TransactionID(), reply.TransactionID())
+ require.Equal(t, discover.GatewayIPAddr(), reply.GatewayIPAddr())
+ require.Equal(t, "User Class Information -> linuxboot", reply.options[0].String())
+}
+
+func TestDHCPv4MessageTypeNil(t *testing.T) {
m, err := New()
require.NoError(t, err)
require.Nil(t, m.MessageType())
diff --git a/dhcpv4/modifiers.go b/dhcpv4/modifiers.go
new file mode 100644
index 0000000..3b6ce70
--- /dev/null
+++ b/dhcpv4/modifiers.go
@@ -0,0 +1,49 @@
+package dhcpv4
+
+// WithUserClass adds a user class option to the packet.
+// The rfc parameter allows you to specify if the userclass should be
+// rfc compliant or not. More details in issue #113
+func WithUserClass(uc []byte, rfc bool) Modifier {
+ // TODO let the user specify multiple user classes
+ return func(d *DHCPv4) *DHCPv4 {
+ ouc := OptUserClass{
+ UserClasses: [][]byte{uc},
+ Rfc3004: rfc,
+ }
+ d.AddOption(&ouc)
+ return d
+ }
+}
+
+// WithNetboot adds bootfile URL and bootfile param options to a DHCPv4 packet.
+func WithNetboot(d *DHCPv4) *DHCPv4 {
+ params := d.GetOneOption(OptionParameterRequestList)
+
+ var (
+ OptParams *OptParameterRequestList
+ foundOptionTFTPServerName bool
+ foundOptionBootfileName bool
+ )
+ if params != nil {
+ OptParams = params.(*OptParameterRequestList)
+ for _, option := range OptParams.RequestedOpts {
+ if option == OptionTFTPServerName {
+ foundOptionTFTPServerName = true
+ } else if option == OptionBootfileName {
+ foundOptionBootfileName = true
+ }
+ }
+ if !foundOptionTFTPServerName {
+ OptParams.RequestedOpts = append(OptParams.RequestedOpts, OptionTFTPServerName)
+ }
+ if !foundOptionBootfileName {
+ OptParams.RequestedOpts = append(OptParams.RequestedOpts, OptionBootfileName)
+ }
+ } else {
+ OptParams = &OptParameterRequestList{
+ RequestedOpts: []OptionCode{OptionTFTPServerName, OptionBootfileName},
+ }
+ d.AddOption(OptParams)
+ }
+ return d
+}
diff --git a/dhcpv4/modifiers_test.go b/dhcpv4/modifiers_test.go
new file mode 100644
index 0000000..2f0e1ed
--- /dev/null
+++ b/dhcpv4/modifiers_test.go
@@ -0,0 +1,69 @@
+package dhcpv4
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestUserClassModifier(t *testing.T) {
+ d, _ := New()
+ userClass := WithUserClass([]byte("linuxboot"), false)
+ d = userClass(d)
+ expected := []byte{
+ 77, // OptionUserClass
+ 9, // length
+ 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
+ }
+ require.Equal(t, "User Class Information -> linuxboot", d.options[0].String())
+ require.Equal(t, expected, d.options[0].ToBytes())
+}
+
+func TestUserClassModifierRFC(t *testing.T) {
+ d, _ := New()
+ userClass := WithUserClass([]byte("linuxboot"), true)
+ d = userClass(d)
+ expected := []byte{
+ 77, // OptionUserClass
+ 10, // length
+ 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
+ }
+ require.Equal(t, "User Class Information -> linuxboot", d.options[0].String())
+ require.Equal(t, expected, d.options[0].ToBytes())
+}
+
+func TestWithNetboot(t *testing.T) {
+ d, _ := New()
+ d = WithNetboot(d)
+ require.Equal(t, "Parameter Request List -> [TFTP Server Name, Bootfile Name]", d.options[0].String())
+}
+
+func TestWithNetbootExistingTFTP(t *testing.T) {
+ d, _ := New()
+ OptParams := &OptParameterRequestList{
+ RequestedOpts: []OptionCode{OptionTFTPServerName},
+ }
+ d.AddOption(OptParams)
+ d = WithNetboot(d)
+ require.Equal(t, "Parameter Request List -> [TFTP Server Name, Bootfile Name]", d.options[0].String())
+}
+
+func TestWithNetbootExistingBootfileName(t *testing.T) {
+ d, _ := New()
+ OptParams := &OptParameterRequestList{
+ RequestedOpts: []OptionCode{OptionBootfileName},
+ }
+ d.AddOption(OptParams)
+ d = WithNetboot(d)
+ require.Equal(t, "Parameter Request List -> [Bootfile Name, TFTP Server Name]", d.options[0].String())
+}
+
+func TestWithNetbootExistingBoth(t *testing.T) {
+ d, _ := New()
+ OptParams := &OptParameterRequestList{
+ RequestedOpts: []OptionCode{OptionBootfileName, OptionTFTPServerName},
+ }
+ d.AddOption(OptParams)
+ d = WithNetboot(d)
+ require.Equal(t, "Parameter Request List -> [Bootfile Name, TFTP Server Name]", d.options[0].String())
+}
diff --git a/dhcpv4/option_archtype.go b/dhcpv4/option_archtype.go
new file mode 100644
index 0000000..16ca98d
--- /dev/null
+++ b/dhcpv4/option_archtype.go
@@ -0,0 +1,109 @@
+package dhcpv4
+
+// This option implements the Client System Architecture Type option
+// https://tools.ietf.org/html/rfc4578
+
+import (
+ "encoding/binary"
+ "fmt"
+)
+
+//ArchType encodes an architecture type in an uint16
+type ArchType uint16
+
+// see rfc4578
+const (
+ INTEL_X86PC ArchType = 0
+ NEC_PC98 ArchType = 1
+ EFI_ITANIUM ArchType = 2
+ DEC_ALPHA ArchType = 3
+ ARC_X86 ArchType = 4
+ INTEL_LEAN_CLIENT ArchType = 5
+ EFI_IA32 ArchType = 6
+ EFI_BC ArchType = 7
+ EFI_XSCALE ArchType = 8
+ EFI_X86_64 ArchType = 9
+)
+
+// ArchTypeToStringMap maps an ArchType to a mnemonic name
+var ArchTypeToStringMap = map[ArchType]string{
+ INTEL_X86PC: "Intel x86PC",
+ NEC_PC98: "NEC/PC98",
+ EFI_ITANIUM: "EFI Itanium",
+ DEC_ALPHA: "DEC Alpha",
+ ARC_X86: "Arc x86",
+ INTEL_LEAN_CLIENT: "Intel Lean Client",
+ EFI_IA32: "EFI IA32",
+ EFI_BC: "EFI BC",
+ EFI_XSCALE: "EFI Xscale",
+ EFI_X86_64: "EFI x86-64",
+}
+
+// OptClientArchType represents an option encapsulating the Client System
+// Architecture Type option Definition.
+type OptClientArchType struct {
+ ArchTypes []ArchType
+}
+
+// Code returns the option code.
+func (o *OptClientArchType) Code() OptionCode {
+ return OptionClientSystemArchitectureType
+}
+
+// ToBytes returns a serialized stream of bytes for this option.
+func (o *OptClientArchType) ToBytes() []byte {
+ ret := []byte{byte(o.Code()), byte(o.Length())}
+ for _, at := range o.ArchTypes {
+ buf := make([]byte, 2)
+ binary.BigEndian.PutUint16(buf[0:2], uint16(at))
+ ret = append(ret, buf...)
+ }
+ return ret
+}
+
+// Length returns the length of the data portion (excluding option code an byte
+// length).
+func (o *OptClientArchType) Length() int {
+ return 2*len(o.ArchTypes)
+}
+
+// String returns a human-readable string.
+func (o *OptClientArchType) String() string {
+ var archTypes string
+ for idx, at := range o.ArchTypes {
+ name, ok := ArchTypeToStringMap[at]
+ if !ok {
+ name = "Unknown"
+ }
+ archTypes += name
+ if idx < len(o.ArchTypes)-1 {
+ archTypes += ", "
+ }
+ }
+ return fmt.Sprintf("Client System Architecture Type -> %v", archTypes)
+}
+
+// ParseOptClientArchType returns a new OptClientArchType from a byte stream,
+// or error if any.
+func ParseOptClientArchType(data []byte) (*OptClientArchType, error) {
+ if len(data) < 2 {
+ return nil, ErrShortByteStream
+ }
+ code := OptionCode(data[0])
+ if code != OptionClientSystemArchitectureType {
+ return nil, fmt.Errorf("expected code %v, got %v", OptionClientSystemArchitectureType, code)
+ }
+ length := int(data[1])
+ if length == 0 || length%2 != 0 {
+ return nil, fmt.Errorf("Invalid length: expected multiple of 2 larger than 2, got %v", length)
+ }
+ if len(data) < 2+length {
+ return nil, ErrShortByteStream
+ }
+ archTypes := make([]ArchType, 0, length%2)
+ for idx := 0; idx < length; idx += 2 {
+ b := data[2+idx : 2+idx+2]
+ archTypes = append(archTypes, ArchType(binary.BigEndian.Uint16(b)))
+ }
+ return &OptClientArchType{ArchTypes: archTypes}, nil
+}
diff --git a/dhcpv4/option_archtype_test.go b/dhcpv4/option_archtype_test.go
new file mode 100644
index 0000000..cb9bc3f
--- /dev/null
+++ b/dhcpv4/option_archtype_test.go
@@ -0,0 +1,68 @@
+package dhcpv4
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestParseOptClientArchType(t *testing.T) {
+ data := []byte{
+ 93, // OptionClientSystemArchitectureType
+ 2, // Length
+ 0, 6, // EFI_IA32
+ }
+ opt, err := ParseOptClientArchType(data)
+ require.NoError(t, err)
+ require.Equal(t, opt.ArchTypes[0], EFI_IA32)
+}
+
+func TestParseOptClientArchTypeMultiple(t *testing.T) {
+ data := []byte{
+ 93, // OptionClientSystemArchitectureType
+ 4, // Length
+ 0, 6, // EFI_IA32
+ 0, 2, // EFI_ITANIUM
+ }
+ opt, err := ParseOptClientArchType(data)
+ require.NoError(t, err)
+ require.Equal(t, opt.ArchTypes[0], EFI_IA32)
+ require.Equal(t, opt.ArchTypes[1], EFI_ITANIUM)
+}
+
+func TestParseOptClientArchTypeInvalid(t *testing.T) {
+ data := []byte{42}
+ _, err := ParseOptClientArchType(data)
+ require.Error(t, err)
+}
+
+func TestOptClientArchTypeParseAndToBytes(t *testing.T) {
+ data := []byte{
+ 93, // OptionClientSystemArchitectureType
+ 2, // Length
+ 0, 8, // EFI_XSCALE
+ }
+ opt, err := ParseOptClientArchType(data)
+ require.NoError(t, err)
+ require.Equal(t, opt.ToBytes(), data)
+}
+
+func TestOptClientArchTypeParseAndToBytesMultiple(t *testing.T) {
+ data := []byte{
+ 93, // OptionClientSystemArchitectureType
+ 4, // Length
+ 0, 8, // EFI_XSCALE
+ 0, 6, // EFI_IA32
+ }
+ opt, err := ParseOptClientArchType(data)
+ require.NoError(t, err)
+ require.Equal(t, opt.ToBytes(), data)
+}
+
+func TestOptClientArchType(t *testing.T) {
+ opt := OptClientArchType{
+ ArchTypes: []ArchType{EFI_ITANIUM},
+ }
+ require.Equal(t, opt.Length(), 2)
+ require.Equal(t, opt.Code(), OptionClientSystemArchitectureType)
+}
diff --git a/dhcpv4/option_bootfile_name.go b/dhcpv4/option_bootfile_name.go
index 73ea625..ca9317b 100644
--- a/dhcpv4/option_bootfile_name.go
+++ b/dhcpv4/option_bootfile_name.go
@@ -28,7 +28,7 @@ func (op *OptBootfileName) Length() int {
}
func (op *OptBootfileName) String() string {
- return fmt.Sprintf("OptBootfileName{BootfileName=%s}", op.BootfileName)
+ return fmt.Sprintf("Bootfile Name -> %s", op.BootfileName)
}
diff --git a/dhcpv4/option_bootfile_name_test.go b/dhcpv4/option_bootfile_name_test.go
index 1f66807..0c7c200 100644
--- a/dhcpv4/option_bootfile_name_test.go
+++ b/dhcpv4/option_bootfile_name_test.go
@@ -58,3 +58,8 @@ func TestParseOptBootfileNameShortLength(t *testing.T) {
require.NoError(t, err)
require.Equal(t, []byte("linu"), opt.BootfileName)
}
+
+func TestOptBootfileNameString(t *testing.T) {
+ o := OptBootfileName{BootfileName: []byte("testy test")}
+ require.Equal(t, "Bootfile Name -> testy test", o.String())
+}
diff --git a/dhcpv4/option_domain_name.go b/dhcpv4/option_domain_name.go
index 71d7ef1..e876b05 100644
--- a/dhcpv4/option_domain_name.go
+++ b/dhcpv4/option_domain_name.go
@@ -2,10 +2,10 @@ package dhcpv4
import "fmt"
-// This option implements the server domani name option
+// This option implements the domain name option
// https://tools.ietf.org/html/rfc2132
-// OptDomainName represents an option encapsulating the server identifier.
+// OptDomainName represents an option encapsulating the domain name.
type OptDomainName struct {
DomainName string
}
@@ -13,7 +13,7 @@ type OptDomainName struct {
// ParseOptDomainName returns a new OptDomainName from a byte
// stream, or error if any.
func ParseOptDomainName(data []byte) (*OptDomainName, error) {
- if len(data) < 2 {
+ if len(data) < 3 {
return nil, ErrShortByteStream
}
code := OptionCode(data[0])
diff --git a/dhcpv4/option_domain_name_server.go b/dhcpv4/option_domain_name_server.go
index 78aaf90..470eaa0 100644
--- a/dhcpv4/option_domain_name_server.go
+++ b/dhcpv4/option_domain_name_server.go
@@ -48,7 +48,7 @@ func (o *OptDomainNameServer) Code() OptionCode {
func (o *OptDomainNameServer) ToBytes() []byte {
ret := []byte{byte(o.Code()), byte(o.Length())}
for _, ns := range o.NameServers {
- ret = append(ret, ns...)
+ ret = append(ret, ns.To4()...)
}
return ret
}
diff --git a/dhcpv4/option_domain_search.go b/dhcpv4/option_domain_search.go
new file mode 100644
index 0000000..daade0a
--- /dev/null
+++ b/dhcpv4/option_domain_search.go
@@ -0,0 +1,63 @@
+package dhcpv4
+
+// This module defines the OptDomainSearch structure.
+// https://tools.ietf.org/html/rfc3397
+
+import (
+ "fmt"
+
+ "github.com/insomniacslk/dhcp/rfc1035label"
+)
+
+// OptDomainSearch represents an option encapsulating a domain search list.
+type OptDomainSearch struct {
+ DomainSearch []string
+}
+
+// Code returns the option code.
+func (op *OptDomainSearch) Code() OptionCode {
+ return OptionDNSDomainSearchList
+}
+
+// ToBytes returns a serialized stream of bytes for this option.
+func (op *OptDomainSearch) ToBytes() []byte {
+ buf := []byte{byte(op.Code()), byte(op.Length())}
+ buf = append(buf, rfc1035label.LabelsToBytes(op.DomainSearch)...)
+ return buf
+}
+
+// Length returns the length of the data portion (excluding option code an byte
+// length).
+func (op *OptDomainSearch) Length() int {
+ var length int
+ for _, label := range op.DomainSearch {
+ length += len(label) + 2 // add the first and the last length bytes
+ }
+ return length
+}
+
+// String returns a human-readable string.
+func (op *OptDomainSearch) String() string {
+ return fmt.Sprintf("DNS Domain Search List -> %v", op.DomainSearch)
+}
+
+// ParseOptDomainSearch returns a new OptDomainSearch from a byte stream, or
+// error if any.
+func ParseOptDomainSearch(data []byte) (*OptDomainSearch, error) {
+ if len(data) < 2 {
+ return nil, ErrShortByteStream
+ }
+ code := OptionCode(data[0])
+ if code != OptionDNSDomainSearchList {
+ return nil, fmt.Errorf("expected code %v, got %v", OptionDNSDomainSearchList, code)
+ }
+ length := int(data[1])
+ if len(data) < 2+length {
+ return nil, ErrShortByteStream
+ }
+ domainSearch, err := rfc1035label.LabelsFromBytes(data[2:length+2])
+ if err != nil {
+ return nil, err
+ }
+ return &OptDomainSearch{DomainSearch: domainSearch}, nil
+}
diff --git a/dhcpv4/option_domain_search_test.go b/dhcpv4/option_domain_search_test.go
new file mode 100644
index 0000000..4848a83
--- /dev/null
+++ b/dhcpv4/option_domain_search_test.go
@@ -0,0 +1,37 @@
+package dhcpv4
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestParseOptDomainSearch(t *testing.T) {
+ data := []byte{
+ 119, // OptionDNSDomainSearchList
+ 33, // length
+ 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0,
+ 6, 's', 'u', 'b', 'n', 'e', 't', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'o', 'r', 'g', 0,
+ }
+ opt, err := ParseOptDomainSearch(data)
+ require.NoError(t, err)
+ require.Equal(t, len(opt.DomainSearch), 2)
+ require.Equal(t, opt.DomainSearch[0], "example.com")
+ require.Equal(t, opt.DomainSearch[1], "subnet.example.org")
+}
+
+func TestOptDomainSearchToBytes(t *testing.T) {
+ expected := []byte{
+ 119, // OptionDNSDomainSearchList
+ 33, // length
+ 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0,
+ 6, 's', 'u', 'b', 'n', 'e', 't', 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'o', 'r', 'g', 0,
+ }
+ opt := OptDomainSearch{
+ DomainSearch: []string{
+ "example.com",
+ "subnet.example.org",
+ },
+ }
+ require.Equal(t, opt.ToBytes(), expected)
+}
diff --git a/dhcpv4/option_host_name.go b/dhcpv4/option_host_name.go
new file mode 100644
index 0000000..a922a2b
--- /dev/null
+++ b/dhcpv4/option_host_name.go
@@ -0,0 +1,49 @@
+package dhcpv4
+
+import "fmt"
+
+// This option implements the host name option
+// https://tools.ietf.org/html/rfc2132
+
+// OptHostName represents an option encapsulating the host name.
+type OptHostName struct {
+ HostName string
+}
+
+// ParseOptHostName returns a new OptHostName from a byte stream, or error if
+// any.
+func ParseOptHostName(data []byte) (*OptHostName, error) {
+ if len(data) < 3 {
+ return nil, ErrShortByteStream
+ }
+ code := OptionCode(data[0])
+ if code != OptionHostName {
+ return nil, fmt.Errorf("expected code %v, got %v", OptionHostName, code)
+ }
+ length := int(data[1])
+ if len(data) < 2+length {
+ return nil, ErrShortByteStream
+ }
+ return &OptHostName{HostName: string(data[2 : 2+length])}, nil
+}
+
+// Code returns the option code.
+func (o *OptHostName) Code() OptionCode {
+ return OptionHostName
+}
+
+// ToBytes returns a serialized stream of bytes for this option.
+func (o *OptHostName) ToBytes() []byte {
+ return append([]byte{byte(o.Code()), byte(o.Length())}, []byte(o.HostName)...)
+}
+
+// String returns a human-readable string.
+func (o *OptHostName) String() string {
+ return fmt.Sprintf("Host Name -> %v", o.HostName)
+}
+
+// Length returns the length of the data portion (excluding option code an byte
+// length).
+func (o *OptHostName) Length() int {
+ return len(o.HostName)
+}
diff --git a/dhcpv4/option_host_name_test.go b/dhcpv4/option_host_name_test.go
new file mode 100644
index 0000000..7f99100
--- /dev/null
+++ b/dhcpv4/option_host_name_test.go
@@ -0,0 +1,41 @@
+package dhcpv4
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestOptHostNameInterfaceMethods(t *testing.T) {
+ o := OptHostName{HostName: "foo"}
+ require.Equal(t, OptionHostName, o.Code(), "Code")
+ require.Equal(t, 3, o.Length(), "Length")
+ require.Equal(t, []byte{byte(OptionHostName), 3, 'f', 'o', 'o'}, o.ToBytes(), "ToBytes")
+}
+
+func TestParseOptHostName(t *testing.T) {
+ data := []byte{byte(OptionHostName), 4, 't', 'e', 's', 't'}
+ o, err := ParseOptHostName(data)
+ require.NoError(t, err)
+ require.Equal(t, &OptHostName{HostName: "test"}, o)
+
+ // Short byte stream
+ data = []byte{byte(OptionHostName)}
+ _, err = ParseOptHostName(data)
+ require.Error(t, err, "should get error from short byte stream")
+
+ // Wrong code
+ data = []byte{54, 2, 1, 1}
+ _, err = ParseOptHostName(data)
+ require.Error(t, err, "should get error from wrong code")
+
+ // Bad length
+ data = []byte{byte(OptionHostName), 6, 1, 1, 1}
+ _, err = ParseOptHostName(data)
+ require.Error(t, err, "should get error from bad length")
+}
+
+func TestOptHostNameString(t *testing.T) {
+ o := OptHostName{HostName: "testy test"}
+ require.Equal(t, "Host Name -> testy test", o.String())
+}
diff --git a/dhcpv4/option_ip_address_lease_time.go b/dhcpv4/option_ip_address_lease_time.go
new file mode 100644
index 0000000..7562c58
--- /dev/null
+++ b/dhcpv4/option_ip_address_lease_time.go
@@ -0,0 +1,57 @@
+package dhcpv4
+
+import (
+ "encoding/binary"
+ "fmt"
+)
+
+// This option implements the IP Address Lease Time option
+// https://tools.ietf.org/html/rfc2132
+
+// OptIPAddressLeaseTime represents the IP Address Lease Time option.
+type OptIPAddressLeaseTime struct {
+ LeaseTime uint32
+}
+
+// ParseOptIPAddressLeaseTime constructs an OptIPAddressLeaseTime struct from a
+// sequence of bytes and returns it, or an error.
+func ParseOptIPAddressLeaseTime(data []byte) (*OptIPAddressLeaseTime, error) {
+ // Should at least have code, length, and lease time.
+ if len(data) < 6 {
+ return nil, ErrShortByteStream
+ }
+ code := OptionCode(data[0])
+ if code != OptionIPAddressLeaseTime {
+ return nil, fmt.Errorf("expected option %v, got %v instead", OptionIPAddressLeaseTime, code)
+ }
+ length := int(data[1])
+ if length != 4 {
+ return nil, fmt.Errorf("expected length 4, got %v instead", length)
+ }
+ leaseTime := binary.BigEndian.Uint32(data[2:6])
+ return &OptIPAddressLeaseTime{LeaseTime: leaseTime}, nil
+}
+
+// Code returns the option code.
+func (o *OptIPAddressLeaseTime) Code() OptionCode {
+ return OptionIPAddressLeaseTime
+}
+
+// ToBytes returns a serialized stream of bytes for this option.
+func (o *OptIPAddressLeaseTime) ToBytes() []byte {
+ serializedTime := make([]byte, 4)
+ binary.BigEndian.PutUint32(serializedTime, o.LeaseTime)
+ serializedOpt := []byte{byte(o.Code()), byte(o.Length())}
+ return append(serializedOpt, serializedTime...)
+}
+
+// String returns a human-readable string for this option.
+func (o *OptIPAddressLeaseTime) String() string {
+ return fmt.Sprintf("IP Addresses Lease Time -> %v", o.LeaseTime)
+}
+
+// Length returns the length of the data portion (excluding option code and byte
+// for length, if any).
+func (o *OptIPAddressLeaseTime) Length() int {
+ return 4
+}
diff --git a/dhcpv4/option_ip_address_lease_time_test.go b/dhcpv4/option_ip_address_lease_time_test.go
new file mode 100644
index 0000000..7d507bf
--- /dev/null
+++ b/dhcpv4/option_ip_address_lease_time_test.go
@@ -0,0 +1,41 @@
+package dhcpv4
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestOptIPAddressLeaseTimeInterfaceMethods(t *testing.T) {
+ o := OptIPAddressLeaseTime{LeaseTime: 43200}
+ require.Equal(t, OptionIPAddressLeaseTime, o.Code(), "Code")
+ require.Equal(t, 4, o.Length(), "Length")
+ require.Equal(t, []byte{51, 4, 0, 0, 168, 192}, o.ToBytes(), "ToBytes")
+}
+
+func TestParseOptIPAddressLeaseTime(t *testing.T) {
+ data := []byte{51, 4, 0, 0, 168, 192}
+ o, err := ParseOptIPAddressLeaseTime(data)
+ require.NoError(t, err)
+ require.Equal(t, &OptIPAddressLeaseTime{LeaseTime: 43200}, o)
+
+ // Short byte stream
+ data = []byte{51, 4, 168, 192}
+ _, err = ParseOptIPAddressLeaseTime(data)
+ require.Error(t, err, "should get error from short byte stream")
+
+ // Wrong code
+ data = []byte{54, 4, 0, 0, 168, 192}
+ _, err = ParseOptIPAddressLeaseTime(data)
+ require.Error(t, err, "should get error from wrong code")
+
+ // Bad length
+ data = []byte{51, 5, 1, 1, 1, 1, 1}
+ _, err = ParseOptIPAddressLeaseTime(data)
+ require.Error(t, err, "should get error from bad length")
+}
+
+func TestOptIPAddressLeaseTimeString(t *testing.T) {
+ o := OptIPAddressLeaseTime{LeaseTime: 43200}
+ require.Equal(t, "IP Addresses Lease Time -> 43200", o.String())
+}
diff --git a/dhcpv4/option_maximum_dhcp_message_size.go b/dhcpv4/option_maximum_dhcp_message_size.go
index 05186f5..e5fedc6 100644
--- a/dhcpv4/option_maximum_dhcp_message_size.go
+++ b/dhcpv4/option_maximum_dhcp_message_size.go
@@ -8,7 +8,7 @@ import (
// This option implements the Maximum DHCP Message size option
// https://tools.ietf.org/html/rfc2132
-// OptMaximumDHCPMessageSize represents the DHCP message type option.
+// OptMaximumDHCPMessageSize represents the Maximum DHCP Message size option.
type OptMaximumDHCPMessageSize struct {
Size uint16
}
@@ -16,7 +16,7 @@ type OptMaximumDHCPMessageSize struct {
// 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.
+ // Should at least have code, length, and message size.
if len(data) < 4 {
return nil, ErrShortByteStream
}
diff --git a/dhcpv4/option_maximum_dhcp_message_size_test.go b/dhcpv4/option_maximum_dhcp_message_size_test.go
index 65a26fc..f24b499 100644
--- a/dhcpv4/option_maximum_dhcp_message_size_test.go
+++ b/dhcpv4/option_maximum_dhcp_message_size_test.go
@@ -14,7 +14,7 @@ func TestOptMaximumDHCPMessageSizeInterfaceMethods(t *testing.T) {
}
func TestParseOptMaximumDHCPMessageSize(t *testing.T) {
- data := []byte{57, 2, 5, 220} // DISCOVER
+ data := []byte{57, 2, 5, 220}
o, err := ParseOptMaximumDHCPMessageSize(data)
require.NoError(t, err)
require.Equal(t, &OptMaximumDHCPMessageSize{Size: 1500}, o)
diff --git a/dhcpv4/option_ntp_servers.go b/dhcpv4/option_ntp_servers.go
new file mode 100644
index 0000000..39881d6
--- /dev/null
+++ b/dhcpv4/option_ntp_servers.go
@@ -0,0 +1,70 @@
+package dhcpv4
+
+import (
+ "fmt"
+ "net"
+)
+
+// This option implements the network time protocol servers option
+// https://tools.ietf.org/html/rfc2132
+
+// OptNTPServers represents an option encapsulating the NTP servers.
+type OptNTPServers struct {
+ NTPServers []net.IP
+}
+
+// ParseOptNTPServers returns a new OptNTPServers from a byte stream, or error if any.
+func ParseOptNTPServers(data []byte) (*OptNTPServers, error) {
+ if len(data) < 2 {
+ return nil, ErrShortByteStream
+ }
+ code := OptionCode(data[0])
+ if code != OptionNTPServers {
+ return nil, fmt.Errorf("expected code %v, got %v", OptionNTPServers, code)
+ }
+ length := int(data[1])
+ if length == 0 || length%4 != 0 {
+ return nil, fmt.Errorf("Invalid length: expected multiple of 4 larger than 4, got %v", length)
+ }
+ if len(data) < 2+length {
+ return nil, ErrShortByteStream
+ }
+ ntpServers := make([]net.IP, 0, length%4)
+ for idx := 0; idx < length; idx += 4 {
+ b := data[2+idx : 2+idx+4]
+ ntpServers = append(ntpServers, net.IPv4(b[0], b[1], b[2], b[3]))
+ }
+ return &OptNTPServers{NTPServers: ntpServers}, nil
+}
+
+// Code returns the option code.
+func (o *OptNTPServers) Code() OptionCode {
+ return OptionNTPServers
+}
+
+// ToBytes returns a serialized stream of bytes for this option.
+func (o *OptNTPServers) ToBytes() []byte {
+ ret := []byte{byte(o.Code()), byte(o.Length())}
+ for _, ntp := range o.NTPServers {
+ ret = append(ret, ntp.To4()...)
+ }
+ return ret
+}
+
+// String returns a human-readable string.
+func (o *OptNTPServers) String() string {
+ var ntpServers string
+ for idx, ntp := range o.NTPServers {
+ ntpServers += ntp.String()
+ if idx < len(o.NTPServers)-1 {
+ ntpServers += ", "
+ }
+ }
+ return fmt.Sprintf("NTP Servers -> %v", ntpServers)
+}
+
+// Length returns the length of the data portion (excluding option code an byte
+// length).
+func (o *OptNTPServers) Length() int {
+ return len(o.NTPServers) * 4
+}
diff --git a/dhcpv4/option_ntp_servers_test.go b/dhcpv4/option_ntp_servers_test.go
new file mode 100644
index 0000000..e7bcefd
--- /dev/null
+++ b/dhcpv4/option_ntp_servers_test.go
@@ -0,0 +1,65 @@
+package dhcpv4
+
+import (
+ "net"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestOptNTPServersInterfaceMethods(t *testing.T) {
+ ntpServers := []net.IP{
+ net.IPv4(192, 168, 0, 10),
+ net.IPv4(192, 168, 0, 20),
+ }
+ o := OptNTPServers{NTPServers: ntpServers}
+ require.Equal(t, OptionNTPServers, o.Code(), "Code")
+ require.Equal(t, net.IPv4len*len(ntpServers), o.Length(), "Length")
+ require.Equal(t, ntpServers, o.NTPServers, "NTPServers")
+}
+
+func TestParseOptNTPServers(t *testing.T) {
+ data := []byte{
+ byte(OptionNTPServers),
+ 8, // Length
+ 192, 168, 0, 10, // NTP server #1
+ 192, 168, 0, 20, // NTP server #2
+ }
+ o, err := ParseOptNTPServers(data)
+ require.NoError(t, err)
+ ntpServers := []net.IP{
+ net.IPv4(192, 168, 0, 10),
+ net.IPv4(192, 168, 0, 20),
+ }
+ require.Equal(t, &OptNTPServers{NTPServers: ntpServers}, o)
+
+ // Short byte stream
+ data = []byte{byte(OptionNTPServers)}
+ _, err = ParseOptNTPServers(data)
+ require.Error(t, err, "should get error from short byte stream")
+
+ // Wrong code
+ data = []byte{54, 2, 1, 1}
+ _, err = ParseOptNTPServers(data)
+ require.Error(t, err, "should get error from wrong code")
+
+ // Bad length
+ data = []byte{byte(OptionNTPServers), 6, 1, 1, 1}
+ _, err = ParseOptNTPServers(data)
+ require.Error(t, err, "should get error from bad length")
+}
+
+func TestParseOptNTPserversNoNTPServers(t *testing.T) {
+ // RFC2132 requires that at least one NTP server IP is specified
+ data := []byte{
+ byte(OptionNTPServers),
+ 0, // Length
+ }
+ _, err := ParseOptNTPServers(data)
+ require.Error(t, err)
+}
+
+func TestOptNTPServersString(t *testing.T) {
+ o := OptNTPServers{NTPServers: []net.IP{net.IPv4(192, 168, 0, 1), net.IPv4(192, 168, 0, 10)}}
+ require.Equal(t, "NTP Servers -> 192.168.0.1, 192.168.0.10", o.String())
+}
diff --git a/dhcpv4/option_router.go b/dhcpv4/option_router.go
new file mode 100644
index 0000000..3154edd
--- /dev/null
+++ b/dhcpv4/option_router.go
@@ -0,0 +1,70 @@
+package dhcpv4
+
+import (
+ "fmt"
+ "net"
+)
+
+// This option implements the router option
+// https://tools.ietf.org/html/rfc2132
+
+// OptRouter represents an option encapsulating the routers.
+type OptRouter struct {
+ Routers []net.IP
+}
+
+// ParseOptRouter returns a new OptRouter from a byte stream, or error if any.
+func ParseOptRouter(data []byte) (*OptRouter, error) {
+ if len(data) < 2 {
+ return nil, ErrShortByteStream
+ }
+ code := OptionCode(data[0])
+ if code != OptionRouter {
+ return nil, fmt.Errorf("expected code %v, got %v", OptionRouter, code)
+ }
+ length := int(data[1])
+ if length == 0 || length%4 != 0 {
+ return nil, fmt.Errorf("Invalid length: expected multiple of 4 larger than 4, got %v", length)
+ }
+ if len(data) < 2+length {
+ return nil, ErrShortByteStream
+ }
+ routers := make([]net.IP, 0, length%4)
+ for idx := 0; idx < length; idx += 4 {
+ b := data[2+idx : 2+idx+4]
+ routers = append(routers, net.IPv4(b[0], b[1], b[2], b[3]))
+ }
+ return &OptRouter{Routers: routers}, nil
+}
+
+// Code returns the option code.
+func (o *OptRouter) Code() OptionCode {
+ return OptionRouter
+}
+
+// ToBytes returns a serialized stream of bytes for this option.
+func (o *OptRouter) ToBytes() []byte {
+ ret := []byte{byte(o.Code()), byte(o.Length())}
+ for _, router := range o.Routers {
+ ret = append(ret, router.To4()...)
+ }
+ return ret
+}
+
+// String returns a human-readable string.
+func (o *OptRouter) String() string {
+ var routers string
+ for idx, router := range o.Routers {
+ routers += router.String()
+ if idx < len(o.Routers)-1 {
+ routers += ", "
+ }
+ }
+ return fmt.Sprintf("Routers -> %v", routers)
+}
+
+// Length returns the length of the data portion (excluding option code an byte
+// length).
+func (o *OptRouter) Length() int {
+ return len(o.Routers) * 4
+}
diff --git a/dhcpv4/option_router_test.go b/dhcpv4/option_router_test.go
new file mode 100644
index 0000000..f492c22
--- /dev/null
+++ b/dhcpv4/option_router_test.go
@@ -0,0 +1,65 @@
+package dhcpv4
+
+import (
+ "net"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestOptRoutersInterfaceMethods(t *testing.T) {
+ routers := []net.IP{
+ net.IPv4(192, 168, 0, 10),
+ net.IPv4(192, 168, 0, 20),
+ }
+ o := OptRouter{Routers: routers}
+ require.Equal(t, OptionRouter, o.Code(), "Code")
+ require.Equal(t, net.IPv4len*len(routers), o.Length(), "Length")
+ require.Equal(t, routers, o.Routers, "Routers")
+}
+
+func TestParseOptRouter(t *testing.T) {
+ data := []byte{
+ byte(OptionRouter),
+ 8, // Length
+ 192, 168, 0, 10, // Router #1
+ 192, 168, 0, 20, // Router #2
+ }
+ o, err := ParseOptRouter(data)
+ require.NoError(t, err)
+ routers := []net.IP{
+ net.IPv4(192, 168, 0, 10),
+ net.IPv4(192, 168, 0, 20),
+ }
+ require.Equal(t, &OptRouter{Routers: routers}, o)
+
+ // Short byte stream
+ data = []byte{byte(OptionRouter)}
+ _, err = ParseOptRouter(data)
+ require.Error(t, err, "should get error from short byte stream")
+
+ // Wrong code
+ data = []byte{54, 2, 1, 1}
+ _, err = ParseOptRouter(data)
+ require.Error(t, err, "should get error from wrong code")
+
+ // Bad length
+ data = []byte{byte(OptionRouter), 6, 1, 1, 1}
+ _, err = ParseOptRouter(data)
+ require.Error(t, err, "should get error from bad length")
+}
+
+func TestParseOptRouterNoRouters(t *testing.T) {
+ // RFC2132 requires that at least one Router IP is specified
+ data := []byte{
+ byte(OptionRouter),
+ 0, // Length
+ }
+ _, err := ParseOptRouter(data)
+ require.Error(t, err)
+}
+
+func TestOptRouterString(t *testing.T) {
+ o := OptRouter{Routers: []net.IP{net.IPv4(192, 168, 0, 1), net.IPv4(192, 168, 0, 10)}}
+ require.Equal(t, "Routers -> 192.168.0.1, 192.168.0.10", o.String())
+}
diff --git a/dhcpv4/option_subnet_mask.go b/dhcpv4/option_subnet_mask.go
new file mode 100644
index 0000000..f1ff4a4
--- /dev/null
+++ b/dhcpv4/option_subnet_mask.go
@@ -0,0 +1,56 @@
+package dhcpv4
+
+import (
+ "fmt"
+ "net"
+)
+
+// This option implements the subnet mask option
+// https://tools.ietf.org/html/rfc2132
+
+// OptSubnetMask represents an option encapsulating the subnet mask.
+type OptSubnetMask struct {
+ SubnetMask net.IPMask
+}
+
+// ParseOptSubnetMask returns a new OptSubnetMask from a byte
+// stream, or error if any.
+func ParseOptSubnetMask(data []byte) (*OptSubnetMask, error) {
+ if len(data) < 2 {
+ return nil, ErrShortByteStream
+ }
+ code := OptionCode(data[0])
+ if code != OptionSubnetMask {
+ return nil, fmt.Errorf("expected code %v, got %v", OptionSubnetMask, 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 &OptSubnetMask{SubnetMask: net.IPMask(data[2 : 2+length])}, nil
+}
+
+// Code returns the option code.
+func (o *OptSubnetMask) Code() OptionCode {
+ return OptionSubnetMask
+}
+
+// ToBytes returns a serialized stream of bytes for this option.
+func (o *OptSubnetMask) ToBytes() []byte {
+ ret := []byte{byte(o.Code()), byte(o.Length())}
+ return append(ret, o.SubnetMask[:4]...)
+}
+
+// String returns a human-readable string.
+func (o *OptSubnetMask) String() string {
+ return fmt.Sprintf("Subnet Mask -> %v", o.SubnetMask.String())
+}
+
+// Length returns the length of the data portion (excluding option code an byte
+// length).
+func (o *OptSubnetMask) Length() int {
+ return 4
+}
diff --git a/dhcpv4/option_subnet_mask_test.go b/dhcpv4/option_subnet_mask_test.go
new file mode 100644
index 0000000..4cb8819
--- /dev/null
+++ b/dhcpv4/option_subnet_mask_test.go
@@ -0,0 +1,44 @@
+package dhcpv4
+
+import (
+ "net"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestOptSubnetMaskInterfaceMethods(t *testing.T) {
+ mask := net.IPMask{255, 255, 255, 0}
+ o := OptSubnetMask{SubnetMask: mask}
+
+ require.Equal(t, OptionSubnetMask, o.Code(), "Code")
+
+ expectedBytes := []byte{1, 4, 255, 255, 255, 0}
+ require.Equal(t, expectedBytes, o.ToBytes(), "ToBytes")
+
+ require.Equal(t, 4, o.Length(), "Length")
+
+ require.Equal(t, "Subnet Mask -> ffffff00", o.String(), "String")
+}
+
+func TestParseOptSubnetMask(t *testing.T) {
+ var (
+ o *OptSubnetMask
+ err error
+ )
+ o, err = ParseOptSubnetMask([]byte{})
+ require.Error(t, err, "empty byte stream")
+
+ o, err = ParseOptSubnetMask([]byte{1, 4, 255})
+ require.Error(t, err, "short byte stream")
+
+ o, err = ParseOptSubnetMask([]byte{1, 3, 255, 255, 255, 0})
+ require.Error(t, err, "wrong IP length")
+
+ o, err = ParseOptSubnetMask([]byte{2, 4, 255, 255, 255})
+ require.Error(t, err, "wrong option code")
+
+ o, err = ParseOptSubnetMask([]byte{1, 4, 255, 255, 255, 0})
+ require.NoError(t, err)
+ require.Equal(t, net.IPMask{255, 255, 255, 0}, o.SubnetMask)
+}
diff --git a/dhcpv4/option_tftp_server_name.go b/dhcpv4/option_tftp_server_name.go
index b4029e1..19dde21 100644
--- a/dhcpv4/option_tftp_server_name.go
+++ b/dhcpv4/option_tftp_server_name.go
@@ -28,7 +28,7 @@ func (op *OptTFTPServerName) Length() int {
}
func (op *OptTFTPServerName) String() string {
- return fmt.Sprintf("OptTFTPServerName{TFTPServerName=%s}", op.TFTPServerName)
+ return fmt.Sprintf("TFTP Server Name -> %s", op.TFTPServerName)
}
// ParseOptTFTPServerName returns a new OptTFTPServerName fomr a byte stream or error if any
diff --git a/dhcpv4/option_tftp_server_name_test.go b/dhcpv4/option_tftp_server_name_test.go
index caddf95..812210f 100644
--- a/dhcpv4/option_tftp_server_name_test.go
+++ b/dhcpv4/option_tftp_server_name_test.go
@@ -58,3 +58,8 @@ func TestParseOptTFTPServerNameShortLength(t *testing.T) {
require.NoError(t, err)
require.Equal(t, []byte("linu"), opt.TFTPServerName)
}
+
+func TestOptTFTPServerNameString(t *testing.T) {
+ o := OptTFTPServerName{TFTPServerName: []byte("testy test")}
+ require.Equal(t, "TFTP Server Name -> testy test", o.String())
+}
diff --git a/dhcpv4/option_userclass.go b/dhcpv4/option_userclass.go
index 1505dbb..d6ddabc 100644
--- a/dhcpv4/option_userclass.go
+++ b/dhcpv4/option_userclass.go
@@ -12,6 +12,7 @@ import (
// OptUserClass represents an option encapsulating User Classes.
type OptUserClass struct {
UserClasses [][]byte
+ Rfc3004 bool
}
// Code returns the option code
@@ -22,6 +23,9 @@ func (op *OptUserClass) Code() OptionCode {
// ToBytes serializes the option and returns it as a sequence of bytes
func (op *OptUserClass) ToBytes() []byte {
buf := []byte{byte(op.Code()), byte(op.Length())}
+ if !op.Rfc3004 {
+ return append(buf, op.UserClasses[0]...)
+ }
for _, uc := range op.UserClasses {
buf = append(buf, byte(len(uc)))
buf = append(buf, uc...)
@@ -32,6 +36,9 @@ func (op *OptUserClass) ToBytes() []byte {
// Length returns the option length
func (op *OptUserClass) Length() int {
ret := 0
+ if !op.Rfc3004 {
+ return len(op.UserClasses[0])
+ }
for _, uc := range op.UserClasses {
ret += 1 + len(uc)
}
@@ -39,11 +46,15 @@ func (op *OptUserClass) Length() int {
}
func (op *OptUserClass) String() string {
- ucStrings := make([]string, len(op.UserClasses))
- for _, uc := range op.UserClasses {
- ucStrings = append(ucStrings, string(uc))
+ ucStrings := make([]string, 0, len(op.UserClasses))
+ if !op.Rfc3004 {
+ ucStrings = append(ucStrings, string(op.UserClasses[0]))
+ } else {
+ for _, uc := range op.UserClasses {
+ ucStrings = append(ucStrings, string(uc))
+ }
}
- return fmt.Sprintf("OptUserClass{userclass=[%s]}", strings.Join(ucStrings, ", "))
+ return fmt.Sprintf("User Class Information -> %v", strings.Join(ucStrings, ", "))
}
// ParseOptUserClass returns a new OptUserClass from a byte stream or
@@ -51,7 +62,7 @@ func (op *OptUserClass) String() string {
func ParseOptUserClass(data []byte) (*OptUserClass, error) {
opt := OptUserClass{}
- if len(data) < 4 {
+ if len(data) < 3 {
return nil, ErrShortByteStream
}
code := OptionCode(data[0])
@@ -66,6 +77,23 @@ func ParseOptUserClass(data []byte) (*OptUserClass, error) {
totalLength, len(data))
}
+ // Check if option is Microsoft style instead of RFC compliant, issue #113
+
+ // User-class options are, according to RFC3004, supposed to contain a set
+ // of strings each with length UC_Len_i. Here we check that this is so,
+ // by seeing if all the UC_Len_i lengths are consistent with the overall
+ // option length. If the lengths don't add up, we assume that the option
+ // is a single string and non RFC3004 compliant
+ var counting int
+ for counting < totalLength {
+ // UC_Len_i does not include itself so add 1
+ counting += int(data[counting]) + 1
+ }
+ if counting != totalLength {
+ opt.UserClasses = append(opt.UserClasses, data[:totalLength])
+ return &opt, nil
+ }
+ opt.Rfc3004 = true
for i := 0; i < totalLength; {
ucLen := int(data[i])
if ucLen == 0 {
diff --git a/dhcpv4/option_userclass_test.go b/dhcpv4/option_userclass_test.go
index 5b71ea5..f6039df 100644
--- a/dhcpv4/option_userclass_test.go
+++ b/dhcpv4/option_userclass_test.go
@@ -9,16 +9,30 @@ import (
func TestOptUserClassToBytes(t *testing.T) {
opt := OptUserClass{
UserClasses: [][]byte{[]byte("linuxboot")},
+ Rfc3004: true,
}
data := opt.ToBytes()
expected := []byte{
- 77, // OPTION_USER_CLASS
+ 77, // OptionUserClass
10, // length
9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
}
require.Equal(t, expected, data)
}
+func TestOptUserClassMicrosoftToBytes(t *testing.T) {
+ opt := OptUserClass{
+ UserClasses: [][]byte{[]byte("linuxboot")},
+ }
+ data := opt.ToBytes()
+ expected := []byte{
+ 77, // OptionUserClass
+ 9, // length
+ 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
+ }
+ require.Equal(t, expected, data)
+}
+
func TestParseOptUserClassMultiple(t *testing.T) {
expected := []byte{
77, 15,
@@ -38,6 +52,36 @@ func TestParseOptUserClassNone(t *testing.T) {
require.Error(t, err)
}
+func TestParseOptUserClassMicrosoft(t *testing.T) {
+ expected := []byte{
+ 77, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
+ }
+ opt, err := ParseOptUserClass(expected)
+ require.NoError(t, err)
+ require.Equal(t, 1, len(opt.UserClasses))
+ require.Equal(t, []byte("linuxboot"), opt.UserClasses[0])
+}
+
+func TestParseOptUserClassMicrosoftShort(t *testing.T) {
+ expected := []byte{
+ 77, 1, 'l',
+ }
+ opt, err := ParseOptUserClass(expected)
+ require.NoError(t, err)
+ require.Equal(t, 1, len(opt.UserClasses))
+ require.Equal(t, []byte("l"), opt.UserClasses[0])
+}
+
+func TestParseOptUserClassMicrosoftLongerThanLength(t *testing.T) {
+ expected := []byte{
+ 77, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 'X',
+ }
+ opt, err := ParseOptUserClass(expected)
+ require.NoError(t, err)
+ require.Equal(t, 1, len(opt.UserClasses))
+ require.Equal(t, []byte("linuxboot"), opt.UserClasses[0])
+}
+
func TestParseOptUserClass(t *testing.T) {
expected := []byte{
77, 10, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
@@ -54,10 +98,11 @@ func TestOptUserClassToBytesMultiple(t *testing.T) {
[]byte("linuxboot"),
[]byte("test"),
},
+ Rfc3004: true,
}
data := opt.ToBytes()
expected := []byte{
- 77, // OPTION_USER_CLASS
+ 77, // OptionUserClass
15, // length
9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
4, 't', 'e', 's', 't',
@@ -75,14 +120,6 @@ func TestParseOptUserClassLongerThanLength(t *testing.T) {
require.Equal(t, []byte("linuxboot"), opt.UserClasses[0])
}
-func TestParseOptUserClassShorterThanLength(t *testing.T) {
- expected := []byte{
- 77, 10, 10, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
- }
- _, err := ParseOptUserClass(expected)
- require.Error(t, err)
-}
-
func TestParseOptUserClassShorterTotalLength(t *testing.T) {
expected := []byte{
77, 11, 10, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't',
diff --git a/dhcpv4/options.go b/dhcpv4/options.go
index 5e6da80..d869b7d 100644
--- a/dhcpv4/options.go
+++ b/dhcpv4/options.go
@@ -40,32 +40,46 @@ func ParseOption(data []byte) (Option, error) {
err error
)
switch OptionCode(data[0]) {
- case OptionDHCPMessageType:
- opt, err = ParseOptMessageType(data)
- case OptionParameterRequestList:
- opt, err = ParseOptParameterRequestList(data)
+ case OptionSubnetMask:
+ opt, err = ParseOptSubnetMask(data)
+ case OptionRouter:
+ opt, err = ParseOptRouter(data)
+ case OptionDomainNameServer:
+ opt, err = ParseOptDomainNameServer(data)
+ case OptionHostName:
+ opt, err = ParseOptHostName(data)
+ case OptionDomainName:
+ opt, err = ParseOptDomainName(data)
+ case OptionBroadcastAddress:
+ opt, err = ParseOptBroadcastAddress(data)
+ case OptionNTPServers:
+ opt, err = ParseOptNTPServers(data)
case OptionRequestedIPAddress:
opt, err = ParseOptRequestedIPAddress(data)
+ case OptionIPAddressLeaseTime:
+ opt, err = ParseOptIPAddressLeaseTime(data)
+ case OptionDHCPMessageType:
+ opt, err = ParseOptMessageType(data)
case OptionServerIdentifier:
opt, err = ParseOptServerIdentifier(data)
- case OptionBroadcastAddress:
- opt, err = ParseOptBroadcastAddress(data)
+ case OptionParameterRequestList:
+ opt, err = ParseOptParameterRequestList(data)
case OptionMaximumDHCPMessageSize:
opt, err = ParseOptMaximumDHCPMessageSize(data)
case OptionClassIdentifier:
opt, err = ParseOptClassIdentifier(data)
- case OptionDomainName:
- opt, err = ParseOptDomainName(data)
- case OptionDomainNameServer:
- opt, err = ParseOptDomainNameServer(data)
- case OptionVendorIdentifyingVendorClass:
- opt, err = ParseOptVIVC(data)
case OptionTFTPServerName:
opt, err = ParseOptTFTPServerName(data)
case OptionBootfileName:
opt, err = ParseOptBootfileName(data)
case OptionUserClassInformation:
opt, err = ParseOptUserClass(data)
+ case OptionClientSystemArchitectureType:
+ opt, err = ParseOptClientArchType(data)
+ case OptionVendorIdentifyingVendorClass:
+ opt, err = ParseOptVIVC(data)
+ case OptionDNSDomainSearchList:
+ opt, err = ParseOptDomainSearch(data)
default:
opt, err = ParseOptionGeneric(data)
}
diff --git a/dhcpv4/options_test.go b/dhcpv4/options_test.go
index 41ef415..0268483 100644
--- a/dhcpv4/options_test.go
+++ b/dhcpv4/options_test.go
@@ -17,20 +17,52 @@ func TestParseOption(t *testing.T) {
require.Equal(t, 4, generic.Length())
require.Equal(t, "Name Server -> [192 168 1 254]", generic.String())
- // Message type
- option = []byte{53, 1, 1}
+ // Option subnet mask
+ option = []byte{1, 4, 255, 255, 255, 0}
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, OptionSubnetMask, opt.Code(), "Code")
+ require.Equal(t, 4, opt.Length(), "Length")
require.Equal(t, option, opt.ToBytes(), "ToBytes")
- // Parameter request list
- option = []byte{55, 3, 5, 53, 61}
+ // Option router
+ option = []byte{3, 4, 192, 168, 1, 1}
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, OptionRouter, opt.Code(), "Code")
+ require.Equal(t, 4, opt.Length(), "Length")
+ require.Equal(t, option, opt.ToBytes(), "ToBytes")
+
+ // Option domain name server
+ option = []byte{6, 4, 192, 168, 1, 1}
+ opt, err = ParseOption(option)
+ require.NoError(t, err)
+ require.Equal(t, OptionDomainNameServer, opt.Code(), "Code")
+ require.Equal(t, 4, opt.Length(), "Length")
+ require.Equal(t, option, opt.ToBytes(), "ToBytes")
+
+ // Option host name
+ option = []byte{12, 4, 't', 'e', 's', 't'}
+ opt, err = ParseOption(option)
+ require.NoError(t, err)
+ require.Equal(t, OptionHostName, opt.Code(), "Code")
+ require.Equal(t, 4, opt.Length(), "Length")
+ require.Equal(t, option, opt.ToBytes(), "ToBytes")
+
+ // Option domain name
+ option = []byte{15, 4, 't', 'e', 's', 't'}
+ opt, err = ParseOption(option)
+ require.NoError(t, err)
+ require.Equal(t, OptionDomainName, opt.Code(), "Code")
+ require.Equal(t, 4, opt.Length(), "Length")
+ require.Equal(t, option, opt.ToBytes(), "ToBytes")
+
+ // Option NTP servers
+ option = []byte{42, 4, 10, 10, 10, 10}
+ opt, err = ParseOption(option)
+ require.NoError(t, err)
+ require.Equal(t, OptionNTPServers, opt.Code(), "Code")
+ require.Equal(t, 4, opt.Length(), "Length")
require.Equal(t, option, opt.ToBytes(), "ToBytes")
// Requested IP address
@@ -41,6 +73,14 @@ func TestParseOption(t *testing.T) {
require.Equal(t, 4, opt.Length(), "Length")
require.Equal(t, option, opt.ToBytes(), "ToBytes")
+ // 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")
+
// Option server ID
option = []byte{54, 4, 1, 2, 3, 4}
opt, err = ParseOption(option)
@@ -49,6 +89,14 @@ func TestParseOption(t *testing.T) {
require.Equal(t, 4, 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")
+
// Option max message size
option = []byte{57, 2, 1, 2}
opt, err = ParseOption(option)
@@ -88,6 +136,14 @@ func TestParseOption(t *testing.T) {
require.Equal(t, OptionUserClassInformation, opt.Code(), "Code")
require.Equal(t, 5, opt.Length(), "Length")
require.Equal(t, option, opt.ToBytes(), "ToBytes")
+
+ // Option client system architecture type option
+ option = []byte{93, 4, 't', 'e', 's', 't'}
+ opt, err = ParseOption(option)
+ require.NoError(t, err)
+ require.Equal(t, OptionClientSystemArchitectureType, opt.Code(), "Code")
+ require.Equal(t, 4, opt.Length(), "Length")
+ require.Equal(t, option, opt.ToBytes(), "ToBytes")
}
func TestParseOptionZeroLength(t *testing.T) {