diff --git a/dhcpv6/dhcpv6message.go b/dhcpv6/dhcpv6message.go index 8886c6c7..f6d72bd0 100644 --- a/dhcpv6/dhcpv6message.go +++ b/dhcpv6/dhcpv6message.go @@ -15,13 +15,96 @@ import ( const MessageHeaderSize = 4 +// NewMessageOption returns new zero-value options for a DHCPv6 Message. +// +// Options listed in RFC 8415 Appendix B & C are used for this list. +func NewMessageOption(code OptionCode) Option { + var opt Option + switch code { + // The following are listed in RFC 8415 Appendix C. + case OptionClientID: + opt = &optClientID{} + case OptionServerID: + opt = &optServerID{} + case OptionIANA: + opt = &OptIANA{} + case OptionIATA: + opt = &OptIATA{} + case OptionIAPD: + opt = &OptIAPD{} + case OptionORO: + opt = &optRequestedOption{} + case OptionElapsedTime: + opt = &optElapsedTime{} + case OptionStatusCode: + opt = &OptStatusCode{} + case OptionUserClass: + opt = &OptUserClass{} + case OptionVendorClass: + opt = &OptVendorClass{} + case OptionVendorOpts: + opt = &OptVendorOpts{} + case OptionInformationRefreshTime: + opt = &optInformationRefreshTime{} + + // RFC 3646 Section 5. + case OptionDNSRecursiveNameServer: + opt = &optDNS{} + case OptionDomainSearchList: + opt = &optDomainSearchList{} + + // RFC 4704 Section 4. + case OptionFQDN: + opt = &OptFQDN{} + + // RFC 5908 Section 5. + case OptionNTPServer: + opt = &OptNTPServer{} + + // RFC 5970 Section 4. + case OptionBootfileURL: + opt = &optBootFileURL{} + case OptionBootfileParam: + opt = &optBootFileParam{} + case OptionClientArchType: + opt = &optClientArchType{} + case OptionNII: + opt = &OptNetworkInterfaceID{} + + // RFC 7341 Section 6. Technically, this is compatible only with + // DHCPv4Query/Response message types. We don't have the ability to + // restrict by message type yet. + case OptionDHCPv4Msg: + opt = &OptDHCPv4Msg{} + case OptionDHCP4oDHCP6Server: + opt = &OptDHCP4oDHCP6Server{} + + // RFC 7600 does not explicitly specify, but we assume this is not + // valid for Relay messages. + case Option4RD: + opt = &Opt4RD{} + + // We have plenty of unimplemented options. + default: + opt = &OptionGeneric{OptionCode: code} + } + return opt +} + // MessageOptions are the options that may appear in a normal DHCPv6 message. // -// RFC 3315 Appendix B lists the valid options that can be used. +// RFC 8415 Appendix C lists the valid options that can be used. type MessageOptions struct { Options } +// FromBytes reads data into o and returns an error if the options are not a +// valid serialized representation of DHCPv6 message options per RFC 8415 +// Appendix B. +func (mo *MessageOptions) FromBytes(data []byte) error { + return mo.FromBytesWithParser(data, NewMessageOption) +} + // ArchTypes returns the architecture type option. func (mo MessageOptions) ArchTypes() iana.Archs { opt := mo.GetOne(OptionClientArchType) @@ -169,8 +252,8 @@ func (mo MessageOptions) BootFileURL() string { if opt == nil { return "" } - if u, ok := opt.(optBootFileURL); ok { - return string(u) + if u, ok := opt.(*optBootFileURL); ok { + return u.url } return "" } @@ -181,8 +264,8 @@ func (mo MessageOptions) BootFileParam() []string { if opt == nil { return nil } - if u, ok := opt.(optBootFileParam); ok { - return []string(u) + if u, ok := opt.(*optBootFileParam); ok { + return u.params } return nil } diff --git a/dhcpv6/dhcpv6relay.go b/dhcpv6/dhcpv6relay.go index 5a29c876..b8b18b11 100644 --- a/dhcpv6/dhcpv6relay.go +++ b/dhcpv6/dhcpv6relay.go @@ -12,6 +12,40 @@ import ( const RelayHeaderSize = 34 +// NewRelayOption returns new zero-value options for a DHCPv6 RelayMessage. +// +// Options listed in RFC 8415 Appendix B & C are used for this list, as well as +// RFC 4649 for the RemoteID, RFC 8357 for the relay port, and RFC 6939 for the +// client link-layer address. +func NewRelayOption(code OptionCode) Option { + var opt Option + switch code { + // RFC 8415 Appendix C + case OptionRelayMsg: + opt = &optRelayMsg{} + // RFC 8415 Appendix C + case OptionVendorOpts: + opt = &OptVendorOpts{} + // RFC 8415 Appendix C + case OptionInterfaceID: + opt = &optInterfaceID{} + // RFC 6939 + case OptionClientLinkLayerAddr: + opt = &optClientLinkLayerAddress{} + // RFC 4649 + case OptionRemoteID: + opt = &OptRemoteID{} + // RFC 8357 + case OptionRelayPort: + opt = &optRelayPort{} + + // We have plenty of unimplemented options. + default: + opt = &OptionGeneric{OptionCode: code} + } + return opt +} + // RelayOptions are the options valid for RelayForw and RelayRepl messages. // // RFC 3315 Appendix B defines them to be InterfaceID and RelayMsg options; RFC @@ -20,6 +54,13 @@ type RelayOptions struct { Options } +// FromBytes reads data into o and returns an error if the options are not a +// valid serialized representation of DHCPv6 relay options per RFC 8415 +// Appendix B. +func (ro *RelayOptions) FromBytes(data []byte) error { + return ro.FromBytesWithParser(data, NewRelayOption) +} + // RelayMessage returns the message embedded. func (ro RelayOptions) RelayMessage() DHCPv6 { opt := ro.Options.GetOne(OptionRelayMsg) diff --git a/dhcpv6/option_4rd.go b/dhcpv6/option_4rd.go index 2faea257..e92aa100 100644 --- a/dhcpv6/option_4rd.go +++ b/dhcpv6/option_4rd.go @@ -8,8 +8,10 @@ import ( ) // Opt4RD represents a 4RD option. It is only a container for 4RD_*_RULE options +// +// Defined in RFC 7600 Section 4.9. type Opt4RD struct { - Options + Options FourRDOptions } // Code returns the Option Code for this option @@ -32,12 +34,36 @@ func (op *Opt4RD) LongString(indentSpace int) string { return fmt.Sprintf("%s: Options=%v", op.Code(), op.Options.LongString(indentSpace)) } -// ParseOpt4RD builds an Opt4RD structure from a sequence of bytes. +// FromBytes builds an Opt4RD structure from a sequence of bytes. // The input data does not include option code and length bytes -func ParseOpt4RD(data []byte) (*Opt4RD, error) { - var opt Opt4RD - err := opt.Options.FromBytes(data) - return &opt, err +func (op *Opt4RD) FromBytes(data []byte) error { + return op.Options.FromBytes(data) +} + +// New4RDOption returns new zero-value options for DHCPv6 4RD suboption. +// +// Options listed in RFC 7600 Section 4.9 are eligible. +func New4RDOption(code OptionCode) Option { + var opt Option + switch code { + case Option4RDMapRule: + opt = &Opt4RDMapRule{} + case Option4RDNonMapRule: + opt = &Opt4RDNonMapRule{} + } + return opt +} + +// FourRDOptions are 4RD suboptions as defined in RFC 7600 Section 4.9. +type FourRDOptions struct { + Options +} + +// FromBytes reads data into fo and returns an error if the options are not a +// valid serialized representation of DHCPv6 4RD options per RFC 7600 Section +// 4.9. +func (fo *FourRDOptions) FromBytes(data []byte) error { + return fo.FromBytesWithParser(data, New4RDOption) } // Opt4RDMapRule represents a 4RD Mapping Rule option @@ -105,18 +131,17 @@ func (op *Opt4RDMapRule) String() string { op.Code(), op.Prefix4.String(), op.Prefix6.String(), op.EABitsLength, op.WKPAuthorized) } -// ParseOpt4RDMapRule builds an Opt4RDMapRule structure from a sequence of bytes. +// FromBytes builds an Opt4RDMapRule structure from a sequence of bytes. // The input data does not include option code and length bytes. -func ParseOpt4RDMapRule(data []byte) (*Opt4RDMapRule, error) { - var opt Opt4RDMapRule +func (op *Opt4RDMapRule) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) - opt.Prefix4.Mask = net.CIDRMask(int(buf.Read8()), 32) - opt.Prefix6.Mask = net.CIDRMask(int(buf.Read8()), 128) - opt.EABitsLength = buf.Read8() - opt.WKPAuthorized = (buf.Read8() & opt4RDWKPAuthorizedMask) != 0 - opt.Prefix4.IP = net.IP(buf.CopyN(net.IPv4len)) - opt.Prefix6.IP = net.IP(buf.CopyN(net.IPv6len)) - return &opt, buf.FinError() + op.Prefix4.Mask = net.CIDRMask(int(buf.Read8()), 32) + op.Prefix6.Mask = net.CIDRMask(int(buf.Read8()), 128) + op.EABitsLength = buf.Read8() + op.WKPAuthorized = (buf.Read8() & opt4RDWKPAuthorizedMask) != 0 + op.Prefix4.IP = net.IP(buf.CopyN(net.IPv4len)) + op.Prefix6.IP = net.IP(buf.CopyN(net.IPv6len)) + return buf.FinError() } // Opt4RDNonMapRule represents 4RD parameters other than mapping rules @@ -165,21 +190,20 @@ func (op *Opt4RDNonMapRule) String() string { op.Code(), op.HubAndSpoke, tClass, op.DomainPMTU) } -// ParseOpt4RDNonMapRule builds an Opt4RDNonMapRule structure from a sequence of bytes. +// FromBytes builds an Opt4RDNonMapRule structure from a sequence of bytes. // The input data does not include option code and length bytes -func ParseOpt4RDNonMapRule(data []byte) (*Opt4RDNonMapRule, error) { - var opt Opt4RDNonMapRule +func (op *Opt4RDNonMapRule) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) flags := buf.Read8() - opt.HubAndSpoke = flags&opt4RDHubAndSpokeMask != 0 + op.HubAndSpoke = flags&opt4RDHubAndSpokeMask != 0 tClass := buf.Read8() if flags&opt4RDTrafficClassMask != 0 { - opt.TrafficClass = &tClass + op.TrafficClass = &tClass } - opt.DomainPMTU = buf.Read16() + op.DomainPMTU = buf.Read16() - return &opt, buf.FinError() + return buf.FinError() } diff --git a/dhcpv6/option_4rd_test.go b/dhcpv6/option_4rd_test.go index ad72445e..2e66eeb3 100644 --- a/dhcpv6/option_4rd_test.go +++ b/dhcpv6/option_4rd_test.go @@ -2,6 +2,7 @@ package dhcpv6 import ( "net" + "reflect" "testing" "github.com/stretchr/testify/require" @@ -9,7 +10,8 @@ import ( func TestOpt4RDNonMapRuleParse(t *testing.T) { data := []byte{0x81, 0xaa, 0x05, 0xd4} - opt, err := ParseOpt4RDNonMapRule(data) + var opt Opt4RDNonMapRule + err := opt.FromBytes(data) require.NoError(t, err) require.True(t, opt.HubAndSpoke) require.NotNil(t, opt.TrafficClass) @@ -18,7 +20,8 @@ func TestOpt4RDNonMapRuleParse(t *testing.T) { // Remove the TrafficClass flag and check value is ignored data[0] = 0x80 - opt, err = ParseOpt4RDNonMapRule(data) + opt = Opt4RDNonMapRule{} + err = opt.FromBytes(data) require.NoError(t, err) require.True(t, opt.HubAndSpoke) require.Nil(t, opt.TrafficClass) @@ -77,7 +80,8 @@ func TestOpt4RDMapRuleParse(t *testing.T) { append(ip4addr.To4(), ip6addr...)..., ) - opt, err := ParseOpt4RDMapRule(data) + var opt Opt4RDMapRule + err = opt.FromBytes(data) require.NoError(t, err) require.EqualValues(t, *ip6net, opt.Prefix6) require.EqualValues(t, *ip4net, opt.Prefix4) @@ -141,30 +145,43 @@ func TestOpt4RDMapRuleString(t *testing.T) { func TestOpt4RDRoundTrip(t *testing.T) { var tClass uint8 = 0xaa opt := Opt4RD{ - Options: Options{ - &Opt4RDMapRule{ - Prefix4: net.IPNet{ - IP: net.IPv4(100, 64, 0, 238).To4(), - Mask: net.CIDRMask(24, 32), + Options: FourRDOptions{ + Options: Options{ + &Opt4RDMapRule{ + Prefix4: net.IPNet{ + IP: net.IPv4(100, 64, 0, 238).To4(), + Mask: net.CIDRMask(24, 32), + }, + Prefix6: net.IPNet{ + IP: net.ParseIP("2001:db8::1234:5678:0:aabb"), + Mask: net.CIDRMask(80, 128), + }, + EABitsLength: 32, + WKPAuthorized: true, }, - Prefix6: net.IPNet{ - IP: net.ParseIP("2001:db8::1234:5678:0:aabb"), - Mask: net.CIDRMask(80, 128), + &Opt4RDNonMapRule{ + HubAndSpoke: true, + TrafficClass: &tClass, + DomainPMTU: 9000, }, - EABitsLength: 32, - WKPAuthorized: true, - }, - &Opt4RDNonMapRule{ - HubAndSpoke: true, - TrafficClass: &tClass, - DomainPMTU: 9000, }, }, } - rtOpt, err := ParseOpt4RD(opt.ToBytes()) + var rtOpt Opt4RD + err := rtOpt.FromBytes(opt.ToBytes()) require.NoError(t, err) require.NotNil(t, rtOpt) - require.Equal(t, opt, *rtOpt) + require.Equal(t, opt, rtOpt) + + var mo MessageOptions + mo.Options.Add(&opt) + + var got MessageOptions + if err := got.FromBytes(mo.ToBytes()); err != nil { + t.Errorf("FromBytes = %v", err) + } else if !reflect.DeepEqual(mo, got) { + t.Errorf("FromBytes = %v, want %v", got, mo) + } } diff --git a/dhcpv6/option_archtype.go b/dhcpv6/option_archtype.go index a5514cd8..2b778d84 100644 --- a/dhcpv6/option_archtype.go +++ b/dhcpv6/option_archtype.go @@ -26,10 +26,6 @@ func (op optClientArchType) String() string { return fmt.Sprintf("%s: %s", op.Code(), op.Archs) } -// parseOptClientArchType builds an OptClientArchType structure from -// a sequence of bytes The input data does not include option code and -// length bytes. -func parseOptClientArchType(data []byte) (*optClientArchType, error) { - var opt optClientArchType - return &opt, opt.FromBytes(data) +func (op *optClientArchType) FromBytes(p []byte) error { + return op.Archs.FromBytes(p) } diff --git a/dhcpv6/option_archtype_test.go b/dhcpv6/option_archtype_test.go index 0481c1a4..8ebd18f7 100644 --- a/dhcpv6/option_archtype_test.go +++ b/dhcpv6/option_archtype_test.go @@ -11,14 +11,16 @@ func TestParseOptClientArchType(t *testing.T) { data := []byte{ 0, 6, // EFI_IA32 } - opt, err := parseOptClientArchType(data) + var opt optClientArchType + err := opt.FromBytes(data) require.NoError(t, err) require.Equal(t, iana.EFI_IA32, opt.Archs[0]) } func TestParseOptClientArchTypeInvalid(t *testing.T) { data := []byte{42} - _, err := parseOptClientArchType(data) + var opt optClientArchType + err := opt.FromBytes(data) require.Error(t, err) } @@ -26,7 +28,8 @@ func TestOptClientArchTypeParseAndToBytes(t *testing.T) { data := []byte{ 0, 8, // EFI_XSCALE } - opt, err := parseOptClientArchType(data) + var opt optClientArchType + err := opt.FromBytes(data) require.NoError(t, err) require.Equal(t, data, opt.ToBytes()) } diff --git a/dhcpv6/option_bootfileparam.go b/dhcpv6/option_bootfileparam.go index 76180809..ba09ca04 100644 --- a/dhcpv6/option_bootfileparam.go +++ b/dhcpv6/option_bootfileparam.go @@ -9,10 +9,12 @@ import ( // OptBootFileParam returns a BootfileParam option as defined in RFC 5970 // Section 3.2. func OptBootFileParam(args ...string) Option { - return optBootFileParam(args) + return &optBootFileParam{args} } -type optBootFileParam []string +type optBootFileParam struct { + params []string +} // Code returns the option code func (optBootFileParam) Code() OptionCode { @@ -22,7 +24,7 @@ func (optBootFileParam) Code() OptionCode { // ToBytes serializes the option and returns it as a sequence of bytes func (op optBootFileParam) ToBytes() []byte { buf := uio.NewBigEndianBuffer(nil) - for _, param := range op { + for _, param := range op.params { if len(param) >= 1<<16 { // TODO: say something here instead of silently ignoring a parameter continue @@ -42,20 +44,16 @@ func (op optBootFileParam) ToBytes() []byte { } func (op optBootFileParam) String() string { - return fmt.Sprintf("%s: %v", op.Code(), ([]string)(op)) + return fmt.Sprintf("%s: %v", op.Code(), op.params) } -// parseOptBootFileParam builds an OptBootFileParam structure from a sequence +// FromBytes builds an OptBootFileParam structure from a sequence // of bytes. The input data does not include option code and length bytes. -func parseOptBootFileParam(data []byte) (optBootFileParam, error) { +func (op *optBootFileParam) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) - var result optBootFileParam for buf.Has(2) { length := buf.Read16() - result = append(result, string(buf.CopyN(int(length)))) - } - if err := buf.FinError(); err != nil { - return nil, err + op.params = append(op.params, string(buf.CopyN(int(length)))) } - return result, nil + return buf.FinError() } diff --git a/dhcpv6/option_bootfileparam_test.go b/dhcpv6/option_bootfileparam_test.go index 467f2455..3c3f3746 100644 --- a/dhcpv6/option_bootfileparam_test.go +++ b/dhcpv6/option_bootfileparam_test.go @@ -3,7 +3,6 @@ package dhcpv6 import ( "bytes" "encoding/binary" - "fmt" "testing" "github.com/stretchr/testify/require" @@ -41,8 +40,8 @@ func compileTestBootfileParams(t *testing.T, params []string) []byte { func TestOptBootFileParam(t *testing.T) { expected := string(compileTestBootfileParams(t, testBootfileParams1)) - opt, err := parseOptBootFileParam([]byte(expected)) - if err != nil { + var opt optBootFileParam + if err := opt.FromBytes([]byte(expected)); err != nil { t.Fatal(err) } if string(opt.ToBytes()) != expected { @@ -52,12 +51,11 @@ func TestOptBootFileParam(t *testing.T) { func TestParsedTypeOptBootFileParam(t *testing.T) { tryParse := func(compiled []byte, expected []string) { - opt, err := ParseOption(OptionBootfileParam, compiled) + var opt optBootFileParam + err := opt.FromBytes([]byte(compiled)) require.NoError(t, err) - bootfileParamOpt, ok := opt.(optBootFileParam) - require.True(t, ok, fmt.Sprintf("invalid type: %T instead of %T", opt, bootfileParamOpt)) - require.Equal(t, compiled, bootfileParamOpt.ToBytes()) - require.Equal(t, expected, ([]string)(bootfileParamOpt)) + require.Equal(t, compiled, opt.ToBytes()) + require.Equal(t, expected, opt.params) } tryParse( diff --git a/dhcpv6/option_bootfileurl.go b/dhcpv6/option_bootfileurl.go index 14611899..7a8e54a4 100644 --- a/dhcpv6/option_bootfileurl.go +++ b/dhcpv6/option_bootfileurl.go @@ -6,10 +6,12 @@ import ( // OptBootFileURL returns a OptionBootfileURL as defined by RFC 5970. func OptBootFileURL(url string) Option { - return optBootFileURL(url) + return &optBootFileURL{url} } -type optBootFileURL string +type optBootFileURL struct { + url string +} // Code returns the option code func (op optBootFileURL) Code() OptionCode { @@ -18,15 +20,16 @@ func (op optBootFileURL) Code() OptionCode { // ToBytes serializes the option and returns it as a sequence of bytes func (op optBootFileURL) ToBytes() []byte { - return []byte(op) + return []byte(op.url) } func (op optBootFileURL) String() string { - return fmt.Sprintf("%s: %s", op.Code(), string(op)) + return fmt.Sprintf("%s: %s", op.Code(), op.url) } -// parseOptBootFileURL builds an optBootFileURL structure from a sequence +// FromBytes builds an optBootFileURL structure from a sequence // of bytes. The input data does not include option code and length bytes. -func parseOptBootFileURL(data []byte) (optBootFileURL, error) { - return optBootFileURL(string(data)), nil +func (op *optBootFileURL) FromBytes(data []byte) error { + op.url = string(data) + return nil } diff --git a/dhcpv6/option_bootfileurl_test.go b/dhcpv6/option_bootfileurl_test.go index ac45ef51..b65d644d 100644 --- a/dhcpv6/option_bootfileurl_test.go +++ b/dhcpv6/option_bootfileurl_test.go @@ -9,11 +9,11 @@ import ( func TestOptBootFileURL(t *testing.T) { expected := "https://insomniac.slackware.it" - opt, err := parseOptBootFileURL([]byte(expected)) - if err != nil { + var opt optBootFileURL + if err := opt.FromBytes([]byte(expected)); err != nil { t.Fatal(err) } - if string(opt) != expected { + if opt.url != expected { t.Fatalf("Invalid boot file URL. Expected %v, got %v", expected, opt) } require.Contains(t, opt.String(), "https://insomniac.slackware.it", "String() should contain the correct BootFileUrl output") diff --git a/dhcpv6/option_clientid.go b/dhcpv6/option_clientid.go index d07c4d50..eea0d013 100644 --- a/dhcpv6/option_clientid.go +++ b/dhcpv6/option_clientid.go @@ -22,13 +22,11 @@ func (op *optClientID) String() string { return fmt.Sprintf("%s: %s", op.Code(), op.DUID) } -// parseOptClientID builds an OptClientId structure from a sequence +// FromBytes builds an optClientID structure from a sequence // of bytes. The input data does not include option code and length // bytes. -func parseOptClientID(data []byte) (*optClientID, error) { - cid, err := DUIDFromBytes(data) - if err != nil { - return nil, err - } - return &optClientID{cid}, nil +func (op *optClientID) FromBytes(data []byte) error { + var err error + op.DUID, err = DUIDFromBytes(data) + return err } diff --git a/dhcpv6/option_clientid_test.go b/dhcpv6/option_clientid_test.go index 0bac4108..dd637aef 100644 --- a/dhcpv6/option_clientid_test.go +++ b/dhcpv6/option_clientid_test.go @@ -2,19 +2,39 @@ package dhcpv6 import ( "net" + "reflect" "testing" "github.com/insomniacslk/dhcp/iana" "github.com/stretchr/testify/require" ) +func TestParseMessageOptionsWithClientID(t *testing.T) { + buf := []byte{ + 0, 1, // Client ID option + 0, 10, // length + 0, 3, // DUID_LL + 0, 1, // hwtype ethernet + 0, 1, 2, 3, 4, 5, // HW addr + } + + want := &DUIDLL{HWType: iana.HWTypeEthernet, LinkLayerAddr: net.HardwareAddr{0, 1, 2, 3, 4, 5}} + var mo MessageOptions + if err := mo.FromBytes(buf); err != nil { + t.Errorf("FromBytes = %v", err) + } else if got := mo.ClientID(); !reflect.DeepEqual(got, want) { + t.Errorf("ClientID = %v, want %v", got, want) + } +} + func TestParseOptClientID(t *testing.T) { data := []byte{ 0, 3, // DUID_LL 0, 1, // hwtype ethernet 0, 1, 2, 3, 4, 5, // hw addr } - opt, err := parseOptClientID(data) + var opt optClientID + err := opt.FromBytes(data) require.NoError(t, err) want := OptClientID( &DUIDLL{ @@ -22,7 +42,7 @@ func TestParseOptClientID(t *testing.T) { LinkLayerAddr: net.HardwareAddr([]byte{0, 1, 2, 3, 4, 5}), }, ) - require.Equal(t, want, opt) + require.Equal(t, want, &opt) } func TestOptClientIdToBytes(t *testing.T) { @@ -46,7 +66,8 @@ func TestOptClientIdDecodeEncode(t *testing.T) { 0, 1, // hwtype ethernet 5, 4, 3, 2, 1, 0, // hw addr } - opt, err := parseOptClientID(data) + var opt optClientID + err := opt.FromBytes(data) require.NoError(t, err) require.Equal(t, data, opt.ToBytes()) } @@ -73,7 +94,8 @@ func TestOptClientIdparseOptClientIDBogusDUID(t *testing.T) { 1, 2, 3, 4, 5, 6, 7, 8, 9, // a UUID should be 18 bytes not 17 10, 11, 12, 13, 14, 15, 16, 17, } - _, err := parseOptClientID(data) + var opt optClientID + err := opt.FromBytes(data) require.Error(t, err, "A truncated OptClientId DUID should return an error") } @@ -81,6 +103,7 @@ func TestOptClientIdparseOptClientIDInvalidTooShort(t *testing.T) { data := []byte{ 0, // truncated: DUIDs are at least 2 bytes } - _, err := parseOptClientID(data) + var opt optClientID + err := opt.FromBytes(data) require.Error(t, err, "A truncated OptClientId should return an error") } diff --git a/dhcpv6/option_clientlinklayeraddress.go b/dhcpv6/option_clientlinklayeraddress.go index d5ec0289..878a5763 100644 --- a/dhcpv6/option_clientlinklayeraddress.go +++ b/dhcpv6/option_clientlinklayeraddress.go @@ -36,12 +36,11 @@ func (op *optClientLinkLayerAddress) String() string { return fmt.Sprintf("%s: Type=%s LinkLayerAddress=%s", op.Code(), op.LinkLayerType, op.LinkLayerAddress) } -// parseOptClientLinkLayerAddress deserializes from bytes -// to build an optClientLinkLayerAddress structure. -func parseOptClientLinkLayerAddress(data []byte) (*optClientLinkLayerAddress, error) { - var opt optClientLinkLayerAddress +// FromBytes deserializes from bytes to build an optClientLinkLayerAddress +// structure. +func (op *optClientLinkLayerAddress) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) - opt.LinkLayerType = iana.HWType(buf.Read16()) - opt.LinkLayerAddress = buf.ReadAll() - return &opt, buf.FinError() + op.LinkLayerType = iana.HWType(buf.Read16()) + op.LinkLayerAddress = buf.ReadAll() + return buf.FinError() } diff --git a/dhcpv6/option_clientlinklayeraddress_test.go b/dhcpv6/option_clientlinklayeraddress_test.go index 9e243a41..1ef40c52 100644 --- a/dhcpv6/option_clientlinklayeraddress_test.go +++ b/dhcpv6/option_clientlinklayeraddress_test.go @@ -14,7 +14,8 @@ func TestParseOptClientLinkLayerAddress(t *testing.T) { 0, 1, // LinkLayerType 164, 131, 231, 227, 223, 136, } - opt, err := parseOptClientLinkLayerAddress(data) + var opt optClientLinkLayerAddress + err := opt.FromBytes(data) require.NoError(t, err) require.Equal(t, OptionClientLinkLayerAddr, opt.Code()) diff --git a/dhcpv6/option_dhcpv4_msg.go b/dhcpv6/option_dhcpv4_msg.go index ad29353c..825ea8a2 100644 --- a/dhcpv6/option_dhcpv4_msg.go +++ b/dhcpv6/option_dhcpv4_msg.go @@ -40,12 +40,10 @@ func (op *OptDHCPv4Msg) LongString(indent int) string { return fmt.Sprintf("%s: {%v%s}", op.Code(), summary, ind) } -// ParseOptDHCPv4Msg builds an OptDHCPv4Msg structure -// from a sequence of bytes. The input data does not include option code and length -// bytes. -func ParseOptDHCPv4Msg(data []byte) (*OptDHCPv4Msg, error) { - var opt OptDHCPv4Msg +// FromBytes builds an OptDHCPv4Msg structure from a sequence of bytes. The +// input data does not include option code and length bytes. +func (op *OptDHCPv4Msg) FromBytes(data []byte) error { var err error - opt.Msg, err = dhcpv4.FromBytes(data) - return &opt, err + op.Msg, err = dhcpv4.FromBytes(data) + return err } diff --git a/dhcpv6/option_dhcpv4_msg_test.go b/dhcpv6/option_dhcpv4_msg_test.go index d8d3f6d7..e7bb1f6d 100644 --- a/dhcpv6/option_dhcpv4_msg_test.go +++ b/dhcpv6/option_dhcpv4_msg_test.go @@ -43,7 +43,8 @@ func TestParseOptDHCPv4Msg(t *testing.T) { // magic cookie, then no options data = append(data, magicCookie[:]...) - opt, err := ParseOptDHCPv4Msg(data) + var opt OptDHCPv4Msg + err := opt.FromBytes(data) d := opt.Msg require.NoError(t, err) require.Equal(t, d.OpCode, dhcpv4.OpcodeBootRequest) diff --git a/dhcpv6/option_dhcpv4_o_dhcpv6_server.go b/dhcpv6/option_dhcpv4_o_dhcpv6_server.go index f1f2d27f..1bd60af5 100644 --- a/dhcpv6/option_dhcpv4_o_dhcpv6_server.go +++ b/dhcpv6/option_dhcpv4_o_dhcpv6_server.go @@ -33,14 +33,12 @@ func (op *OptDHCP4oDHCP6Server) String() string { return fmt.Sprintf("%s: %v", op.Code(), op.DHCP4oDHCP6Servers) } -// ParseOptDHCP4oDHCP6Server builds an OptDHCP4oDHCP6Server structure -// from a sequence of bytes. The input data does not include option code and length -// bytes. -func ParseOptDHCP4oDHCP6Server(data []byte) (*OptDHCP4oDHCP6Server, error) { - var opt OptDHCP4oDHCP6Server +// FromBytes builds an OptDHCP4oDHCP6Server structure from a sequence of bytes. +// The input data does not include option code and length bytes. +func (op *OptDHCP4oDHCP6Server) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) for buf.Has(net.IPv6len) { - opt.DHCP4oDHCP6Servers = append(opt.DHCP4oDHCP6Servers, buf.CopyN(net.IPv6len)) + op.DHCP4oDHCP6Servers = append(op.DHCP4oDHCP6Servers, buf.CopyN(net.IPv6len)) } - return &opt, buf.FinError() + return buf.FinError() } diff --git a/dhcpv6/option_dhcpv4_o_dhcpv6_server_test.go b/dhcpv6/option_dhcpv4_o_dhcpv6_server_test.go index ae5bcdc7..e9674c2a 100644 --- a/dhcpv6/option_dhcpv4_o_dhcpv6_server_test.go +++ b/dhcpv6/option_dhcpv4_o_dhcpv6_server_test.go @@ -2,11 +2,28 @@ package dhcpv6 import ( "net" + "reflect" "testing" "github.com/stretchr/testify/require" ) +func TestParseMessageOptionsWithDHCP4oDHCP6Server(t *testing.T) { + ip := net.IP{0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, 0xfa, 0xce, 0xb0, 0x0c, 0x00, 0x00, 0x00, 0x35} + data := append([]byte{ + 0, 88, // DHCP4oDHCP6 option. + 0, 16, // length + }, ip...) + + want := []net.IP{ip} + var mo MessageOptions + if err := mo.FromBytes(data); err != nil { + t.Errorf("FromBytes = %v", err) + } else if got := mo.DHCP4oDHCP6Server(); !reflect.DeepEqual(got.DHCP4oDHCP6Servers, want) { + t.Errorf("FromBytes = %v, want %v", got.DHCP4oDHCP6Servers, want) + } +} + func TestParseOptDHCP4oDHCP6Server(t *testing.T) { data := []byte{ 0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, 0xfa, 0xce, 0xb0, 0x0c, 0x00, 0x00, 0x00, 0x35, @@ -14,7 +31,8 @@ func TestParseOptDHCP4oDHCP6Server(t *testing.T) { expected := []net.IP{ net.IP(data), } - opt, err := ParseOptDHCP4oDHCP6Server(data) + var opt OptDHCP4oDHCP6Server + err := opt.FromBytes(data) require.NoError(t, err) require.Equal(t, expected, opt.DHCP4oDHCP6Servers) require.Equal(t, OptionDHCP4oDHCP6Server, opt.Code()) @@ -24,17 +42,17 @@ func TestParseOptDHCP4oDHCP6Server(t *testing.T) { func TestOptDHCP4oDHCP6ServerToBytes(t *testing.T) { ip1 := net.ParseIP("2a03:2880:fffe:c:face:b00c:0:35") ip2 := net.ParseIP("2001:4860:4860::8888") - servers := []net.IP{ip1, ip2} - expected := append([]byte{}, []byte(ip1)...) - expected = append(expected, []byte(ip2)...) - opt := OptDHCP4oDHCP6Server{DHCP4oDHCP6Servers: servers} - require.Equal(t, expected, opt.ToBytes()) + opt := OptDHCP4oDHCP6Server{DHCP4oDHCP6Servers: []net.IP{ip1, ip2}} + + want := []byte(append(ip1, ip2...)) + require.Equal(t, want, opt.ToBytes()) } func TestParseOptDHCP4oDHCP6ServerParseNoAddr(t *testing.T) { data := []byte{} var expected []net.IP - opt, err := ParseOptDHCP4oDHCP6Server(data) + var opt OptDHCP4oDHCP6Server + err := opt.FromBytes(data) require.NoError(t, err) require.Equal(t, expected, opt.DHCP4oDHCP6Servers) } @@ -49,6 +67,7 @@ func TestParseOptDHCP4oDHCP6ServerParseBogus(t *testing.T) { data := []byte{ 0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, // invalid IPv6 address } - _, err := ParseOptDHCP4oDHCP6Server(data) + var opt OptDHCP4oDHCP6Server + err := opt.FromBytes(data) require.Error(t, err, "An invalid IPv6 address should return an error") } diff --git a/dhcpv6/option_dns.go b/dhcpv6/option_dns.go index f69973a1..af9bafea 100644 --- a/dhcpv6/option_dns.go +++ b/dhcpv6/option_dns.go @@ -34,14 +34,12 @@ func (op *optDNS) String() string { return fmt.Sprintf("%s: %v", op.Code(), op.NameServers) } -// parseOptDNS builds an optDNS structure -// from a sequence of bytes. The input data does not include option code and length -// bytes. -func parseOptDNS(data []byte) (*optDNS, error) { - var opt optDNS +// FromBytes builds an optDNS structure from a sequence of bytes. The input +// data does not include option code and length bytes. +func (op *optDNS) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) for buf.Has(net.IPv6len) { - opt.NameServers = append(opt.NameServers, buf.CopyN(net.IPv6len)) + op.NameServers = append(op.NameServers, buf.CopyN(net.IPv6len)) } - return &opt, buf.FinError() + return buf.FinError() } diff --git a/dhcpv6/option_dns_test.go b/dhcpv6/option_dns_test.go index 34f22f7d..2c810764 100644 --- a/dhcpv6/option_dns_test.go +++ b/dhcpv6/option_dns_test.go @@ -14,7 +14,8 @@ func TestParseOptDNS(t *testing.T) { expected := []net.IP{ net.IP(data), } - opt, err := parseOptDNS(data) + var opt optDNS + err := opt.FromBytes(data) require.NoError(t, err) require.Equal(t, expected, opt.NameServers) require.Equal(t, OptionDNSRecursiveNameServer, opt.Code()) @@ -35,6 +36,7 @@ func TestParseOptDNSBogus(t *testing.T) { data := []byte{ 0x2a, 0x03, 0x28, 0x80, 0xff, 0xfe, 0x00, 0x0c, // invalid IPv6 address } - _, err := parseOptDNS(data) + var opt optDNS + err := opt.FromBytes(data) require.Error(t, err, "An invalid nameserver IPv6 address should return an error") } diff --git a/dhcpv6/option_domainsearchlist.go b/dhcpv6/option_domainsearchlist.go index e94799db..fd8aa01c 100644 --- a/dhcpv6/option_domainsearchlist.go +++ b/dhcpv6/option_domainsearchlist.go @@ -28,14 +28,10 @@ func (op *optDomainSearchList) String() string { return fmt.Sprintf("%s: %s", op.Code(), op.DomainSearchList) } -// ParseOptDomainSearchList builds an OptDomainSearchList structure from a sequence -// of bytes. The input data does not include option code and length bytes. -func parseOptDomainSearchList(data []byte) (*optDomainSearchList, error) { - var opt optDomainSearchList +// FromBytes builds an OptDomainSearchList structure from a sequence of bytes. +// The input data does not include option code and length bytes. +func (op *optDomainSearchList) FromBytes(data []byte) error { var err error - opt.DomainSearchList, err = rfc1035label.FromBytes(data) - if err != nil { - return nil, err - } - return &opt, nil + op.DomainSearchList, err = rfc1035label.FromBytes(data) + return err } diff --git a/dhcpv6/option_domainsearchlist_test.go b/dhcpv6/option_domainsearchlist_test.go index 433f710a..c03759c5 100644 --- a/dhcpv6/option_domainsearchlist_test.go +++ b/dhcpv6/option_domainsearchlist_test.go @@ -12,7 +12,8 @@ func TestParseOptDomainSearchList(t *testing.T) { 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 := parseOptDomainSearchList(data) + var opt optDomainSearchList + err := opt.FromBytes(data) require.NoError(t, err) require.Equal(t, OptionDomainSearchList, opt.Code()) require.Equal(t, 2, len(opt.DomainSearchList.Labels)) @@ -42,6 +43,7 @@ func TestParseOptDomainSearchListInvalidLength(t *testing.T) { 7, 'e', 'x', 'a', 'm', 'p', 'l', 'e', 3, 'c', 'o', 'm', 0, 6, 's', 'u', 'b', 'n', 'e', 't', 7, 'e', // truncated } - _, err := parseOptDomainSearchList(data) + var opt optDomainSearchList + err := opt.FromBytes(data) require.Error(t, err, "A truncated OptDomainSearchList should return an error") } diff --git a/dhcpv6/option_elapsedtime.go b/dhcpv6/option_elapsedtime.go index 823c9425..14414285 100644 --- a/dhcpv6/option_elapsedtime.go +++ b/dhcpv6/option_elapsedtime.go @@ -32,11 +32,10 @@ func (op *optElapsedTime) String() string { return fmt.Sprintf("%s: %s", op.Code(), op.ElapsedTime) } -// build an optElapsedTime structure from a sequence of bytes. +// FromBytes builds an optElapsedTime structure from a sequence of bytes. // The input data does not include option code and length bytes. -func parseOptElapsedTime(data []byte) (*optElapsedTime, error) { - var opt optElapsedTime +func (op *optElapsedTime) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) - opt.ElapsedTime = time.Duration(buf.Read16()) * 10 * time.Millisecond - return &opt, buf.FinError() + op.ElapsedTime = time.Duration(buf.Read16()) * 10 * time.Millisecond + return buf.FinError() } diff --git a/dhcpv6/option_elapsedtime_test.go b/dhcpv6/option_elapsedtime_test.go index 2155539f..b0d2dc1f 100644 --- a/dhcpv6/option_elapsedtime_test.go +++ b/dhcpv6/option_elapsedtime_test.go @@ -9,7 +9,8 @@ import ( ) func TestOptElapsedTime(t *testing.T) { - opt, err := parseOptElapsedTime([]byte{0xaa, 0xbb}) + var opt optElapsedTime + err := opt.FromBytes([]byte{0xaa, 0xbb}) if err != nil { t.Fatal(err) } @@ -35,9 +36,10 @@ func TestOptElapsedTimeString(t *testing.T) { } func TestOptElapsedTimeParseInvalidOption(t *testing.T) { - _, err := parseOptElapsedTime([]byte{0xaa}) + var opt optElapsedTime + err := opt.FromBytes([]byte{0xaa}) require.Error(t, err, "A short option should return an error") - _, err = parseOptElapsedTime([]byte{0xaa, 0xbb, 0xcc}) + err = opt.FromBytes([]byte{0xaa, 0xbb, 0xcc}) require.Error(t, err, "An option with too many bytes should return an error") } diff --git a/dhcpv6/option_fqdn.go b/dhcpv6/option_fqdn.go index 73a525a5..b85cb13a 100644 --- a/dhcpv6/option_fqdn.go +++ b/dhcpv6/option_fqdn.go @@ -9,7 +9,7 @@ import ( // OptFQDN implements OptionFQDN option. // -// https://tools.ietf.org/html/rfc4704 +// Defined by RFC 4704. type OptFQDN struct { Flags uint8 DomainName *rfc1035label.Labels @@ -32,15 +32,14 @@ func (op *OptFQDN) String() string { return fmt.Sprintf("%s: {Flags=%d DomainName=%s}", op.Code(), op.Flags, op.DomainName) } -// ParseOptFQDN deserializes from bytes to build a OptFQDN structure. -func ParseOptFQDN(data []byte) (*OptFQDN, error) { - var opt OptFQDN +// FromBytes deserializes from bytes to build a OptFQDN structure. +func (op *OptFQDN) FromBytes(data []byte) error { var err error buf := uio.NewBigEndianBuffer(data) - opt.Flags = buf.Read8() - opt.DomainName, err = rfc1035label.FromBytes(buf.ReadAll()) + op.Flags = buf.Read8() + op.DomainName, err = rfc1035label.FromBytes(buf.ReadAll()) if err != nil { - return nil, err + return err } - return &opt, buf.FinError() + return buf.FinError() } diff --git a/dhcpv6/option_fqdn_test.go b/dhcpv6/option_fqdn_test.go index 69720bad..0bb2ab46 100644 --- a/dhcpv6/option_fqdn_test.go +++ b/dhcpv6/option_fqdn_test.go @@ -14,7 +14,8 @@ func TestParseOptFQDN(t *testing.T) { 4, 'c', 'n', 'o', 's', 9, 'l', 'o', 'c', 'a', 'l', 'h', 'o', 's', 't', 0, } - opt, err := ParseOptFQDN(data) + var opt OptFQDN + err := opt.FromBytes(data) require.NoError(t, err) require.Equal(t, OptionFQDN, opt.Code()) diff --git a/dhcpv6/option_iaaddress.go b/dhcpv6/option_iaaddress.go index 072ba65f..915e5a21 100644 --- a/dhcpv6/option_iaaddress.go +++ b/dhcpv6/option_iaaddress.go @@ -28,10 +28,29 @@ func (ao AddressOptions) Status() *OptStatusCode { return sc } +// FromBytes reads data into fo and returns an error if the options are not a +// valid serialized representation of DHCPv6 Address options per RFC 8415 +// Appendix C. +func (ao *AddressOptions) FromBytes(data []byte) error { + return ao.FromBytesWithParser(data, newAddressOption) +} + +// newAddressOption returns new zero-value options for DHCPv6 IAAddress +// suboption. +// +// Options listed in RFC 8415 Appendix C for IAAddress are eligible. +func newAddressOption(code OptionCode) Option { + var opt Option + switch code { + case OptionStatusCode: + opt = &OptStatusCode{} + } + return opt +} + // OptIAAddress represents an OptionIAAddr. // -// This module defines the OptIAAddress structure. -// https://www.ietf.org/rfc/rfc3633.txt +// This module defines the OptIAAddress structure. RFC 8415 Section 21.6. type OptIAAddress struct { IPv6Addr net.IP PreferredLifetime time.Duration @@ -69,22 +88,20 @@ func (op *OptIAAddress) LongString(indent int) string { op.Code(), op.IPv6Addr, op.PreferredLifetime, op.ValidLifetime, op.Options.LongString(indent)) } -// ParseOptIAAddress builds an OptIAAddress structure from a sequence -// of bytes. The input data does not include option code and length -// bytes. -func ParseOptIAAddress(data []byte) (*OptIAAddress, error) { - var opt OptIAAddress +// FromBytes builds an OptIAAddress structure from a sequence of bytes. The +// input data does not include option code and length bytes. +func (op *OptIAAddress) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) - opt.IPv6Addr = net.IP(buf.CopyN(net.IPv6len)) + op.IPv6Addr = net.IP(buf.CopyN(net.IPv6len)) var t1, t2 Duration t1.Unmarshal(buf) t2.Unmarshal(buf) - opt.PreferredLifetime = t1.Duration - opt.ValidLifetime = t2.Duration + op.PreferredLifetime = t1.Duration + op.ValidLifetime = t2.Duration - if err := opt.Options.FromBytes(buf.ReadAll()); err != nil { - return nil, err + if err := op.Options.FromBytes(buf.ReadAll()); err != nil { + return err } - return &opt, buf.FinError() + return buf.FinError() } diff --git a/dhcpv6/option_iaaddress_test.go b/dhcpv6/option_iaaddress_test.go index 26f1732f..3e5a5944 100644 --- a/dhcpv6/option_iaaddress_test.go +++ b/dhcpv6/option_iaaddress_test.go @@ -13,9 +13,10 @@ func TestOptIAAddressParse(t *testing.T) { data := append(ipaddr, []byte{ 0xa, 0xb, 0xc, 0xd, // preferred lifetime 0xe, 0xf, 0x1, 0x2, // valid lifetime - 0, 8, 0, 2, 0xaa, 0xbb, // options + 0, 13, 0, 2, 0xaa, 0xbb, // options }...) - opt, err := ParseOptIAAddress(data) + var opt OptIAAddress + err := opt.FromBytes(data) require.NoError(t, err) require.Equal(t, net.IP(ipaddr), opt.IPv6Addr) require.Equal(t, 0x0a0b0c0d*time.Second, opt.PreferredLifetime) @@ -28,7 +29,8 @@ func TestOptIAAddressParseInvalidTooShort(t *testing.T) { 0xa, 0xb, 0xc, 0xd, // preferred lifetime // truncated here } - _, err := ParseOptIAAddress(data) + var opt OptIAAddress + err := opt.FromBytes(data) require.Error(t, err) } @@ -37,9 +39,10 @@ func TestOptIAAddressParseInvalidBrokenOptions(t *testing.T) { 0x24, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 0xa, 0xb, 0xc, 0xd, // preferred lifetime 0xe, 0xf, 0x1, 0x2, // valid lifetime - 0, 8, 0, 2, 0xaa, // broken options + 0, 13, 0, 2, 0xaa, // broken options } - _, err := ParseOptIAAddress(data) + var opt OptIAAddress + err := opt.FromBytes(data) require.Error(t, err) } @@ -58,14 +61,14 @@ func TestOptIAAddressToBytes(t *testing.T) { expected := append(ipBytes, []byte{ 0xa, 0xb, 0xc, 0xd, // preferred lifetime 0xe, 0xf, 0x1, 0x2, // valid lifetime - 0, 8, 0, 2, 0x00, 0x01, // options + 0, 13, 0, 2, 0x00, 0x01, // options }...) opt := OptIAAddress{ IPv6Addr: net.IP(ipBytes), PreferredLifetime: 0x0a0b0c0d * time.Second, ValidLifetime: 0x0e0f0102 * time.Second, Options: AddressOptions{[]Option{ - OptElapsedTime(10 * time.Millisecond), + &OptStatusCode{StatusCode: 0x1}, }}, } require.Equal(t, expected, opt.ToBytes()) @@ -76,9 +79,10 @@ func TestOptIAAddressString(t *testing.T) { data := append(ipaddr, []byte{ 0x00, 0x00, 0x00, 70, // preferred lifetime 0x00, 0x00, 0x00, 50, // valid lifetime - 0, 8, 0, 2, 0xaa, 0xbb, // options + 0, 13, 0, 2, 0xaa, 0xbb, // options }...) - opt, err := ParseOptIAAddress(data) + var opt OptIAAddress + err := opt.FromBytes(data) require.NoError(t, err) str := opt.String() diff --git a/dhcpv6/option_iapd.go b/dhcpv6/option_iapd.go index 62961031..b23ad01a 100644 --- a/dhcpv6/option_iapd.go +++ b/dhcpv6/option_iapd.go @@ -40,6 +40,26 @@ func (po PDOptions) Status() *OptStatusCode { return sc } +// FromBytes reads data into fo and returns an error if the options are not a +// valid serialized representation of DHCPv6 IAPD options per RFC 3633. +func (po *PDOptions) FromBytes(data []byte) error { + return po.FromBytesWithParser(data, newIAPDOption) +} + +// newIAPDOption returns new zero-value options for DHCPv6 IAPD suboption. +// +// Options listed in RFC 3633 for IAPD are eligible. +func newIAPDOption(code OptionCode) Option { + var opt Option + switch code { + case OptionStatusCode: + opt = &OptStatusCode{} + case OptionIAPrefix: + opt = &OptIAPrefix{} + } + return opt +} + // OptIAPD implements the identity association for prefix // delegation option defined by RFC 3633, Section 9. type OptIAPD struct { @@ -79,21 +99,20 @@ func (op *OptIAPD) LongString(indentSpace int) string { return fmt.Sprintf("%s: IAID=%#x T1=%v T2=%v Options=%v", op.Code(), op.IaId, op.T1, op.T2, op.Options.LongString(indentSpace)) } -// ParseOptIAPD builds an OptIAPD structure from a sequence of bytes. -// The input data does not include option code and length bytes. -func ParseOptIAPD(data []byte) (*OptIAPD, error) { - var opt OptIAPD +// FromBytes builds an OptIAPD structure from a sequence of bytes. The input +// data does not include option code and length bytes. +func (op *OptIAPD) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) - buf.ReadBytes(opt.IaId[:]) + buf.ReadBytes(op.IaId[:]) var t1, t2 Duration t1.Unmarshal(buf) t2.Unmarshal(buf) - opt.T1 = t1.Duration - opt.T2 = t2.Duration + op.T1 = t1.Duration + op.T2 = t2.Duration - if err := opt.Options.FromBytes(buf.ReadAll()); err != nil { - return nil, err + if err := op.Options.FromBytes(buf.ReadAll()); err != nil { + return err } - return &opt, buf.FinError() + return buf.FinError() } diff --git a/dhcpv6/option_iapd_test.go b/dhcpv6/option_iapd_test.go index 13175f46..398a23e3 100644 --- a/dhcpv6/option_iapd_test.go +++ b/dhcpv6/option_iapd_test.go @@ -2,12 +2,50 @@ package dhcpv6 import ( "net" + "reflect" "testing" "time" "github.com/stretchr/testify/require" ) +func TestParseMessageWithIAPD(t *testing.T) { + data := []byte{ + 0, 25, // IAPD option code + 0, 41, // length + 1, 0, 0, 0, // IAID + 0, 0, 0, 1, // T1 + 0, 0, 0, 2, // T2 + 0, 26, 0, 25, // 26 = IAPrefix Option, 25 = length + 0, 0, 0, 2, // IAPrefix preferredLifetime + 0, 0, 0, 4, // IAPrefix validLifetime + 36, // IAPrefix prefixLength + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // IAPrefix ipv6Prefix + } + var got MessageOptions + if err := got.FromBytes(data); err != nil { + t.Errorf("FromBytes = %v", err) + } + + want := &OptIAPD{ + IaId: [4]byte{1, 0, 0, 0}, + T1: 1 * time.Second, + T2: 2 * time.Second, + Options: PDOptions{Options: Options{&OptIAPrefix{ + PreferredLifetime: 2 * time.Second, + ValidLifetime: 4 * time.Second, + Prefix: &net.IPNet{ + Mask: net.CIDRMask(36, 128), + IP: net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, + }, + Options: PrefixOptions{Options: Options{}}, + }}}, + } + if gotIAPD := got.OneIAPD(); !reflect.DeepEqual(gotIAPD, want) { + t.Errorf("OneIAPD = %v, want %v", gotIAPD, want) + } +} + func TestOptIAPDParseOptIAPD(t *testing.T) { data := []byte{ 1, 0, 0, 0, // IAID @@ -19,7 +57,8 @@ func TestOptIAPDParseOptIAPD(t *testing.T) { 36, // IAPrefix prefixLength 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // IAPrefix ipv6Prefix } - opt, err := ParseOptIAPD(data) + var opt OptIAPD + err := opt.FromBytes(data) require.NoError(t, err) require.Equal(t, OptionIAPD, opt.Code()) require.Equal(t, [4]byte{1, 0, 0, 0}, opt.IaId) @@ -33,7 +72,8 @@ func TestOptIAPDParseOptIAPDInvalidLength(t *testing.T) { 0, 0, 0, 1, // T1 // truncated from here } - _, err := ParseOptIAPD(data) + var opt OptIAPD + err := opt.FromBytes(data) require.Error(t, err) } @@ -48,7 +88,8 @@ func TestOptIAPDParseOptIAPDInvalidOptions(t *testing.T) { 36, // IAPrefix prefixLength 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // IAPrefix ipv6Prefix missing last byte } - _, err := ParseOptIAPD(data) + var opt OptIAPD + err := opt.FromBytes(data) require.Error(t, err) } @@ -92,7 +133,8 @@ func TestOptIAPDString(t *testing.T) { 36, // IAPrefix prefixLength 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // IAPrefix ipv6Prefix } - opt, err := ParseOptIAPD(data) + var opt OptIAPD + err := opt.FromBytes(data) require.NoError(t, err) str := opt.String() diff --git a/dhcpv6/option_iaprefix.go b/dhcpv6/option_iaprefix.go index fc41e0a7..5d508da1 100644 --- a/dhcpv6/option_iaprefix.go +++ b/dhcpv6/option_iaprefix.go @@ -32,6 +32,26 @@ func (po PrefixOptions) Status() *OptStatusCode { return sc } +// FromBytes reads data into fo and returns an error if the options are not a +// valid serialized representation of DHCPv6 IAPrefix options per RFC 8415 +// Appendix C. +func (po *PrefixOptions) FromBytes(data []byte) error { + return po.FromBytesWithParser(data, newPrefixOption) +} + +// newPrefixOption returns new zero-value options for DHCPv6 IAPrefix +// suboption. +// +// Options listed in RFC 8415 Appendix C for IAPrefix are eligible. +func newPrefixOption(code OptionCode) Option { + var opt Option + switch code { + case OptionStatusCode: + opt = &OptStatusCode{} + } + return opt +} + // OptIAPrefix implements the IAPrefix option. // // This module defines the OptIAPrefix structure. @@ -74,31 +94,30 @@ func (op *OptIAPrefix) String() string { op.Code(), op.PreferredLifetime, op.ValidLifetime, op.Prefix, op.Options) } -// ParseOptIAPrefix an OptIAPrefix structure from a sequence of bytes. The -// input data does not include option code and length bytes. -func ParseOptIAPrefix(data []byte) (*OptIAPrefix, error) { +// FromBytes an OptIAPrefix structure from a sequence of bytes. The input data +// does not include option code and length bytes. +func (op *OptIAPrefix) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) - var opt OptIAPrefix var t1, t2 Duration t1.Unmarshal(buf) t2.Unmarshal(buf) - opt.PreferredLifetime = t1.Duration - opt.ValidLifetime = t2.Duration + op.PreferredLifetime = t1.Duration + op.ValidLifetime = t2.Duration length := buf.Read8() ip := net.IP(buf.CopyN(net.IPv6len)) if length == 0 { - opt.Prefix = nil + op.Prefix = nil } else { - opt.Prefix = &net.IPNet{ + op.Prefix = &net.IPNet{ Mask: net.CIDRMask(int(length), 128), IP: ip, } } - if err := opt.Options.FromBytes(buf.ReadAll()); err != nil { - return nil, err + if err := op.Options.FromBytes(buf.ReadAll()); err != nil { + return err } - return &opt, buf.FinError() + return buf.FinError() } diff --git a/dhcpv6/option_iaprefix_test.go b/dhcpv6/option_iaprefix_test.go index 27d0c951..be7e232d 100644 --- a/dhcpv6/option_iaprefix_test.go +++ b/dhcpv6/option_iaprefix_test.go @@ -17,8 +17,8 @@ func TestOptIAPrefix(t *testing.T) { 36, // prefixLength 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, // ipv6Prefix } - opt, err := ParseOptIAPrefix(buf) - if err != nil { + var opt OptIAPrefix + if err := opt.FromBytes(buf); err != nil { t.Fatal(err) } want := &OptIAPrefix{ @@ -30,7 +30,7 @@ func TestOptIAPrefix(t *testing.T) { }, Options: PrefixOptions{[]Option{}}, } - if !reflect.DeepEqual(want, opt) { + if !reflect.DeepEqual(want, &opt) { t.Errorf("parseIAPrefix = %v, want %v", opt, want) } } @@ -79,7 +79,8 @@ func TestOptIAPrefixParseInvalidTooShort(t *testing.T) { 36, // prefixLength 0, 0, 0, 0, 0, 0, 0, // truncated ipv6Prefix } - if opt, err := ParseOptIAPrefix(buf); err == nil { + var opt OptIAPrefix + if err := opt.FromBytes(buf); err == nil { t.Fatalf("ParseOptIAPrefix: Expected error on truncated option, got %v", opt) } } @@ -91,7 +92,8 @@ func TestOptIAPrefixString(t *testing.T) { 36, // prefixLength 0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // ipv6Prefix } - opt, err := ParseOptIAPrefix(buf) + var opt OptIAPrefix + err := opt.FromBytes(buf) require.NoError(t, err) str := opt.String() diff --git a/dhcpv6/option_informationrefreshtime.go b/dhcpv6/option_informationrefreshtime.go index 8b47b468..e0ba43c0 100644 --- a/dhcpv6/option_informationrefreshtime.go +++ b/dhcpv6/option_informationrefreshtime.go @@ -35,14 +35,13 @@ func (op *optInformationRefreshTime) String() string { return fmt.Sprintf("%s: %v", op.Code(), op.InformationRefreshtime) } -// parseOptInformationRefreshTime builds an optInformationRefreshTime structure from a sequence -// of bytes. The input data does not include option code and length bytes. -func parseOptInformationRefreshTime(data []byte) (*optInformationRefreshTime, error) { - var opt optInformationRefreshTime +// FromBytes builds an optInformationRefreshTime structure from a sequence of +// bytes. The input data does not include option code and length bytes. +func (op *optInformationRefreshTime) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) var irt Duration irt.Unmarshal(buf) - opt.InformationRefreshtime = irt.Duration - return &opt, buf.FinError() + op.InformationRefreshtime = irt.Duration + return buf.FinError() } diff --git a/dhcpv6/option_informationrefreshtime_test.go b/dhcpv6/option_informationrefreshtime_test.go index 0428a1c6..452ac1d6 100644 --- a/dhcpv6/option_informationrefreshtime_test.go +++ b/dhcpv6/option_informationrefreshtime_test.go @@ -7,7 +7,8 @@ import ( ) func TestOptInformationRefreshTime(t *testing.T) { - opt, err := parseOptInformationRefreshTime([]byte{0xaa, 0xbb, 0xcc, 0xdd}) + var opt optInformationRefreshTime + err := opt.FromBytes([]byte{0xaa, 0xbb, 0xcc, 0xdd}) if err != nil { t.Fatal(err) } diff --git a/dhcpv6/option_interfaceid.go b/dhcpv6/option_interfaceid.go index 163af7bb..a6debba6 100644 --- a/dhcpv6/option_interfaceid.go +++ b/dhcpv6/option_interfaceid.go @@ -26,10 +26,9 @@ func (op *optInterfaceID) String() string { return fmt.Sprintf("%s: %v", op.Code(), op.ID) } -// build an optInterfaceID structure from a sequence of bytes. -// The input data does not include option code and length bytes. -func parseOptInterfaceID(data []byte) (*optInterfaceID, error) { - var opt optInterfaceID - opt.ID = append([]byte(nil), data...) - return &opt, nil +// FromBytes builds an optInterfaceID structure from a sequence of bytes. The +// input data does not include option code and length bytes. +func (op *optInterfaceID) FromBytes(data []byte) error { + op.ID = append([]byte(nil), data...) + return nil } diff --git a/dhcpv6/option_interfaceid_test.go b/dhcpv6/option_interfaceid_test.go index 45c17990..b38cadbf 100644 --- a/dhcpv6/option_interfaceid_test.go +++ b/dhcpv6/option_interfaceid_test.go @@ -9,8 +9,8 @@ import ( func TestParseOptInterfaceID(t *testing.T) { expected := []byte("DSLAM01 eth2/1/01/21") - opt, err := parseOptInterfaceID(expected) - if err != nil { + var opt optInterfaceID + if err := opt.FromBytes(expected); err != nil { t.Fatal(err) } if url := opt.ID; !bytes.Equal(url, expected) { diff --git a/dhcpv6/option_nontemporaryaddress.go b/dhcpv6/option_nontemporaryaddress.go index 76f6f4cd..cf114129 100644 --- a/dhcpv6/option_nontemporaryaddress.go +++ b/dhcpv6/option_nontemporaryaddress.go @@ -64,6 +64,28 @@ func (io IdentityOptions) Status() *OptStatusCode { return sc } +// FromBytes reads data into fo and returns an error if the options are not a +// valid serialized representation of DHCPv6 IANA/IATA options per RFC 8415 +// Appendix C. +func (io *IdentityOptions) FromBytes(data []byte) error { + return io.FromBytesWithParser(data, newIdentityOption) +} + +// newIdentityOption returns new zero-value options for DHCPv6 IANA/IATA +// suboption. +// +// Options listed in RFC 8415 Appendix C for IANA/IATA are eligible. +func newIdentityOption(code OptionCode) Option { + var opt Option + switch code { + case OptionStatusCode: + opt = &OptStatusCode{} + case OptionIAAddr: + opt = &OptIAAddress{} + } + return opt +} + // OptIANA implements the identity association for non-temporary addresses // option. // @@ -102,21 +124,20 @@ func (op *OptIANA) LongString(indentSpace int) string { return fmt.Sprintf("%s: IAID=%#x T1=%s T2=%s Options=%s", op.Code(), op.IaId, op.T1, op.T2, op.Options.LongString(indentSpace)) } -// ParseOptIANA builds an OptIANA structure from a sequence of bytes. The +// FromBytes builds an OptIANA structure from a sequence of bytes. The // input data does not include option code and length bytes. -func ParseOptIANA(data []byte) (*OptIANA, error) { - var opt OptIANA +func (op *OptIANA) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) - buf.ReadBytes(opt.IaId[:]) + buf.ReadBytes(op.IaId[:]) var t1, t2 Duration t1.Unmarshal(buf) t2.Unmarshal(buf) - opt.T1 = t1.Duration - opt.T2 = t2.Duration + op.T1 = t1.Duration + op.T2 = t2.Duration - if err := opt.Options.FromBytes(buf.ReadAll()); err != nil { - return nil, err + if err := op.Options.FromBytes(buf.ReadAll()); err != nil { + return err } - return &opt, buf.FinError() + return buf.FinError() } diff --git a/dhcpv6/option_nontemporaryaddress_test.go b/dhcpv6/option_nontemporaryaddress_test.go index 1ec58e81..3e5c55b1 100644 --- a/dhcpv6/option_nontemporaryaddress_test.go +++ b/dhcpv6/option_nontemporaryaddress_test.go @@ -2,12 +2,43 @@ package dhcpv6 import ( "net" + "reflect" "testing" "time" "github.com/stretchr/testify/require" ) +func TestParseMessageWithIANA(t *testing.T) { + data := []byte{ + 0, 3, // IANA option code + 0, 40, // length + 1, 0, 0, 0, // IAID + 0, 0, 0, 1, // T1 + 0, 0, 0, 2, // T2 + 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, 0, 0, 0, 2, 0, 0, 0, 4, // options + } + var got MessageOptions + if err := got.FromBytes(data); err != nil { + t.Errorf("FromBytes = %v", err) + } + + want := &OptIANA{ + IaId: [4]byte{1, 0, 0, 0}, + T1: 1 * time.Second, + T2: 2 * time.Second, + Options: IdentityOptions{Options: Options{&OptIAAddress{ + IPv6Addr: net.IP{0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0}, + PreferredLifetime: 2 * time.Second, + ValidLifetime: 4 * time.Second, + Options: AddressOptions{Options: Options{}}, + }}}, + } + if gotIANA := got.OneIANA(); !reflect.DeepEqual(gotIANA, want) { + t.Errorf("OneIANA = %v, want %v", gotIANA, want) + } +} + func TestOptIANAParseOptIANA(t *testing.T) { data := []byte{ 1, 0, 0, 0, // IAID @@ -15,7 +46,8 @@ func TestOptIANAParseOptIANA(t *testing.T) { 0, 0, 0, 2, // T2 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, 0, 0, 0xb2, 0x7a, 0, 0, 0xc0, 0x8a, // options } - opt, err := ParseOptIANA(data) + var opt OptIANA + err := opt.FromBytes(data) require.NoError(t, err) require.Equal(t, OptionIANA, opt.Code()) } @@ -26,7 +58,8 @@ func TestOptIANAParseOptIANAInvalidLength(t *testing.T) { 0, 0, 0, 1, // T1 // truncated from here } - _, err := ParseOptIANA(data) + var opt OptIANA + err := opt.FromBytes(data) require.Error(t, err) } @@ -37,7 +70,8 @@ func TestOptIANAParseOptIANAInvalidOptions(t *testing.T) { 0, 0, 0, 2, // T2 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, 0, 0, 0xb2, 0x7a, // truncated options } - _, err := ParseOptIANA(data) + var opt OptIANA + err := opt.FromBytes(data) require.Error(t, err) } @@ -118,7 +152,8 @@ func TestOptIANAString(t *testing.T) { 0, 0, 0, 2, // T2 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, 0, 0, 0xb2, 0x7a, 0, 0, 0xc0, 0x8a, // options } - opt, err := ParseOptIANA(data) + var opt OptIANA + err := opt.FromBytes(data) require.NoError(t, err) str := opt.String() diff --git a/dhcpv6/option_ntp_server.go b/dhcpv6/option_ntp_server.go index a7aafb76..9f816e91 100644 --- a/dhcpv6/option_ntp_server.go +++ b/dhcpv6/option_ntp_server.go @@ -18,17 +18,20 @@ func (n *NTPSuboptionSrvAddr) Code() OptionCode { // ToBytes returns the byte serialization of the suboption. func (n *NTPSuboptionSrvAddr) ToBytes() []byte { - buf := uio.NewBigEndianBuffer(nil) - buf.Write16(uint16(NTPSuboptionSrvAddrCode)) - buf.Write16(uint16(net.IPv6len)) - buf.WriteBytes(net.IP(*n).To16()) - return buf.Data() + return net.IP(*n).To16() } func (n *NTPSuboptionSrvAddr) String() string { return fmt.Sprintf("Server Address: %s", net.IP(*n).String()) } +// FromBytes parses NTP server address from a byte slice p. +func (n *NTPSuboptionSrvAddr) FromBytes(p []byte) error { + buf := uio.NewBigEndianBuffer(p) + *n = NTPSuboptionSrvAddr(buf.CopyN(net.IPv6len)) + return buf.FinError() +} + // NTPSuboptionMCAddr is NTP_SUBOPTION_MC_ADDR according to RFC 5908. type NTPSuboptionMCAddr net.IP @@ -39,19 +42,24 @@ func (n *NTPSuboptionMCAddr) Code() OptionCode { // ToBytes returns the byte serialization of the suboption. func (n *NTPSuboptionMCAddr) ToBytes() []byte { - buf := uio.NewBigEndianBuffer(nil) - buf.Write16(uint16(NTPSuboptionMCAddrCode)) - buf.Write16(uint16(net.IPv6len)) - buf.WriteBytes(net.IP(*n).To16()) - return buf.Data() + return net.IP(*n).To16() } func (n *NTPSuboptionMCAddr) String() string { return fmt.Sprintf("Multicast Address: %s", net.IP(*n).String()) } +// FromBytes parses NTP multicast address from a byte slice p. +func (n *NTPSuboptionMCAddr) FromBytes(p []byte) error { + buf := uio.NewBigEndianBuffer(p) + *n = NTPSuboptionMCAddr(buf.CopyN(net.IPv6len)) + return buf.FinError() +} + // NTPSuboptionSrvFQDN is NTP_SUBOPTION_SRV_FQDN according to RFC 5908. -type NTPSuboptionSrvFQDN rfc1035label.Labels +type NTPSuboptionSrvFQDN struct { + rfc1035label.Labels +} // Code returns the suboption code. func (n *NTPSuboptionSrvFQDN) Code() OptionCode { @@ -60,17 +68,16 @@ func (n *NTPSuboptionSrvFQDN) Code() OptionCode { // ToBytes returns the byte serialization of the suboption. func (n *NTPSuboptionSrvFQDN) ToBytes() []byte { - buf := uio.NewBigEndianBuffer(nil) - buf.Write16(uint16(NTPSuboptionSrvFQDNCode)) - l := rfc1035label.Labels(*n) - buf.Write16(uint16(l.Length())) - buf.WriteBytes(l.ToBytes()) - return buf.Data() + return n.Labels.ToBytes() } func (n *NTPSuboptionSrvFQDN) String() string { - l := rfc1035label.Labels(*n) - return fmt.Sprintf("Server FQDN: %s", l.String()) + return fmt.Sprintf("Server FQDN: %s", n.Labels.String()) +} + +// FromBytes parses an NTP server FQDN from a byte slice p. +func (n *NTPSuboptionSrvFQDN) FromBytes(p []byte) error { + return n.Labels.FromBytes(p) } // NTPSuboptionSrvAddr is the value of NTP_SUBOPTION_SRV_ADDR according to RFC 5908. @@ -80,55 +87,20 @@ const ( NTPSuboptionSrvFQDNCode = OptionCode(3) ) -// parseNTPSuboption implements the OptionParser interface. -func parseNTPSuboption(code OptionCode, data []byte) (Option, error) { - //var o Options - buf := uio.NewBigEndianBuffer(data) - length := len(data) - data, err := buf.ReadN(length) - if err != nil { - return nil, fmt.Errorf("failed to read %d bytes for suboption: %w", length, err) - } +// newNTPOption implements the OptionParser interface. +func newNTPOption(code OptionCode) Option { + var o Option switch code { - case NTPSuboptionSrvAddrCode, NTPSuboptionMCAddrCode: - if length != net.IPv6len { - return nil, fmt.Errorf("invalid suboption length, want %d, got %d", net.IPv6len, length) - } - var so Option - switch code { - case NTPSuboptionSrvAddrCode: - sos := NTPSuboptionSrvAddr(data) - so = &sos - case NTPSuboptionMCAddrCode: - som := NTPSuboptionMCAddr(data) - so = &som - } - return so, nil + case NTPSuboptionSrvAddrCode: + o = &NTPSuboptionSrvAddr{} + case NTPSuboptionMCAddrCode: + o = &NTPSuboptionMCAddr{} case NTPSuboptionSrvFQDNCode: - l, err := rfc1035label.FromBytes(data) - if err != nil { - return nil, fmt.Errorf("failed to parse rfc1035 labels: %w", err) - } - // TODO according to rfc3315, this label must not be compressed. - // Need to add support for compression detection to the - // `rfc1035label` package in order to do that. - so := NTPSuboptionSrvFQDN(*l) - return &so, nil + o = &NTPSuboptionSrvFQDN{} default: - gopt := OptionGeneric{OptionCode: code, OptionData: data} - return &gopt, nil - } -} - -// ParseOptNTPServer parses a sequence of bytes into an OptNTPServer object. -func ParseOptNTPServer(data []byte) (*OptNTPServer, error) { - var so Options - if err := so.FromBytesWithParser(data, parseNTPSuboption); err != nil { - return nil, err + o = &OptionGeneric{OptionCode: code} } - return &OptNTPServer{ - Suboptions: so, - }, nil + return o } // OptNTPServer is an option NTP server as defined by RFC 5908. @@ -141,13 +113,14 @@ func (op *OptNTPServer) Code() OptionCode { return OptionNTPServer } +// FromBytes parses a sequence of bytes into an OptNTPServer object. +func (op *OptNTPServer) FromBytes(data []byte) error { + return op.Suboptions.FromBytesWithParser(data, newNTPOption) +} + // ToBytes returns the option serialized to bytes. func (op *OptNTPServer) ToBytes() []byte { - buf := uio.NewBigEndianBuffer(nil) - for _, so := range op.Suboptions { - buf.WriteBytes(so.ToBytes()) - } - return buf.Data() + return op.Suboptions.ToBytes() } func (op *OptNTPServer) String() string { diff --git a/dhcpv6/option_ntp_server_test.go b/dhcpv6/option_ntp_server_test.go index 105a753e..59b3c698 100644 --- a/dhcpv6/option_ntp_server_test.go +++ b/dhcpv6/option_ntp_server_test.go @@ -13,25 +13,22 @@ func TestSuboptionSrvAddr(t *testing.T) { ip := net.ParseIP("2a03:2880:fffe:c:face:b00c:0:35") so := NTPSuboptionSrvAddr(ip) assert.Equal(t, NTPSuboptionSrvAddrCode, so.Code()) - expected := append([]byte{0x00, 0x01, 0x00, 0x10}, ip...) - assert.Equal(t, expected, so.ToBytes()) + assert.Equal(t, []byte(ip), so.ToBytes()) } func TestSuboptionMCAddr(t *testing.T) { ip := net.ParseIP("2a03:2880:fffe:c:face:b00c:0:35") so := NTPSuboptionMCAddr(ip) assert.Equal(t, NTPSuboptionMCAddrCode, so.Code()) - expected := append([]byte{0x00, 0x02, 0x00, 0x10}, ip...) - assert.Equal(t, expected, so.ToBytes()) + assert.Equal(t, []byte(ip), so.ToBytes()) } func TestSuboptionSrvFQDN(t *testing.T) { fqdn, err := rfc1035label.FromBytes([]byte("\x03ntp\x07example\x03com")) require.NoError(t, err) - so := NTPSuboptionSrvFQDN(*fqdn) + so := NTPSuboptionSrvFQDN{*fqdn} assert.Equal(t, NTPSuboptionSrvFQDNCode, so.Code()) - expected := append([]byte{0x00, 0x03, 0x00, 0x10}, fqdn.ToBytes()...) - assert.Equal(t, expected, so.ToBytes()) + assert.Equal(t, fqdn.ToBytes(), so.ToBytes()) } func TestSuboptionGeneric(t *testing.T) { @@ -40,7 +37,8 @@ func TestSuboptionGeneric(t *testing.T) { 0x00, 0x04, // length, 4 bytes 0x74, 0x65, 0x73, 0x74, // the ASCII bytes for the string "test" } - o, err := ParseOptNTPServer(data) + var o OptNTPServer + err := o.FromBytes(data) require.NoError(t, err) require.Equal(t, 1, len(o.Suboptions)) assert.IsType(t, &OptionGeneric{}, o.Suboptions[0]) @@ -67,7 +65,8 @@ func TestParseOptNTPServer(t *testing.T) { }...) data = append(data, fqdn.ToBytes()...) - o, err := ParseOptNTPServer(data) + var o OptNTPServer + err = o.FromBytes(data) require.NoError(t, err) require.NotNil(t, o) assert.Equal(t, 2, len(o.Suboptions)) @@ -78,11 +77,11 @@ func TestParseOptNTPServer(t *testing.T) { optFQDN, ok := o.Suboptions[1].(*NTPSuboptionSrvFQDN) require.True(t, ok) - assert.Equal(t, *fqdn, rfc1035label.Labels(*optFQDN)) + assert.Equal(t, *fqdn, optFQDN.Labels) var mo MessageOptions assert.Nil(t, mo.NTPServers()) - mo.Add(o) + mo.Add(&o) // MessageOptions.NTPServers only returns server address values. assert.Equal(t, []net.IP{ip}, mo.NTPServers()) } diff --git a/dhcpv6/option_relaymsg.go b/dhcpv6/option_relaymsg.go index 216f53ba..99627565 100644 --- a/dhcpv6/option_relaymsg.go +++ b/dhcpv6/option_relaymsg.go @@ -33,14 +33,10 @@ func (op *optRelayMsg) LongString(indent int) string { return fmt.Sprintf("%s: %v", op.Code(), op.Msg.LongString(indent)) } -// build an optRelayMsg structure from a sequence of bytes. -// The input data does not include option code and length bytes. -func parseOptRelayMsg(data []byte) (*optRelayMsg, error) { +// FromBytes build an optRelayMsg structure from a sequence of bytes. The input +// data does not include option code and length bytes. +func (op *optRelayMsg) FromBytes(data []byte) error { var err error - var opt optRelayMsg - opt.Msg, err = FromBytes(data) - if err != nil { - return nil, err - } - return &opt, nil + op.Msg, err = FromBytes(data) + return err } diff --git a/dhcpv6/option_relaymsg_test.go b/dhcpv6/option_relaymsg_test.go index e77a6c67..ea4d04ae 100644 --- a/dhcpv6/option_relaymsg_test.go +++ b/dhcpv6/option_relaymsg_test.go @@ -9,7 +9,8 @@ import ( ) func TestRelayMsgParseOptRelayMsg(t *testing.T) { - opt, err := parseOptRelayMsg([]byte{ + var opt optRelayMsg + err := opt.FromBytes([]byte{ 1, // MessageTypeSolicit 0xaa, 0xbb, 0xcc, // transaction ID 0, 8, // option: elapsed time @@ -27,7 +28,7 @@ func TestRelayMsgParseOptRelayMsg(t *testing.T) { } func TestRelayMsgOptionsFromBytes(t *testing.T) { - var opts Options + var opts RelayOptions err := opts.FromBytes([]byte{ 0, 9, // option: relay message 0, 10, // relayed message length @@ -40,10 +41,10 @@ func TestRelayMsgOptionsFromBytes(t *testing.T) { if err != nil { t.Fatal(err) } - if len(opts) != 1 { - t.Fatalf("Invalid number of options. Expected 1, got %v", len(opts)) + if len(opts.Options) != 1 { + t.Fatalf("Invalid number of options. Expected 1, got %v", len(opts.Options)) } - opt := opts[0] + opt := opts.Options[0] if code := opt.Code(); code != OptionRelayMsg { t.Fatalf("Invalid option code. Expected OptionRelayMsg (%v), got %v", OptionRelayMsg, code, @@ -148,7 +149,8 @@ func TestSample(t *testing.T) { } func TestRelayMsgParseOptRelayMsgTooShort(t *testing.T) { - _, err := parseOptRelayMsg([]byte{ + var opt optRelayMsg + err := opt.FromBytes([]byte{ 1, // MessageTypeSolicit 0xaa, 0xbb, 0xcc, // transaction ID 0, 8, // option: elapsed time @@ -158,7 +160,8 @@ func TestRelayMsgParseOptRelayMsgTooShort(t *testing.T) { } func TestRelayMsgString(t *testing.T) { - opt, err := parseOptRelayMsg([]byte{ + var opt optRelayMsg + err := opt.FromBytes([]byte{ 1, // MessageTypeSolicit 0xaa, 0xbb, 0xcc, // transaction ID 0, 8, // option: elapsed time diff --git a/dhcpv6/option_relayport.go b/dhcpv6/option_relayport.go index 48736935..3dd35a10 100644 --- a/dhcpv6/option_relayport.go +++ b/dhcpv6/option_relayport.go @@ -9,7 +9,9 @@ import ( "github.com/u-root/uio/uio" ) -// OptRelayPort specifies an UDP port to use for the downstream relay +// OptRelayPort specifies an UDP port to use for the downstream relay. +// +// Defined in RFC 8357. func OptRelayPort(port uint16) Option { return &optRelayPort{DownstreamSourcePort: port} } @@ -32,11 +34,10 @@ func (op *optRelayPort) String() string { return fmt.Sprintf("%s: %d", op.Code(), op.DownstreamSourcePort) } -// build an optRelayPort structure from a sequence of bytes. -// The input data does not include option code and length bytes. -func parseOptRelayPort(data []byte) (*optRelayPort, error) { - var opt optRelayPort +// FromBytes build an optRelayPort structure from a sequence of bytes. The +// input data does not include option code and length bytes. +func (op *optRelayPort) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) - opt.DownstreamSourcePort = buf.Read16() - return &opt, buf.FinError() + op.DownstreamSourcePort = buf.Read16() + return buf.FinError() } diff --git a/dhcpv6/option_relayport_test.go b/dhcpv6/option_relayport_test.go index 124af62a..d80e268b 100644 --- a/dhcpv6/option_relayport_test.go +++ b/dhcpv6/option_relayport_test.go @@ -7,9 +7,10 @@ import ( ) func TestParseRelayPort(t *testing.T) { - opt, err := parseOptRelayPort([]byte{0x12, 0x32}) + var opt optRelayPort + err := opt.FromBytes([]byte{0x12, 0x32}) require.NoError(t, err) - require.Equal(t, &optRelayPort{DownstreamSourcePort: 0x1232}, opt) + require.Equal(t, optRelayPort{DownstreamSourcePort: 0x1232}, opt) } func TestRelayPortToBytes(t *testing.T) { diff --git a/dhcpv6/option_remoteid.go b/dhcpv6/option_remoteid.go index 98331493..d07b0a2c 100644 --- a/dhcpv6/option_remoteid.go +++ b/dhcpv6/option_remoteid.go @@ -31,12 +31,11 @@ func (op *OptRemoteID) String() string { ) } -// ParseOptRemoteId builds an OptRemoteId structure from a sequence of bytes. -// The input data does not include option code and length bytes. -func ParseOptRemoteID(data []byte) (*OptRemoteID, error) { - var opt OptRemoteID +// FromBytes builds an OptRemoteId structure from a sequence of bytes. The +// input data does not include option code and length bytes. +func (op *OptRemoteID) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) - opt.EnterpriseNumber = buf.Read32() - opt.RemoteID = buf.ReadAll() - return &opt, buf.FinError() + op.EnterpriseNumber = buf.Read32() + op.RemoteID = buf.ReadAll() + return buf.FinError() } diff --git a/dhcpv6/option_remoteid_test.go b/dhcpv6/option_remoteid_test.go index 77835de3..d8f848b2 100644 --- a/dhcpv6/option_remoteid_test.go +++ b/dhcpv6/option_remoteid_test.go @@ -11,8 +11,8 @@ func TestOptRemoteID(t *testing.T) { expected := []byte{0xaa, 0xbb, 0xcc, 0xdd} remoteId := []byte("DSLAM01 eth2/1/01/21") expected = append(expected, remoteId...) - opt, err := ParseOptRemoteID(expected) - if err != nil { + var opt OptRemoteID + if err := opt.FromBytes(expected); err != nil { t.Fatal(err) } if en := opt.EnterpriseNumber; en != 0xaabbccdd { @@ -37,7 +37,8 @@ func TestOptRemoteIDToBytes(t *testing.T) { func TestOptRemoteIDParseOptRemoteIDTooShort(t *testing.T) { buf := []byte{0xaa, 0xbb, 0xcc} - _, err := ParseOptRemoteID(buf) + var opt OptRemoteID + err := opt.FromBytes(buf) require.Error(t, err, "A short option should return an error") } @@ -46,7 +47,8 @@ func TestOptRemoteIDString(t *testing.T) { remoteId := []byte("Test1234") buf = append(buf, remoteId...) - opt, err := ParseOptRemoteID(buf) + var opt OptRemoteID + err := opt.FromBytes(buf) require.NoError(t, err) str := opt.String() require.Contains( diff --git a/dhcpv6/option_serverid.go b/dhcpv6/option_serverid.go index bdfc2903..4c35cc1c 100644 --- a/dhcpv6/option_serverid.go +++ b/dhcpv6/option_serverid.go @@ -22,12 +22,10 @@ func (op *optServerID) String() string { return fmt.Sprintf("%s: %v", op.Code(), op.DUID) } -// parseOptServerID builds an optServerID structure from a sequence of bytes. -// The input data does not include option code and length bytes. -func parseOptServerID(data []byte) (*optServerID, error) { - sid, err := DUIDFromBytes(data) - if err != nil { - return nil, err - } - return &optServerID{sid}, nil +// FromBytes builds an optServerID structure from a sequence of bytes. The +// input data does not include option code and length bytes. +func (op *optServerID) FromBytes(data []byte) error { + var err error + op.DUID, err = DUIDFromBytes(data) + return err } diff --git a/dhcpv6/option_serverid_test.go b/dhcpv6/option_serverid_test.go index 556f5150..b0250c23 100644 --- a/dhcpv6/option_serverid_test.go +++ b/dhcpv6/option_serverid_test.go @@ -2,19 +2,39 @@ package dhcpv6 import ( "net" + "reflect" "testing" "github.com/insomniacslk/dhcp/iana" "github.com/stretchr/testify/require" ) +func TestParseMessageOptionsWithServerID(t *testing.T) { + buf := []byte{ + 0, 2, // Server ID option + 0, 10, // length + 0, 3, // DUID_LL + 0, 1, // hwtype ethernet + 0, 1, 2, 3, 4, 5, // HW addr + } + + want := &DUIDLL{HWType: iana.HWTypeEthernet, LinkLayerAddr: net.HardwareAddr{0, 1, 2, 3, 4, 5}} + var mo MessageOptions + if err := mo.FromBytes(buf); err != nil { + t.Errorf("FromBytes = %v", err) + } else if got := mo.ServerID(); !reflect.DeepEqual(got, want) { + t.Errorf("ServerID = %v, want %v", got, want) + } +} + func TestParseOptServerID(t *testing.T) { data := []byte{ 0, 3, // DUID_LL 0, 1, // hwtype ethernet 0, 1, 2, 3, 4, 5, // hw addr } - opt, err := parseOptServerID(data) + var opt optServerID + err := opt.FromBytes(data) require.NoError(t, err) want := OptServerID( &DUIDLL{ @@ -22,7 +42,7 @@ func TestParseOptServerID(t *testing.T) { LinkLayerAddr: net.HardwareAddr{0, 1, 2, 3, 4, 5}, }, ) - require.Equal(t, opt, want) + require.Equal(t, &opt, want) } func TestOptServerIdToBytes(t *testing.T) { @@ -46,7 +66,8 @@ func TestOptServerIdDecodeEncode(t *testing.T) { 0, 1, // hwtype ethernet 5, 4, 3, 2, 1, 0, // hw addr } - opt, err := parseOptServerID(data) + var opt optServerID + err := opt.FromBytes(data) require.NoError(t, err) require.Equal(t, data, opt.ToBytes()) } @@ -73,7 +94,8 @@ func TestOptServerIdparseOptServerIDBogusDUID(t *testing.T) { 1, 2, 3, 4, 5, 6, 7, 8, 9, // a UUID should be 18 bytes not 17 10, 11, 12, 13, 14, 15, 16, 17, } - _, err := parseOptServerID(data) + var opt optServerID + err := opt.FromBytes(data) require.Error(t, err, "A truncated OptServerId DUID should return an error") } @@ -81,6 +103,7 @@ func TestOptServerIdparseOptServerIDInvalidTooShort(t *testing.T) { data := []byte{ 0, // truncated: DUIDs are at least 2 bytes } - _, err := parseOptServerID(data) + var opt optServerID + err := opt.FromBytes(data) require.Error(t, err, "A truncated OptServerId should return an error") } diff --git a/dhcpv6/option_statuscode.go b/dhcpv6/option_statuscode.go index 067b9739..c745bc19 100644 --- a/dhcpv6/option_statuscode.go +++ b/dhcpv6/option_statuscode.go @@ -35,12 +35,11 @@ func (op *OptStatusCode) String() string { op.Code(), op.StatusCode, op.StatusCode, op.StatusMessage) } -// ParseOptStatusCode builds an OptStatusCode structure from a sequence of -// bytes. The input data does not include option code and length bytes. -func ParseOptStatusCode(data []byte) (*OptStatusCode, error) { - var opt OptStatusCode +// FromBytes builds an OptStatusCode structure from a sequence of bytes. The +// input data does not include option code and length bytes. +func (op *OptStatusCode) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) - opt.StatusCode = iana.StatusCode(buf.Read16()) - opt.StatusMessage = string(buf.ReadAll()) - return &opt, buf.FinError() + op.StatusCode = iana.StatusCode(buf.Read16()) + op.StatusMessage = string(buf.ReadAll()) + return buf.FinError() } diff --git a/dhcpv6/option_statuscode_test.go b/dhcpv6/option_statuscode_test.go index 82225e0e..45829099 100644 --- a/dhcpv6/option_statuscode_test.go +++ b/dhcpv6/option_statuscode_test.go @@ -12,7 +12,8 @@ func TestParseOptStatusCode(t *testing.T) { 0, 5, // StatusUseMulticast 'u', 's', 'e', ' ', 'm', 'u', 'l', 't', 'i', 'c', 'a', 's', 't', } - opt, err := ParseOptStatusCode(data) + var opt OptStatusCode + err := opt.FromBytes(data) require.NoError(t, err) require.Equal(t, iana.StatusUseMulticast, opt.StatusCode) require.Equal(t, "use multicast", opt.StatusMessage) @@ -32,7 +33,8 @@ func TestOptStatusCodeToBytes(t *testing.T) { } func TestOptStatusCodeParseOptStatusCodeTooShort(t *testing.T) { - _, err := ParseOptStatusCode([]byte{0}) + var opt OptStatusCode + err := opt.FromBytes([]byte{0}) require.Error(t, err, "ParseOptStatusCode: Expected error on truncated option") } @@ -41,7 +43,8 @@ func TestOptStatusCodeString(t *testing.T) { 0, 5, // StatusUseMulticast 'u', 's', 'e', ' ', 'm', 'u', 'l', 't', 'i', 'c', 'a', 's', 't', } - opt, err := ParseOptStatusCode(data) + var opt OptStatusCode + err := opt.FromBytes(data) require.NoError(t, err) require.Contains( diff --git a/dhcpv6/option_temporaryaddress.go b/dhcpv6/option_temporaryaddress.go index 4a79bac8..8ccbe7b1 100644 --- a/dhcpv6/option_temporaryaddress.go +++ b/dhcpv6/option_temporaryaddress.go @@ -38,15 +38,14 @@ func (op *OptIATA) LongString(indentSpace int) string { return fmt.Sprintf("%s: IAID=%#x Options=%v", op.Code(), op.IaId, op.Options.LongString(indentSpace)) } -// ParseOptIATA builds an OptIATA structure from a sequence of bytes. The -// input data does not include option code and length bytes. -func ParseOptIATA(data []byte) (*OptIATA, error) { - var opt OptIATA +// FromBytes builds an OptIATA structure from a sequence of bytes. The input +// data does not include option code and length bytes. +func (op *OptIATA) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) - buf.ReadBytes(opt.IaId[:]) + buf.ReadBytes(op.IaId[:]) - if err := opt.Options.FromBytes(buf.ReadAll()); err != nil { - return nil, err + if err := op.Options.FromBytes(buf.ReadAll()); err != nil { + return err } - return &opt, buf.FinError() + return buf.FinError() } diff --git a/dhcpv6/option_temporaryaddress_test.go b/dhcpv6/option_temporaryaddress_test.go index 9b48d5a5..8704d5c0 100644 --- a/dhcpv6/option_temporaryaddress_test.go +++ b/dhcpv6/option_temporaryaddress_test.go @@ -13,7 +13,8 @@ func TestOptIATAParseOptIATA(t *testing.T) { 1, 0, 0, 0, // IAID 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, 0, 0, 0xb2, 0x7a, 0, 0, 0xc0, 0x8a, // options } - opt, err := ParseOptIATA(data) + var opt OptIATA + err := opt.FromBytes(data) require.NoError(t, err) require.Equal(t, OptionIATA, opt.Code()) } @@ -22,7 +23,8 @@ func TestOptIATAParseOptIATAInvalidLength(t *testing.T) { data := []byte{ 1, 0, 0, // truncated IAID } - _, err := ParseOptIATA(data) + var opt OptIATA + err := opt.FromBytes(data) require.Error(t, err) } @@ -31,7 +33,8 @@ func TestOptIATAParseOptIATAInvalidOptions(t *testing.T) { 1, 0, 0, 0, // IAID 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, 0, 0, 0xb2, 0x7a, // truncated options } - _, err := ParseOptIATA(data) + var opt OptIATA + err := opt.FromBytes(data) require.Error(t, err) } @@ -106,7 +109,8 @@ func TestOptIATAString(t *testing.T) { 1, 0, 0, 0, // IAID 0, 5, 0, 0x18, 0x24, 1, 0xdb, 0, 0x30, 0x10, 0xc0, 0x8f, 0xfa, 0xce, 0, 0, 0, 0x44, 0, 0, 0, 0, 0xb2, 0x7a, 0, 0, 0xc0, 0x8a, // options } - opt, err := ParseOptIATA(data) + var opt OptIATA + err := opt.FromBytes(data) require.NoError(t, err) str := opt.String() diff --git a/dhcpv6/option_userclass.go b/dhcpv6/option_userclass.go index 00abcbd7..643fdd13 100644 --- a/dhcpv6/option_userclass.go +++ b/dhcpv6/option_userclass.go @@ -38,17 +38,16 @@ func (op *OptUserClass) String() string { return fmt.Sprintf("%s: [%s]", op.Code(), strings.Join(ucStrings, ", ")) } -// ParseOptUserClass builds an OptUserClass structure from a sequence of -// bytes. The input data does not include option code and length bytes. -func ParseOptUserClass(data []byte) (*OptUserClass, error) { - var opt OptUserClass +// FromBytes builds an OptUserClass structure from a sequence of bytes. The +// input data does not include option code and length bytes. +func (op *OptUserClass) FromBytes(data []byte) error { if len(data) == 0 { - return nil, fmt.Errorf("user class option must not be empty") + return fmt.Errorf("user class option must not be empty") } buf := uio.NewBigEndianBuffer(data) for buf.Has(2) { len := buf.Read16() - opt.UserClasses = append(opt.UserClasses, buf.CopyN(int(len))) + op.UserClasses = append(op.UserClasses, buf.CopyN(int(len))) } - return &opt, buf.FinError() + return buf.FinError() } diff --git a/dhcpv6/option_userclass_test.go b/dhcpv6/option_userclass_test.go index 50bc438b..2612da16 100644 --- a/dhcpv6/option_userclass_test.go +++ b/dhcpv6/option_userclass_test.go @@ -10,7 +10,8 @@ func TestParseOptUserClass(t *testing.T) { expected := []byte{ 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', } - opt, err := ParseOptUserClass(expected) + var opt OptUserClass + err := opt.FromBytes(expected) require.NoError(t, err) require.Equal(t, 1, len(opt.UserClasses)) require.Equal(t, []byte("linuxboot"), opt.UserClasses[0]) @@ -21,7 +22,8 @@ func TestParseOptUserClassMultiple(t *testing.T) { 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 0, 4, 't', 'e', 's', 't', } - opt, err := ParseOptUserClass(expected) + var opt OptUserClass + err := opt.FromBytes(expected) require.NoError(t, err) require.Equal(t, len(opt.UserClasses), 2) require.Equal(t, []byte("linuxboot"), opt.UserClasses[0]) @@ -30,7 +32,8 @@ func TestParseOptUserClassMultiple(t *testing.T) { func TestParseOptUserClassNone(t *testing.T) { expected := []byte{} - _, err := ParseOptUserClass(expected) + var opt OptUserClass + err := opt.FromBytes(expected) require.Error(t, err) } @@ -65,14 +68,15 @@ func TestOptUserClassParseOptUserClassTooShort(t *testing.T) { 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 0, 4, 't', 'e', } - _, err := ParseOptUserClass(buf) + var opt OptUserClass + err := opt.FromBytes(buf) require.Error(t, err, "ParseOptUserClass() should error if given truncated user classes") buf = []byte{ 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 0, } - _, err = ParseOptUserClass(buf) + err = opt.FromBytes(buf) require.Error(t, err, "ParseOptUserClass() should error if given a truncated length") } @@ -81,7 +85,8 @@ func TestOptUserClassString(t *testing.T) { 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 0, 4, 't', 'e', 's', 't', } - opt, err := ParseOptUserClass(data) + var opt OptUserClass + err := opt.FromBytes(data) require.NoError(t, err) require.Contains( diff --git a/dhcpv6/option_vendor_opts.go b/dhcpv6/option_vendor_opts.go index 11811c02..9cbde90e 100644 --- a/dhcpv6/option_vendor_opts.go +++ b/dhcpv6/option_vendor_opts.go @@ -38,22 +38,21 @@ func (op *OptVendorOpts) LongString(indent int) string { return fmt.Sprintf("%s: EnterpriseNumber=%v VendorOptions=%s", op.Code(), op.EnterpriseNumber, op.VendorOpts.LongString(indent)) } -// ParseOptVendorOpts builds an OptVendorOpts structure from a sequence of bytes. -// The input data does not include option code and length bytes. -func ParseOptVendorOpts(data []byte) (*OptVendorOpts, error) { - var opt OptVendorOpts +// FromBytes builds an OptVendorOpts structure from a sequence of bytes. The +// input data does not include option code and length bytes. +func (op *OptVendorOpts) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) - opt.EnterpriseNumber = buf.Read32() - if err := opt.VendorOpts.FromBytesWithParser(buf.ReadAll(), vendParseOption); err != nil { - return nil, err + op.EnterpriseNumber = buf.Read32() + if err := op.VendorOpts.FromBytesWithParser(buf.ReadAll(), newVendorOption); err != nil { + return err } - return &opt, buf.FinError() + return buf.FinError() } -// vendParseOption builds a GenericOption from a slice of bytes +// newVendorOption builds a GenericOption from a slice of bytes // We cannot use the existing ParseOption function in options.go because the // sub-options include codes specific to each vendor. There are overlaps in these // codes with RFC standard codes. -func vendParseOption(code OptionCode, data []byte) (Option, error) { - return &OptionGeneric{OptionCode: code, OptionData: data}, nil +func newVendorOption(code OptionCode) Option { + return &OptionGeneric{OptionCode: code} } diff --git a/dhcpv6/option_vendor_opts_test.go b/dhcpv6/option_vendor_opts_test.go index f6c2b2a6..5caba9c7 100644 --- a/dhcpv6/option_vendor_opts_test.go +++ b/dhcpv6/option_vendor_opts_test.go @@ -17,13 +17,16 @@ func TestOptVendorOpts(t *testing.T) { expectedOpts := OptVendorOpts{} var vendorOpts []Option expectedOpts.VendorOpts = append(vendorOpts, &OptionGeneric{OptionCode: 1, OptionData: optData}) - opt, err := ParseOptVendorOpts(expected) + + var opt OptVendorOpts + err := opt.FromBytes(expected) require.NoError(t, err) require.Equal(t, uint32(0xaabbccdd), opt.EnterpriseNumber) require.Equal(t, expectedOpts.VendorOpts, opt.VendorOpts) shortData := make([]byte, 1) - _, err = ParseOptVendorOpts(shortData) + var opt2 OptVendorOpts + err = opt2.FromBytes(shortData) require.Error(t, err) } diff --git a/dhcpv6/option_vendorclass.go b/dhcpv6/option_vendorclass.go index 33297dc6..954dbd05 100644 --- a/dhcpv6/option_vendorclass.go +++ b/dhcpv6/option_vendorclass.go @@ -39,18 +39,18 @@ func (op *OptVendorClass) String() string { return fmt.Sprintf("%s: {EnterpriseNumber=%d Data=[%s]}", op.Code(), op.EnterpriseNumber, strings.Join(vcStrings, ", ")) } -// ParseOptVendorClass builds an OptVendorClass structure from a sequence of -// bytes. The input data does not include option code and length bytes. -func ParseOptVendorClass(data []byte) (*OptVendorClass, error) { - var opt OptVendorClass +// FromBytes builds an OptVendorClass structure from a sequence of bytes. The +// input data does not include option code and length bytes. +func (op *OptVendorClass) FromBytes(data []byte) error { buf := uio.NewBigEndianBuffer(data) - opt.EnterpriseNumber = buf.Read32() + *op = OptVendorClass{} + op.EnterpriseNumber = buf.Read32() for buf.Has(2) { len := buf.Read16() - opt.Data = append(opt.Data, buf.CopyN(int(len))) + op.Data = append(op.Data, buf.CopyN(int(len))) } - if len(opt.Data) < 1 { - return nil, errors.New("ParseOptVendorClass: at least one vendor class data is required") + if len(op.Data) < 1 { + return errors.New("ParseOptVendorClass: at least one vendor class data is required") } - return &opt, buf.FinError() + return buf.FinError() } diff --git a/dhcpv6/option_vendorclass_test.go b/dhcpv6/option_vendorclass_test.go index 819f5f6c..c691176d 100644 --- a/dhcpv6/option_vendorclass_test.go +++ b/dhcpv6/option_vendorclass_test.go @@ -12,7 +12,8 @@ func TestParseOptVendorClass(t *testing.T) { 0, 10, 'H', 'T', 'T', 'P', 'C', 'l', 'i', 'e', 'n', 't', 0, 4, 't', 'e', 's', 't', } - opt, err := ParseOptVendorClass(data) + var opt OptVendorClass + err := opt.FromBytes(data) require.NoError(t, err) require.Equal(t, OptionVendorClass, opt.Code()) require.Equal(t, 2, len(opt.Data)) @@ -42,13 +43,14 @@ func TestOptVendorClassParseOptVendorClassMalformed(t *testing.T) { buf := []byte{ 0xaa, 0xbb, // truncated EnterpriseNumber } - _, err := ParseOptVendorClass(buf) + var opt OptVendorClass + err := opt.FromBytes(buf) require.Error(t, err, "ParseOptVendorClass() should error if given truncated EnterpriseNumber") buf = []byte{ 0xaa, 0xbb, 0xcc, 0xdd, // EnterpriseNumber } - _, err = ParseOptVendorClass(buf) + err = opt.FromBytes(buf) require.Error(t, err, "ParseOptVendorClass() should error if given no vendor classes") buf = []byte{ @@ -56,7 +58,7 @@ func TestOptVendorClassParseOptVendorClassMalformed(t *testing.T) { 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 0, 4, 't', 'e', } - _, err = ParseOptVendorClass(buf) + err = opt.FromBytes(buf) require.Error(t, err, "ParseOptVendorClass() should error if given truncated vendor classes") buf = []byte{ @@ -64,7 +66,7 @@ func TestOptVendorClassParseOptVendorClassMalformed(t *testing.T) { 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 0, } - _, err = ParseOptVendorClass(buf) + err = opt.FromBytes(buf) require.Error(t, err, "ParseOptVendorClass() should error if given a truncated length") } @@ -74,7 +76,8 @@ func TestOptVendorClassString(t *testing.T) { 0, 9, 'l', 'i', 'n', 'u', 'x', 'b', 'o', 'o', 't', 0, 4, 't', 'e', 's', 't', } - opt, err := ParseOptVendorClass(data) + var opt OptVendorClass + err := opt.FromBytes(data) require.NoError(t, err) str := opt.String() diff --git a/dhcpv6/options.go b/dhcpv6/options.go index 308fc2c0..70dee9f6 100644 --- a/dhcpv6/options.go +++ b/dhcpv6/options.go @@ -12,6 +12,7 @@ type Option interface { Code() OptionCode ToBytes() []byte String() string + FromBytes([]byte) error } type OptionGeneric struct { @@ -34,90 +35,10 @@ func (og *OptionGeneric) String() string { return fmt.Sprintf("%s: %v", og.OptionCode, og.OptionData) } -// ParseOption parses data according to the given code. -func ParseOption(code OptionCode, optData []byte) (Option, error) { - // Parse a sequence of bytes as a single DHCPv6 option. - // Returns the option structure, or an error if any. - var ( - err error - opt Option - ) - switch code { - case OptionClientID: - opt, err = parseOptClientID(optData) - case OptionServerID: - opt, err = parseOptServerID(optData) - case OptionIANA: - opt, err = ParseOptIANA(optData) - case OptionIATA: - opt, err = ParseOptIATA(optData) - case OptionIAAddr: - opt, err = ParseOptIAAddress(optData) - case OptionORO: - var o optRequestedOption - err = o.FromBytes(optData) - opt = &o - case OptionElapsedTime: - opt, err = parseOptElapsedTime(optData) - case OptionRelayMsg: - opt, err = parseOptRelayMsg(optData) - case OptionStatusCode: - opt, err = ParseOptStatusCode(optData) - case OptionUserClass: - opt, err = ParseOptUserClass(optData) - case OptionVendorClass: - opt, err = ParseOptVendorClass(optData) - case OptionVendorOpts: - opt, err = ParseOptVendorOpts(optData) - case OptionInterfaceID: - opt, err = parseOptInterfaceID(optData) - case OptionDNSRecursiveNameServer: - opt, err = parseOptDNS(optData) - case OptionDomainSearchList: - opt, err = parseOptDomainSearchList(optData) - case OptionIAPD: - opt, err = ParseOptIAPD(optData) - case OptionIAPrefix: - opt, err = ParseOptIAPrefix(optData) - case OptionInformationRefreshTime: - opt, err = parseOptInformationRefreshTime(optData) - case OptionRemoteID: - opt, err = ParseOptRemoteID(optData) - case OptionFQDN: - opt, err = ParseOptFQDN(optData) - case OptionNTPServer: - opt, err = ParseOptNTPServer(optData) - case OptionBootfileURL: - opt, err = parseOptBootFileURL(optData) - case OptionBootfileParam: - opt, err = parseOptBootFileParam(optData) - case OptionClientArchType: - opt, err = parseOptClientArchType(optData) - case OptionNII: - var o OptNetworkInterfaceID - err = o.FromBytes(optData) - opt = &o - case OptionClientLinkLayerAddr: - opt, err = parseOptClientLinkLayerAddress(optData) - case OptionDHCPv4Msg: - opt, err = ParseOptDHCPv4Msg(optData) - case OptionDHCP4oDHCP6Server: - opt, err = ParseOptDHCP4oDHCP6Server(optData) - case Option4RD: - opt, err = ParseOpt4RD(optData) - case Option4RDMapRule: - opt, err = ParseOpt4RDMapRule(optData) - case Option4RDNonMapRule: - opt, err = ParseOpt4RDNonMapRule(optData) - case OptionRelayPort: - opt, err = parseOptRelayPort(optData) - default: - opt = &OptionGeneric{OptionCode: code, OptionData: optData} - } - if err != nil { - return nil, err - } - return opt, nil +// FromBytes resets OptionData to p. +func (og *OptionGeneric) FromBytes(p []byte) error { + og.OptionData = append([]byte(nil), p...) + return nil } type longStringer interface { @@ -214,18 +135,12 @@ func (o Options) ToBytes() []byte { return buf.Data() } -// FromBytes reads data into o and returns an error if the options are not a -// valid serialized representation of DHCPv6 options per RFC 3315. -func (o *Options) FromBytes(data []byte) error { - return o.FromBytesWithParser(data, ParseOption) -} - -// OptionParser is a function signature for option parsing -type OptionParser func(code OptionCode, data []byte) (Option, error) +// Optioner returns a new zero-value option for the given option code. +type Optioner func(code OptionCode) Option // FromBytesWithParser parses Options from byte sequences using the parsing // function that is passed in as a paremeter -func (o *Options) FromBytesWithParser(data []byte, parser OptionParser) error { +func (o *Options) FromBytesWithParser(data []byte, optioner Optioner) error { *o = make(Options, 0, 10) if len(data) == 0 { // no options, no party @@ -241,8 +156,14 @@ func (o *Options) FromBytesWithParser(data []byte, parser OptionParser) error { // pertinent data. optData := buf.Consume(length) - opt, err := parser(code, optData) - if err != nil { + opt := optioner(code) + if opt == nil { + // Most RFCs that define options say that options + // should be ignored in messages where they don't + // belong. + opt = &OptionGeneric{OptionCode: code} + } + if err := opt.FromBytes(optData); err != nil { return err } *o = append(*o, opt) diff --git a/dhcpv6/prettyprint_test.go b/dhcpv6/prettyprint_test.go index def212c9..a07bae58 100644 --- a/dhcpv6/prettyprint_test.go +++ b/dhcpv6/prettyprint_test.go @@ -23,7 +23,7 @@ func TestPrint(t *testing.T) { oneiata.Options.Add(iaaddr) fourrd := &Opt4RD{} - fourrd.Add(&Opt4RDMapRule{ + fourrd.Options.Add(&Opt4RDMapRule{ Prefix4: net.IPNet{ IP: net.IP{123, 123, 0, 0}, Mask: net.CIDRMask(16, 32), @@ -33,7 +33,7 @@ func TestPrint(t *testing.T) { Mask: net.CIDRMask(64, 128), }, }) - fourrd.Add(&Opt4RDNonMapRule{ + fourrd.Options.Add(&Opt4RDNonMapRule{ HubAndSpoke: true, })