Skip to content

Commit 814335d

Browse files
committed
fix: update auto-approved advertised routes when set after registration
1 parent 644b99b commit 814335d

File tree

6 files changed

+188
-2
lines changed

6 files changed

+188
-2
lines changed

internal/handlers/poll_net_map.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"github.com/klauspost/compress/zstd"
1212
"github.com/labstack/echo/v4"
1313
"net/http"
14+
"slices"
1415
"sync"
1516
"tailscale.com/smallzstd"
1617
"tailscale.com/tailcfg"
@@ -82,6 +83,10 @@ func (h *PollNetMapHandler) handlePollNetMap(c echo.Context, m *domain.Machine,
8283
}
8384

8485
if !mapRequest.Stream {
86+
if !slices.Equal(m.HostInfo.RoutableIPs, mapRequest.Hostinfo.RoutableIPs) {
87+
m.AutoAllowIPs = m.Tailnet.ACLPolicy.Get().FindAutoApprovedIPs(mapRequest.Hostinfo.RoutableIPs, m.Tags, &m.User)
88+
}
89+
8590
m.HostInfo = domain.HostInfo(*mapRequest.Hostinfo)
8691
m.DiscoKey = mapRequest.DiscoKey.String()
8792
m.Endpoints = mapRequest.Endpoints

tests/auto_approvers_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package tests
2+
3+
import (
4+
"github.com/jsiebens/ionscale/pkg/client/ionscale"
5+
"github.com/jsiebens/ionscale/pkg/defaults"
6+
"github.com/jsiebens/ionscale/tests/sc"
7+
"github.com/jsiebens/ionscale/tests/tsn"
8+
"github.com/stretchr/testify/require"
9+
"net/netip"
10+
"testing"
11+
)
12+
13+
func TestAdvertiseRoutesAutoApprovedOnNewNode(t *testing.T) {
14+
route1 := netip.MustParsePrefix("10.1.0.0/24")
15+
route2 := netip.MustParsePrefix("10.2.0.0/24")
16+
17+
sc.Run(t, func(s *sc.Scenario) {
18+
aclPolicy := defaults.DefaultACLPolicy()
19+
aclPolicy.AutoApprovers = &ionscale.ACLAutoApprovers{
20+
Routes: map[string][]string{
21+
route1.String(): {"tag:test-route"},
22+
},
23+
}
24+
25+
tailnet := s.CreateTailnet()
26+
s.SetACLPolicy(tailnet.Id, aclPolicy)
27+
28+
testNode := s.NewTailscaleNode()
29+
require.NoError(t, testNode.Up(
30+
s.CreateAuthKey(tailnet.Id, true, "tag:test-route"),
31+
tsn.WithAdvertiseTags("tag:test-route"),
32+
tsn.WithAdvertiseRoutes([]string{
33+
route1.String(),
34+
route2.String()},
35+
),
36+
))
37+
38+
require.NoError(t, testNode.WaitFor(tsn.HasTailnet(tailnet.Name)))
39+
40+
mid, err := s.FindMachine(tailnet.Id, testNode.Hostname())
41+
require.NoError(t, err)
42+
43+
machineRoutes := s.GetMachineRoutes(mid)
44+
require.NoError(t, err)
45+
46+
require.Equal(t, []string{route1.String(), route2.String()}, machineRoutes.AdvertisedRoutes)
47+
require.Equal(t, []string{route1.String()}, machineRoutes.EnabledRoutes)
48+
49+
require.NoError(t, testNode.Check(tsn.HasAllowedIP(route1)))
50+
require.NoError(t, testNode.Check(tsn.IsMissingAllowedIP(route2)))
51+
})
52+
}
53+
54+
func TestAdvertiseRoutesAutoApprovedOnExistingNode(t *testing.T) {
55+
route1 := netip.MustParsePrefix("10.1.0.0/24")
56+
route2 := netip.MustParsePrefix("10.2.0.0/24")
57+
route3 := netip.MustParsePrefix("10.3.0.0/24")
58+
59+
sc.Run(t, func(s *sc.Scenario) {
60+
aclPolicy := defaults.DefaultACLPolicy()
61+
aclPolicy.AutoApprovers = &ionscale.ACLAutoApprovers{
62+
Routes: map[string][]string{
63+
route1.String(): {"tag:test-route"},
64+
route3.String(): {"tag:test-route"},
65+
},
66+
}
67+
68+
tailnet := s.CreateTailnet()
69+
s.SetACLPolicy(tailnet.Id, aclPolicy)
70+
71+
testNode := s.NewTailscaleNode()
72+
require.NoError(t, testNode.Up(
73+
s.CreateAuthKey(tailnet.Id, true, "tag:test-route"),
74+
tsn.WithAdvertiseTags("tag:test-route"),
75+
))
76+
77+
require.NoError(t, testNode.Check(tsn.HasTailnet(tailnet.Name)))
78+
79+
testNode.Set(tsn.WithAdvertiseRoutes([]string{
80+
route3.String(),
81+
route1.String(),
82+
route2.String()},
83+
))
84+
85+
require.NoError(t, testNode.WaitFor(tsn.HasAllowedIP(route1)))
86+
require.NoError(t, testNode.WaitFor(tsn.HasAllowedIP(route3)))
87+
require.NoError(t, testNode.WaitFor(tsn.IsMissingAllowedIP(route2)))
88+
89+
mid, err := s.FindMachine(tailnet.Id, testNode.Hostname())
90+
require.NoError(t, err)
91+
92+
machineRoutes := s.GetMachineRoutes(mid)
93+
require.NoError(t, err)
94+
95+
require.Equal(t, []string{route1.String(), route2.String(), route3.String()}, machineRoutes.AdvertisedRoutes)
96+
require.Equal(t, []string{route1.String(), route3.String()}, machineRoutes.EnabledRoutes)
97+
})
98+
}
99+
100+
func TestAdvertiseRemoveRoutesAutoApprovedOnExistingNode(t *testing.T) {
101+
route1 := netip.MustParsePrefix("10.1.0.0/24")
102+
route2 := netip.MustParsePrefix("10.2.0.0/24")
103+
104+
sc.Run(t, func(s *sc.Scenario) {
105+
aclPolicy := defaults.DefaultACLPolicy()
106+
aclPolicy.AutoApprovers = &ionscale.ACLAutoApprovers{
107+
Routes: map[string][]string{
108+
route1.String(): {"tag:test-route"},
109+
route2.String(): {"tag:test-route"},
110+
},
111+
}
112+
113+
tailnet := s.CreateTailnet()
114+
s.SetACLPolicy(tailnet.Id, aclPolicy)
115+
116+
testNode := s.NewTailscaleNode()
117+
require.NoError(t, testNode.Up(
118+
s.CreateAuthKey(tailnet.Id, true, "tag:test-route"),
119+
tsn.WithAdvertiseTags("tag:test-route"),
120+
tsn.WithAdvertiseRoutes([]string{
121+
route1.String(),
122+
route2.String()},
123+
),
124+
))
125+
126+
require.NoError(t, testNode.WaitFor(tsn.HasTailnet(tailnet.Name)))
127+
require.NoError(t, testNode.Check(tsn.HasAllowedIP(route1)))
128+
require.NoError(t, testNode.Check(tsn.HasAllowedIP(route2)))
129+
130+
testNode.Set(tsn.WithAdvertiseRoutes([]string{
131+
route1.String(),
132+
}))
133+
134+
require.NoError(t, testNode.WaitFor(tsn.IsMissingAllowedIP(route2)))
135+
})
136+
}

tests/sc/scenario.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ func (s *Scenario) EnableMachineAutorization(tailnetID uint64) {
117117
require.NoError(s.t, err)
118118
}
119119

120+
func (s *Scenario) GetMachineRoutes(machineID uint64) *api.MachineRoutes {
121+
routes, err := s.ionscaleClient.GetMachineRoutes(context.Background(), connect.NewRequest(&api.GetMachineRoutesRequest{MachineId: machineID}))
122+
require.NoError(s.t, err)
123+
return routes.Msg.Routes
124+
}
125+
120126
func (s *Scenario) PushOIDCUser(sub, email, preferredUsername string) {
121127
_, err := s.mockoidcClient.PushUser(context.Background(), connect.NewRequest(&mockoidcv1.PushUserRequest{Subject: sub, Email: email, PreferredUsername: preferredUsername}))
122128
require.NoError(s.t, err)

tests/tsn/conditions.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package tsn
22

33
import (
4+
"net/netip"
45
"slices"
56
"strings"
67
"tailscale.com/ipn/ipnstate"
@@ -60,6 +61,24 @@ func HasUser(email string) Condition {
6061
}
6162
}
6263

64+
func HasAllowedIP(route netip.Prefix) Condition {
65+
return func(status *ipnstate.Status) bool {
66+
if status.Self == nil || status.Self.AllowedIPs.Len() == 0 {
67+
return false
68+
}
69+
return slices.Contains(status.Self.AllowedIPs.AsSlice(), route)
70+
}
71+
}
72+
73+
func IsMissingAllowedIP(route netip.Prefix) Condition {
74+
return func(status *ipnstate.Status) bool {
75+
if status.Self == nil || status.Self.AllowedIPs.Len() == 0 {
76+
return true
77+
}
78+
return !slices.Contains(status.Self.AllowedIPs.AsSlice(), route)
79+
}
80+
}
81+
6382
func PeerCount(expected int) Condition {
6483
return func(status *ipnstate.Status) bool {
6584
return len(status.Peers()) == expected

tests/tsn/node.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,25 @@ func (t *TailscaleNode) Hostname() string {
3838
return t.hostname
3939
}
4040

41-
func (t *TailscaleNode) Up(authkey string) error {
42-
t.mustExecTailscaleCmd("up", "--login-server", t.loginServer, "--authkey", authkey)
41+
func (t *TailscaleNode) Up(authkey string, flags ...UpFlag) error {
42+
cmd := []string{"up", "--login-server", t.loginServer, "--authkey", authkey}
43+
for _, f := range flags {
44+
cmd = append(cmd, f...)
45+
}
46+
47+
t.mustExecTailscaleCmd(cmd...)
4348
return t.WaitFor(Connected())
4449
}
4550

51+
func (t *TailscaleNode) Set(flags ...UpFlag) string {
52+
cmd := []string{"set"}
53+
for _, f := range flags {
54+
cmd = append(cmd, f...)
55+
}
56+
57+
return t.mustExecTailscaleCmd(cmd...)
58+
}
59+
4660
func (t *TailscaleNode) LoginWithOidc(flags ...UpFlag) (int, error) {
4761
check := func(stdout, stderr string) bool {
4862
return strings.Contains(stderr, "To authenticate, visit:")

tests/tsn/opts.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
package tsn
22

3+
import "strings"
4+
35
type UpFlag = []string
46

57
func WithAdvertiseTags(tags string) UpFlag {
68
return []string{"--advertise-tags", tags}
79
}
10+
11+
func WithAdvertiseRoutes(routes []string) UpFlag {
12+
return []string{"--advertise-routes", strings.Join(routes, ",")}
13+
}

0 commit comments

Comments
 (0)