mirror of
https://github.com/juanfont/headscale.git
synced 2026-05-04 19:46:12 +02:00
servertest: cover via-grant exit-node visibility end-to-end
TestGrantViaExitNodeInternetVisibility boots a server, applies a policy that scopes autogroup:internet to a tag, registers a tagged exit advertiser and a regular client, and asserts the client's netmap surfaces the exit node with 0.0.0.0/0 and ::/0 in AllowedIPs — the substrate the Tailscale client reads to populate `tailscale exit-node list`. TestGrantViaExitNodeNoFilterRules retains its assertion (literal /0 absent from the exit node's PacketFilter, matching SaaS PacketFilter encoding); only its docstring is updated to reflect that the exit node now does receive a TheInternet-shaped rule, just not the literal /0 form. Updates #3233
This commit is contained in:
parent
2b7f15abaa
commit
76ee29352b
@ -772,11 +772,12 @@ func TestGrantViaSubnetFilterRules(t *testing.T) {
|
||||
"without per-node filter compilation for via grants, these rules are missing")
|
||||
}
|
||||
|
||||
// TestGrantViaExitNodeNoFilterRules verifies that exit nodes with via grants
|
||||
// for autogroup:internet do NOT receive PacketFilter rules for exit traffic.
|
||||
// Tailscale SaaS handles exit traffic forwarding through the client's exit
|
||||
// node selection mechanism, not through PacketFilter rules. Verified by
|
||||
// golden captures GRANT-V14 through GRANT-V36.
|
||||
// TestGrantViaExitNodeNoFilterRules verifies wire-format SaaS compat:
|
||||
// the exit node's PacketFilter must not contain literal 0.0.0.0/0 or
|
||||
// ::/0 destinations. Internally, autogroup:internet via grants emit a
|
||||
// rule whose DstPorts enumerate util.TheInternet() prefixes;
|
||||
// ReduceFilterRules preserves it on exit-route advertisers but it
|
||||
// never surfaces as the literal /0 form.
|
||||
func TestGrantViaExitNodeNoFilterRules(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@ -852,9 +853,11 @@ func TestGrantViaExitNodeNoFilterRules(t *testing.T) {
|
||||
return nm != nil
|
||||
})
|
||||
|
||||
// The exit node's PacketFilter must NOT contain rules for exit traffic.
|
||||
// The only rules should be from the peer connectivity grant (tag:exit-a
|
||||
// and tag:group-a can talk to each other at their Tailscale IPs).
|
||||
// The exit node's PacketFilter must not surface exit-route
|
||||
// destinations as the literal /0 form. The autogroup:internet via
|
||||
// grant emits a rule with DstPorts enumerating util.TheInternet()
|
||||
// prefixes; literal /0 would diverge from SaaS PacketFilter
|
||||
// encoding.
|
||||
exitNM := exitA.Netmap()
|
||||
require.NotNil(t, exitNM)
|
||||
|
||||
@ -862,12 +865,113 @@ func TestGrantViaExitNodeNoFilterRules(t *testing.T) {
|
||||
for _, dst := range m.Dsts {
|
||||
dstPrefix := netip.PrefixFrom(dst.Net.Addr(), dst.Net.Bits())
|
||||
assert.Falsef(t, dstPrefix == exitRouteV4 || dstPrefix == exitRouteV6,
|
||||
"exit node PacketFilter should NOT contain exit route destinations (0.0.0.0/0 or ::/0); "+
|
||||
"autogroup:internet via grants do not produce filter rules on exit nodes (verified against Tailscale SaaS)")
|
||||
"exit node PacketFilter must not contain literal 0.0.0.0/0 or ::/0; "+
|
||||
"autogroup:internet via rules use util.TheInternet() prefixes")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGrantViaExitNodeInternetVisibility verifies that a via grant
|
||||
// scoping autogroup:internet to a tag surfaces the via-tagged exit
|
||||
// node as a peer in the source client's netmap with 0.0.0.0/0 and
|
||||
// ::/0 in its AllowedIPs — the substrate the Tailscale client reads
|
||||
// to populate `tailscale exit-node list`.
|
||||
func TestGrantViaExitNodeInternetVisibility(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
srv := servertest.NewServer(t)
|
||||
exitUser := srv.CreateUser(t, "exit-user")
|
||||
clientUser := srv.CreateUser(t, "cl-user")
|
||||
|
||||
exitRouteV4 := netip.MustParsePrefix("0.0.0.0/0")
|
||||
exitRouteV6 := netip.MustParsePrefix("::/0")
|
||||
|
||||
changed, err := srv.State().SetPolicy([]byte(`{
|
||||
"tagOwners": {
|
||||
"tag:exit-a": ["exit-user@"],
|
||||
"tag:group-a": ["cl-user@"]
|
||||
},
|
||||
"grants": [
|
||||
{
|
||||
"src": ["tag:exit-a", "tag:group-a"],
|
||||
"dst": ["tag:exit-a", "tag:group-a"],
|
||||
"ip": ["*"]
|
||||
},
|
||||
{
|
||||
"src": ["tag:group-a"],
|
||||
"dst": ["autogroup:internet"],
|
||||
"ip": ["*"],
|
||||
"via": ["tag:exit-a"]
|
||||
}
|
||||
],
|
||||
"autoApprovers": {
|
||||
"exitNode": ["tag:exit-a"]
|
||||
}
|
||||
}`))
|
||||
require.NoError(t, err)
|
||||
|
||||
if changed {
|
||||
changes, err := srv.State().ReloadPolicy()
|
||||
require.NoError(t, err)
|
||||
srv.App.Change(changes...)
|
||||
}
|
||||
|
||||
exitA := servertest.NewClient(t, srv, "exit-a",
|
||||
servertest.WithUser(exitUser),
|
||||
servertest.WithTags("tag:exit-a"))
|
||||
clientA := servertest.NewClient(t, srv, "client-a",
|
||||
servertest.WithUser(clientUser),
|
||||
servertest.WithTags("tag:group-a"))
|
||||
|
||||
exitA.WaitForPeers(t, 1, 15*time.Second)
|
||||
clientA.WaitForPeers(t, 1, 15*time.Second)
|
||||
|
||||
exitA.Direct().SetHostinfo(&tailcfg.Hostinfo{
|
||||
BackendLogID: "servertest-exit-a-internet",
|
||||
Hostname: "exit-a",
|
||||
RoutableIPs: []netip.Prefix{exitRouteV4, exitRouteV6},
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_ = exitA.Direct().SendUpdate(ctx)
|
||||
|
||||
exitAID := findNodeID(t, srv, "exit-a")
|
||||
_, routeChange, err := srv.State().SetApprovedRoutes(
|
||||
exitAID, []netip.Prefix{exitRouteV4, exitRouteV6})
|
||||
require.NoError(t, err)
|
||||
srv.App.Change(routeChange)
|
||||
|
||||
clientA.WaitForCondition(t, "client-a sees exit-a with exit AllowedIPs",
|
||||
15*time.Second,
|
||||
func(nm *netmap.NetworkMap) bool {
|
||||
for _, p := range nm.Peers {
|
||||
hi := p.Hostinfo()
|
||||
if !hi.Valid() || hi.Hostname() != "exit-a" {
|
||||
continue
|
||||
}
|
||||
|
||||
var sawV4, sawV6 bool
|
||||
|
||||
for i := range p.AllowedIPs().Len() {
|
||||
switch p.AllowedIPs().At(i) {
|
||||
case exitRouteV4:
|
||||
sawV4 = true
|
||||
case exitRouteV6:
|
||||
sawV6 = true
|
||||
}
|
||||
}
|
||||
|
||||
if sawV4 && sawV6 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
// hasCapMatches returns true if any Match in the slice contains a
|
||||
// non-empty Caps (CapMatch) list.
|
||||
func hasCapMatches(matches []filtertype.Match) bool {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user