mirror of
https://github.com/juanfont/headscale.git
synced 2026-10-27 00:36:49 +09:00
mapper: reconcile self on policy recomputes
Self renders from the same state as peers, so leaving it out of policy responses hid an exit node's approved routes until it reconnected. Fixes #3502
This commit is contained in:
1 parent
ba413fca3d
commit
ea6b0bf0b7
7 files changed
+259
-34
No files matched your search
@@ -222,36 +222,128 @@ func TestIssuesRoutes(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
// When the server approves routes for a node, that node
|
||||
// should receive a self-update reflecting the change.
|
||||
t.Run("self_update_after_route_approval", func(t *testing.T) {
|
||||
// The client derives ExitNodeOption from its own SelfNode.AllowedIPs,
|
||||
// and SelfNode only changes when a MapResponse carries Node. SaaS
|
||||
// captures show approved exit routes on the exit node's own SelfNode
|
||||
// (routes-ea*, routes-b17/b18). Peers are the control: they must see
|
||||
// the routes, proving the change was dispatched.
|
||||
// https://github.com/juanfont/headscale/issues/3502
|
||||
t.Run("exit_route_approval_reaches_self", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
srv := servertest.NewServer(t)
|
||||
user := srv.CreateUser(t, "selfup-user")
|
||||
user := srv.CreateUser(t, "selfexit-user")
|
||||
|
||||
c1 := servertest.NewClient(t, srv, "selfup-node1",
|
||||
exit := servertest.NewClient(t, srv, "selfexit-node",
|
||||
servertest.WithUser(user))
|
||||
servertest.NewClient(t, srv, "selfup-node2",
|
||||
obs := servertest.NewClient(t, srv, "selfexit-obs",
|
||||
servertest.WithUser(user))
|
||||
|
||||
c1.WaitForPeers(t, 1, 10*time.Second)
|
||||
exit.WaitForPeers(t, 1, 10*time.Second)
|
||||
advertiseRoutes(t, exit, obs, tsaddr.ExitRoutes())
|
||||
|
||||
nodeID := findNodeID(t, srv, "selfup-node1")
|
||||
route := netip.MustParsePrefix("10.77.0.0/24")
|
||||
|
||||
countBefore := c1.UpdateCount()
|
||||
|
||||
_, routeChange, err := srv.State().SetApprovedRoutes(
|
||||
nodeID, []netip.Prefix{route})
|
||||
_, c, err := srv.State().SetApprovedRoutes(
|
||||
findNodeID(t, srv, "selfexit-node"), tsaddr.ExitRoutes())
|
||||
require.NoError(t, err)
|
||||
srv.App.Change(routeChange)
|
||||
srv.App.Change(c)
|
||||
|
||||
c1.WaitForCondition(t, "self-update after route approval",
|
||||
10*time.Second,
|
||||
func(nm *netmap.NetworkMap) bool {
|
||||
return c1.UpdateCount() > countBefore
|
||||
})
|
||||
obs.WaitForCondition(t, "peer offers exit node", 10*time.Second,
|
||||
peerOffersExit("selfexit-node"))
|
||||
exit.WaitForCondition(t, "self offers exit node", 10*time.Second,
|
||||
selfOffersExit)
|
||||
})
|
||||
|
||||
// Advertising an already-approved exit route goes through
|
||||
// UpdateNodeFromMapRequest instead of SetApprovedRoutes. This is the
|
||||
// re-advertise workaround from the issue that 0.29.4 broke.
|
||||
t.Run("exit_route_advertised_after_approval_reaches_self", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
srv := servertest.NewServer(t)
|
||||
user := srv.CreateUser(t, "preexit-user")
|
||||
|
||||
exit := servertest.NewClient(t, srv, "preexit-node",
|
||||
servertest.WithUser(user))
|
||||
obs := servertest.NewClient(t, srv, "preexit-obs",
|
||||
servertest.WithUser(user))
|
||||
|
||||
exit.WaitForPeers(t, 1, 10*time.Second)
|
||||
|
||||
_, c, err := srv.State().SetApprovedRoutes(
|
||||
findNodeID(t, srv, "preexit-node"), tsaddr.ExitRoutes())
|
||||
require.NoError(t, err)
|
||||
srv.App.Change(c)
|
||||
|
||||
advertiseRoutes(t, exit, obs, tsaddr.ExitRoutes())
|
||||
|
||||
obs.WaitForCondition(t, "peer offers exit node", 10*time.Second,
|
||||
peerOffersExit("preexit-node"))
|
||||
exit.WaitForCondition(t, "self offers exit node", 10*time.Second,
|
||||
selfOffersExit)
|
||||
})
|
||||
|
||||
// A policy reload that auto-approves an advertised exit route goes
|
||||
// through autoApproveNodes, a third producer of the same change.
|
||||
t.Run("exit_route_auto_approved_on_reload_reaches_self", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
srv := servertest.NewServer(t)
|
||||
user := srv.CreateUser(t, "reloadexit-user")
|
||||
|
||||
exit := servertest.NewClient(t, srv, "reloadexit-node",
|
||||
servertest.WithUser(user))
|
||||
obs := servertest.NewClient(t, srv, "reloadexit-obs",
|
||||
servertest.WithUser(user))
|
||||
|
||||
exit.WaitForPeers(t, 1, 10*time.Second)
|
||||
advertiseRoutes(t, exit, obs, tsaddr.ExitRoutes())
|
||||
|
||||
_, err := srv.State().SetPolicyInDB(`{
|
||||
"acls": [{"action": "accept", "src": ["*"], "dst": ["*:*"]}],
|
||||
"autoApprovers": {"exitNode": ["reloadexit-user@"]}
|
||||
}`)
|
||||
require.NoError(t, err)
|
||||
|
||||
changes, err := srv.State().ReloadPolicy()
|
||||
require.NoError(t, err)
|
||||
srv.App.Change(changes...)
|
||||
|
||||
obs.WaitForCondition(t, "peer offers exit node", 10*time.Second,
|
||||
peerOffersExit("reloadexit-node"))
|
||||
exit.WaitForCondition(t, "self offers exit node", 10*time.Second,
|
||||
selfOffersExit)
|
||||
})
|
||||
|
||||
// An auto-approver approves the route inside the advertising map
|
||||
// request, a fourth producer of the same change.
|
||||
t.Run("exit_route_auto_approved_on_advertise_reaches_self", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
srv := servertest.NewServer(t)
|
||||
user := srv.CreateUser(t, "advexit-user")
|
||||
|
||||
_, err := srv.State().SetPolicyInDB(`{
|
||||
"acls": [{"action": "accept", "src": ["*"], "dst": ["*:*"]}],
|
||||
"autoApprovers": {"exitNode": ["advexit-user@"]}
|
||||
}`)
|
||||
require.NoError(t, err)
|
||||
|
||||
changes, err := srv.State().ReloadPolicy()
|
||||
require.NoError(t, err)
|
||||
srv.App.Change(changes...)
|
||||
|
||||
exit := servertest.NewClient(t, srv, "advexit-node",
|
||||
servertest.WithUser(user))
|
||||
obs := servertest.NewClient(t, srv, "advexit-obs",
|
||||
servertest.WithUser(user))
|
||||
|
||||
exit.WaitForPeers(t, 1, 10*time.Second)
|
||||
advertiseRoutes(t, exit, obs, tsaddr.ExitRoutes())
|
||||
|
||||
obs.WaitForCondition(t, "peer offers exit node", 10*time.Second,
|
||||
peerOffersExit("advexit-node"))
|
||||
exit.WaitForCondition(t, "self offers exit node", 10*time.Second,
|
||||
selfOffersExit)
|
||||
})
|
||||
|
||||
// [tailcfg.Hostinfo] route advertisement should be stored on server.
|
||||
@@ -1104,3 +1196,50 @@ func findNodeID(tb testing.TB, srv *servertest.TestServer, hostname string) type
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// advertiseRoutes pushes routes in c's Hostinfo and waits until obs sees
|
||||
// them, so the server has stored the announcement.
|
||||
func advertiseRoutes(tb testing.TB, c, obs *servertest.TestClient, routes []netip.Prefix) {
|
||||
tb.Helper()
|
||||
|
||||
c.Direct().SetHostinfo(&tailcfg.Hostinfo{
|
||||
BackendLogID: "servertest-" + c.Name,
|
||||
Hostname: c.Name,
|
||||
RoutableIPs: routes,
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_ = c.Direct().SendUpdate(ctx)
|
||||
|
||||
obs.WaitForCondition(tb, "routes in peer hostinfo", 10*time.Second,
|
||||
func(nm *netmap.NetworkMap) bool {
|
||||
p, ok := peerByHostname(nm, c.Name)
|
||||
|
||||
return ok && p.Hostinfo().RoutableIPs().Len() == len(routes)
|
||||
})
|
||||
}
|
||||
|
||||
// selfOffersExit mirrors how tailscaled computes Self.ExitNodeOption.
|
||||
func selfOffersExit(nm *netmap.NetworkMap) bool {
|
||||
return nm.SelfNode.Valid() && tsaddr.ContainsExitRoutes(nm.SelfNode.AllowedIPs())
|
||||
}
|
||||
|
||||
func peerOffersExit(hostname string) func(*netmap.NetworkMap) bool {
|
||||
return func(nm *netmap.NetworkMap) bool {
|
||||
p, ok := peerByHostname(nm, hostname)
|
||||
|
||||
return ok && tsaddr.ContainsExitRoutes(p.AllowedIPs())
|
||||
}
|
||||
}
|
||||
|
||||
func peerByHostname(nm *netmap.NetworkMap, hostname string) (tailcfg.NodeView, bool) {
|
||||
for _, p := range nm.Peers {
|
||||
if hi := p.Hostinfo(); hi.Valid() && hi.Hostname() == hostname {
|
||||
return p, true
|
||||
}
|
||||
}
|
||||
|
||||
return tailcfg.NodeView{}, false
|
||||
}
|
||||
@@ -255,6 +255,11 @@ func runViaMapCompat(t *testing.T, c *testcapture.Capture) {
|
||||
nm := cl.Netmap()
|
||||
require.NotNil(tt, nm, "netmap is nil")
|
||||
|
||||
// Routes were approved on live sessions: verify that approval
|
||||
// reached SelfNode throughout the stable comparison window.
|
||||
require.Equal(tt, selfOffersExit(capture.Netmap), selfOffersExit(nm),
|
||||
"SelfNode exit routes should match SaaS")
|
||||
|
||||
compareNetmap(tt, nm, capture, clients, saasAddrs)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user