mirror of
https://github.com/go-gost/x.git
synced 2026-10-19 16:16:32 +00:00
fix(tun): resolve a prefix route's requester from the packet source
This commit is contained in:
+56
-3
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user