diff options
Diffstat (limited to 'dhcpv4')
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) { |