fix(tun): resolve a prefix route's requester from the packet source

This commit is contained in:
ginuerzh
2026-10-09 06:57:23 +00:00
parent be86cfef74
commit bb682a90c9
2 changed files with 146 additions and 4 deletions
+56 -3
View File
@@ -264,12 +264,30 @@ func (h *p2pHub) dispatch(pkt []byte) {
return
}
// The requester an allow list gates is the member that owns the packet's
// source address: a member's tun IP maps to its peer key at
// registration, so a packet from that address is that member's to answer
// for. A source no member owns resolves to no requester, the empty key.
from := ""
if src, ok := sourceOf(pkt); ok {
if owner, ok := h.router.table.lookup(src); ok {
from = owner
}
}
// deliverFrom, not deliver: a destination no exact route claims may sit
// in a LAN one member sits in front of, and the prefix table — consulted
// only after the exact table misses — names the member that reaches it.
// The hub itself is the requester: its device traffic originates with no
// peer, so from is empty and an allow list gates it like anyone else.
switch err := h.router.deliverFrom(dst, pkt, ""); {
//
// The empty requester key is a choice with two edges, both deliberate.
// An unrestricted route (no allow list) admits it: a host on the LAN
// behind a spoke is not a member — its address is a LAN address, not
// any member's tun IP — and LAN-to-LAN through the hub is the very
// traffic a prefix route exists to carry, so denying no-requester
// sources would strand exactly that traffic. A restricted route denies
// it: no allow list names the empty key, so a requester nobody can
// vouch for never crosses a gate someone drew.
switch err := h.router.deliverFrom(dst, pkt, from); {
case errors.Is(err, ErrNoRoute):
// The destination is named in the warning because a spoke that never
// registered is otherwise indistinguishable from silence: the
@@ -468,6 +486,41 @@ func keepAliveReply(peerKey string) []byte {
return reply
}
// sourceOf parses the source address out of an IP packet, IPv4 or IPv6 —
// the other half of destinationOf, and the address the requester resolution
// in dispatch keys on. Same refusal contract: ok is false for anything that
// is neither or whose header does not parse.
//
// It is a sibling rather than destinationOf returning both because every
// existing caller wants exactly one end of the packet; one function per end
// leaves them all untouched.
func sourceOf(pkt []byte) (net.IP, bool) {
// waterutil.IsIPv4 indexes packet[0] unconditionally, so an empty buffer —
// a device that returned no bytes — panics there rather than reporting
// "unknown packet".
if len(pkt) == 0 {
return nil, false
}
if waterutil.IsIPv4(pkt) {
header, err := ipv4.ParseHeader(pkt)
if err != nil {
return nil, false
}
return header.Src, true
}
if waterutil.IsIPv6(pkt) {
header, err := ipv6.ParseHeader(pkt)
if err != nil {
return nil, false
}
return header.Src, true
}
return nil, false
}
// destinationOf parses the destination address out of an IP packet, IPv4 or
// IPv6. ok is false for anything that is neither or whose header does not parse,
// which is what the socket server's two loops treat as an unknown packet.
+90 -1
View File
@@ -6,6 +6,8 @@ import (
"errors"
"net"
"net/netip"
"strings"
"sync"
"testing"
)
@@ -39,7 +41,11 @@ func TestPrefixLookupLongestPrefixAndAllow(t *testing.T) {
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.
// packet reaches that peer's stream and no other. The source is owned
// by no member and the route is unrestricted, so this case also pins the
// no-requester edge: a LAN host is not a member, and an empty allow list
// must admit it or the LAN-to-LAN traffic the route exists for never
// moves.
_, peerLanEnd := newDatagramPipe(4)
_, peerOtherEnd := newDatagramPipe(4)
@@ -77,3 +83,86 @@ func TestDispatchFallsThroughExactThenPrefixThenNoRoute(t *testing.T) {
t.Fatalf("deliverFrom = %v, want ErrNoRoute with no prefix table", err)
}
}
// A restricted LAN route admits its allow list and no one else, and the
// requester it gates is resolved per packet from the source address: the
// member that owns the source is the member asking. A member inside the
// list is delivered; 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 TestDispatchAllowGatesBySourceOwner(t *testing.T) {
_, peerLanEnd := newDatagramPipe(4)
var mu sync.Mutex
var warnings []string
router := newTestRouter()
router.install(newPeerStream(context.Background(), "peer-lan", peerLanEnd))
// The requesting members, one inside the route's allow list and one
// outside it. Their tun addresses are what the requester resolution keys
// on — the peer keys travel in no packet.
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 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 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)
}
}