mirror of
https://github.com/soypat/lneto.git
synced 2026-08-07 16:33:40 +00:00
e0ef681085
Signed-off-by: Marvin Drees <marvin.drees@9elements.com>
332 lines
11 KiB
Go
332 lines
11 KiB
Go
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))
|
|
}
|