diff --git a/conn_test.go b/conn_test.go index 116ab74..57ffdc8 100644 --- a/conn_test.go +++ b/conn_test.go @@ -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 } diff --git a/internal/ndpcmd/print.go b/internal/ndpcmd/print.go index dce82da..888e1bf 100644 --- a/internal/ndpcmd/print.go +++ b/internal/ndpcmd/print.go @@ -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: diff --git a/option.go b/option.go index 0d20ebe..f27cae6 100644 --- a/option.go +++ b/option.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "io" + "math" "net" "net/netip" "net/url" @@ -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 @@ -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 { @@ -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: diff --git a/option_test.go b/option_test.go index 740363b..98f1d12 100644 --- a/option_test.go +++ b/option_test.go @@ -71,6 +71,10 @@ func TestOptionMarshalUnmarshal(t *testing.T) { name: "captive portal", subs: cpTests(), }, + { + name: "pref64", + subs: pref64Tests(), + }, { name: "nonce", subs: nonceTests(), @@ -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) } }) @@ -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() @@ -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 +}