mirror of
https://github.com/soypat/lneto.git
synced 2026-09-10 08:39:30 +00:00
Handle IA_NA/IA_PD requests and config options in DHCPv6 (#147)
* feat(dhcpv6): handle IA_NA/IA_PD requests and config options Adds the DHCPv6 client request exchange with IA_NA/IA_PD handling, reconfigure-renew (RFC 8415), DNS server and domain search options (RFC 3646) and NTP server suboptions (RFC 5908). Signed-off-by: Marvin Drees <marvin.drees@9elements.com> * fix(dhcpv6): bound server-supplied option counts to prevent OOM A DHCPv6 server may place arbitrarily many DNS servers, search domains, NTP entries and delegated prefixes in a single message (RFC 8415). The client appended every entry into growable slices with no upper bound, so a malicious or misconfigured server could drive unbounded allocation. Add a configurable Limits to RequestConfig (with safe defaults) capping each repeated option. The backing arrays are sized to their caps once in BeginRequest and reused across resets, so parsing a server message now performs no allocation and stored entries stay bounded. Add tests asserting the caps are enforced against a flooded Reply and that option parsing is allocation-free after BeginRequest. Signed-off-by: Marvin Drees <marvin.drees@9elements.com> --------- Signed-off-by: Marvin Drees <marvin.drees@9elements.com>
This commit is contained in:
@@ -109,7 +109,9 @@ ok github.com/soypat/lneto/x/xnet 2.926s
|
|||||||
| Ethernet PHY/MDIO | IEEE 802.3 cl.22/45 | ✅ | `phy` | — | Bare-metal PHY management via MDIO |
|
| Ethernet PHY/MDIO | IEEE 802.3 cl.22/45 | ✅ | `phy` | — | Bare-metal PHY management via MDIO |
|
||||||
| IPv6 | RFC 8200 | ✅ | `ipv6` | — | Frame parsing and stack handling |
|
| IPv6 | RFC 8200 | ✅ | `ipv6` | — | Frame parsing and stack handling |
|
||||||
| ICMPv6 | RFC 4443 | ✅ | `ipv6/icmpv6` | — | Echo+NDP frame parsing and stack handling |
|
| ICMPv6 | RFC 4443 | ✅ | `ipv6/icmpv6` | — | Echo+NDP frame parsing and stack handling |
|
||||||
| DHCPv6 | RFC 8415 | 🟡 | `dhcp/dhcpv6` | — | Frame parsing and standalone handling |
|
| DHCPv6 DNS Configuration | RFC 3646 | 🟡 | `dhcp/dhcpv6` | — | Recursive DNS server and domain search options parsed and exposed |
|
||||||
|
| DHCPv6 NTP Configuration | RFC 5908 | ✅ | `dhcp/dhcpv6` | — | NTP server address, multicast address, and FQDN suboptions parsed and exposed |
|
||||||
|
| DHCPv6 | RFC 8415 | 🟡 | `dhcp/dhcpv6` | — | Client IA_NA/IA_PD request handling and reconfigure-renew; no relay agent or dynamic server pools |
|
||||||
| TLS 1.3 | RFC 8446 | ❌ | — | — | Not implemented |
|
| TLS 1.3 | RFC 8446 | ❌ | — | — | Not implemented |
|
||||||
|
|
||||||
¹ `BenchmarkARPExchange` — full ARP request/response exchange over Ethernet: **0 B/op, 0 allocs/op**
|
¹ `BenchmarkARPExchange` — full ARP request/response exchange over Ethernet: **0 B/op, 0 allocs/op**
|
||||||
|
|||||||
+337
-8
@@ -6,14 +6,77 @@ import (
|
|||||||
"net/netip"
|
"net/netip"
|
||||||
|
|
||||||
"github.com/soypat/lneto"
|
"github.com/soypat/lneto"
|
||||||
|
"github.com/soypat/lneto/dns"
|
||||||
"github.com/soypat/lneto/internal"
|
"github.com/soypat/lneto/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Default caps for the repeated, server-supplied options the client retains.
|
||||||
|
// A DHCPv6 server may place arbitrarily many of each in a single message
|
||||||
|
// (RFC 8415), so the client bounds them to prevent a malicious or
|
||||||
|
// misconfigured server from driving unbounded allocation (an OOM vector).
|
||||||
|
const (
|
||||||
|
defaultMaxDNSServers = 4
|
||||||
|
defaultMaxDomainSearch = 6
|
||||||
|
defaultMaxNTPServers = 4
|
||||||
|
defaultMaxNTPMulticastServers = 2
|
||||||
|
defaultMaxNTPServerNames = 4
|
||||||
|
defaultMaxDelegatedPrefixes = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
// Limits bounds how many entries of each repeated, server-supplied option the
|
||||||
|
// client retains from a single exchange. The backing arrays are sized to these
|
||||||
|
// caps once in [Client.BeginRequest] and reused across resets, so parsing a
|
||||||
|
// server message performs no further allocation and a hostile server cannot
|
||||||
|
// grow client memory without bound. A zero (or negative) field selects its
|
||||||
|
// default; see the default* constants.
|
||||||
|
type Limits struct {
|
||||||
|
MaxDNSServers int // OptDNSServers addresses.
|
||||||
|
MaxDomainSearch int // OptDomainList search names.
|
||||||
|
MaxNTPServers int // OptNTPServer suboption 1 addresses.
|
||||||
|
MaxNTPMulticastServers int // OptNTPServer suboption 2 addresses.
|
||||||
|
MaxNTPServerNames int // OptNTPServer suboption 3 FQDNs.
|
||||||
|
MaxDelegatedPrefixes int // OptIAPD/OptIAPrefix delegated prefixes.
|
||||||
|
}
|
||||||
|
|
||||||
|
// withDefaults returns a copy of l with every non-positive field replaced by
|
||||||
|
// its default cap, so the resolved limits are always usable.
|
||||||
|
func (l Limits) withDefaults() Limits {
|
||||||
|
if l.MaxDNSServers <= 0 {
|
||||||
|
l.MaxDNSServers = defaultMaxDNSServers
|
||||||
|
}
|
||||||
|
if l.MaxDomainSearch <= 0 {
|
||||||
|
l.MaxDomainSearch = defaultMaxDomainSearch
|
||||||
|
}
|
||||||
|
if l.MaxNTPServers <= 0 {
|
||||||
|
l.MaxNTPServers = defaultMaxNTPServers
|
||||||
|
}
|
||||||
|
if l.MaxNTPMulticastServers <= 0 {
|
||||||
|
l.MaxNTPMulticastServers = defaultMaxNTPMulticastServers
|
||||||
|
}
|
||||||
|
if l.MaxNTPServerNames <= 0 {
|
||||||
|
l.MaxNTPServerNames = defaultMaxNTPServerNames
|
||||||
|
}
|
||||||
|
if l.MaxDelegatedPrefixes <= 0 {
|
||||||
|
l.MaxDelegatedPrefixes = defaultMaxDelegatedPrefixes
|
||||||
|
}
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
// RequestConfig holds the parameters for starting a DHCPv6 exchange.
|
// RequestConfig holds the parameters for starting a DHCPv6 exchange.
|
||||||
type RequestConfig struct {
|
type RequestConfig struct {
|
||||||
// ClientHardwareAddr is the client's Ethernet MAC address.
|
// ClientHardwareAddr is the client's Ethernet MAC address.
|
||||||
// It is used to construct the client DUID-LL and IAID.
|
// It is used to construct the client DUID-LL and IAID.
|
||||||
ClientHardwareAddr [6]byte
|
ClientHardwareAddr [6]byte
|
||||||
|
// Limits bounds how many of each repeated server-supplied option the client
|
||||||
|
// stores. The zero value selects safe defaults for every field.
|
||||||
|
Limits Limits
|
||||||
|
}
|
||||||
|
|
||||||
|
// DelegatedPrefix is an IPv6 prefix delegated by a DHCPv6 server.
|
||||||
|
type DelegatedPrefix struct {
|
||||||
|
Prefix netip.Prefix
|
||||||
|
PreferredLifetime uint32
|
||||||
|
ValidLifetime uint32
|
||||||
}
|
}
|
||||||
|
|
||||||
// Client is a stateful DHCPv6 client implementing the [lneto.StackNode] interface.
|
// Client is a stateful DHCPv6 client implementing the [lneto.StackNode] interface.
|
||||||
@@ -39,6 +102,21 @@ type Client struct {
|
|||||||
// dns accumulates DNS recursive name server addresses (OptDNSServers).
|
// dns accumulates DNS recursive name server addresses (OptDNSServers).
|
||||||
// Client owns the backing array; cleared on reset, capacity reused.
|
// Client owns the backing array; cleared on reset, capacity reused.
|
||||||
dns []netip.Addr
|
dns []netip.Addr
|
||||||
|
// domainSearch accumulates DNS domain search names (OptDomainList).
|
||||||
|
// Client owns the backing array; cleared on reset, capacity reused.
|
||||||
|
domainSearch []dns.Name
|
||||||
|
// ntps accumulates NTP server addresses (OptNTPServer, suboption 1).
|
||||||
|
// Client owns the backing array; cleared on reset, capacity reused.
|
||||||
|
ntps []netip.Addr
|
||||||
|
// ntpMulticast accumulates NTP multicast addresses (OptNTPServer, suboption 2).
|
||||||
|
// Client owns the backing array; cleared on reset, capacity reused.
|
||||||
|
ntpMulticast []netip.Addr
|
||||||
|
// ntpNames accumulates NTP server FQDNs (OptNTPServer, suboption 3).
|
||||||
|
// Client owns the backing array; cleared on reset, capacity reused.
|
||||||
|
ntpNames []dns.Name
|
||||||
|
// delegatedPrefixes accumulates IA_PD prefixes (OptIAPD/OptIAPrefix).
|
||||||
|
// Client owns the backing array; cleared on reset, capacity reused.
|
||||||
|
delegatedPrefixes []DelegatedPrefix
|
||||||
|
|
||||||
assignedAddr [16]byte
|
assignedAddr [16]byte
|
||||||
assignedAddrValid bool
|
assignedAddrValid bool
|
||||||
@@ -50,9 +128,18 @@ type Client struct {
|
|||||||
t1, t2 uint32
|
t1, t2 uint32
|
||||||
preferredLifetime uint32
|
preferredLifetime uint32
|
||||||
validLifetime uint32
|
validLifetime uint32
|
||||||
|
// IA_PD timers from the server's Advertise/Reply.
|
||||||
|
pdT1, pdT2 uint32
|
||||||
|
reconfigureMsg MsgType
|
||||||
|
reconfigureOK bool
|
||||||
|
|
||||||
clientMAC [6]byte
|
clientMAC [6]byte
|
||||||
|
|
||||||
|
// limits is the resolved (defaults-applied) cap on each repeated option.
|
||||||
|
// It is set in BeginRequest and preserved across resets so the bounded
|
||||||
|
// backing arrays are reused without reallocation.
|
||||||
|
limits Limits
|
||||||
|
|
||||||
// auxbuf is a scratch buffer used during Encapsulate to avoid allocations.
|
// auxbuf is a scratch buffer used during Encapsulate to avoid allocations.
|
||||||
auxbuf [128]byte
|
auxbuf [128]byte
|
||||||
}
|
}
|
||||||
@@ -70,23 +157,65 @@ func (c *Client) BeginRequest(xid uint32, cfg RequestConfig) error {
|
|||||||
c.clientMAC = cfg.ClientHardwareAddr
|
c.clientMAC = cfg.ClientHardwareAddr
|
||||||
c.iaid = [4]byte(cfg.ClientHardwareAddr[:4])
|
c.iaid = [4]byte(cfg.ClientHardwareAddr[:4])
|
||||||
c.xid = xid & 0xFFFFFF
|
c.xid = xid & 0xFFFFFF
|
||||||
|
c.limits = cfg.Limits.withDefaults()
|
||||||
c.reset()
|
c.reset()
|
||||||
|
// Size the per-option backing arrays to their caps once, here, so the
|
||||||
|
// parse path on the network-facing Demux never allocates and stored
|
||||||
|
// entries stay bounded across the connection's lifetime.
|
||||||
|
c.dns = ensureAddrCap(c.dns, c.limits.MaxDNSServers)
|
||||||
|
c.ntps = ensureAddrCap(c.ntps, c.limits.MaxNTPServers)
|
||||||
|
c.ntpMulticast = ensureAddrCap(c.ntpMulticast, c.limits.MaxNTPMulticastServers)
|
||||||
|
c.domainSearch = ensureNameCap(c.domainSearch, c.limits.MaxDomainSearch)
|
||||||
|
c.ntpNames = ensureNameCap(c.ntpNames, c.limits.MaxNTPServerNames)
|
||||||
|
c.delegatedPrefixes = ensurePrefixCap(c.delegatedPrefixes, c.limits.MaxDelegatedPrefixes)
|
||||||
c.duid = AppendDUIDLL(c.duid[:0], cfg.ClientHardwareAddr)
|
c.duid = AppendDUIDLL(c.duid[:0], cfg.ClientHardwareAddr)
|
||||||
c.state = StateInit
|
c.state = StateInit
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ensureAddrCap returns s truncated to length 0 with capacity at least n,
|
||||||
|
// allocating a new backing array only when the existing one is too small.
|
||||||
|
func ensureAddrCap(s []netip.Addr, n int) []netip.Addr {
|
||||||
|
if cap(s) < n {
|
||||||
|
return make([]netip.Addr, 0, n)
|
||||||
|
}
|
||||||
|
return s[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureNameCap(s []dns.Name, n int) []dns.Name {
|
||||||
|
if cap(s) < n {
|
||||||
|
return make([]dns.Name, 0, n)
|
||||||
|
}
|
||||||
|
return s[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensurePrefixCap(s []DelegatedPrefix, n int) []DelegatedPrefix {
|
||||||
|
if cap(s) < n {
|
||||||
|
return make([]DelegatedPrefix, 0, n)
|
||||||
|
}
|
||||||
|
return s[:0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset clears the DHCPv6 exchange state and invalidates stack registrations.
|
||||||
|
func (c *Client) Reset() { c.reset() }
|
||||||
|
|
||||||
// reset clears exchange state while preserving slice backing arrays and the
|
// reset clears exchange state while preserving slice backing arrays and the
|
||||||
// connection ID is incremented to invalidate any existing stack registrations.
|
// connection ID is incremented to invalidate any existing stack registrations.
|
||||||
func (c *Client) reset() {
|
func (c *Client) reset() {
|
||||||
*c = Client{
|
*c = Client{
|
||||||
connID: c.connID + 1,
|
connID: c.connID + 1,
|
||||||
xid: c.xid,
|
xid: c.xid,
|
||||||
clientMAC: c.clientMAC,
|
clientMAC: c.clientMAC,
|
||||||
iaid: c.iaid,
|
iaid: c.iaid,
|
||||||
duid: c.duid,
|
limits: c.limits,
|
||||||
serverDUID: c.serverDUID[:0],
|
duid: c.duid,
|
||||||
dns: c.dns[:0],
|
serverDUID: c.serverDUID[:0],
|
||||||
|
dns: c.dns[:0],
|
||||||
|
domainSearch: c.domainSearch[:0],
|
||||||
|
ntps: c.ntps[:0],
|
||||||
|
ntpMulticast: c.ntpMulticast[:0],
|
||||||
|
ntpNames: c.ntpNames[:0],
|
||||||
|
delegatedPrefixes: c.delegatedPrefixes[:0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +244,8 @@ func (c *Client) Encapsulate(carrierData []byte, _, offsetToFrame int) (int, err
|
|||||||
frm.SetTransactionID(c.xid)
|
frm.SetTransactionID(c.xid)
|
||||||
n, _ := EncodeOption(dst[OptionsOffset+numOpts:], OptClientID, c.duid...)
|
n, _ := EncodeOption(dst[OptionsOffset+numOpts:], OptClientID, c.duid...)
|
||||||
numOpts += n
|
numOpts += n
|
||||||
|
n, _ = EncodeOption(dst[OptionsOffset+numOpts:], OptReconfAccept)
|
||||||
|
numOpts += n
|
||||||
n, _ = EncodeOptionIANA(dst[OptionsOffset+numOpts:], c.iaid, 0, 0, nil)
|
n, _ = EncodeOptionIANA(dst[OptionsOffset+numOpts:], c.iaid, 0, 0, nil)
|
||||||
numOpts += n
|
numOpts += n
|
||||||
n, _ = EncodeOption(dst[OptionsOffset+numOpts:], OptORO, defaultOptRequestList...)
|
n, _ = EncodeOption(dst[OptionsOffset+numOpts:], OptORO, defaultOptRequestList...)
|
||||||
@@ -130,6 +261,8 @@ func (c *Client) Encapsulate(carrierData []byte, _, offsetToFrame int) (int, err
|
|||||||
numOpts += n
|
numOpts += n
|
||||||
n, _ = EncodeOption(dst[OptionsOffset+numOpts:], OptServerID, c.serverDUID...)
|
n, _ = EncodeOption(dst[OptionsOffset+numOpts:], OptServerID, c.serverDUID...)
|
||||||
numOpts += n
|
numOpts += n
|
||||||
|
n, _ = EncodeOption(dst[OptionsOffset+numOpts:], OptReconfAccept)
|
||||||
|
numOpts += n
|
||||||
auxN, _ := EncodeOptionIAAddr(c.auxbuf[:], c.assignedAddr, 0, 0)
|
auxN, _ := EncodeOptionIAAddr(c.auxbuf[:], c.assignedAddr, 0, 0)
|
||||||
n, _ = EncodeOptionIANA(dst[OptionsOffset+numOpts:], c.iaid, 0, 0, c.auxbuf[:auxN])
|
n, _ = EncodeOptionIANA(dst[OptionsOffset+numOpts:], c.iaid, 0, 0, c.auxbuf[:auxN])
|
||||||
numOpts += n
|
numOpts += n
|
||||||
@@ -185,6 +318,9 @@ func (c *Client) Demux(carrierData []byte, frameOffset int) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if frm.MsgType() == MsgReconfigure {
|
||||||
|
return c.handleReconfigure(frm)
|
||||||
|
}
|
||||||
if frm.TransactionID() != c.xid {
|
if frm.TransactionID() != c.xid {
|
||||||
return lneto.ErrMismatch
|
return lneto.ErrMismatch
|
||||||
}
|
}
|
||||||
@@ -213,6 +349,41 @@ func (c *Client) Demux(carrierData []byte, frameOffset int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) handleReconfigure(frm Frame) error {
|
||||||
|
if !c.state.HasIP() {
|
||||||
|
return lneto.ErrPacketDrop
|
||||||
|
}
|
||||||
|
var clientOK, serverOK bool
|
||||||
|
var reconf MsgType
|
||||||
|
err := frm.ForEachOption(func(_ int, code OptCode, data []byte) error {
|
||||||
|
switch code {
|
||||||
|
case OptClientID:
|
||||||
|
clientOK = internal.BytesEqual(data, c.duid)
|
||||||
|
case OptServerID:
|
||||||
|
serverOK = len(c.serverDUID) == 0 || internal.BytesEqual(data, c.serverDUID)
|
||||||
|
case OptReconfMsg:
|
||||||
|
if len(data) != 1 {
|
||||||
|
return lneto.ErrInvalidLengthField
|
||||||
|
}
|
||||||
|
reconf = MsgType(data[0])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !clientOK || !serverOK {
|
||||||
|
return lneto.ErrMismatch
|
||||||
|
}
|
||||||
|
c.reconfigureMsg = reconf
|
||||||
|
c.reconfigureOK = true
|
||||||
|
if reconf != MsgRenew {
|
||||||
|
return lneto.ErrUnsupported
|
||||||
|
}
|
||||||
|
c.state = StateRenewing
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// setOptions parses all DHCPv6 options in frm and stores relevant values.
|
// setOptions parses all DHCPv6 options in frm and stores relevant values.
|
||||||
func (c *Client) setOptions(frm Frame) error {
|
func (c *Client) setOptions(frm Frame) error {
|
||||||
return frm.ForEachOption(func(_ int, code OptCode, data []byte) error {
|
return frm.ForEachOption(func(_ int, code OptCode, data []byte) error {
|
||||||
@@ -221,18 +392,121 @@ func (c *Client) setOptions(frm Frame) error {
|
|||||||
c.serverDUID = append(c.serverDUID[:0], data...)
|
c.serverDUID = append(c.serverDUID[:0], data...)
|
||||||
case OptIANA:
|
case OptIANA:
|
||||||
c.parseIANA(data)
|
c.parseIANA(data)
|
||||||
|
case OptIAPD:
|
||||||
|
c.parseIAPD(data)
|
||||||
case OptDNSServers:
|
case OptDNSServers:
|
||||||
if len(c.dns) > 0 || len(data)%16 != 0 {
|
if len(c.dns) > 0 || len(data)%16 != 0 {
|
||||||
break // skip if already populated or malformed
|
break // skip if already populated or malformed
|
||||||
}
|
}
|
||||||
for i := 0; i+16 <= len(data); i += 16 {
|
for i := 0; i+16 <= len(data) && len(c.dns) < c.limits.MaxDNSServers; i += 16 {
|
||||||
c.dns = append(c.dns, netip.AddrFrom16([16]byte(data[i:i+16])))
|
c.dns = append(c.dns, netip.AddrFrom16([16]byte(data[i:i+16])))
|
||||||
}
|
}
|
||||||
|
case OptDomainList:
|
||||||
|
c.parseDomainSearch(data)
|
||||||
|
case OptNTPServer:
|
||||||
|
c.parseNTPServer(data)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) parseIAPD(data []byte) {
|
||||||
|
if len(c.delegatedPrefixes) > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(data) < 12 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t1 := binary.BigEndian.Uint32(data[4:8])
|
||||||
|
t2 := binary.BigEndian.Uint32(data[8:12])
|
||||||
|
|
||||||
|
for ptr := 12; ptr+4 <= len(data); {
|
||||||
|
subCode := OptCode(binary.BigEndian.Uint16(data[ptr:]))
|
||||||
|
subLen := int(binary.BigEndian.Uint16(data[ptr+2:]))
|
||||||
|
if ptr+4+subLen > len(data) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if subCode == OptIAPrefix && subLen >= 25 && len(c.delegatedPrefixes) < c.limits.MaxDelegatedPrefixes {
|
||||||
|
sub := data[ptr+4 : ptr+4+subLen]
|
||||||
|
bits := int(sub[8])
|
||||||
|
prefix := netip.PrefixFrom(netip.AddrFrom16([16]byte(sub[9:25])), bits)
|
||||||
|
if prefix.IsValid() {
|
||||||
|
c.delegatedPrefixes = append(c.delegatedPrefixes, DelegatedPrefix{
|
||||||
|
Prefix: prefix.Masked(),
|
||||||
|
PreferredLifetime: binary.BigEndian.Uint32(sub[:4]),
|
||||||
|
ValidLifetime: binary.BigEndian.Uint32(sub[4:8]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ptr += 4 + subLen
|
||||||
|
}
|
||||||
|
if len(c.delegatedPrefixes) > 0 {
|
||||||
|
c.pdT1 = t1
|
||||||
|
c.pdT2 = t2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) parseNTPServer(data []byte) {
|
||||||
|
if len(c.ntps) > 0 || len(c.ntpMulticast) > 0 || len(c.ntpNames) > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for ptr := 0; ptr+4 <= len(data); {
|
||||||
|
subCode := binary.BigEndian.Uint16(data[ptr:])
|
||||||
|
subLen := int(binary.BigEndian.Uint16(data[ptr+2:]))
|
||||||
|
if ptr+4+subLen > len(data) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
subData := data[ptr+4 : ptr+4+subLen]
|
||||||
|
switch subCode {
|
||||||
|
case 1:
|
||||||
|
if subLen == 16 && len(c.ntps) < c.limits.MaxNTPServers {
|
||||||
|
c.ntps = append(c.ntps, netip.AddrFrom16([16]byte(data[ptr+4:ptr+20])))
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
if subLen == 16 && len(c.ntpMulticast) < c.limits.MaxNTPMulticastServers {
|
||||||
|
c.ntpMulticast = append(c.ntpMulticast, netip.AddrFrom16([16]byte(data[ptr+4:ptr+20])))
|
||||||
|
}
|
||||||
|
case 3:
|
||||||
|
idx := len(c.ntpNames)
|
||||||
|
if idx >= c.limits.MaxNTPServerNames {
|
||||||
|
break // suboption switch: cap reached, drop further names.
|
||||||
|
}
|
||||||
|
if idx < cap(c.ntpNames) {
|
||||||
|
c.ntpNames = c.ntpNames[:idx+1]
|
||||||
|
} else {
|
||||||
|
c.ntpNames = append(c.ntpNames, dns.Name{})
|
||||||
|
}
|
||||||
|
next, err := c.ntpNames[idx].Decode(subData, 0)
|
||||||
|
if err != nil || next != uint16(len(subData)) {
|
||||||
|
c.ntpNames[idx].Reset()
|
||||||
|
c.ntpNames = c.ntpNames[:idx]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ptr += 4 + subLen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) parseDomainSearch(data []byte) {
|
||||||
|
if len(c.domainSearch) > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for off := uint16(0); off < uint16(len(data)) && len(c.domainSearch) < c.limits.MaxDomainSearch; {
|
||||||
|
idx := len(c.domainSearch)
|
||||||
|
if idx < cap(c.domainSearch) {
|
||||||
|
c.domainSearch = c.domainSearch[:idx+1]
|
||||||
|
} else {
|
||||||
|
c.domainSearch = append(c.domainSearch, dns.Name{})
|
||||||
|
}
|
||||||
|
next, err := c.domainSearch[idx].Decode(data, off)
|
||||||
|
if err != nil || next <= off {
|
||||||
|
c.domainSearch[idx].Reset()
|
||||||
|
c.domainSearch = c.domainSearch[:idx]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
off = next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// parseIANA processes the payload of an OptIANA option, extracting the
|
// parseIANA processes the payload of an OptIANA option, extracting the
|
||||||
// assigned address and lease timers from any embedded OptIAAddr sub-option.
|
// assigned address and lease timers from any embedded OptIAAddr sub-option.
|
||||||
func (c *Client) parseIANA(data []byte) {
|
func (c *Client) parseIANA(data []byte) {
|
||||||
@@ -273,15 +547,70 @@ func (c *Client) isClosed() bool { return c.state == 0 || c.xid == 0 }
|
|||||||
// State returns the current client state.
|
// State returns the current client state.
|
||||||
func (c *Client) State() ClientState { return c.state }
|
func (c *Client) State() ClientState { return c.state }
|
||||||
|
|
||||||
|
// LastReconfigure returns the last accepted Reconfigure message target.
|
||||||
|
func (c *Client) LastReconfigure() (MsgType, bool) { return c.reconfigureMsg, c.reconfigureOK }
|
||||||
|
|
||||||
// AssignedAddr returns the IPv6 address assigned by the server and whether it is valid.
|
// AssignedAddr returns the IPv6 address assigned by the server and whether it is valid.
|
||||||
func (c *Client) AssignedAddr() ([16]byte, bool) { return c.assignedAddr, c.assignedAddrValid }
|
func (c *Client) AssignedAddr() ([16]byte, bool) { return c.assignedAddr, c.assignedAddrValid }
|
||||||
|
|
||||||
|
// RebindingSeconds returns the IA_NA T2 timer from the server.
|
||||||
|
func (c *Client) RebindingSeconds() uint32 { return c.t2 }
|
||||||
|
|
||||||
|
// RenewalSeconds returns the IA_NA T1 timer from the server.
|
||||||
|
func (c *Client) RenewalSeconds() uint32 { return c.t1 }
|
||||||
|
|
||||||
|
// PreferredLifetimeSeconds returns the preferred lifetime of the assigned address.
|
||||||
|
func (c *Client) PreferredLifetimeSeconds() uint32 { return c.preferredLifetime }
|
||||||
|
|
||||||
|
// ValidLifetimeSeconds returns the valid lifetime of the assigned address.
|
||||||
|
func (c *Client) ValidLifetimeSeconds() uint32 { return c.validLifetime }
|
||||||
|
|
||||||
|
// PrefixDelegationRebindingSeconds returns the IA_PD T2 timer from the server.
|
||||||
|
func (c *Client) PrefixDelegationRebindingSeconds() uint32 { return c.pdT2 }
|
||||||
|
|
||||||
|
// PrefixDelegationRenewalSeconds returns the IA_PD T1 timer from the server.
|
||||||
|
func (c *Client) PrefixDelegationRenewalSeconds() uint32 { return c.pdT1 }
|
||||||
|
|
||||||
|
// AppendDelegatedPrefixes appends delegated IPv6 prefixes received from the server to dst.
|
||||||
|
func (c *Client) AppendDelegatedPrefixes(dst []DelegatedPrefix) []DelegatedPrefix {
|
||||||
|
return append(dst, c.delegatedPrefixes...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumDelegatedPrefixes returns the number of delegated IPv6 prefixes received.
|
||||||
|
func (c *Client) NumDelegatedPrefixes() int { return len(c.delegatedPrefixes) }
|
||||||
|
|
||||||
// AppendDNSServers appends the DNS server addresses received from the server to dst.
|
// AppendDNSServers appends the DNS server addresses received from the server to dst.
|
||||||
func (c *Client) AppendDNSServers(dst []netip.Addr) []netip.Addr { return append(dst, c.dns...) }
|
func (c *Client) AppendDNSServers(dst []netip.Addr) []netip.Addr { return append(dst, c.dns...) }
|
||||||
|
|
||||||
// NumDNSServers returns the number of DNS server addresses received.
|
// NumDNSServers returns the number of DNS server addresses received.
|
||||||
func (c *Client) NumDNSServers() int { return len(c.dns) }
|
func (c *Client) NumDNSServers() int { return len(c.dns) }
|
||||||
|
|
||||||
|
// AppendDomainSearch appends the DNS domain search names received from the server to dst.
|
||||||
|
func (c *Client) AppendDomainSearch(dst []dns.Name) []dns.Name { return append(dst, c.domainSearch...) }
|
||||||
|
|
||||||
|
// NumDomainSearch returns the number of DNS domain search names received.
|
||||||
|
func (c *Client) NumDomainSearch() int { return len(c.domainSearch) }
|
||||||
|
|
||||||
|
// AppendNTPServers appends the NTP server addresses received from the server to dst.
|
||||||
|
func (c *Client) AppendNTPServers(dst []netip.Addr) []netip.Addr { return append(dst, c.ntps...) }
|
||||||
|
|
||||||
|
// NumNTPServers returns the number of NTP server addresses received.
|
||||||
|
func (c *Client) NumNTPServers() int { return len(c.ntps) }
|
||||||
|
|
||||||
|
// AppendNTPMulticastServers appends NTP multicast addresses received from the server to dst.
|
||||||
|
func (c *Client) AppendNTPMulticastServers(dst []netip.Addr) []netip.Addr {
|
||||||
|
return append(dst, c.ntpMulticast...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NumNTPMulticastServers returns the number of NTP multicast addresses received.
|
||||||
|
func (c *Client) NumNTPMulticastServers() int { return len(c.ntpMulticast) }
|
||||||
|
|
||||||
|
// AppendNTPServerNames appends NTP server FQDNs received from the server to dst.
|
||||||
|
func (c *Client) AppendNTPServerNames(dst []dns.Name) []dns.Name { return append(dst, c.ntpNames...) }
|
||||||
|
|
||||||
|
// NumNTPServerNames returns the number of NTP server FQDNs received.
|
||||||
|
func (c *Client) NumNTPServerNames() int { return len(c.ntpNames) }
|
||||||
|
|
||||||
// ConnectionID returns a pointer to the client's connection ID.
|
// ConnectionID returns a pointer to the client's connection ID.
|
||||||
// The value increments on each reset; callers should discard registrations when it changes.
|
// The value increments on each reset; callers should discard registrations when it changes.
|
||||||
// Implements [lneto.StackNode].
|
// Implements [lneto.StackNode].
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ package dhcpv6
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
|
"net/netip"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/soypat/lneto/dns"
|
||||||
)
|
)
|
||||||
|
|
||||||
// writeOpt6 encodes a single DHCPv6 option into dst using the 4-byte TLV header
|
// writeOpt6 encodes a single DHCPv6 option into dst using the 4-byte TLV header
|
||||||
@@ -124,6 +127,9 @@ func TestClientSolicitRequest(t *testing.T) {
|
|||||||
if n == 0 {
|
if n == 0 {
|
||||||
t.Fatal("Encapsulate (Solicit): wrote 0 bytes")
|
t.Fatal("Encapsulate (Solicit): wrote 0 bytes")
|
||||||
}
|
}
|
||||||
|
if !frameHasOption(t, buf[:n], OptReconfAccept) {
|
||||||
|
t.Fatal("Solicit missing Reconfigure Accept option")
|
||||||
|
}
|
||||||
if cl.State() != StateSoliciting {
|
if cl.State() != StateSoliciting {
|
||||||
t.Fatalf("after Solicit: want StateSoliciting, got %v", cl.State())
|
t.Fatalf("after Solicit: want StateSoliciting, got %v", cl.State())
|
||||||
}
|
}
|
||||||
@@ -164,6 +170,291 @@ func TestClientSolicitRequest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClientReconfigureRenew(t *testing.T) {
|
||||||
|
const xid = 0x112233
|
||||||
|
clientMAC := [6]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}
|
||||||
|
serverDUID := []byte{0, 3, 0, 1, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
|
||||||
|
assignedAddr := [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
|
||||||
|
iaid := [4]byte{clientMAC[0], clientMAC[1], clientMAC[2], clientMAC[3]}
|
||||||
|
var cl Client
|
||||||
|
if err := cl.BeginRequest(xid, RequestConfig{ClientHardwareAddr: clientMAC}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var buf [1024]byte
|
||||||
|
if _, err := cl.Encapsulate(buf[:], -1, 0); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := cl.Demux(buildServerFrame(MsgAdvertise, xid, serverDUID, iaid, assignedAddr), 0); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := cl.Encapsulate(buf[:], -1, 0); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := cl.Demux(buildServerFrame(MsgReply, xid, serverDUID, iaid, assignedAddr), 0); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if cl.State() != StateBound {
|
||||||
|
t.Fatalf("state before reconfigure = %v, want %v", cl.State(), StateBound)
|
||||||
|
}
|
||||||
|
reconf := buildReconfigureFrame(0x445566, serverDUID, cl.duid, MsgRenew)
|
||||||
|
if err := cl.Demux(reconf, 0); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if cl.State() != StateRenewing {
|
||||||
|
t.Fatalf("state after reconfigure = %v, want %v", cl.State(), StateRenewing)
|
||||||
|
}
|
||||||
|
msg, ok := cl.LastReconfigure()
|
||||||
|
if !ok || msg != MsgRenew {
|
||||||
|
t.Fatalf("LastReconfigure = %v, %v, want %v, true", msg, ok, MsgRenew)
|
||||||
|
}
|
||||||
|
n, err := cl.Encapsulate(buf[:], -1, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
frm, err := NewFrame(buf[:n])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if frm.MsgType() != MsgRenew {
|
||||||
|
t.Fatalf("Encapsulate after Reconfigure type = %v, want %v", frm.MsgType(), MsgRenew)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildReconfigureFrame(xid uint32, serverDUID, clientDUID []byte, msg MsgType) []byte {
|
||||||
|
buf := make([]byte, 128)
|
||||||
|
buf[0] = byte(MsgReconfigure)
|
||||||
|
buf[1] = byte(xid >> 16)
|
||||||
|
buf[2] = byte(xid >> 8)
|
||||||
|
buf[3] = byte(xid)
|
||||||
|
n := OptionsOffset
|
||||||
|
n += writeOpt6(buf[n:], OptServerID, serverDUID...)
|
||||||
|
n += writeOpt6(buf[n:], OptClientID, clientDUID...)
|
||||||
|
n += writeOpt6(buf[n:], OptReconfMsg, byte(msg))
|
||||||
|
return buf[:n]
|
||||||
|
}
|
||||||
|
|
||||||
|
func frameHasOption(t testing.TB, b []byte, want OptCode) bool {
|
||||||
|
t.Helper()
|
||||||
|
frm, err := NewFrame(b)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
found := false
|
||||||
|
if err := frm.ForEachOption(func(_ int, code OptCode, _ []byte) error {
|
||||||
|
if code == want {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return found
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientParsesNTPServerOption(t *testing.T) {
|
||||||
|
const xid = 0x102030
|
||||||
|
clientMAC := [6]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}
|
||||||
|
serverDUID := []byte{0, 3, 0, 1, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
|
||||||
|
assignedAddr := [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
|
||||||
|
ntpAddr := [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x7b}
|
||||||
|
ntpMulticast := [16]byte{0xff, 0x05, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0x01}
|
||||||
|
iaid := [4]byte{clientMAC[0], clientMAC[1], clientMAC[2], clientMAC[3]}
|
||||||
|
|
||||||
|
var cl Client
|
||||||
|
if err := cl.BeginRequest(xid, RequestConfig{ClientHardwareAddr: clientMAC}); err != nil {
|
||||||
|
t.Fatal("BeginRequest:", err)
|
||||||
|
}
|
||||||
|
var buf [1024]byte
|
||||||
|
if _, err := cl.Encapsulate(buf[:], -1, 0); err != nil {
|
||||||
|
t.Fatal("Encapsulate (Solicit):", err)
|
||||||
|
}
|
||||||
|
advFrame := buildServerFrame(MsgAdvertise, xid, serverDUID, iaid, assignedAddr)
|
||||||
|
if err := cl.Demux(advFrame, 0); err != nil {
|
||||||
|
t.Fatal("Demux (Advertise):", err)
|
||||||
|
}
|
||||||
|
if _, err := cl.Encapsulate(buf[:], -1, 0); err != nil {
|
||||||
|
t.Fatal("Encapsulate (Request):", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
replyFrame := appendNTPServerOption(t, buildServerFrame(MsgReply, xid, serverDUID, iaid, assignedAddr), ntpAddr, ntpMulticast, "time.example.com")
|
||||||
|
if err := cl.Demux(replyFrame, 0); err != nil {
|
||||||
|
t.Fatal("Demux (Reply):", err)
|
||||||
|
}
|
||||||
|
var ntps []netip.Addr
|
||||||
|
ntps = cl.AppendNTPServers(ntps)
|
||||||
|
if len(ntps) != 1 || ntps[0] != netip.AddrFrom16(ntpAddr) {
|
||||||
|
t.Fatalf("AppendNTPServers = %v, want %v", ntps, netip.AddrFrom16(ntpAddr))
|
||||||
|
}
|
||||||
|
if n := cl.NumNTPServers(); n != 1 {
|
||||||
|
t.Fatalf("NumNTPServers = %d, want 1", n)
|
||||||
|
}
|
||||||
|
var multicasts []netip.Addr
|
||||||
|
multicasts = cl.AppendNTPMulticastServers(multicasts)
|
||||||
|
if len(multicasts) != 1 || multicasts[0] != netip.AddrFrom16(ntpMulticast) {
|
||||||
|
t.Fatalf("AppendNTPMulticastServers = %v, want %v", multicasts, netip.AddrFrom16(ntpMulticast))
|
||||||
|
}
|
||||||
|
if n := cl.NumNTPMulticastServers(); n != 1 {
|
||||||
|
t.Fatalf("NumNTPMulticastServers = %d, want 1", n)
|
||||||
|
}
|
||||||
|
var names []dns.Name
|
||||||
|
names = cl.AppendNTPServerNames(names)
|
||||||
|
if len(names) != 1 || !names[0].EqualString("time.example.com") {
|
||||||
|
t.Fatalf("AppendNTPServerNames = %v, want time.example.com", names)
|
||||||
|
}
|
||||||
|
if n := cl.NumNTPServerNames(); n != 1 {
|
||||||
|
t.Fatalf("NumNTPServerNames = %d, want 1", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientParsesDomainSearchOption(t *testing.T) {
|
||||||
|
const xid = 0x203040
|
||||||
|
clientMAC := [6]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}
|
||||||
|
serverDUID := []byte{0, 3, 0, 1, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
|
||||||
|
assignedAddr := [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
|
||||||
|
iaid := [4]byte{clientMAC[0], clientMAC[1], clientMAC[2], clientMAC[3]}
|
||||||
|
|
||||||
|
var cl Client
|
||||||
|
if err := cl.BeginRequest(xid, RequestConfig{ClientHardwareAddr: clientMAC}); err != nil {
|
||||||
|
t.Fatal("BeginRequest:", err)
|
||||||
|
}
|
||||||
|
var buf [1024]byte
|
||||||
|
if _, err := cl.Encapsulate(buf[:], -1, 0); err != nil {
|
||||||
|
t.Fatal("Encapsulate (Solicit):", err)
|
||||||
|
}
|
||||||
|
advFrame := buildServerFrame(MsgAdvertise, xid, serverDUID, iaid, assignedAddr)
|
||||||
|
if err := cl.Demux(advFrame, 0); err != nil {
|
||||||
|
t.Fatal("Demux (Advertise):", err)
|
||||||
|
}
|
||||||
|
if _, err := cl.Encapsulate(buf[:], -1, 0); err != nil {
|
||||||
|
t.Fatal("Encapsulate (Request):", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
replyFrame := appendDomainSearchOption(t, buildServerFrame(MsgReply, xid, serverDUID, iaid, assignedAddr), "example.com", "corp.example.com")
|
||||||
|
if err := cl.Demux(replyFrame, 0); err != nil {
|
||||||
|
t.Fatal("Demux (Reply):", err)
|
||||||
|
}
|
||||||
|
var domains []dns.Name
|
||||||
|
domains = cl.AppendDomainSearch(domains)
|
||||||
|
if len(domains) != 2 {
|
||||||
|
t.Fatalf("AppendDomainSearch len = %d, want 2", len(domains))
|
||||||
|
}
|
||||||
|
if !domains[0].EqualString("example.com") || !domains[1].EqualString("corp.example.com") {
|
||||||
|
t.Fatalf("AppendDomainSearch = %q, %q", domains[0].String(), domains[1].String())
|
||||||
|
}
|
||||||
|
if n := cl.NumDomainSearch(); n != 2 {
|
||||||
|
t.Fatalf("NumDomainSearch = %d, want 2", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientParsesDelegatedPrefix(t *testing.T) {
|
||||||
|
const xid = 0x304050
|
||||||
|
clientMAC := [6]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}
|
||||||
|
serverDUID := []byte{0, 3, 0, 1, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
|
||||||
|
assignedAddr := [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
|
||||||
|
delegated := netip.MustParsePrefix("2001:db8:abcd::/48")
|
||||||
|
iaid := [4]byte{clientMAC[0], clientMAC[1], clientMAC[2], clientMAC[3]}
|
||||||
|
|
||||||
|
var cl Client
|
||||||
|
if err := cl.BeginRequest(xid, RequestConfig{ClientHardwareAddr: clientMAC}); err != nil {
|
||||||
|
t.Fatal("BeginRequest:", err)
|
||||||
|
}
|
||||||
|
var buf [1024]byte
|
||||||
|
if _, err := cl.Encapsulate(buf[:], -1, 0); err != nil {
|
||||||
|
t.Fatal("Encapsulate (Solicit):", err)
|
||||||
|
}
|
||||||
|
advFrame := buildServerFrame(MsgAdvertise, xid, serverDUID, iaid, assignedAddr)
|
||||||
|
if err := cl.Demux(advFrame, 0); err != nil {
|
||||||
|
t.Fatal("Demux (Advertise):", err)
|
||||||
|
}
|
||||||
|
if _, err := cl.Encapsulate(buf[:], -1, 0); err != nil {
|
||||||
|
t.Fatal("Encapsulate (Request):", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
replyFrame := appendIAPDOption(buildServerFrame(MsgReply, xid, serverDUID, iaid, assignedAddr), iaid, delegated)
|
||||||
|
if err := cl.Demux(replyFrame, 0); err != nil {
|
||||||
|
t.Fatal("Demux (Reply):", err)
|
||||||
|
}
|
||||||
|
var prefixes []DelegatedPrefix
|
||||||
|
prefixes = cl.AppendDelegatedPrefixes(prefixes)
|
||||||
|
if len(prefixes) != 1 {
|
||||||
|
t.Fatalf("AppendDelegatedPrefixes len = %d, want 1", len(prefixes))
|
||||||
|
}
|
||||||
|
if prefixes[0].Prefix != delegated || prefixes[0].PreferredLifetime != 1800 || prefixes[0].ValidLifetime != 3600 {
|
||||||
|
t.Fatalf("delegated prefix = %+v, want %s preferred=1800 valid=3600", prefixes[0], delegated)
|
||||||
|
}
|
||||||
|
if n := cl.NumDelegatedPrefixes(); n != 1 {
|
||||||
|
t.Fatalf("NumDelegatedPrefixes = %d, want 1", n)
|
||||||
|
}
|
||||||
|
if cl.PrefixDelegationRenewalSeconds() != 900 || cl.PrefixDelegationRebindingSeconds() != 1800 {
|
||||||
|
t.Fatalf("IA_PD timers = renewal:%d rebind:%d, want 900/1800", cl.PrefixDelegationRenewalSeconds(), cl.PrefixDelegationRebindingSeconds())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendNTPServerOption(t testing.TB, frame []byte, addr, multicast [16]byte, fqdn string) []byte {
|
||||||
|
t.Helper()
|
||||||
|
var ntpPayload []byte
|
||||||
|
ntpPayload = appendNTPServerAddrSuboption(ntpPayload, 1, addr)
|
||||||
|
ntpPayload = appendNTPServerAddrSuboption(ntpPayload, 2, multicast)
|
||||||
|
name, err := dns.NewName(fqdn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
nameStart := len(ntpPayload) + 4
|
||||||
|
ntpPayload = append(ntpPayload, 0, 3, 0, 0)
|
||||||
|
ntpPayload, err = name.AppendTo(ntpPayload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
binary.BigEndian.PutUint16(ntpPayload[nameStart-2:nameStart], uint16(len(ntpPayload)-nameStart))
|
||||||
|
buf := append(frame[:len(frame):len(frame)], make([]byte, 4+len(ntpPayload))...)
|
||||||
|
writeOpt6(buf[len(frame):], OptNTPServer, ntpPayload...)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendNTPServerAddrSuboption(dst []byte, code uint16, addr [16]byte) []byte {
|
||||||
|
start := len(dst)
|
||||||
|
dst = append(dst, 0, 0, 0, 16)
|
||||||
|
binary.BigEndian.PutUint16(dst[start:start+2], code)
|
||||||
|
return append(dst, addr[:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendDomainSearchOption(t testing.TB, frame []byte, domains ...string) []byte {
|
||||||
|
t.Helper()
|
||||||
|
var payload []byte
|
||||||
|
for _, domain := range domains {
|
||||||
|
name, err := dns.NewName(domain)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
payload, err = name.AppendTo(payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buf := append(frame[:len(frame):len(frame)], make([]byte, 4+len(payload))...)
|
||||||
|
writeOpt6(buf[len(frame):], OptDomainList, payload...)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func appendIAPDOption(frame []byte, iaid [4]byte, prefix netip.Prefix) []byte {
|
||||||
|
var iaPrefix [29]byte
|
||||||
|
binary.BigEndian.PutUint16(iaPrefix[0:2], uint16(OptIAPrefix))
|
||||||
|
binary.BigEndian.PutUint16(iaPrefix[2:4], 25)
|
||||||
|
binary.BigEndian.PutUint32(iaPrefix[4:8], 1800)
|
||||||
|
binary.BigEndian.PutUint32(iaPrefix[8:12], 3600)
|
||||||
|
iaPrefix[12] = byte(prefix.Bits())
|
||||||
|
prefixAddr := prefix.Addr().As16()
|
||||||
|
copy(iaPrefix[13:], prefixAddr[:])
|
||||||
|
var iapd [41]byte
|
||||||
|
copy(iapd[:4], iaid[:])
|
||||||
|
binary.BigEndian.PutUint32(iapd[4:8], 900)
|
||||||
|
binary.BigEndian.PutUint32(iapd[8:12], 1800)
|
||||||
|
copy(iapd[12:], iaPrefix[:])
|
||||||
|
buf := append(frame[:len(frame):len(frame)], make([]byte, 4+len(iapd))...)
|
||||||
|
writeOpt6(buf[len(frame):], OptIAPD, iapd[:]...)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
// TestClientEncapsulateSolicit verifies that the first Encapsulate call
|
// TestClientEncapsulateSolicit verifies that the first Encapsulate call
|
||||||
// writes a Solicit message with at least OptClientID and OptIANA.
|
// writes a Solicit message with at least OptClientID and OptIANA.
|
||||||
func TestClientEncapsulateSolicit(t *testing.T) {
|
func TestClientEncapsulateSolicit(t *testing.T) {
|
||||||
@@ -242,3 +533,188 @@ func TestClientDoubleTapEncapsulate(t *testing.T) {
|
|||||||
t.Errorf("second Encapsulate: want 0 bytes (idempotent), got %d", n2)
|
t.Errorf("second Encapsulate: want 0 bytes (idempotent), got %d", n2)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// driveToRequesting advances a fresh client through Solicit→Advertise→Request so
|
||||||
|
// the next Demux of a Reply runs the option-parsing path.
|
||||||
|
func driveToRequesting(t testing.TB, cl *Client, xid uint32, serverDUID []byte, iaid [4]byte, addr [16]byte) {
|
||||||
|
t.Helper()
|
||||||
|
var buf [1024]byte
|
||||||
|
if _, err := cl.Encapsulate(buf[:], -1, 0); err != nil {
|
||||||
|
t.Fatal("Encapsulate (Solicit):", err)
|
||||||
|
}
|
||||||
|
if err := cl.Demux(buildServerFrame(MsgAdvertise, xid, serverDUID, iaid, addr), 0); err != nil {
|
||||||
|
t.Fatal("Demux (Advertise):", err)
|
||||||
|
}
|
||||||
|
if _, err := cl.Encapsulate(buf[:], -1, 0); err != nil {
|
||||||
|
t.Fatal("Encapsulate (Request):", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// floodDNSServersOption appends an OptDNSServers carrying n distinct addresses.
|
||||||
|
func floodDNSServersOption(frame []byte, n int) []byte {
|
||||||
|
payload := make([]byte, 0, n*16)
|
||||||
|
for i := range n {
|
||||||
|
var a [16]byte
|
||||||
|
a[0], a[1], a[15] = 0x20, 0x01, byte(i+1)
|
||||||
|
payload = append(payload, a[:]...)
|
||||||
|
}
|
||||||
|
buf := append(frame[:len(frame):len(frame)], make([]byte, 4+len(payload))...)
|
||||||
|
writeOpt6(buf[len(frame):], OptDNSServers, payload...)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// floodIAPDOption appends an OptIAPD carrying n OptIAPrefix sub-options.
|
||||||
|
func floodIAPDOption(frame []byte, iaid [4]byte, n int) []byte {
|
||||||
|
payload := make([]byte, 12)
|
||||||
|
copy(payload[:4], iaid[:])
|
||||||
|
binary.BigEndian.PutUint32(payload[4:8], 900)
|
||||||
|
binary.BigEndian.PutUint32(payload[8:12], 1800)
|
||||||
|
for i := range n {
|
||||||
|
var iaPrefix [29]byte
|
||||||
|
binary.BigEndian.PutUint16(iaPrefix[0:2], uint16(OptIAPrefix))
|
||||||
|
binary.BigEndian.PutUint16(iaPrefix[2:4], 25)
|
||||||
|
binary.BigEndian.PutUint32(iaPrefix[4:8], 1800)
|
||||||
|
binary.BigEndian.PutUint32(iaPrefix[8:12], 3600)
|
||||||
|
iaPrefix[12] = 48
|
||||||
|
iaPrefix[13], iaPrefix[14], iaPrefix[15] = 0x20, 0x01, byte(i+1)
|
||||||
|
payload = append(payload, iaPrefix[:]...)
|
||||||
|
}
|
||||||
|
buf := append(frame[:len(frame):len(frame)], make([]byte, 4+len(payload))...)
|
||||||
|
writeOpt6(buf[len(frame):], OptIAPD, payload...)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
// floodNTPOption appends an OptNTPServer carrying nAddr unicast (suboption 1),
|
||||||
|
// nMulticast (suboption 2) and nNames FQDN (suboption 3) entries.
|
||||||
|
func floodNTPOption(t testing.TB, frame []byte, nAddr, nMulticast, nNames int) []byte {
|
||||||
|
t.Helper()
|
||||||
|
var payload []byte
|
||||||
|
for i := range nAddr {
|
||||||
|
var a [16]byte
|
||||||
|
a[0], a[15] = 0x20, byte(i+1)
|
||||||
|
payload = appendNTPServerAddrSuboption(payload, 1, a)
|
||||||
|
}
|
||||||
|
for i := range nMulticast {
|
||||||
|
var a [16]byte
|
||||||
|
a[0], a[1], a[15] = 0xff, 0x05, byte(i+1)
|
||||||
|
payload = appendNTPServerAddrSuboption(payload, 2, a)
|
||||||
|
}
|
||||||
|
name, err := dns.NewName("ntp.example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for range nNames {
|
||||||
|
start := len(payload) + 4
|
||||||
|
payload = append(payload, 0, 3, 0, 0)
|
||||||
|
payload, err = name.AppendTo(payload)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
binary.BigEndian.PutUint16(payload[start-2:start], uint16(len(payload)-start))
|
||||||
|
}
|
||||||
|
buf := append(frame[:len(frame):len(frame)], make([]byte, 4+len(payload))...)
|
||||||
|
writeOpt6(buf[len(frame):], OptNTPServer, payload...)
|
||||||
|
return buf
|
||||||
|
}
|
||||||
|
|
||||||
|
func repeatStrings(s string, n int) []string {
|
||||||
|
out := make([]string, n)
|
||||||
|
for i := range out {
|
||||||
|
out[i] = s
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestClientLimitsCapServerData verifies that the client stores no more than
|
||||||
|
// the configured number of each repeated option, so a server flooding a Reply
|
||||||
|
// cannot drive unbounded allocation (the OOM concern raised in the PR review).
|
||||||
|
func TestClientLimitsCapServerData(t *testing.T) {
|
||||||
|
const xid = 0x515151
|
||||||
|
mac := [6]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}
|
||||||
|
serverDUID := []byte{0, 3, 0, 1, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
|
||||||
|
iaid := [4]byte{mac[0], mac[1], mac[2], mac[3]}
|
||||||
|
addr := [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
|
||||||
|
limits := Limits{
|
||||||
|
MaxDNSServers: 2, MaxDomainSearch: 2, MaxNTPServers: 2,
|
||||||
|
MaxNTPMulticastServers: 1, MaxNTPServerNames: 2, MaxDelegatedPrefixes: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
var cl Client
|
||||||
|
if err := cl.BeginRequest(xid, RequestConfig{ClientHardwareAddr: mac, Limits: limits}); err != nil {
|
||||||
|
t.Fatal("BeginRequest:", err)
|
||||||
|
}
|
||||||
|
driveToRequesting(t, &cl, xid, serverDUID, iaid, addr)
|
||||||
|
|
||||||
|
// A Reply far exceeding every configured cap.
|
||||||
|
reply := buildServerFrame(MsgReply, xid, serverDUID, iaid, addr)
|
||||||
|
reply = floodDNSServersOption(reply, 8)
|
||||||
|
reply = floodIAPDOption(reply, iaid, 8)
|
||||||
|
reply = appendDomainSearchOption(t, reply, repeatStrings("example.com", 8)...)
|
||||||
|
reply = floodNTPOption(t, reply, 8, 8, 8)
|
||||||
|
if err := cl.Demux(reply, 0); err != nil {
|
||||||
|
t.Fatal("Demux (Reply):", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
got int
|
||||||
|
want int
|
||||||
|
}{
|
||||||
|
{"DNS servers", cl.NumDNSServers(), limits.MaxDNSServers},
|
||||||
|
{"domain search", cl.NumDomainSearch(), limits.MaxDomainSearch},
|
||||||
|
{"NTP servers", cl.NumNTPServers(), limits.MaxNTPServers},
|
||||||
|
{"NTP multicast", cl.NumNTPMulticastServers(), limits.MaxNTPMulticastServers},
|
||||||
|
{"NTP names", cl.NumNTPServerNames(), limits.MaxNTPServerNames},
|
||||||
|
{"delegated prefixes", cl.NumDelegatedPrefixes(), limits.MaxDelegatedPrefixes},
|
||||||
|
} {
|
||||||
|
if tc.got != tc.want {
|
||||||
|
t.Errorf("%s stored = %d, want capped at %d", tc.name, tc.got, tc.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestClientParseNoAllocs verifies that once BeginRequest has sized the option
|
||||||
|
// backing arrays, parsing a server message performs no further allocation: the
|
||||||
|
// bounded slices are reused, keeping the network-facing path off the heap.
|
||||||
|
func TestClientParseNoAllocs(t *testing.T) {
|
||||||
|
const xid = 0x123456
|
||||||
|
mac := [6]byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}
|
||||||
|
serverDUID := []byte{0, 3, 0, 1, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66}
|
||||||
|
iaid := [4]byte{mac[0], mac[1], mac[2], mac[3]}
|
||||||
|
addr := [16]byte{0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}
|
||||||
|
|
||||||
|
var cl Client
|
||||||
|
if err := cl.BeginRequest(xid, RequestConfig{ClientHardwareAddr: mac}); err != nil {
|
||||||
|
t.Fatal("BeginRequest:", err)
|
||||||
|
}
|
||||||
|
reply := buildServerFrame(MsgReply, xid, serverDUID, iaid, addr)
|
||||||
|
reply = floodDNSServersOption(reply, cl.limits.MaxDNSServers)
|
||||||
|
reply = floodIAPDOption(reply, iaid, cl.limits.MaxDelegatedPrefixes)
|
||||||
|
reply = appendDomainSearchOption(t, reply, repeatStrings("example.com", cl.limits.MaxDomainSearch)...)
|
||||||
|
reply = floodNTPOption(t, reply, cl.limits.MaxNTPServers, cl.limits.MaxNTPMulticastServers, cl.limits.MaxNTPServerNames)
|
||||||
|
frm, err := NewFrame(reply)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("NewFrame:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
clearStores := func() {
|
||||||
|
cl.serverDUID = cl.serverDUID[:0]
|
||||||
|
cl.dns = cl.dns[:0]
|
||||||
|
cl.domainSearch = cl.domainSearch[:0]
|
||||||
|
cl.ntps = cl.ntps[:0]
|
||||||
|
cl.ntpMulticast = cl.ntpMulticast[:0]
|
||||||
|
cl.ntpNames = cl.ntpNames[:0]
|
||||||
|
cl.delegatedPrefixes = cl.delegatedPrefixes[:0]
|
||||||
|
}
|
||||||
|
clearStores()
|
||||||
|
if err := cl.setOptions(frm); err != nil { // warm up the dns.Name backing arrays.
|
||||||
|
t.Fatal("setOptions:", err)
|
||||||
|
}
|
||||||
|
allocs := testing.AllocsPerRun(50, func() {
|
||||||
|
clearStores()
|
||||||
|
_ = cl.setOptions(frm)
|
||||||
|
})
|
||||||
|
if allocs != 0 {
|
||||||
|
t.Errorf("setOptions must not allocate after BeginRequest sizing, got %v allocs/op", allocs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user