mirror of
https://github.com/soypat/lneto.git
synced 2026-08-09 01:13:41 +00:00
feat: add RFC 3927 support (#114)
Signed-off-by: Marvin Drees <marvin.drees@9elements.com>
This commit is contained in:
@@ -100,6 +100,7 @@ ok github.com/soypat/lneto/x/xnet 2.926s
|
||||
| TCP | RFC 9293 | ✅ | `tcp` | 0 ²³ | Full state machine, SYN cookies, retransmit queue, `Conn`/`Listener` |
|
||||
| DNS | RFC 1035 | ✅ | `dns` | — | Client (A/AAAA query) |
|
||||
| DHCPv4 | RFC 2131 | ✅ | `dhcp/dhcpv4` | — | Client + Server |
|
||||
| IPv4 Link-Local (APIPA) | RFC 3927 | ✅ | `ipv4/linklocal4` | 0 | Heapless claim-and-defend state machine for 169.254.x.x |
|
||||
| NTP | RFC 5905 | ✅ | `ntp` | — | Client |
|
||||
| NTS | RFC 8915 | ✅ | `x/nts` | — | Client + Server; key exchange over TLS 1.3, authenticated NTP. Requires caller-supplied AEAD_AES_SIV_CMAC_256 |
|
||||
| mDNS | RFC 6762 | ✅ | `dns/mdns` | — | Client (service announcement + query) |
|
||||
@@ -122,6 +123,7 @@ ok github.com/soypat/lneto/x/xnet 2.926s
|
||||
- [`lneto/internet/pcap`](./internal/pcap): Packet capture and field breakdown utilities. Wireshark in the making.
|
||||
- [`lneto/http/httpraw`](./http/httpraw/): Heapless HTTP header processing and validation. Does no implement header normalization.
|
||||
- [`lneto/tcp`](./tcp): TCP implementation and low level logic.
|
||||
- [`lneto/ipv4/linklocal4`](./ipv4/linklocal4): RFC 3927 IPv4 link-local (APIPA) address autoconfiguration. Heapless claim-and-defend state machine for self-assigned 169.254.x.x addresses when no DHCP server is available.
|
||||
- [`lneto/dhcpv4`](./dhcpv4): DHCP version 4 protocol implementation and low level logic.
|
||||
- [`lneto/dns`](./dns): DNS protocol implementation and low level logic.
|
||||
- [`lneto/ntp`](./ntp): NTP implementation and low level logic. Includes NTP time primitives manipulation and conversion to Go native types.
|
||||
|
||||
@@ -18,6 +18,14 @@ func IsMulticast(addr [4]byte) bool {
|
||||
return addr[0]&0xf0 == 0xe0
|
||||
}
|
||||
|
||||
// IsLinkLocal reports whether addr is within the IPv4 link-local prefix
|
||||
// 169.254.0.0/16 reserved for dynamic link-local configuration as defined in [RFC3927].
|
||||
//
|
||||
// [RFC3927]: https://datatracker.ietf.org/doc/html/rfc3927
|
||||
func IsLinkLocal(addr [4]byte) bool {
|
||||
return addr[0] == 169 && addr[1] == 254
|
||||
}
|
||||
|
||||
// ToS represents the Traffic Class (a.k.a Type of Service). It is 8 bits long. 6 MSB are Differentiated Services; 2 LSB are Explicit Congenstion Notification.
|
||||
type ToS uint8
|
||||
|
||||
|
||||
@@ -31,6 +31,27 @@ func TestAppendFormatAddr(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsLinkLocal(t *testing.T) {
|
||||
tests := []struct {
|
||||
addr [4]byte
|
||||
want bool
|
||||
}{
|
||||
{addr: [4]byte{169, 254, 0, 0}, want: true},
|
||||
{addr: [4]byte{169, 254, 1, 1}, want: true},
|
||||
{addr: [4]byte{169, 254, 254, 255}, want: true},
|
||||
{addr: [4]byte{169, 254, 255, 255}, want: true},
|
||||
{addr: [4]byte{169, 253, 1, 1}, want: false},
|
||||
{addr: [4]byte{169, 255, 1, 1}, want: false},
|
||||
{addr: [4]byte{192, 168, 1, 1}, want: false},
|
||||
{addr: [4]byte{0, 0, 0, 0}, want: false},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
if got := IsLinkLocal(tc.addr); got != tc.want {
|
||||
t.Errorf("IsLinkLocal(%v): got %v, want %v", tc.addr, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendFormatAddr_noAllocs(t *testing.T) {
|
||||
var buf [24]byte
|
||||
addr := [4]byte{192, 168, 1, 1}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// Package linklocal4 implements dynamic configuration of IPv4 link-local addresses
|
||||
// (a.k.a. APIPA, "self-assigned" 169.254.x.x addresses) as specified in [RFC3927].
|
||||
//
|
||||
// The [Handler] is a heapless state machine that claims and defends a link-local
|
||||
// address using ARP probes and announcements. It performs no allocations during
|
||||
// steady-state operation and holds no buffers of its own; the caller provides the
|
||||
// scratch buffer on each call. This makes it suitable for memory constrained and
|
||||
// bare-metal targets.
|
||||
//
|
||||
// [RFC3927]: https://datatracker.ietf.org/doc/html/rfc3927
|
||||
package linklocal4
|
||||
|
||||
import "time"
|
||||
|
||||
// Protocol constants as defined in [RFC3927] section 9.
|
||||
//
|
||||
// [RFC3927]: https://datatracker.ietf.org/doc/html/rfc3927#section-9
|
||||
const (
|
||||
// probeWait is the initial random delay before sending the first probe.
|
||||
probeWait = 1 * time.Second
|
||||
// probeNum is the number of ARP probes to send.
|
||||
probeNum = 3
|
||||
// probeMin and probeMax bound the random spacing between probes.
|
||||
probeMin = 1 * time.Second
|
||||
probeMax = 2 * time.Second
|
||||
// announceWait is the delay after the last probe before announcing.
|
||||
announceWait = 2 * time.Second
|
||||
// announceNum is the number of ARP announcements to send.
|
||||
announceNum = 2
|
||||
// announceInterval is the spacing between announcements.
|
||||
announceInterval = 2 * time.Second
|
||||
// maxConflicts is the number of conflicts after which probing is rate limited.
|
||||
maxConflicts = 10
|
||||
// rateLimitInterval bounds the rate of address claiming once maxConflicts is exceeded.
|
||||
rateLimitInterval = 60 * time.Second
|
||||
// defendInterval is the minimum spacing between defensive announcements before
|
||||
// the address is abandoned to break an endless defense loop.
|
||||
defendInterval = 10 * time.Second
|
||||
)
|
||||
|
||||
// arpIPv4Size is the size of an ARP-over-Ethernet IPv4 packet (RFC826):
|
||||
// 8 byte fixed header + 2*(6 byte hardware addr + 4 byte protocol addr).
|
||||
const arpIPv4Size = 28
|
||||
|
||||
// State represents the stage of the link-local address autoconfiguration
|
||||
// state machine. The transition order during a successful claim is:
|
||||
//
|
||||
// StateWaiting -> StateProbing -> StateAnnouncing -> StateBound
|
||||
type State uint8
|
||||
|
||||
const (
|
||||
// StateInvalid is the zero value; the handler has not been configured.
|
||||
StateInvalid State = iota
|
||||
// StateWaiting is the initial random delay before the first probe is sent.
|
||||
StateWaiting
|
||||
// StateProbing sends ARP probes to detect whether the candidate is in use.
|
||||
StateProbing
|
||||
// StateAnnouncing has claimed the candidate and is broadcasting announcements.
|
||||
StateAnnouncing
|
||||
// StateBound owns the address and defends it against conflicts.
|
||||
StateBound
|
||||
// StateRateLimited is waiting out rateLimitInterval after too many conflicts.
|
||||
StateRateLimited
|
||||
)
|
||||
|
||||
// IsBound reports whether the state machine has successfully claimed an address.
|
||||
func (s State) IsBound() bool { return s == StateBound }
|
||||
|
||||
func (s State) String() string {
|
||||
switch s {
|
||||
case StateInvalid:
|
||||
return "invalid"
|
||||
case StateWaiting:
|
||||
return "waiting"
|
||||
case StateProbing:
|
||||
return "probing"
|
||||
case StateAnnouncing:
|
||||
return "announcing"
|
||||
case StateBound:
|
||||
return "bound"
|
||||
case StateRateLimited:
|
||||
return "rate-limited"
|
||||
default:
|
||||
return "linklocal4.State(?)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
package linklocal4
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/arp"
|
||||
"github.com/soypat/lneto/ethernet"
|
||||
"github.com/soypat/lneto/internal"
|
||||
"github.com/soypat/lneto/ipv4"
|
||||
)
|
||||
|
||||
// Handler implements the [RFC3927] IPv4 link-local address autoconfiguration
|
||||
// state machine. It is a [lneto.StackNode] over the ARP EtherType: it produces
|
||||
// ARP probes and announcements on [Handler.Encapsulate] and inspects incoming
|
||||
// ARP traffic for conflicts on [Handler.Demux].
|
||||
//
|
||||
// Handler is heapless and performs zero allocations after [Handler.Reset];
|
||||
// the only allocation is the one-time capture of the clock function. It holds
|
||||
// no internal buffers and operates entirely on the caller-supplied scratch
|
||||
// buffer, making it suitable for memory constrained targets.
|
||||
//
|
||||
// A Handler claims and defends a single address. Normal "who-has" ARP
|
||||
// resolution of the claimed address is the responsibility of the ARP layer
|
||||
// (see [arp.Handler]); this Handler only manages the claim-and-defend protocol.
|
||||
//
|
||||
// [RFC3927]: https://datatracker.ietf.org/doc/html/rfc3927
|
||||
type Handler struct {
|
||||
connID uint64
|
||||
now func() time.Time
|
||||
|
||||
// nextActionAt is the time at which the next probe/announcement is due.
|
||||
nextActionAt time.Time
|
||||
// lastDefend is the time the most recent defensive announcement was sent.
|
||||
lastDefend time.Time
|
||||
|
||||
prng uint32
|
||||
candidate [4]byte
|
||||
firstCandidate [4]byte
|
||||
hw [6]byte
|
||||
|
||||
state State
|
||||
probesSent uint8
|
||||
announceSent uint8
|
||||
conflicts uint8
|
||||
|
||||
haveFirst bool
|
||||
defendDue bool
|
||||
defendValid bool
|
||||
vld lneto.Validator
|
||||
}
|
||||
|
||||
var _ lneto.StackNode = (*Handler)(nil)
|
||||
|
||||
// linkLocalNet is the RFC3927 IPv4 link-local prefix (169.254.0.0/16). The first
|
||||
// and last /24 within it (169.254.0.x and 169.254.255.x) are reserved per
|
||||
// section 2.1, so the usable host range is 169.254.1.0-169.254.254.255.
|
||||
var linkLocalNet = ipv4.PrefixFrom([4]byte{169, 254, 0, 0}, 16)
|
||||
|
||||
// Config configures a [Handler] for link-local address acquisition.
|
||||
type Config struct {
|
||||
// HardwareAddr is the interface MAC address used as the ARP sender hardware address.
|
||||
HardwareAddr [6]byte
|
||||
// Now is the monotonic clock source used to schedule probes and announcements.
|
||||
// It is required.
|
||||
Now func() time.Time
|
||||
// Seed seeds the pseudo-random address generator. It is required and must be
|
||||
// non-zero. Per RFC3927 section 2.1 it SHOULD be derived from a persistent
|
||||
// per-host value such as the MAC address so that different hosts pick different
|
||||
// sequences and a host tends to reuse the same address across reboots.
|
||||
Seed uint64
|
||||
// FirstCandidate, if within 169.254.1.0-169.254.254.255, is tried before any
|
||||
// random address. Use it to retry a previously recorded address.
|
||||
FirstCandidate [4]byte
|
||||
}
|
||||
|
||||
// Reset configures the handler and begins link-local address acquisition,
|
||||
// transitioning to [StateWaiting]. It increments the connection ID, invalidating
|
||||
// any prior registration.
|
||||
func (h *Handler) Reset(cfg Config) error {
|
||||
if cfg.Now == nil || internal.IsZeroed(cfg.HardwareAddr[:]...) || cfg.Seed == 0 {
|
||||
return lneto.ErrInvalidConfig
|
||||
}
|
||||
first := cfg.FirstCandidate
|
||||
haveFirst := linkLocalNet.Contains(first) && first[2] >= 1 && first[2] <= 254
|
||||
*h = Handler{
|
||||
connID: h.connID + 1,
|
||||
now: cfg.Now,
|
||||
prng: uint32(cfg.Seed) ^ uint32(cfg.Seed>>32),
|
||||
hw: cfg.HardwareAddr,
|
||||
firstCandidate: first,
|
||||
haveFirst: haveFirst,
|
||||
}
|
||||
if h.prng == 0 {
|
||||
h.prng = 1 // Fold of a non-zero seed can still be zero; xorshift cannot escape the zero state.
|
||||
}
|
||||
h.beginProbing(cfg.Now(), randDelay(h.prand(), probeWait))
|
||||
return nil
|
||||
}
|
||||
|
||||
// LocalPort implements [lneto.StackNode]. It always returns 0.
|
||||
func (h *Handler) LocalPort() uint16 { return 0 }
|
||||
|
||||
// Protocol implements [lneto.StackNode], returning the ARP EtherType.
|
||||
func (h *Handler) Protocol() uint64 { return uint64(ethernet.TypeARP) }
|
||||
|
||||
// ConnectionID implements [lneto.StackNode].
|
||||
func (h *Handler) ConnectionID() *uint64 { return &h.connID }
|
||||
|
||||
// State returns the current autoconfiguration state.
|
||||
func (h *Handler) State() State { return h.state }
|
||||
|
||||
// Addr returns the claimed link-local address. ok is true only once the address
|
||||
// has been successfully claimed (state [StateBound]).
|
||||
func (h *Handler) Addr() (addr [4]byte, ok bool) {
|
||||
return h.candidate, h.state == StateBound
|
||||
}
|
||||
|
||||
// Candidate returns the address currently being probed, announced or defended.
|
||||
func (h *Handler) Candidate() [4]byte { return h.candidate }
|
||||
|
||||
// Conflicts returns the number of address conflicts encountered so far.
|
||||
func (h *Handler) Conflicts() int { return int(h.conflicts) }
|
||||
|
||||
// Encapsulate implements [lneto.StackNode]. It writes the next ARP probe or
|
||||
// announcement into carrierData at offsetToFrame when one is due, returning the
|
||||
// number of bytes written, or 0 when no action is pending. The Ethernet
|
||||
// destination, if present before offsetToFrame, is set to broadcast.
|
||||
func (h *Handler) Encapsulate(carrierData []byte, _, offsetToFrame int) (int, error) {
|
||||
if offsetToFrame < 0 || len(carrierData)-offsetToFrame < arpIPv4Size {
|
||||
return 0, lneto.ErrShortBuffer
|
||||
}
|
||||
now := h.now()
|
||||
b := carrierData[offsetToFrame:]
|
||||
var senderProto [4]byte // zero = ARP probe; candidate = ARP announcement.
|
||||
switch h.state {
|
||||
case StateWaiting, StateProbing:
|
||||
if now.Before(h.nextActionAt) {
|
||||
return 0, nil
|
||||
}
|
||||
if h.probesSent < probeNum {
|
||||
h.state = StateProbing
|
||||
h.probesSent++
|
||||
if h.probesSent < probeNum {
|
||||
h.nextActionAt = now.Add(randInterval(h.prand(), probeMin, probeMax))
|
||||
} else {
|
||||
h.nextActionAt = now.Add(announceWait)
|
||||
}
|
||||
// senderProto stays zero: this is a probe.
|
||||
} else {
|
||||
// announceWait elapsed with no conflict: claim the address.
|
||||
h.state = StateAnnouncing
|
||||
h.announceSent = 1
|
||||
h.nextActionAt = now.Add(announceInterval)
|
||||
senderProto = h.candidate
|
||||
}
|
||||
|
||||
case StateAnnouncing:
|
||||
if now.Before(h.nextActionAt) {
|
||||
return 0, nil
|
||||
}
|
||||
h.announceSent++
|
||||
senderProto = h.candidate
|
||||
if h.announceSent >= announceNum {
|
||||
h.state = StateBound
|
||||
} else {
|
||||
h.nextActionAt = now.Add(announceInterval)
|
||||
}
|
||||
|
||||
case StateBound:
|
||||
if !h.defendDue {
|
||||
return 0, nil
|
||||
}
|
||||
h.defendDue = false
|
||||
senderProto = h.candidate
|
||||
|
||||
case StateRateLimited:
|
||||
if now.Before(h.nextActionAt) {
|
||||
return 0, nil
|
||||
}
|
||||
// onConflict already selected a fresh candidate; resume probing it.
|
||||
h.beginProbing(now, randDelay(h.prand(), probeWait))
|
||||
return 0, nil
|
||||
|
||||
default:
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
h.putARP(b, senderProto)
|
||||
if offsetToFrame >= 14 {
|
||||
// TODO: Support VLAN-tagged Ethernet headers when setting the broadcast destination.
|
||||
broadcast := ethernet.BroadcastAddr()
|
||||
copy(carrierData[offsetToFrame-14:offsetToFrame-8], broadcast[:])
|
||||
}
|
||||
return arpIPv4Size, nil
|
||||
}
|
||||
|
||||
// Demux implements [lneto.StackNode]. It inspects an incoming ARP frame for
|
||||
// address conflicts per RFC3927 sections 2.2.1 and 2.5, updating the state
|
||||
// machine to reconfigure or defend as required.
|
||||
func (h *Handler) Demux(carrierData []byte, frameOffset int) error {
|
||||
if h.state == StateInvalid {
|
||||
return nil
|
||||
}
|
||||
afrm, err := arp.NewFrame(carrierData[frameOffset:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.vld.ResetErr()
|
||||
afrm.ValidateSize(&h.vld)
|
||||
if h.vld.HasError() {
|
||||
return h.vld.ErrPop()
|
||||
}
|
||||
ptype, plen := afrm.Protocol()
|
||||
if ptype != ethernet.TypeIPv4 || plen != 4 {
|
||||
return nil // Not IPv4 ARP; irrelevant to link-local conflict detection.
|
||||
}
|
||||
senderHW, senderProto := afrm.Sender4()
|
||||
_, targetProto := afrm.Target4()
|
||||
now := h.now()
|
||||
|
||||
switch h.state {
|
||||
case StateWaiting, StateProbing:
|
||||
// Conflict if anyone else uses the candidate as a sender address, or
|
||||
// is probing for the same candidate from a different hardware address.
|
||||
conflict := *senderProto == h.candidate ||
|
||||
(afrm.Operation() == arp.OpRequest && internal.IsZeroed(senderProto[:]...) &&
|
||||
*targetProto == h.candidate && *senderHW != h.hw)
|
||||
if conflict {
|
||||
h.onConflict(now)
|
||||
}
|
||||
|
||||
case StateAnnouncing, StateBound:
|
||||
// We own the address; a conflicting sender hardware address means another
|
||||
// host claims it too.
|
||||
if *senderProto == h.candidate && *senderHW != h.hw {
|
||||
h.onDefend(now)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// onConflict handles a conflict detected while probing: pick a new candidate and
|
||||
// restart, rate limiting once maxConflicts is exceeded.
|
||||
func (h *Handler) onConflict(now time.Time) {
|
||||
if h.conflicts < 255 {
|
||||
h.conflicts++
|
||||
}
|
||||
h.selectCandidate()
|
||||
if h.conflicts > maxConflicts {
|
||||
h.state = StateRateLimited
|
||||
h.nextActionAt = now.Add(rateLimitInterval)
|
||||
return
|
||||
}
|
||||
h.beginProbing(now, randDelay(h.prand(), probeWait))
|
||||
}
|
||||
|
||||
// onDefend handles a conflict on an address we own per RFC3927 section 2.5(b):
|
||||
// defend once with a single announcement, but abandon the address if conflicts
|
||||
// recur within defendInterval to avoid an endless defense loop.
|
||||
func (h *Handler) onDefend(now time.Time) {
|
||||
if !h.defendValid || now.Sub(h.lastDefend) >= defendInterval {
|
||||
h.lastDefend = now
|
||||
h.defendValid = true
|
||||
h.defendDue = true
|
||||
return
|
||||
}
|
||||
// Second conflict within defendInterval: give up and reconfigure.
|
||||
h.onConflict(now)
|
||||
}
|
||||
|
||||
// beginProbing resets probe/announce counters and schedules the first probe after delay.
|
||||
func (h *Handler) beginProbing(now time.Time, delay time.Duration) {
|
||||
if internal.IsZeroed(h.candidate[:]...) {
|
||||
h.selectCandidate()
|
||||
}
|
||||
h.state = StateWaiting
|
||||
h.probesSent = 0
|
||||
h.announceSent = 0
|
||||
h.defendDue = false
|
||||
h.defendValid = false
|
||||
h.nextActionAt = now.Add(delay)
|
||||
}
|
||||
|
||||
// selectCandidate picks the next address to try. It uses FirstCandidate once if
|
||||
// provided, otherwise a uniform pseudo-random address in 169.254.1.0-169.254.254.255
|
||||
// per RFC3927 section 2.1 (the first and last /24 are reserved).
|
||||
func (h *Handler) selectCandidate() {
|
||||
if h.haveFirst {
|
||||
h.haveFirst = false
|
||||
h.candidate = h.firstCandidate
|
||||
return
|
||||
}
|
||||
// 254*256 = 65024 usable addresses; offset by one /24 to skip 169.254.0.x.
|
||||
low := 256 + h.prand()%65024
|
||||
h.candidate = [4]byte{169, 254, byte(low >> 8), byte(low)}
|
||||
}
|
||||
|
||||
// putARP marshals an ARP request (probe or announcement) into dst. A probe has
|
||||
// an all-zero sender protocol address; an announcement repeats the candidate.
|
||||
func (h *Handler) putARP(dst []byte, senderProto [4]byte) {
|
||||
f, _ := arp.NewFrame(dst)
|
||||
f.SetHardware(1, 6)
|
||||
f.SetProtocol(ethernet.TypeIPv4, 4)
|
||||
f.SetOperation(arp.OpRequest)
|
||||
shw, sproto := f.Sender4()
|
||||
*shw = h.hw
|
||||
*sproto = senderProto
|
||||
thw, tproto := f.Target4()
|
||||
*thw = [6]byte{} // Target hardware address ignored; set to zero per RFC3927 section 2.2.1.
|
||||
*tproto = h.candidate
|
||||
}
|
||||
|
||||
func (h *Handler) prand() uint32 {
|
||||
h.prng = internal.Prand32(h.prng)
|
||||
return h.prng
|
||||
}
|
||||
|
||||
// randDelay returns a duration uniformly in [0, max] derived from r.
|
||||
func randDelay(r uint32, max time.Duration) time.Duration {
|
||||
return time.Duration(uint64(r) % uint64(max+1))
|
||||
}
|
||||
|
||||
// randInterval returns a duration uniformly in [min, max] derived from r.
|
||||
func randInterval(r uint32, min, max time.Duration) time.Duration {
|
||||
span := max - min
|
||||
if span <= 0 {
|
||||
return min
|
||||
}
|
||||
return min + time.Duration(uint64(r)%uint64(span+1))
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
package linklocal4
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/arp"
|
||||
"github.com/soypat/lneto/ethernet"
|
||||
"github.com/soypat/lneto/ipv4"
|
||||
)
|
||||
|
||||
type fakeClock struct{ t time.Time }
|
||||
|
||||
func (c *fakeClock) now() time.Time { return c.t }
|
||||
func (c *fakeClock) advance(d time.Duration) { c.t = c.t.Add(d) }
|
||||
|
||||
var (
|
||||
ourHW = [6]byte{0x02, 0x00, 0x00, 0x00, 0x00, 0x01}
|
||||
otherHW = [6]byte{0x02, 0x00, 0x00, 0x00, 0x00, 0x02}
|
||||
)
|
||||
|
||||
const frameOff = 14 // pretend there is an ethernet header before the ARP frame.
|
||||
|
||||
func newHandler(t *testing.T, clk *fakeClock) *Handler {
|
||||
t.Helper()
|
||||
var h Handler
|
||||
err := h.Reset(Config{
|
||||
HardwareAddr: ourHW,
|
||||
Now: clk.now,
|
||||
Seed: 0xC0FFEE,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h.State() != StateWaiting {
|
||||
t.Fatalf("expected StateWaiting after Reset, got %s", h.State())
|
||||
}
|
||||
return &h
|
||||
}
|
||||
|
||||
// step advances the clock past any pending interval and runs one Encapsulate.
|
||||
func step(t *testing.T, h *Handler, clk *fakeClock, buf []byte) (arp.Frame, int) {
|
||||
t.Helper()
|
||||
clk.advance(3 * time.Second) // larger than any RFC3927 probe/announce interval.
|
||||
n, err := h.Encapsulate(buf, -1, frameOff)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n == 0 {
|
||||
return arp.Frame{}, 0
|
||||
}
|
||||
f, err := arp.NewFrame(buf[frameOff : frameOff+n])
|
||||
if err != nil {
|
||||
t.Fatalf("invalid arp produced: %v", err)
|
||||
}
|
||||
var vld lneto.Validator
|
||||
f.ValidateSize(&vld)
|
||||
if vld.HasError() {
|
||||
t.Fatalf("invalid arp size: %v", vld.ErrPop())
|
||||
}
|
||||
if f.Operation() != arp.OpRequest {
|
||||
t.Fatalf("link-local ARP must be a request, got %s", f.Operation())
|
||||
}
|
||||
// Ethernet destination must be broadcast.
|
||||
bc := ethernet.BroadcastAddr()
|
||||
for i := range 6 {
|
||||
if buf[i] != bc[i] {
|
||||
t.Fatalf("ethernet destination not broadcast: %x", buf[:6])
|
||||
}
|
||||
}
|
||||
return f, n
|
||||
}
|
||||
|
||||
func TestClaim(t *testing.T) {
|
||||
clk := &fakeClock{t: time.Unix(1000, 0)}
|
||||
h := newHandler(t, clk)
|
||||
buf := make([]byte, 64)
|
||||
|
||||
cand := h.Candidate()
|
||||
if !ipv4.IsLinkLocal(cand) || cand[2] < 1 || cand[2] > 254 {
|
||||
t.Fatalf("candidate %v not a valid link-local address", cand)
|
||||
}
|
||||
|
||||
var probes, announces int
|
||||
for i := 0; i < 10 && h.State() != StateBound; i++ {
|
||||
f, n := step(t, h, clk, buf)
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
_, sproto := f.Sender4()
|
||||
_, tproto := f.Target4()
|
||||
shw, _ := f.Sender4()
|
||||
if *shw != ourHW {
|
||||
t.Fatalf("sender hardware address mismatch: %x", *shw)
|
||||
}
|
||||
if *tproto != cand {
|
||||
t.Fatalf("target proto must be candidate %v, got %v", cand, *tproto)
|
||||
}
|
||||
if *sproto == ([4]byte{}) {
|
||||
probes++
|
||||
} else if *sproto == cand {
|
||||
announces++
|
||||
} else {
|
||||
t.Fatalf("unexpected sender proto %v", *sproto)
|
||||
}
|
||||
}
|
||||
if h.State() != StateBound {
|
||||
t.Fatalf("expected StateBound, got %s", h.State())
|
||||
}
|
||||
if probes != probeNum {
|
||||
t.Errorf("expected %d probes, got %d", probeNum, probes)
|
||||
}
|
||||
if announces != announceNum {
|
||||
t.Errorf("expected %d announcements, got %d", announceNum, announces)
|
||||
}
|
||||
addr, ok := h.Addr()
|
||||
if !ok || addr != cand {
|
||||
t.Fatalf("Addr()=%v,%v want %v,true", addr, ok, cand)
|
||||
}
|
||||
}
|
||||
|
||||
// makeARP builds an ARP IPv4 frame in buf for conflict-detection tests.
|
||||
func makeARP(t *testing.T, buf []byte, op arp.Operation, senderHW [6]byte, senderProto, targetProto [4]byte) []byte {
|
||||
t.Helper()
|
||||
f, err := arp.NewFrame(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
f.SetHardware(1, 6)
|
||||
f.SetProtocol(ethernet.TypeIPv4, 4)
|
||||
f.SetOperation(op)
|
||||
shw, sp := f.Sender4()
|
||||
*shw = senderHW
|
||||
*sp = senderProto
|
||||
thw, tp := f.Target4()
|
||||
*thw = [6]byte{}
|
||||
*tp = targetProto
|
||||
return buf[:arpIPv4Size]
|
||||
}
|
||||
|
||||
func TestConflictDuringProbe(t *testing.T) {
|
||||
clk := &fakeClock{t: time.Unix(1000, 0)}
|
||||
h := newHandler(t, clk)
|
||||
buf := make([]byte, 64)
|
||||
|
||||
// Send the first probe.
|
||||
_, n := step(t, h, clk, buf)
|
||||
if n == 0 {
|
||||
t.Fatal("expected first probe")
|
||||
}
|
||||
cand := h.Candidate()
|
||||
|
||||
// Another host replies/uses the candidate as its sender address: conflict.
|
||||
var arpbuf [64]byte
|
||||
frame := makeARP(t, arpbuf[:], arp.OpReply, otherHW, cand, [4]byte{169, 254, 1, 1})
|
||||
if err := h.Demux(frame, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h.Conflicts() != 1 {
|
||||
t.Fatalf("expected 1 conflict, got %d", h.Conflicts())
|
||||
}
|
||||
if h.Candidate() == cand {
|
||||
t.Fatal("expected a new candidate after conflict")
|
||||
}
|
||||
if h.State() != StateWaiting {
|
||||
t.Fatalf("expected restart in StateWaiting, got %s", h.State())
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbeConflictFromSimultaneousProbe(t *testing.T) {
|
||||
clk := &fakeClock{t: time.Unix(1000, 0)}
|
||||
h := newHandler(t, clk)
|
||||
buf := make([]byte, 64)
|
||||
step(t, h, clk, buf) // first probe
|
||||
cand := h.Candidate()
|
||||
|
||||
// Another host probes for the same candidate (zero sender proto, different HW).
|
||||
var arpbuf [64]byte
|
||||
frame := makeARP(t, arpbuf[:], arp.OpRequest, otherHW, [4]byte{}, cand)
|
||||
if err := h.Demux(frame, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h.Conflicts() != 1 || h.Candidate() == cand {
|
||||
t.Fatalf("simultaneous probe should cause conflict: conflicts=%d cand=%v", h.Conflicts(), h.Candidate())
|
||||
}
|
||||
}
|
||||
|
||||
func driveToBound(t *testing.T, h *Handler, clk *fakeClock, buf []byte) {
|
||||
t.Helper()
|
||||
for i := 0; i < 10 && h.State() != StateBound; i++ {
|
||||
step(t, h, clk, buf)
|
||||
}
|
||||
if h.State() != StateBound {
|
||||
t.Fatalf("failed to reach StateBound, stuck at %s", h.State())
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefense(t *testing.T) {
|
||||
clk := &fakeClock{t: time.Unix(1000, 0)}
|
||||
h := newHandler(t, clk)
|
||||
buf := make([]byte, 64)
|
||||
driveToBound(t, h, clk, buf)
|
||||
cand := h.Candidate()
|
||||
|
||||
// A conflicting ARP from another host: handler should defend with one announcement.
|
||||
var arpbuf [64]byte
|
||||
frame := makeARP(t, arpbuf[:], arp.OpRequest, otherHW, cand, [4]byte{169, 254, 1, 1})
|
||||
if err := h.Demux(frame, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n, err := h.Encapsulate(buf, -1, frameOff)
|
||||
if err != nil || n == 0 {
|
||||
t.Fatalf("expected defensive announcement, n=%d err=%v", n, err)
|
||||
}
|
||||
f, _ := arp.NewFrame(buf[frameOff : frameOff+n])
|
||||
_, sproto := f.Sender4()
|
||||
if *sproto != cand {
|
||||
t.Fatalf("defensive announcement must use candidate as sender, got %v", *sproto)
|
||||
}
|
||||
if h.State() != StateBound {
|
||||
t.Fatalf("should remain bound after a single defense, got %s", h.State())
|
||||
}
|
||||
// Subsequent Encapsulate yields nothing more.
|
||||
if n, _ := h.Encapsulate(buf, -1, frameOff); n != 0 {
|
||||
t.Fatal("expected only a single defensive announcement")
|
||||
}
|
||||
|
||||
// A second conflict within defendInterval forces reconfiguration.
|
||||
clk.advance(defendInterval / 2)
|
||||
frame = makeARP(t, arpbuf[:], arp.OpReply, otherHW, cand, [4]byte{169, 254, 1, 1})
|
||||
if err := h.Demux(frame, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h.State() == StateBound {
|
||||
t.Fatal("expected reconfiguration after repeated conflict within defendInterval")
|
||||
}
|
||||
if h.Candidate() == cand {
|
||||
t.Fatal("expected new candidate after giving up address")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoSelfConflict(t *testing.T) {
|
||||
clk := &fakeClock{t: time.Unix(1000, 0)}
|
||||
h := newHandler(t, clk)
|
||||
buf := make([]byte, 64)
|
||||
driveToBound(t, h, clk, buf)
|
||||
cand := h.Candidate()
|
||||
|
||||
// Our own announcement (same hardware address) must not be treated as a conflict.
|
||||
var arpbuf [64]byte
|
||||
frame := makeARP(t, arpbuf[:], arp.OpRequest, ourHW, cand, cand)
|
||||
if err := h.Demux(frame, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n, _ := h.Encapsulate(buf, -1, frameOff); n != 0 {
|
||||
t.Fatal("self-sent ARP must not trigger a defense")
|
||||
}
|
||||
if h.State() != StateBound {
|
||||
t.Fatalf("state changed on self ARP: %s", h.State())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirstCandidate(t *testing.T) {
|
||||
clk := &fakeClock{t: time.Unix(1000, 0)}
|
||||
want := [4]byte{169, 254, 42, 7}
|
||||
var h Handler
|
||||
err := h.Reset(Config{
|
||||
HardwareAddr: ourHW,
|
||||
Now: clk.now,
|
||||
Seed: 0xC0FFEE,
|
||||
FirstCandidate: want,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h.Candidate() != want {
|
||||
t.Fatalf("expected FirstCandidate %v, got %v", want, h.Candidate())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRateLimit(t *testing.T) {
|
||||
clk := &fakeClock{t: time.Unix(1000, 0)}
|
||||
h := newHandler(t, clk)
|
||||
buf := make([]byte, 64)
|
||||
var arpbuf [64]byte
|
||||
// Force more than maxConflicts conflicts during probing.
|
||||
for i := 0; i <= maxConflicts; i++ {
|
||||
step(t, h, clk, buf) // emit a probe for the current candidate.
|
||||
cand := h.Candidate()
|
||||
frame := makeARP(t, arpbuf[:], arp.OpReply, otherHW, cand, [4]byte{169, 254, 1, 1})
|
||||
if err := h.Demux(frame, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if h.State() != StateRateLimited {
|
||||
t.Fatalf("expected StateRateLimited after %d conflicts, got %s", h.Conflicts(), h.State())
|
||||
}
|
||||
// Before rateLimitInterval elapses no probe is emitted.
|
||||
if n, _ := h.Encapsulate(buf, -1, frameOff); n != 0 {
|
||||
t.Fatal("must not probe while rate limited")
|
||||
}
|
||||
// After the interval the machine resumes probing.
|
||||
clk.advance(rateLimitInterval + time.Second)
|
||||
if n, _ := h.Encapsulate(buf, -1, frameOff); n != 0 {
|
||||
t.Fatal("rate-limit recovery should reschedule, not emit immediately")
|
||||
}
|
||||
if h.State() != StateWaiting {
|
||||
t.Fatalf("expected StateWaiting after rate-limit recovery, got %s", h.State())
|
||||
}
|
||||
}
|
||||
|
||||
func TestZeroAlloc(t *testing.T) {
|
||||
clk := &fakeClock{t: time.Unix(1000, 0)}
|
||||
h := newHandler(t, clk)
|
||||
buf := make([]byte, 64)
|
||||
driveToBound(t, h, clk, buf)
|
||||
cand := h.Candidate()
|
||||
var arpbuf [64]byte
|
||||
frame := makeARP(t, arpbuf[:], arp.OpRequest, otherHW, cand, [4]byte{1, 2, 3, 4})
|
||||
|
||||
if n := testing.AllocsPerRun(100, func() {
|
||||
_, _ = h.Encapsulate(buf, -1, frameOff)
|
||||
}); n != 0 {
|
||||
t.Errorf("Encapsulate allocated %g times, want 0", n)
|
||||
}
|
||||
if n := testing.AllocsPerRun(100, func() {
|
||||
_ = h.Demux(frame, 0)
|
||||
}); n != 0 {
|
||||
t.Errorf("Demux allocated %g times, want 0", n)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user