Files
ginuerzh c2c78f3279 feat(tun): install prefix routes into the p2p handler
A hub that decides which peer LANs exist is handed this handler as a
handler.Handler, so the table has to be reachable by shape: the route type
is exported and the p2p handler takes the same call the socket one does,
installing into the peerTable its packet path already reads.
2026-10-09 11:01:04 +00:00

274 lines
10 KiB
Go

package tun
import (
"bytes"
"context"
"errors"
"net"
"net/netip"
"strings"
"sync"
"testing"
)
func TestPrefixLookupLongestPrefixAndAllow(t *testing.T) {
pt := newPeerTable(nil, 0, "tun-service", nil)
pt.SetPrefixRoutes(map[netip.Prefix]PrefixRoute{
netip.MustParsePrefix("192.168.0.0/16"): {Peer: "peerWide"},
netip.MustParsePrefix("192.168.50.0/24"): {Peer: "peerB"},
})
// Longest match wins.
if peer, _ := pt.lookupPrefix(net.ParseIP("192.168.50.9"), "peerA"); peer != "peerB" {
t.Fatalf("dst in the /24 resolved to %q, want peerB", peer)
}
if peer, _ := pt.lookupPrefix(net.ParseIP("192.168.77.9"), "peerA"); peer != "peerWide" {
t.Fatalf("dst outside the /24 resolved to %q, want peerWide", peer)
}
// A member's exact route still outranks any prefix.
pt.set(net.ParseIP("10.10.100.5"), "peerA")
if peer, _ := pt.lookupPrefix(net.ParseIP("10.10.100.5"), "peerA"); peer != "peerA" {
t.Fatal("an exact route must outrank a prefix")
}
// An allow list gates use.
pt.SetPrefixRoutes(map[netip.Prefix]PrefixRoute{
netip.MustParsePrefix("192.168.50.0/24"): {Peer: "peerB", Allow: []string{"peerA"}},
})
if _, ok := pt.lookupPrefix(net.ParseIP("192.168.50.9"), "peerZ"); ok {
t.Fatal("a peer outside allow must not use the route")
}
}
// TestP2PHandlerSetPrefixRoutes: the hub a p2p entrypoint builds installs its
// LAN routes through the same call the socket handler takes, because the
// consumer that holds the routes (wisper's hub) is handed only the
// handler.Handler interface and reaches the method by shape. What it installs
// has to land in the table the packet path reads.
func TestP2PHandlerSetPrefixRoutes(t *testing.T) {
devA, devB := net.Pipe()
defer devA.Close()
defer devB.Close()
h := NewP2PHandler(devA, nil).(*p2pHandler)
h.SetPrefixRoutes(map[netip.Prefix]PrefixRoute{
netip.MustParsePrefix("192.168.50.0/24"): {Peer: "peerB", Allow: []string{"peerA"}},
})
if name, ok := h.router.table.lookupPrefix(net.ParseIP("192.168.50.9"), "peerA"); !ok || name != "peerB" {
t.Fatalf("a LAN address resolved to (%q,%v), want (peerB,true)", name, ok)
}
if _, ok := h.router.table.lookupPrefix(net.ParseIP("192.168.50.9"), "peerZ"); ok {
t.Fatal("a peer outside the route's allow list must not resolve it")
}
if _, ok := h.router.table.lookupPrefix(net.ParseIP("8.8.8.8"), "peerA"); ok {
t.Fatal("an address no prefix covers must not resolve")
}
}
func TestDispatchFallsThroughExactThenPrefixThenNoRoute(t *testing.T) {
// One destination in a claimed LAN is delivered by prefix: the table has
// no exact route for it, the prefix table names the LAN's owner, and the
// packet reaches that peer's stream and no other. The source is owned by
// no member — this is hub-local traffic, which has no member key — and
// the route is unrestricted, so this case also pins the no-requester
// edge: no allow list is drawn, so the hub's own traffic crosses.
_, peerLanEnd := newDatagramPipe(4)
_, peerOtherEnd := newDatagramPipe(4)
router := newTestRouter()
router.install(newPeerStream(context.Background(), "peer-lan", peerLanEnd))
router.install(newPeerStream(context.Background(), "peer-other", peerOtherEnd))
router.table.SetPrefixRoutes(map[netip.Prefix]PrefixRoute{
netip.MustParsePrefix("192.168.50.0/24"): {Peer: "peer-lan"},
})
hub := newP2PHub(nopDevice{}, router, nil, nil)
pkt := udpPacket("10.10.0.9", "192.168.50.9", 1, 1)
hub.dispatch(pkt)
var got [MaxMessageSize]byte
n, err := peerLanEnd.Read(got[:])
if err != nil {
t.Fatalf("peer-lan read: %v", err)
}
if !bytes.Equal(got[:n], pkt) {
t.Fatalf("peer-lan got % x, want % x", got[:n], pkt)
}
select {
case pkt := <-peerOtherEnd.queue:
t.Fatalf("peer-other received % x, want nothing", pkt)
default:
}
// The same destination with an empty prefix table still takes the
// existing ErrNoRoute path — the fall-through added a case, not a new
// outcome.
router.table.SetPrefixRoutes(nil)
err = router.deliverFrom(net.ParseIP("192.168.50.9"), udpPacket("10.10.0.9", "192.168.50.9", 1, 2), "")
if !errors.Is(err, ErrNoRoute) {
t.Fatalf("deliverFrom = %v, want ErrNoRoute with no prefix table", err)
}
}
// A restricted LAN route admits its allow list and no one else, and for
// hub-local traffic — the only traffic dispatch sees, every remote peer
// entering through fromSpoke's authenticated gate — the requester is
// resolved from the source address, best-effort: the hub's own host is the
// only stack that can originate here, and no member key vouches for it. A
// source owned by a member inside the list is delivered; one owned by a
// member outside it is denied; a source no member owns has no requester to
// vouch for it, so it is denied too. The denials are unrouted events in the
// existing accounting, not a new failure path.
func TestDispatchHubLocalRequesterResolvedFromSource(t *testing.T) {
_, peerLanEnd := newDatagramPipe(4)
var mu sync.Mutex
var warnings []string
router := newTestRouter()
router.install(newPeerStream(context.Background(), "peer-lan", peerLanEnd))
// The requester resolution keys on the source address: a hub-host
// process using a member's tun address resolves to that member. One
// member sits inside the route's allow list, one outside it.
router.table.set(net.ParseIP("10.10.0.3"), "peer-in")
router.table.set(net.ParseIP("10.10.0.4"), "peer-out")
router.table.SetPrefixRoutes(map[netip.Prefix]PrefixRoute{
netip.MustParsePrefix("192.168.50.0/24"): {Peer: "peer-lan", Allow: []string{"peer-in"}},
})
// A hub per case, not one shared: a denial is counted per unrouted
// window, so the second denial on one hub would be within the first's
// window and warn nothing — each case needs its own tally to see its
// own warning.
newHub := func() *p2pHub {
mu.Lock()
warnings = nil
mu.Unlock()
return newP2PHub(nopDevice{}, router, nil, func(msg string) {
mu.Lock()
warnings = append(warnings, msg)
mu.Unlock()
})
}
// A source owned by a member inside the allow list is delivered.
hub := newHub()
in := udpPacket("10.10.0.3", "192.168.50.9", 1, 1)
hub.dispatch(in)
var got [MaxMessageSize]byte
n, err := peerLanEnd.Read(got[:])
if err != nil {
t.Fatalf("peer-lan read: %v", err)
}
if !bytes.Equal(got[:n], in) {
t.Fatalf("peer-lan got % x, want % x", got[:n], in)
}
// A source owned by a member outside the allow list is denied: no
// delivery, and the denial is the existing unrouted accounting.
hub = newHub()
hub.dispatch(udpPacket("10.10.0.4", "192.168.50.9", 2, 1))
select {
case pkt := <-peerLanEnd.queue:
t.Fatalf("a member outside allow was delivered % x", pkt)
default:
}
mu.Lock()
w := append([]string(nil), warnings...)
mu.Unlock()
if len(w) != 1 || !strings.Contains(w[0], "no route for 192.168.50.9") {
t.Fatalf("warnings = %v, want one unrouted warning for the denied member", w)
}
// A source no member owns is denied the same way: no requester means no
// one to admit, and a restricted route must never treat that as a pass.
hub = newHub()
hub.dispatch(udpPacket("10.10.0.99", "192.168.50.9", 3, 1))
select {
case pkt := <-peerLanEnd.queue:
t.Fatalf("an unowned source was delivered % x", pkt)
default:
}
mu.Lock()
w = append([]string(nil), warnings...)
mu.Unlock()
if len(w) != 1 || !strings.Contains(w[0], "no route for 192.168.50.9") {
t.Fatalf("warnings = %v, want one unrouted warning for the unowned source", w)
}
}
// The allow gate keys off the authenticated stream peer, not the packet's
// self-declared source field: a packet arriving on a spoke's stream whose
// source claims an allow-listed member's address is denied when the spoke it
// arrived from is outside the route's allow list. Dropped before the device,
// too — sent on instead, it would come back through dispatch, whose requester
// is the source field's owner, and the claimed source would vouch for it.
func TestFromSpokeAllowGatesOnAuthenticatedPeer(t *testing.T) {
device := newWriteRecorder(MaxMessageSize)
_, peerLanEnd := newDatagramPipe(4)
_, outEnd := newDatagramPipe(4)
_, inEnd := newDatagramPipe(4)
router := newTestRouter()
router.install(newPeerStream(context.Background(), "peer-lan", peerLanEnd))
out := newPeerStream(context.Background(), "peer-out", outEnd)
in := newPeerStream(context.Background(), "peer-in", inEnd)
router.install(out)
router.install(in)
// The route is restricted to peer-in, and the packets below all claim
// peer-in's tun address as their source: only the stream a packet
// arrives on tells the two members apart.
router.table.set(net.ParseIP("10.10.0.3"), "peer-in")
router.table.SetPrefixRoutes(map[netip.Prefix]PrefixRoute{
netip.MustParsePrefix("192.168.50.0/24"): {Peer: "peer-lan", Allow: []string{"peer-in"}},
})
var mu sync.Mutex
var warnings []string
hub := newP2PHub(device, router, nil, func(msg string) {
mu.Lock()
warnings = append(warnings, msg)
mu.Unlock()
})
// A spoofed source does not cross the gate: the destination matches the
// route, the route admits peer-in, and the source claims peer-in's
// address — but the packet arrives on peer-out's stream, so it is denied:
// not delivered, not written to the device, and counted as unrouted.
pkt := udpPacket("10.10.0.3", "192.168.50.9", 1, 1)
if err := hub.fromSpoke(out, pkt); err != nil {
t.Fatalf("fromSpoke(spoofed source): %v", err)
}
select {
case pkt := <-peerLanEnd.queue:
t.Fatalf("a spoofed source crossed the allow gate: % x", pkt)
default:
}
if got := device.received(); len(got) != 0 {
t.Fatalf("a denied packet was written to the device: % x", got)
}
mu.Lock()
w := append([]string(nil), warnings...)
mu.Unlock()
if len(w) != 1 || !strings.Contains(w[0], "no route for 192.168.50.9") {
t.Fatalf("warnings = %v, want one unrouted warning for the denied spoke", w)
}
// The authenticated peer crosses: the same claimed source, arriving on
// peer-in's own stream, is delivered by prefix — and directly, not
// through the device.
pkt = udpPacket("10.10.0.3", "192.168.50.9", 2, 1)
if err := hub.fromSpoke(in, pkt); err != nil {
t.Fatalf("fromSpoke(allowed): %v", err)
}
var got [MaxMessageSize]byte
n, err := peerLanEnd.Read(got[:])
if err != nil {
t.Fatalf("peer-lan read: %v", err)
}
if !bytes.Equal(got[:n], pkt) {
t.Fatalf("peer-lan got % x, want % x", got[:n], pkt)
}
if got := device.received(); len(got) != 0 {
t.Fatalf("an allowed packet took the device: % x", got)
}
}