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")
|
"without per-node filter compilation for via grants, these rules are missing")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestGrantViaExitNodeNoFilterRules verifies that exit nodes with via grants
|
// TestGrantViaExitNodeNoFilterRules verifies wire-format SaaS compat:
|
||||||
// for autogroup:internet do NOT receive PacketFilter rules for exit traffic.
|
// the exit node's PacketFilter must not contain literal 0.0.0.0/0 or
|
||||||
// Tailscale SaaS handles exit traffic forwarding through the client's exit
|
// ::/0 destinations. Internally, autogroup:internet via grants emit a
|
||||||
// node selection mechanism, not through PacketFilter rules. Verified by
|
// rule whose DstPorts enumerate util.TheInternet() prefixes;
|
||||||
// golden captures GRANT-V14 through GRANT-V36.
|
// ReduceFilterRules preserves it on exit-route advertisers but it
|
||||||
|
// never surfaces as the literal /0 form.
|
||||||
func TestGrantViaExitNodeNoFilterRules(t *testing.T) {
|
func TestGrantViaExitNodeNoFilterRules(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@ -852,9 +853,11 @@ func TestGrantViaExitNodeNoFilterRules(t *testing.T) {
|
|||||||
return nm != nil
|
return nm != nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// The exit node's PacketFilter must NOT contain rules for exit traffic.
|
// The exit node's PacketFilter must not surface exit-route
|
||||||
// The only rules should be from the peer connectivity grant (tag:exit-a
|
// destinations as the literal /0 form. The autogroup:internet via
|
||||||
// and tag:group-a can talk to each other at their Tailscale IPs).
|
// grant emits a rule with DstPorts enumerating util.TheInternet()
|
||||||
|
// prefixes; literal /0 would diverge from SaaS PacketFilter
|
||||||
|
// encoding.
|
||||||
exitNM := exitA.Netmap()
|
exitNM := exitA.Netmap()
|
||||||
require.NotNil(t, exitNM)
|
require.NotNil(t, exitNM)
|
||||||
|
|
||||||
@ -862,12 +865,113 @@ func TestGrantViaExitNodeNoFilterRules(t *testing.T) {
|
|||||||
for _, dst := range m.Dsts {
|
for _, dst := range m.Dsts {
|
||||||
dstPrefix := netip.PrefixFrom(dst.Net.Addr(), dst.Net.Bits())
|
dstPrefix := netip.PrefixFrom(dst.Net.Addr(), dst.Net.Bits())
|
||||||
assert.Falsef(t, dstPrefix == exitRouteV4 || dstPrefix == exitRouteV6,
|
assert.Falsef(t, dstPrefix == exitRouteV4 || dstPrefix == exitRouteV6,
|
||||||
"exit node PacketFilter should NOT contain exit route destinations (0.0.0.0/0 or ::/0); "+
|
"exit node PacketFilter must not contain literal 0.0.0.0/0 or ::/0; "+
|
||||||
"autogroup:internet via grants do not produce filter rules on exit nodes (verified against Tailscale SaaS)")
|
"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
|
// hasCapMatches returns true if any Match in the slice contains a
|
||||||
// non-empty Caps (CapMatch) list.
|
// non-empty Caps (CapMatch) list.
|
||||||
func hasCapMatches(matches []filtertype.Match) bool {
|
func hasCapMatches(matches []filtertype.Match) bool {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user