diff options
-rw-r--r-- | dhcpv6/dhcpv6message.go | 12 | ||||
-rw-r--r-- | dhcpv6/option_4rd.go | 53 | ||||
-rw-r--r-- | dhcpv6/option_4rd_test.go | 415 |
3 files changed, 358 insertions, 122 deletions
diff --git a/dhcpv6/dhcpv6message.go b/dhcpv6/dhcpv6message.go index 45fa5f1..72332c5 100644 --- a/dhcpv6/dhcpv6message.go +++ b/dhcpv6/dhcpv6message.go @@ -106,6 +106,18 @@ func (mo MessageOptions) OneIAPD() *OptIAPD { return iapds[0] } +// FourRD returns all 4RD options. +func (mo MessageOptions) FourRD() []*Opt4RD { + opts := mo.Get(Option4RD) + var frds []*Opt4RD + for _, o := range opts { + if m, ok := o.(*Opt4RD); ok { + frds = append(frds, m) + } + } + return frds +} + // Status returns the status code associated with this option. func (mo MessageOptions) Status() *OptStatusCode { opt := mo.Options.GetOne(OptionStatusCode) diff --git a/dhcpv6/option_4rd.go b/dhcpv6/option_4rd.go index 34e1fb9..3b074f4 100644 --- a/dhcpv6/option_4rd.go +++ b/dhcpv6/option_4rd.go @@ -9,7 +9,7 @@ import ( // Opt4RD represents a 4RD option. It is only a container for 4RD_*_RULE options type Opt4RD struct { - Options + FourRDOptions } // Code returns the Option Code for this option @@ -38,16 +38,56 @@ func (op *Opt4RD) FromBytes(data []byte) error { return op.Options.FromBytes(data) } -// Opt4RDMapRule represents a 4RD Mapping Rule option -// The option is described in https://tools.ietf.org/html/rfc7600#section-4.9 -// The 4RD mapping rules are described in https://tools.ietf.org/html/rfc7600#section-4.2 +// FourRDOptions are options that can be encapsulated with the 4RD option. +type FourRDOptions struct { + Options +} + +// MapRules returns the map rules associated with the 4RD option. +// +// "The OPTION_4RD DHCPv6 option contains at least one encapsulated +// OPTION_4RD_MAP_RULE option." (RFC 7600 Section 4.9) +func (frdo FourRDOptions) MapRules() []*Opt4RDMapRule { + opts := frdo.Options.Get(Option4RDMapRule) + var mrs []*Opt4RDMapRule + for _, o := range opts { + if m, ok := o.(*Opt4RDMapRule); ok { + mrs = append(mrs, m) + } + } + return mrs +} + +// NonMapRule returns the non-map-rule associated with this option. +// +// "The OPTION_4RD DHCPv6 option contains ... a maximum of one +// encapsulated OPTION_4RD_NON_MAP_RULE option." (RFC 7600 Section 4.9) +func (frdo FourRDOptions) NonMapRule() *Opt4RDNonMapRule { + opt := frdo.Options.GetOne(Option4RDNonMapRule) + if opt == nil { + return nil + } + nmr, ok := opt.(*Opt4RDNonMapRule) + if !ok { + return nil + } + return nmr +} + +// Opt4RDMapRule represents a 4RD Mapping Rule option. +// +// The option is described in RFC 7600 Section 4.9. The 4RD mapping rules are +// described in RFC 7600 Section 4.2. type Opt4RDMapRule struct { // Prefix4 is the IPv4 prefix mapped by this rule Prefix4 net.IPNet + // Prefix6 is the IPv6 prefix mapped by this rule Prefix6 net.IPNet + // EABitsLength is the number of bits of an address used in constructing the mapped address EABitsLength uint8 + // WKPAuthorized determines if well-known ports are assigned to addresses in an A+P mapping // It can only be set if the length of Prefix4 + EABits > 32 WKPAuthorized bool @@ -120,8 +160,10 @@ func (op *Opt4RDMapRule) FromBytes(data []byte) error { type Opt4RDNonMapRule struct { // HubAndSpoke is whether the network topology is hub-and-spoke or meshed HubAndSpoke bool + // TrafficClass is an optional 8-bit tunnel traffic class identifier TrafficClass *uint8 + // DomainPMTU is the Path MTU for this 4RD domain DomainPMTU uint16 } @@ -158,8 +200,7 @@ func (op *Opt4RDNonMapRule) String() string { tClass = *op.TrafficClass } - return fmt.Sprintf("%s: {HubAndSpoke=%t, TrafficClass=%v, DomainPMTU=%d}", - op.Code(), op.HubAndSpoke, tClass, op.DomainPMTU) + return fmt.Sprintf("%s: {HubAndSpoke=%t, TrafficClass=%v, DomainPMTU=%d}", op.Code(), op.HubAndSpoke, tClass, op.DomainPMTU) } // FromBytes builds an Opt4RDNonMapRule structure from a sequence of bytes. diff --git a/dhcpv6/option_4rd_test.go b/dhcpv6/option_4rd_test.go index 8cd5e01..ca85bdd 100644 --- a/dhcpv6/option_4rd_test.go +++ b/dhcpv6/option_4rd_test.go @@ -1,49 +1,314 @@ package dhcpv6 import ( + "bytes" + "errors" + "fmt" "net" "reflect" "testing" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" + "github.com/u-root/uio/uio" ) -func TestOpt4RDNonMapRuleParse(t *testing.T) { - data := []byte{0x81, 0xaa, 0x05, 0xd4} - var opt Opt4RDNonMapRule - err := opt.FromBytes(data) - require.NoError(t, err) - require.True(t, opt.HubAndSpoke) - require.NotNil(t, opt.TrafficClass) - require.EqualValues(t, 0xaa, *opt.TrafficClass) - require.EqualValues(t, 1492, opt.DomainPMTU) +func Test4RDParseAndGetter(t *testing.T) { + for i, tt := range []struct { + buf []byte + err error + want []*Opt4RD + }{ + { + buf: []byte{ + 0, 97, // 4RD option code + 0, 28, // length + 0, 98, // 4RD Map Rule option + 0, 24, // length + 16, // prefix4-length + 16, // prefix6-length + 8, // ea-len + 0, // WKPAuthorized + 192, 168, 0, 1, // rule-ipv4-prefix + 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // rule-ipv6-prefix + }, + want: []*Opt4RD{ + &Opt4RD{ + FourRDOptions: FourRDOptions{Options: Options{ + &Opt4RDMapRule{ + Prefix4: net.IPNet{ + IP: net.IP{192, 168, 0, 1}, + Mask: net.CIDRMask(16, 32), + }, + Prefix6: net.IPNet{ + IP: net.ParseIP("fe80::"), + Mask: net.CIDRMask(16, 128), + }, + EABitsLength: 8, + }, + }}, + }, + }, + }, + { + buf: []byte{ + 0, 97, // 4RD option code + 0, 28, // length + 0, 98, // 4RD Map Rule option + 0, 24, // length + 16, // prefix4-length + 16, // prefix6-length + 8, // ea-len + 0, // WKPAuthorized + 192, 168, 0, 1, // rule-ipv4-prefix + 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // rule-ipv6-prefix - // Remove the TrafficClass flag and check value is ignored - data[0] = 0x80 - opt = Opt4RDNonMapRule{} - err = opt.FromBytes(data) - require.NoError(t, err) - require.True(t, opt.HubAndSpoke) - require.Nil(t, opt.TrafficClass) - require.EqualValues(t, 1492, opt.DomainPMTU) + 0, 97, // 4RD + 0, 8, // length + 0, 99, // 4RD non map rule + 0, 4, // length + 0x80, 0x00, 0x05, 0xd4, + }, + want: []*Opt4RD{ + &Opt4RD{ + FourRDOptions: FourRDOptions{Options: Options{ + &Opt4RDMapRule{ + Prefix4: net.IPNet{ + IP: net.IP{192, 168, 0, 1}, + Mask: net.CIDRMask(16, 32), + }, + Prefix6: net.IPNet{ + IP: net.ParseIP("fe80::"), + Mask: net.CIDRMask(16, 128), + }, + EABitsLength: 8, + }, + }}, + }, + &Opt4RD{ + FourRDOptions: FourRDOptions{Options: Options{ + &Opt4RDNonMapRule{ + HubAndSpoke: true, + DomainPMTU: 1492, + }, + }}, + }, + }, + }, + { + buf: []byte{0, 97, 0, 1, 0}, + want: nil, + err: uio.ErrUnreadBytes, + }, + { + // Allowed, because the RFC doesn't really specify that + // it can't be empty. RFC doesn't really specify + // anything, frustratingly. + buf: []byte{ + 0, 97, // 4RD option code + 0, 0, // length + }, + want: []*Opt4RD{&Opt4RD{FourRDOptions: FourRDOptions{Options: Options{}}}}, + }, + { + buf: []byte{ + 0, 97, // 4RD option code + 0, 6, // length + 0, 98, // 4RD Map Rule option + 0, 4, // length + 16, // prefix4-length + 16, // prefix6-length + 8, // ea-len + 0, // WKPAuthorized + // Missing + }, + want: nil, + err: uio.ErrBufferTooShort, + }, + } { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + var mo MessageOptions + if err := mo.FromBytes(tt.buf); !errors.Is(err, tt.err) { + t.Errorf("FromBytes = %v, want %v", err, tt.err) + } + if got := mo.FourRD(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("FourRD = %v, want %v", got, tt.want) + } + if len(tt.want) >= 1 { + var b MessageOptions + for _, frd := range tt.want { + b.Add(frd) + } + got := b.ToBytes() + if diff := cmp.Diff(tt.buf, got); diff != "" { + t.Errorf("ToBytes mismatch (-want, +got): %s", diff) + } + } + }) + } } -func TestOpt4RDNonMapRuleToBytes(t *testing.T) { - var tClass uint8 = 0xaa - opt := Opt4RDNonMapRule{ - HubAndSpoke: true, - TrafficClass: &tClass, - DomainPMTU: 1492, +func Test4RDMapRuleParseAndGetter(t *testing.T) { + for i, tt := range []struct { + buf []byte + err error + want []*Opt4RDMapRule + }{ + { + buf: []byte{ + 0, 98, // 4RD Map Rule option + 0, 24, // length + 16, // prefix4-length + 16, // prefix6-length + 8, // ea-len + 0, // WKPAuthorized + 192, 168, 0, 1, // rule-ipv4-prefix + 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // rule-ipv6-prefix + }, + want: []*Opt4RDMapRule{ + &Opt4RDMapRule{ + Prefix4: net.IPNet{ + IP: net.IP{192, 168, 0, 1}, + Mask: net.CIDRMask(16, 32), + }, + Prefix6: net.IPNet{ + IP: net.ParseIP("fe80::"), + Mask: net.CIDRMask(16, 128), + }, + EABitsLength: 8, + }, + }, + }, + { + buf: []byte{ + 0, 98, // 4RD Map Rule option + 0, 24, // length + 16, // prefix4-length + 16, // prefix6-length + 8, // ea-len + 1 << 7, // WKPAuthorized + 192, 168, 0, 1, // rule-ipv4-prefix + 0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // rule-ipv6-prefix + }, + want: []*Opt4RDMapRule{ + &Opt4RDMapRule{ + Prefix4: net.IPNet{ + IP: net.IP{192, 168, 0, 1}, + Mask: net.CIDRMask(16, 32), + }, + Prefix6: net.IPNet{ + IP: net.ParseIP("fe80::"), + Mask: net.CIDRMask(16, 128), + }, + EABitsLength: 8, + WKPAuthorized: true, + }, + }, + }, + { + buf: []byte{0, 98, 0, 1, 0}, + want: nil, + err: uio.ErrBufferTooShort, + }, + { + buf: []byte{ + 0, 98, // 4RD Map Rule option + 0, 4, // length + 16, // prefix4-length + 16, // prefix6-length + 8, // ea-len + 0, // WKPAuthorized + // Missing + }, + want: nil, + err: uio.ErrBufferTooShort, + }, + } { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + var frdo FourRDOptions + if err := frdo.FromBytes(tt.buf); !errors.Is(err, tt.err) { + t.Errorf("FromBytes = %v, want %v", err, tt.err) + } + if got := frdo.MapRules(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("MapRules = %v, want %v", got, tt.want) + } + if len(tt.want) >= 1 { + var b FourRDOptions + for _, frd := range tt.want { + b.Add(frd) + } + got := b.ToBytes() + if diff := cmp.Diff(tt.buf, got); diff != "" { + t.Errorf("ToBytes mismatch (-want, +got): %s", diff) + } + } + }) } - expected := []byte{0x81, 0xaa, 0x05, 0xd4} - - require.Equal(t, expected, opt.ToBytes()) - - // Unsetting TrafficClass should zero the corresponding bytes in the output - opt.TrafficClass = nil - expected[0], expected[1] = 0x80, 0x00 +} - require.Equal(t, expected, opt.ToBytes()) +func Test4RDNonMapRuleParseAndGetter(t *testing.T) { + trafficClassOne := uint8(1) + for i, tt := range []struct { + buf []byte + err error + want *Opt4RDNonMapRule + }{ + { + buf: []byte{ + 0, 99, // 4RD Non Map Rule option + 0, 4, // length + 0x80, 0, 0x05, 0xd4, + }, + want: &Opt4RDNonMapRule{ + HubAndSpoke: true, + DomainPMTU: 1492, + }, + }, + { + buf: []byte{ + 0, 99, // 4RD Non Map Rule option + 0, 4, // length + 0, 0, 0x05, 0xd4, + }, + want: &Opt4RDNonMapRule{ + DomainPMTU: 1492, + }, + }, + { + buf: []byte{ + 0, 99, // 4RD Non Map Rule option + 0, 4, // length + 0x1, 0x01, 0x05, 0xd4, + }, + want: &Opt4RDNonMapRule{ + TrafficClass: &trafficClassOne, + DomainPMTU: 1492, + }, + }, + { + buf: []byte{0, 99, 0, 1, 0}, + want: nil, + err: uio.ErrBufferTooShort, + }, + } { + t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { + var frdo FourRDOptions + if err := frdo.FromBytes(tt.buf); !errors.Is(err, tt.err) { + t.Errorf("FromBytes = %v, want %v", err, tt.err) + } + if got := frdo.NonMapRule(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NonMapRule = %v, want %v", got, tt.want) + } + if tt.want != nil { + var b FourRDOptions + b.Add(tt.want) + got := b.ToBytes() + if diff := cmp.Diff(tt.buf, got); diff != "" { + t.Errorf("ToBytes mismatch (-want, +got): %s", diff) + } + } + }) + } } func TestOpt4RDNonMapRuleString(t *testing.T) { @@ -64,59 +329,21 @@ func TestOpt4RDNonMapRuleString(t *testing.T) { "String() should contain the domain PMTU") } -func TestOpt4RDMapRuleParse(t *testing.T) { - ip6addr, ip6net, err := net.ParseCIDR("2001:db8::1234:5678:0:aabb/64") - ip6net.IP = ip6addr // We want to keep the entire address however, not apply the mask - require.NoError(t, err) - ip4addr, ip4net, err := net.ParseCIDR("100.64.0.234/10") - ip4net.IP = ip4addr.To4() - require.NoError(t, err) - data := append([]byte{ - 10, // IPv4 prefix length - 64, // IPv6 prefix length - 32, // EA-bits - 0x80, // WKPs authorized - }, - append(ip4addr.To4(), ip6addr...)..., - ) - - var opt Opt4RDMapRule - err = opt.FromBytes(data) - require.NoError(t, err) - require.EqualValues(t, *ip6net, opt.Prefix6) - require.EqualValues(t, *ip4net, opt.Prefix4) - require.EqualValues(t, 32, opt.EABitsLength) - require.True(t, opt.WKPAuthorized) -} - func TestOpt4RDMapRuleToBytes(t *testing.T) { opt := Opt4RDMapRule{ - Prefix4: net.IPNet{ - IP: net.IPv4(100, 64, 0, 238), - 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, } expected := append([]byte{ - 24, // v4 prefix length - 80, // v6 prefix length + 0, // v4 prefix length + 0, // v6 prefix length 32, // EA-bits 0x80, // WKPs authorized - }, - append(opt.Prefix4.IP.To4(), opt.Prefix6.IP.To16()...)..., - ) - + }, bytes.Repeat([]byte{0x00}, 4+16)...) require.Equal(t, expected, opt.ToBytes()) } -// FIXME: Invalid packets are serialized without error - func TestOpt4RDMapRuleString(t *testing.T) { opt := Opt4RDMapRule{ Prefix4: net.IPNet{ @@ -139,47 +366,3 @@ func TestOpt4RDMapRuleString(t *testing.T) { "String() should include the IPv4 prefix") require.Contains(t, str, "EA-Bits=32", "String() should include the value for EA-Bits") } - -// This test round-trip serialization/deserialization of both kinds of 4RD -// options, and the container option -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), - }, - Prefix6: net.IPNet{ - IP: net.ParseIP("2001:db8::1234:5678:0:aabb"), - Mask: net.CIDRMask(80, 128), - }, - EABitsLength: 32, - WKPAuthorized: true, - }, - &Opt4RDNonMapRule{ - HubAndSpoke: true, - TrafficClass: &tClass, - DomainPMTU: 9000, - }, - }, - } - - var rtOpt Opt4RD - err := rtOpt.FromBytes(opt.ToBytes()) - - require.NoError(t, err) - require.NotNil(t, 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) - } -} |