mirror of
https://github.com/juanfont/headscale.git
synced 2026-11-04 04:34:07 +09:00
policy/v2: apply via only to the ip part of a grant
SaaS treats app as if via were absent: v80 and v84 peer the same, v87 peers a non-via router, v88 and v91 send the route on the primary. Dropping a via grant with no ip at compile left grant-k12's nodes with no peers. Updates #3626
This commit is contained in:
1 parent
aa7b2ab404
commit
9fff03f4b5
8 files changed
+96
-12
No files matched your search
@@ -95,6 +95,10 @@ func TestBuildPeerMapMatchesPrefixReference(t *testing.T) {
|
||||
{name: "global", policy: benchPolicies[0].policy},
|
||||
{name: "self", policy: benchPolicies[1].policy},
|
||||
{name: "via", policy: benchPolicies[2].policy},
|
||||
{name: "via capability only", policy: `{
|
||||
"tagOwners": {"tag:router": ["u1@"]},
|
||||
"grants": [{"src": ["u2@"], "dst": ["10.0.0.0/8"], "via": ["tag:router"],
|
||||
"app": {"example.com/cap/test": [{}]}}]}`},
|
||||
{name: "subnet source", policy: `{
|
||||
"acls": [{"action": "accept", "src": ["10.0.0.0/8", "2001:db8::/32"],
|
||||
"dst": ["u2@:*"]}]}`},
|
||||
|
||||
@@ -265,12 +265,12 @@ func (pol *Policy) compileGrants(
|
||||
compiled := make([]compiledGrant, 0, len(grants))
|
||||
srcs := sourceCache{}
|
||||
|
||||
for _, grant := range grants {
|
||||
compile := func(grant Grant) {
|
||||
cg, err := pol.compileOneGrant(grant, users, nodes, srcs)
|
||||
if err != nil {
|
||||
log.Trace().Err(err).Msg("compiling grant")
|
||||
|
||||
continue
|
||||
return
|
||||
}
|
||||
|
||||
if cg != nil {
|
||||
@@ -278,6 +278,14 @@ func (pol *Policy) compileGrants(
|
||||
}
|
||||
}
|
||||
|
||||
for _, grant := range grants {
|
||||
compile(grant)
|
||||
|
||||
if app, ok := grant.appPart(); ok {
|
||||
compile(app)
|
||||
}
|
||||
}
|
||||
|
||||
return compiled
|
||||
}
|
||||
|
||||
@@ -294,7 +302,7 @@ func (pol *Policy) compileOneGrant(
|
||||
srcs sourceCache,
|
||||
) (*compiledGrant, error) {
|
||||
// Via grants: resolve sources, store deferred data.
|
||||
if len(grant.Via) > 0 {
|
||||
if grant.HasVia() {
|
||||
return pol.compileOneViaGrant(grant, users, nodes)
|
||||
}
|
||||
|
||||
@@ -448,10 +456,6 @@ func (pol *Policy) compileOneViaGrant(
|
||||
users types.Users,
|
||||
nodes views.Slice[types.NodeView],
|
||||
) (*compiledGrant, error) {
|
||||
if len(grant.InternetProtocols) == 0 {
|
||||
return nil, nil //nolint:nilnil
|
||||
}
|
||||
|
||||
resolvedSrcs, _, err := resolveSources(
|
||||
pol, grant.Sources, users, nodes,
|
||||
)
|
||||
|
||||
@@ -3913,7 +3913,7 @@ func TestCompileViaGrant(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "app-only via grant with no ip field returns nil",
|
||||
name: "app-only via grant gets no filter rule",
|
||||
grant: Grant{
|
||||
Sources: Aliases{up("testuser@")},
|
||||
Destinations: Aliases{pp("10.0.0.0/24")},
|
||||
@@ -3990,8 +3990,9 @@ func TestCompileViaGrant(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestViaGrantPeers pins who a via grant links, as via-grant-v81 records: with
|
||||
// ports, sources peer via routers only, also when a router is itself a source.
|
||||
// TestViaGrantPeers pins who a via grant links, as via-grant-v80 and v81 record:
|
||||
// with no ports via changes nothing, so sources peer everything serving the
|
||||
// destination and nobody gets a filter; with ports, sources peer via routers only.
|
||||
func TestViaGrantPeers(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -4030,11 +4031,26 @@ func TestViaGrantPeers(t *testing.T) {
|
||||
grant string
|
||||
want map[types.NodeID][]types.NodeID
|
||||
}{
|
||||
{
|
||||
name: "capability only",
|
||||
grant: `"app": {"example.com/cap/test": [{}]}`,
|
||||
want: map[types.NodeID][]types.NodeID{1: {2, 5}, 2: {1, 5}, 5: {1, 2}},
|
||||
},
|
||||
{
|
||||
name: "ports",
|
||||
grant: `"ip": ["tcp:80"]`,
|
||||
want: map[types.NodeID][]types.NodeID{1: {2}, 2: {1}},
|
||||
},
|
||||
{
|
||||
name: "ports and an empty capability map",
|
||||
grant: `"ip": ["tcp:80"], "app": {}`,
|
||||
want: map[types.NodeID][]types.NodeID{1: {2}, 2: {1}},
|
||||
},
|
||||
{
|
||||
name: "ports and a capability",
|
||||
grant: `"ip": ["tcp:80"], "app": {"example.com/cap/test": [{}]}`,
|
||||
want: map[types.NodeID][]types.NodeID{1: {2, 5}, 2: {1, 5}, 5: {1, 2}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -4052,6 +4068,24 @@ func TestViaGrantPeers(t *testing.T) {
|
||||
assert.Equal(t, tt.want, pm.BuildPeerMap(nodes.ViewSlice()))
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("capability only sends no filter", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pm, err := NewPolicyManager([]byte(`{
|
||||
"tagOwners": {"tag:client": [], "tag:router": [], "tag:other": []},
|
||||
"grants": [{"src": ["tag:client"], "dst": ["10.1.0.0/24"], "via": ["tag:router"], "app": {"example.com/cap/test": [{}]}}]
|
||||
}`), nil, nodes.ViewSlice())
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, nd := range nodes {
|
||||
filter, err := pm.FilterForNode(nd.View())
|
||||
require.NoError(t, err)
|
||||
assert.Emptyf(t, filter, "node %d", nd.ID)
|
||||
}
|
||||
|
||||
assert.Empty(t, pm.ViaRoutesForPeer(nodes[0].View(), nodes[1].View()))
|
||||
})
|
||||
}
|
||||
|
||||
func TestCompileGrantWithAutogroupSelf_GrantPaths(t *testing.T) {
|
||||
|
||||
@@ -37,6 +37,13 @@ func TestNodeNeedsPeerRecompute(t *testing.T) {
|
||||
]
|
||||
}`
|
||||
|
||||
viaAppPol := `{
|
||||
"tagOwners": {"tag:via": ["user1@"]},
|
||||
"grants": [
|
||||
{"src": ["*"], "dst": ["10.0.0.0/24"], "app": {"example.com/cap/test": [{}]}, "via": ["tag:via"]}
|
||||
]
|
||||
}`
|
||||
|
||||
taildrivePol := `{
|
||||
"tagOwners": {"tag:drive": ["user1@"]},
|
||||
"grants": [
|
||||
@@ -108,6 +115,13 @@ func TestNodeNeedsPeerRecompute(t *testing.T) {
|
||||
subject: viaTarget,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "capability-only via tag does not need recompute",
|
||||
pol: viaAppPol,
|
||||
nodes: types.Nodes{viaTarget, ordinary},
|
||||
subject: viaTarget,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "ordinary node in a via-using tailnet does not need recompute",
|
||||
pol: viaPol,
|
||||
|
||||
@@ -228,6 +228,12 @@ func scanRandomPolicy(r *rand.Rand) []byte {
|
||||
g := map[string]any{"src": src, "dst": dst, "ip": [][]string{{"*"}, {"tcp:443"}, {"icmp:*"}, {"udp:53", "tcp:22"}}[r.IntN(4)]}
|
||||
if r.IntN(8) == 0 {
|
||||
g["via"] = []string{[]string{"tag:router", "tag:exit"}[r.IntN(2)]}
|
||||
|
||||
// Capability only: via changes nothing, unlike a via grant with ports.
|
||||
if r.IntN(2) == 0 {
|
||||
delete(g, "ip")
|
||||
g["app"] = map[string]any{"example.com/cap/test": []map[string]any{{}}}
|
||||
}
|
||||
}
|
||||
|
||||
grants = append(grants, g)
|
||||
|
||||
@@ -1589,6 +1589,13 @@ func (pm *PolicyManager) ViaRoutesForPeer(viewer, peer types.NodeView) types.Via
|
||||
grants = append(grants, Grant{Sources: acl.Sources, Destinations: dsts})
|
||||
}
|
||||
|
||||
// A grant's app part covers its routes as a regular grant does.
|
||||
for _, grant := range pm.pol.Grants {
|
||||
if app, ok := grant.appPart(); ok {
|
||||
grants = append(grants, app)
|
||||
}
|
||||
}
|
||||
|
||||
// matchViewer resolves a grant lazily and at most once: non-via
|
||||
// grants are only needed once a via grant has matched, and
|
||||
// destinations only for grants that match the viewer.
|
||||
|
||||
@@ -1918,9 +1918,22 @@ type Grant struct {
|
||||
Via []Tag `json:"via,omitzero"`
|
||||
}
|
||||
|
||||
// HasVia reports whether the grant routes through via-tagged nodes.
|
||||
// HasVia reports whether the grant steers traffic through via-tagged nodes.
|
||||
// Only its ip part does: via-grant-v80, v84 and v88 show that SaaS treats a
|
||||
// capability-only grant the same with or without via.
|
||||
func (g Grant) HasVia() bool {
|
||||
return len(g.Via) > 0
|
||||
return len(g.Via) > 0 && len(g.InternetProtocols) > 0
|
||||
}
|
||||
|
||||
// appPart returns the capabilities of a grant with via as a grant of their own,
|
||||
// since via does not apply to them. via-grant-v89 and v91 show SaaS treating one
|
||||
// grant holding both parts like two. An empty map is no capability (v92).
|
||||
func (g Grant) appPart() (Grant, bool) {
|
||||
if !g.HasVia() || len(g.App) == 0 {
|
||||
return Grant{}, false
|
||||
}
|
||||
|
||||
return Grant{Sources: g.Sources, Destinations: g.Destinations, App: g.App}, true
|
||||
}
|
||||
|
||||
// NodeAttrGrant attaches Tailscale node capabilities (and/or an IP-pool
|
||||
|
||||
@@ -42,6 +42,8 @@ var viaCompatTests = []struct {
|
||||
{"grant-v11", "tag src steers subnet via router"},
|
||||
{"grant-v12", "member src steers subnet via router"},
|
||||
{"grant-v13", "group src steers subnet via router to members only"},
|
||||
{"grant-k12", "capability-only via grant to a subnet"},
|
||||
{"grant-k13", "the same via grant with a port"},
|
||||
{"via-grant-v14", "tag src steers the internet via tag:exit"},
|
||||
{"via-grant-v15", "wildcard src steers the internet via tag:exit"},
|
||||
{"via-grant-v16", "tag and member srcs steer the internet via tag:exit"},
|
||||
|
||||
Reference in new issue
Block a user