Skip to content

Commit

Permalink
ndp: add PREF64 support (#31)
Browse files Browse the repository at this point in the history
This change brings PREF64 support as specified in RFC 8781.
  • Loading branch information
jmbaur authored Mar 12, 2024
1 parent 42423ff commit 2971198
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 2 deletions.
3 changes: 2 additions & 1 deletion conn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,4 +193,5 @@ func TestSolicitedNodeMulticast(t *testing.T) {
}
}

func addrEqual(x, y netip.Addr) bool { return x == y }
func addrEqual(x, y netip.Addr) bool { return x == y }
func prefixEqual(x, y netip.Prefix) bool { return x == y }
2 changes: 2 additions & 0 deletions internal/ndpcmd/print.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ func optStr(o ndp.Option) string {
return fmt.Sprintf("DNS search list: lifetime: %s, domain names: %s", o.Lifetime, strings.Join(o.DomainNames, ", "))
case *ndp.CaptivePortal:
return fmt.Sprintf("captive portal: %s", o.URI)
case *ndp.PREF64:
return fmt.Sprintf("pref64: %s, lifetime: %s", o.Prefix, o.Lifetime)
case *ndp.Nonce:
return fmt.Sprintf("nonce: %s", o)
default:
Expand Down
118 changes: 118 additions & 0 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"io"
"math"
"net"
"net/netip"
"net/url"
Expand Down Expand Up @@ -43,6 +44,7 @@ const (
optRAFlagsExtension = 26
optDNSSL = 31
optCaptivePortal = 37
optPREF64 = 38
)

// A Direction specifies the direction of a LinkLayerAddress Option as a source
Expand Down Expand Up @@ -767,6 +769,120 @@ func (cp *CaptivePortal) unmarshal(b []byte) error {
return nil
}

// PREF64 is a PREF64 option, as described in RFC 8781, Section 4. The prefix
// must have a prefix length of 96, 64, 56, 40, or 32. The lifetime is used to
// indicate to clients how long the PREF64 prefix is valid for. A lifetime of 0
// indicates the prefix is no longer valid. If unsure, refer to RFC 8781
// Section 4.1 for how to calculate an appropriate lifetime.
type PREF64 struct {
Lifetime time.Duration
Prefix netip.Prefix
}

func (p *PREF64) Code() byte { return optPREF64 }

func (p *PREF64) marshal() ([]byte, error) {
var plc uint8
switch p.Prefix.Bits() {
case 96:
plc = 0
case 64:
plc = 1
case 56:
plc = 2
case 48:
plc = 3
case 40:
plc = 4
case 32:
plc = 5
default:
return nil, errors.New("ndp: invalid pref64 prefix size")
}

scaledLifetime := uint16(math.Round(p.Lifetime.Seconds() / 8))

// The scaled lifetime must be less than the maximum of 8191.
if scaledLifetime > 8191 {
return nil, errors.New("ndp: pref64 scaled lifetime is too large")
}

value := []byte{}

// The scaled lifetime and PLC values live within the same 16-bit field.
// Here we move the scaled lifetime to the left-most 13 bits and place the
// PLC at the last 3 bits of the 16-bit field.
value = binary.BigEndian.AppendUint16(
value,
(scaledLifetime<<3&(0xffff^0b111))|uint16(plc&0b111),
)

allPrefixBits := p.Prefix.Masked().Addr().As16()
optionPrefixBits := allPrefixBits[:96/8]
value = append(value, optionPrefixBits...)

raw := &RawOption{
Type: p.Code(),
Length: (uint8(len(value)) + 2) / 8,
Value: value,
}

return raw.marshal()
}

func (p *PREF64) unmarshal(b []byte) error {
raw := new(RawOption)
if err := raw.unmarshal(b); err != nil {
return err
}

if raw.Type != optPREF64 {
return errors.New("ndp: invalid pref64 type")
}

if len(raw.Value) != (96/8)+2 {
return errors.New("ndp: invalid pref64 message length")
}

lifetimeAndPlc := binary.BigEndian.Uint16(raw.Value[:2])
plc := uint8(lifetimeAndPlc & 0b111)

var prefixSize int
switch plc {
case 0:
prefixSize = 96
case 1:
prefixSize = 64
case 2:
prefixSize = 56
case 3:
prefixSize = 48
case 4:
prefixSize = 40
case 5:
prefixSize = 32
default:
return errors.New("ndp: invalid pref64 prefix length code")
}

addr := [16]byte{}
copy(addr[:], raw.Value[2:])
prefix, err := netip.AddrFrom16(addr).Prefix(int(prefixSize))
if err != nil {
return err
}

scaledLifetime := (lifetimeAndPlc & (0xffff ^ 0b111)) >> 3
lifetime := time.Duration(scaledLifetime) * 8 * time.Second

*p = PREF64{
Lifetime: lifetime,
Prefix: prefix,
}

return nil
}

// A RAFlagsExtension is a Router Advertisement Flags Extension (or Expansion)
// option, as described in RFC 5175, Section 4.
type RAFlagsExtension struct {
Expand Down Expand Up @@ -998,6 +1114,8 @@ func parseOptions(b []byte) ([]Option, error) {
o = new(DNSSearchList)
case optCaptivePortal:
o = new(CaptivePortal)
case optPREF64:
o = new(PREF64)
case optNonce:
o = new(Nonce)
default:
Expand Down
101 changes: 100 additions & 1 deletion option_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ func TestOptionMarshalUnmarshal(t *testing.T) {
name: "captive portal",
subs: cpTests(),
},
{
name: "pref64",
subs: pref64Tests(),
},
{
name: "nonce",
subs: nonceTests(),
Expand Down Expand Up @@ -104,7 +108,7 @@ func TestOptionMarshalUnmarshal(t *testing.T) {
t.Fatalf("failed to unmarshal options: %v", err)
}

if diff := cmp.Diff(st.os, got, cmp.Comparer(addrEqual)); diff != "" {
if diff := cmp.Diff(st.os, got, cmp.Comparer(addrEqual), cmp.Comparer(prefixEqual)); diff != "" {
t.Fatalf("unexpected options (-want +got):\n%s", diff)
}
})
Expand Down Expand Up @@ -1021,6 +1025,101 @@ func cpTests() []optionSub {
}
}

func pref64Tests() []optionSub {
return []optionSub{
{
name: "bad, invalid prefix size",
os: []Option{
&PREF64{Prefix: netip.MustParsePrefix("2001:db8::/33"), Lifetime: time.Duration(0)},
},
},
{
name: "bad, invalid lifetime",
os: []Option{
&PREF64{Prefix: netip.MustParsePrefix("2001:db8::/32"), Lifetime: time.Hour * 24},
},
},
{
name: "ok, smallest prefix, max lifetime",
os: []Option{
&PREF64{Prefix: netip.MustParsePrefix("2001:db8::/96"), Lifetime: time.Second * 8 * 8191},
},
bs: [][]byte{
{0x26, 0x02}, {
0xff, 0xf8, 0x20, 0x01, 0x0d, 0xb8, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
},
ok: true,
},
{
name: "ok, /64 prefix",
os: []Option{
&PREF64{Prefix: netip.MustParsePrefix("2001:db8::/64"), Lifetime: time.Second * 8 * 8191},
},
bs: [][]byte{
{0x26, 0x02}, {
0xff, 0xf9, 0x20, 0x01, 0x0d, 0xb8, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
},
ok: true,
},
{
name: "ok, /56 prefix",
os: []Option{
&PREF64{Prefix: netip.MustParsePrefix("2001:db8::/56"), Lifetime: time.Second * 8 * 8191},
},
bs: [][]byte{
{0x26, 0x02}, {
0xff, 0xfa, 0x20, 0x01, 0x0d, 0xb8, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
},
ok: true,
},
{
name: "ok, /48 prefix",
os: []Option{
&PREF64{Prefix: netip.MustParsePrefix("2001:db8::/48"), Lifetime: time.Second * 8 * 8191},
},
bs: [][]byte{
{0x26, 0x02}, {
0xff, 0xfb, 0x20, 0x01, 0x0d, 0xb8, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
},
ok: true,
},
{
name: "ok, /40 prefix",
os: []Option{
&PREF64{Prefix: netip.MustParsePrefix("2001:db8::/40"), Lifetime: time.Second * 8 * 8191},
},
bs: [][]byte{
{0x26, 0x02}, {
0xff, 0xfc, 0x20, 0x01, 0x0d, 0xb8, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
},
ok: true,
},
{
name: "ok, maximum prefix, small lifetime",
os: []Option{
&PREF64{Prefix: netip.MustParsePrefix("2001:db8::/32"), Lifetime: time.Minute * 10},
},
bs: [][]byte{
{0x26, 0x02}, {
0x02, 0x5d, 0x20, 0x01, 0x0d, 0xb8, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
},
ok: true,
},
}
}

func nonceTests() []optionSub {
nonce := NewNonce()

Expand Down

0 comments on commit 2971198

Please sign in to comment.