diff options
author | Anatole Denis <natolumin@unverle.fr> | 2019-11-03 16:46:39 +0100 |
---|---|---|
committer | Anatole Denis <natolumin@unverle.fr> | 2019-11-03 17:00:50 +0100 |
commit | 51aead750bd8a5a9fb757f63d4663ba1b4be9bb6 (patch) | |
tree | 4b7b0a0e634181a8f058dc77f8218f0cf54bdd22 /dhcpv6 | |
parent | 80d8a71da7249766be583e8dd64d3cd566ab86c5 (diff) |
dhcpv6: Add support for 4RD options
IPv4 Residual Deployment (4RD) is a strategy for providing IPv4
connectivity in IPv6-only networks.
The standard includes autoconfiguration via DHCPv6, as described in
RFC7600. This adds support for the 3 options defined in that RFC
Signed-off-by: Anatole Denis <natolumin@unverle.fr>
Diffstat (limited to 'dhcpv6')
-rw-r--r-- | dhcpv6/option_4rd.go | 178 | ||||
-rw-r--r-- | dhcpv6/option_4rd_test.go | 168 | ||||
-rw-r--r-- | dhcpv6/options.go | 6 |
3 files changed, 352 insertions, 0 deletions
diff --git a/dhcpv6/option_4rd.go b/dhcpv6/option_4rd.go new file mode 100644 index 0000000..9bac74a --- /dev/null +++ b/dhcpv6/option_4rd.go @@ -0,0 +1,178 @@ +package dhcpv6 + +import ( + "fmt" + "net" + + "github.com/u-root/u-root/pkg/uio" +) + +// Opt4RD represents a 4RD option. It is only a container for 4RD_*_RULE options +type Opt4RD Options + +// Code returns the Option Code for this option +func (op *Opt4RD) Code() OptionCode { + return Option4RD +} + +// ToBytes serializes this option +func (op *Opt4RD) ToBytes() []byte { + return (*Options)(op).ToBytes() +} + +// String returns a human-readable representation of the option +func (op *Opt4RD) String() string { + return fmt.Sprintf("Opt4RD{%v}", (*Options)(op)) +} + +// ParseOpt4RD 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 Options + err := opt.FromBytes(data) + return (*Opt4RD)(&opt), err +} + +// 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 +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 +} + +const ( + // opt4RDWKPAuthorizedMask is the mask for the WKPAuthorized flag in its + // byte in Opt4RDMapRule + opt4RDWKPAuthorizedMask = 1 << 7 + // opt4RDHubAndSpokeMask is the mask for the HubAndSpoke flag in its + // byte in Opt4RDNonMapRule + opt4RDHubAndSpokeMask = 1 << 7 + // opt4RDTrafficClassMask is the mask for the TrafficClass flag in its + // byte in Opt4RDNonMapRule + opt4RDTrafficClassMask = 1 << 0 +) + +// Code returns the option code representing this option +func (op *Opt4RDMapRule) Code() OptionCode { return Option4RDMapRule } + +// ToBytes serializes this option +func (op *Opt4RDMapRule) ToBytes() []byte { + buf := uio.NewBigEndianBuffer(nil) + p4Len, _ := op.Prefix4.Mask.Size() + p6Len, _ := op.Prefix6.Mask.Size() + buf.Write8(uint8(p4Len)) + buf.Write8(uint8(p6Len)) + buf.Write8(op.EABitsLength) + if op.WKPAuthorized { + buf.Write8(opt4RDWKPAuthorizedMask) + } else { + buf.Write8(0) + } + if op.Prefix4.IP.To4() == nil { + // The API prevents us from returning an error here + // We just write zeros instead, which is pretty bad behaviour + buf.Write32(0) + } else { + buf.WriteBytes(op.Prefix4.IP.To4()) + } + if op.Prefix6.IP.To16() == nil { + buf.Write64(0) + buf.Write64(0) + } else { + buf.WriteBytes(op.Prefix6.IP.To16()) + } + return buf.Data() +} + +// String returns a human-readable description of this option +func (op *Opt4RDMapRule) String() string { + return fmt.Sprintf("Opt4RDMapRule{Prefix4=%s, Prefix6=%s, EA-Bits=%d, WKPAuthorized=%t}", + op.Prefix4.String(), op.Prefix6.String(), op.EABitsLength, op.WKPAuthorized) +} + +// ParseOpt4RDMapRule 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 + 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() +} + +// Opt4RDNonMapRule represents 4RD parameters other than mapping rules +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 +} + +// Code returns the option code for this option +func (op *Opt4RDNonMapRule) Code() OptionCode { + return Option4RDNonMapRule +} + +// ToBytes serializes this option +func (op *Opt4RDNonMapRule) ToBytes() []byte { + buf := uio.NewBigEndianBuffer(nil) + var flags uint8 + var trafficClassValue uint8 + if op.HubAndSpoke { + flags |= opt4RDHubAndSpokeMask + } + if op.TrafficClass != nil { + flags |= opt4RDTrafficClassMask + trafficClassValue = *op.TrafficClass + } + + buf.Write8(flags) + buf.Write8(trafficClassValue) + buf.Write16(op.DomainPMTU) + + return buf.Data() +} + +// String returns a human-readable description of this option +func (op *Opt4RDNonMapRule) String() string { + var tClass interface{} = false + if op.TrafficClass != nil { + tClass = *op.TrafficClass + } + + return fmt.Sprintf("Opt4RDNonMapRule{HubAndSpoke=%t, TrafficClass=%v, DomainPMTU=%d}", + op.HubAndSpoke, tClass, op.DomainPMTU) +} + +// ParseOpt4RDNonMapRule 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 + buf := uio.NewBigEndianBuffer(data) + flags := buf.Read8() + + opt.HubAndSpoke = flags&opt4RDHubAndSpokeMask != 0 + + tClass := buf.Read8() + if flags&opt4RDTrafficClassMask != 0 { + opt.TrafficClass = &tClass + } + + opt.DomainPMTU = buf.Read16() + + return &opt, buf.FinError() +} diff --git a/dhcpv6/option_4rd_test.go b/dhcpv6/option_4rd_test.go new file mode 100644 index 0000000..1251b13 --- /dev/null +++ b/dhcpv6/option_4rd_test.go @@ -0,0 +1,168 @@ +package dhcpv6 + +import ( + "net" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestOpt4RDNonMapRuleParse(t *testing.T) { + data := []byte{0x81, 0xaa, 0x05, 0xd4} + opt, err := ParseOpt4RDNonMapRule(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) + + // Remove the TrafficClass flag and check value is ignored + data[0] = 0x80 + opt, err = ParseOpt4RDNonMapRule(data) + require.NoError(t, err) + require.True(t, opt.HubAndSpoke) + require.Nil(t, opt.TrafficClass) + require.EqualValues(t, 1492, opt.DomainPMTU) +} + +func TestOpt4RDNonMapRuleToBytes(t *testing.T) { + var tClass uint8 = 0xaa + opt := Opt4RDNonMapRule{ + HubAndSpoke: true, + TrafficClass: &tClass, + DomainPMTU: 1492, + } + 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 TestOpt4RDNonMapRuleString(t *testing.T) { + var tClass uint8 = 120 + opt := Opt4RDNonMapRule{ + HubAndSpoke: true, + TrafficClass: &tClass, + DomainPMTU: 9000, + } + + str := opt.String() + + require.Contains(t, str, "HubAndSpoke=true", + "String() should contain the HubAndSpoke flag value") + require.Contains(t, str, "TrafficClass=120", + "String() should contain the TrafficClass flag value") + require.Contains(t, str, "DomainPMTU=9000", + "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...)..., + ) + + opt, err := ParseOpt4RDMapRule(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 + 32, // EA-bits + 0x80, // WKPs authorized + }, + append(opt.Prefix4.IP.To4(), opt.Prefix6.IP.To16()...)..., + ) + + require.Equal(t, expected, opt.ToBytes()) +} + +// FIXME: Invalid packets are serialized without error + +func TestOpt4RDMapRuleString(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, + } + + str := opt.String() + require.Contains(t, str, "WKPAuthorized=true", "String() should write the flag values") + require.Contains(t, str, "Prefix6=2001:db8::1234:5678:0:aabb/80", + "String() should include the IPv6 prefix") + require.Contains(t, str, "Prefix4=100.64.0.238/24", + "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{ + &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, + }, + } + + rtOpt, err := ParseOpt4RD(opt.ToBytes()) + + require.NoError(t, err) + require.NotNil(t, rtOpt) + require.Equal(t, opt, *rtOpt) +} diff --git a/dhcpv6/options.go b/dhcpv6/options.go index 47ccdca..cf192fc 100644 --- a/dhcpv6/options.go +++ b/dhcpv6/options.go @@ -79,6 +79,12 @@ func ParseOption(code OptionCode, optData []byte) (Option, error) { opt, err = ParseOptClientArchType(optData) case OptionNII: opt, err = ParseOptNetworkInterfaceId(optData) + case Option4RD: + opt, err = ParseOpt4RD(optData) + case Option4RDMapRule: + opt, err = ParseOpt4RDMapRule(optData) + case Option4RDNonMapRule: + opt, err = ParseOpt4RDNonMapRule(optData) default: opt = &OptionGeneric{OptionCode: code, OptionData: optData} } |