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..60d4557 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..95de367 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,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 { @@ -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: diff --git a/option_test.go b/option_test.go index 740363b..e06a4c8 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,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()