Files
lneto/ipv4/linklocal4/linklocal4_test.go
T
Marvin Drees e0ef681085 feat: add RFC 3927 support (#114)
Signed-off-by: Marvin Drees <marvin.drees@9elements.com>
2026-06-18 10:21:25 -03:00

333 lines
9.2 KiB
Go

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)
}
}