summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--dhcpv6/dhcpv6message.go12
-rw-r--r--dhcpv6/option_4rd.go53
-rw-r--r--dhcpv6/option_4rd_test.go415
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)
- }
-}