Skip to content

Commit

Permalink
ndp: add PREF64 support
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 committed Mar 10, 2024
1 parent 239470e commit d810a7e
Show file tree
Hide file tree
Showing 4 changed files with 196 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
141 changes: 141 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,143 @@ func (cp *CaptivePortal) unmarshal(b []byte) error {
return nil
}

// Pref64 is a PREF64 option, as described in RFC 8781, Section 4.
type Pref64 struct {
Lifetime time.Duration
Prefix netip.Prefix
}

func NewPref64(prefix netip.Prefix, maxInterval time.Duration) (*Pref64, error) {
// Calculate the scaled lifetime using MaxRtrAdvInterval.
// See https://datatracker.ietf.org/doc/html/rfc8781#section-4.1-2
lifetime := time.Duration(8191*8) * time.Second
maxIntervalSeconds := int(maxInterval.Seconds())
if maxIntervalSeconds*3 < int(lifetime.Seconds()) {
// TODO(jared): RFC8781 implies that 65535 is the maximum lifetime
// value, but everywhere else in the RFC, 8191*8 is used as the
// maximum. See https://datatracker.ietf.org/doc/html/rfc8781#section-4.1-3.
maxLifetimeSeconds := 1<<16 - 1

lifetimeSeconds := maxIntervalSeconds

if r := int(maxIntervalSeconds) % 8; r > 0 {
lifetimeSeconds += 8 - r
}

if lifetimeSeconds > maxLifetimeSeconds {
lifetimeSeconds = maxLifetimeSeconds
}

lifetime = time.Duration(lifetimeSeconds) * time.Second
}

return &Pref64{
Lifetime: lifetime,
Prefix: prefix,
}, nil
}

func (p *Pref64) Code() byte { return optPref64 }

func (p *Pref64) marshal() ([]byte, error) {
value := []byte{}

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))

if scaledLifetime&(0b111<<(5+8)) > 0 {
return nil, errors.New("ndp: pref64 scaled lifetime is too large")
}

value = binary.BigEndian.AppendUint16(
value,
(scaledLifetime&((0b11111<<8)|0xff))<<3|uint16(plc&0b111),
)

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

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

b, err := raw.marshal()
return b, err
}

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 +1137,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
52 changes: 51 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,43 @@ func cpTests() []optionSub {
}
}

func pref64Tests() []optionSub {
return []optionSub{
{
name: "bad, invalid prefix size",
os: []Option{
&Pref64{Prefix: netip.MustParsePrefix("2001:db8::/33")},
},
},
{
name: "ok, small lifetime",
os: []Option{
mustPref64(netip.MustParsePrefix("2001:db8::/96"), time.Minute*10),
},
bs: [][]byte{
{0x26, 0x02}, {
0x02, 0x58, 0x20, 0x01, 0x0d, 0xb8, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
},
ok: true,
},
{
name: "ok, maximum prefix",
os: []Option{
mustPref64(netip.MustParsePrefix("2001:db8::/32"), time.Hour*24),
},
bs: [][]byte{
{0x26, 0x02}, {
0xff, 0xfd, 0x20, 0x01, 0x0d, 0xb8, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
},
},
ok: true,
},
}
}

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

Expand Down Expand Up @@ -1074,3 +1115,12 @@ func mustCaptivePortal(uri string) *CaptivePortal {

return cp
}

func mustPref64(prefix netip.Prefix, maxInterval time.Duration) *Pref64 {
pref64, err := NewPref64(prefix, maxInterval)
if err != nil {
panicf("failed to create pref64 option: %v", err)
}

return pref64
}

0 comments on commit d810a7e

Please sign in to comment.