mirror of
https://github.com/tailscale/tailscale.git
synced 2025-01-05 23:07:44 +00:00
syspolicy: add exit node related policies (#10172)
Adds policy keys ExitNodeID and ExitNodeIP. Uses the policy keys to determine the exit node in preferences. Fixes tailscale/corp#15683 Signed-off-by: Claire Wang <claire@tailscale.com>
This commit is contained in:
parent
ecd1ccb917
commit
8af503b0c5
@ -530,7 +530,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from github.com/klauspost/compress/zstd+
|
||||
runtime/pprof from tailscale.com/ipn/ipnlocal+
|
||||
runtime/trace from net/http/pprof
|
||||
runtime/trace from net/http/pprof+
|
||||
slices from tailscale.com/wgengine/magicsock+
|
||||
sort from compress/flate+
|
||||
strconv from compress/flate+
|
||||
@ -538,6 +538,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
sync from compress/flate+
|
||||
sync/atomic from context+
|
||||
syscall from crypto/rand+
|
||||
testing from tailscale.com/util/syspolicy
|
||||
text/tabwriter from runtime/pprof
|
||||
text/template from html/template
|
||||
text/template/parse from html/template+
|
||||
|
@ -88,6 +88,7 @@
|
||||
"tailscale.com/util/osshare"
|
||||
"tailscale.com/util/rands"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/syspolicy"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/util/testenv"
|
||||
"tailscale.com/util/uniq"
|
||||
@ -1311,6 +1312,24 @@ func (b *LocalBackend) updateNetmapDeltaLocked(muts []netmap.NodeMutation) (hand
|
||||
// setExitNodeID updates prefs to reference an exit node by ID, rather
|
||||
// than by IP. It returns whether prefs was mutated.
|
||||
func setExitNodeID(prefs *ipn.Prefs, nm *netmap.NetworkMap) (prefsChanged bool) {
|
||||
if exitNodeIDStr, _ := syspolicy.GetString(syspolicy.ExitNodeID, ""); exitNodeIDStr != "" {
|
||||
exitNodeID := tailcfg.StableNodeID(exitNodeIDStr)
|
||||
changed := prefs.ExitNodeID != exitNodeID || prefs.ExitNodeIP.IsValid()
|
||||
prefs.ExitNodeID = exitNodeID
|
||||
prefs.ExitNodeIP = netip.Addr{}
|
||||
return changed
|
||||
}
|
||||
|
||||
oldExitNodeID := prefs.ExitNodeID
|
||||
if exitNodeIPStr, _ := syspolicy.GetString(syspolicy.ExitNodeIP, ""); exitNodeIPStr != "" {
|
||||
exitNodeIP, err := netip.ParseAddr(exitNodeIPStr)
|
||||
if exitNodeIP.IsValid() && err == nil {
|
||||
prefsChanged = prefs.ExitNodeID != "" || prefs.ExitNodeIP != exitNodeIP
|
||||
prefs.ExitNodeID = ""
|
||||
prefs.ExitNodeIP = exitNodeIP
|
||||
}
|
||||
}
|
||||
|
||||
if nm == nil {
|
||||
// No netmap, can't resolve anything.
|
||||
return false
|
||||
@ -1338,7 +1357,7 @@ func setExitNodeID(prefs *ipn.Prefs, nm *netmap.NetworkMap) (prefsChanged bool)
|
||||
// reference it directly for next time.
|
||||
prefs.ExitNodeID = peer.StableID()
|
||||
prefs.ExitNodeIP = netip.Addr{}
|
||||
return true
|
||||
return oldExitNodeID != prefs.ExitNodeID
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,7 @@
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/must"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/syspolicy"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
@ -1330,3 +1331,272 @@ func (rc *routeCollector) AdvertiseRoute(pfx netip.Prefix) error {
|
||||
rc.routes = append(rc.routes, pfx)
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockSyspolicyHandler struct {
|
||||
t *testing.T
|
||||
key syspolicy.Key
|
||||
s string
|
||||
exitNodeIDKey bool
|
||||
exitNodeIPKey bool
|
||||
exitNodeID string
|
||||
exitNodeIP string
|
||||
}
|
||||
|
||||
func (h *mockSyspolicyHandler) ReadString(key string) (string, error) {
|
||||
if h.exitNodeIDKey && key == string(syspolicy.ExitNodeID) {
|
||||
return h.exitNodeID, nil
|
||||
}
|
||||
if h.exitNodeIPKey && key == string(syspolicy.ExitNodeIP) {
|
||||
return h.exitNodeIP, nil
|
||||
}
|
||||
return "", syspolicy.ErrNoSuchKey
|
||||
}
|
||||
|
||||
func (h *mockSyspolicyHandler) ReadUInt64(key string) (uint64, error) {
|
||||
h.t.Errorf("ReadUInt64(%q) unexpectedly called", key)
|
||||
return 0, syspolicy.ErrNoSuchKey
|
||||
}
|
||||
|
||||
func (h *mockSyspolicyHandler) ReadBoolean(key string) (bool, error) {
|
||||
h.t.Errorf("ReadBoolean(%q) unexpectedly called", key)
|
||||
return false, syspolicy.ErrNoSuchKey
|
||||
}
|
||||
|
||||
func TestSetExitNodeIDPolicy(t *testing.T) {
|
||||
pfx := netip.MustParsePrefix
|
||||
tests := []struct {
|
||||
name string
|
||||
exitNodeIPKey bool
|
||||
exitNodeIDKey bool
|
||||
exitNodeID string
|
||||
exitNodeIP string
|
||||
prefs *ipn.Prefs
|
||||
exitNodeIPWant string
|
||||
exitNodeIDWant string
|
||||
prefsChanged bool
|
||||
nm *netmap.NetworkMap
|
||||
}{
|
||||
{
|
||||
name: "ExitNodeID key is set",
|
||||
exitNodeIDKey: true,
|
||||
exitNodeID: "123",
|
||||
exitNodeIDWant: "123",
|
||||
prefsChanged: true,
|
||||
},
|
||||
{
|
||||
name: "ExitNodeID key not set",
|
||||
exitNodeIDKey: true,
|
||||
exitNodeIDWant: "",
|
||||
prefsChanged: false,
|
||||
},
|
||||
{
|
||||
name: "ExitNodeID key set, ExitNodeIP preference set",
|
||||
exitNodeIDKey: true,
|
||||
exitNodeID: "123",
|
||||
prefs: &ipn.Prefs{ExitNodeIP: netip.MustParseAddr("127.0.0.1")},
|
||||
exitNodeIDWant: "123",
|
||||
prefsChanged: true,
|
||||
},
|
||||
{
|
||||
name: "ExitNodeID key not set, ExitNodeIP key set",
|
||||
exitNodeIPKey: true,
|
||||
exitNodeIP: "127.0.0.1",
|
||||
prefs: &ipn.Prefs{ExitNodeIP: netip.MustParseAddr("127.0.0.1")},
|
||||
exitNodeIPWant: "127.0.0.1",
|
||||
prefsChanged: false,
|
||||
},
|
||||
{
|
||||
name: "ExitNodeIP key set, existing ExitNodeIP pref",
|
||||
exitNodeIPKey: true,
|
||||
exitNodeIP: "127.0.0.1",
|
||||
prefs: &ipn.Prefs{ExitNodeIP: netip.MustParseAddr("127.0.0.1")},
|
||||
exitNodeIPWant: "127.0.0.1",
|
||||
prefsChanged: false,
|
||||
},
|
||||
{
|
||||
name: "existing preferences match policy",
|
||||
exitNodeIDKey: true,
|
||||
exitNodeID: "123",
|
||||
prefs: &ipn.Prefs{ExitNodeID: tailcfg.StableNodeID("123")},
|
||||
exitNodeIDWant: "123",
|
||||
prefsChanged: false,
|
||||
},
|
||||
{
|
||||
name: "ExitNodeIP set if net map does not have corresponding node",
|
||||
exitNodeIPKey: true,
|
||||
prefs: &ipn.Prefs{ExitNodeIP: netip.MustParseAddr("127.0.0.1")},
|
||||
exitNodeIP: "127.0.0.1",
|
||||
exitNodeIPWant: "127.0.0.1",
|
||||
prefsChanged: false,
|
||||
nm: &netmap.NetworkMap{
|
||||
Name: "foo.tailnet",
|
||||
SelfNode: (&tailcfg.Node{
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100.102.103.104/32"),
|
||||
pfx("100::123/128"),
|
||||
},
|
||||
}).View(),
|
||||
Peers: []tailcfg.NodeView{
|
||||
(&tailcfg.Node{
|
||||
Name: "a.tailnet",
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100.0.0.201/32"),
|
||||
pfx("100::201/128"),
|
||||
},
|
||||
}).View(),
|
||||
(&tailcfg.Node{
|
||||
Name: "b.tailnet",
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100::202/128"),
|
||||
},
|
||||
}).View(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ExitNodeIP cleared if net map has corresponding node - policy matches prefs",
|
||||
prefs: &ipn.Prefs{ExitNodeIP: netip.MustParseAddr("127.0.0.1")},
|
||||
exitNodeIPKey: true,
|
||||
exitNodeIP: "127.0.0.1",
|
||||
exitNodeIPWant: "",
|
||||
exitNodeIDWant: "123",
|
||||
prefsChanged: true,
|
||||
nm: &netmap.NetworkMap{
|
||||
Name: "foo.tailnet",
|
||||
SelfNode: (&tailcfg.Node{
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100.102.103.104/32"),
|
||||
pfx("100::123/128"),
|
||||
},
|
||||
}).View(),
|
||||
Peers: []tailcfg.NodeView{
|
||||
(&tailcfg.Node{
|
||||
Name: "a.tailnet",
|
||||
StableID: tailcfg.StableNodeID("123"),
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("127.0.0.1/32"),
|
||||
pfx("100::201/128"),
|
||||
},
|
||||
}).View(),
|
||||
(&tailcfg.Node{
|
||||
Name: "b.tailnet",
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100::202/128"),
|
||||
},
|
||||
}).View(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ExitNodeIP cleared if net map has corresponding node - no policy set",
|
||||
prefs: &ipn.Prefs{ExitNodeIP: netip.MustParseAddr("127.0.0.1")},
|
||||
exitNodeIPWant: "",
|
||||
exitNodeIDWant: "123",
|
||||
prefsChanged: true,
|
||||
nm: &netmap.NetworkMap{
|
||||
Name: "foo.tailnet",
|
||||
SelfNode: (&tailcfg.Node{
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100.102.103.104/32"),
|
||||
pfx("100::123/128"),
|
||||
},
|
||||
}).View(),
|
||||
Peers: []tailcfg.NodeView{
|
||||
(&tailcfg.Node{
|
||||
Name: "a.tailnet",
|
||||
StableID: tailcfg.StableNodeID("123"),
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("127.0.0.1/32"),
|
||||
pfx("100::201/128"),
|
||||
},
|
||||
}).View(),
|
||||
(&tailcfg.Node{
|
||||
Name: "b.tailnet",
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100::202/128"),
|
||||
},
|
||||
}).View(),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ExitNodeIP cleared if net map has corresponding node - different exit node IP in policy",
|
||||
exitNodeIPKey: true,
|
||||
prefs: &ipn.Prefs{ExitNodeIP: netip.MustParseAddr("127.0.0.1")},
|
||||
exitNodeIP: "100.64.5.6",
|
||||
exitNodeIPWant: "",
|
||||
exitNodeIDWant: "123",
|
||||
prefsChanged: true,
|
||||
nm: &netmap.NetworkMap{
|
||||
Name: "foo.tailnet",
|
||||
SelfNode: (&tailcfg.Node{
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100.102.103.104/32"),
|
||||
pfx("100::123/128"),
|
||||
},
|
||||
}).View(),
|
||||
Peers: []tailcfg.NodeView{
|
||||
(&tailcfg.Node{
|
||||
Name: "a.tailnet",
|
||||
StableID: tailcfg.StableNodeID("123"),
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100.64.5.6/32"),
|
||||
pfx("100::201/128"),
|
||||
},
|
||||
}).View(),
|
||||
(&tailcfg.Node{
|
||||
Name: "b.tailnet",
|
||||
Addresses: []netip.Prefix{
|
||||
pfx("100::202/128"),
|
||||
},
|
||||
}).View(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
b := newTestBackend(t)
|
||||
syspolicy.SetHandlerForTest(t, &mockSyspolicyHandler{
|
||||
t: t,
|
||||
exitNodeID: test.exitNodeID,
|
||||
exitNodeIP: test.exitNodeIP,
|
||||
exitNodeIDKey: test.exitNodeIDKey,
|
||||
exitNodeIPKey: test.exitNodeIPKey,
|
||||
})
|
||||
if test.nm == nil {
|
||||
test.nm = new(netmap.NetworkMap)
|
||||
}
|
||||
if test.prefs == nil {
|
||||
test.prefs = ipn.NewPrefs()
|
||||
}
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
pm.prefs = test.prefs.View()
|
||||
b.netMap = test.nm
|
||||
b.pm = pm
|
||||
changed := setExitNodeID(b.pm.prefs.AsStruct(), test.nm)
|
||||
b.SetPrefs(pm.CurrentPrefs().AsStruct())
|
||||
if test.exitNodeIDKey {
|
||||
got := b.pm.prefs.ExitNodeID()
|
||||
if got != tailcfg.StableNodeID(test.exitNodeIDWant) {
|
||||
t.Errorf("got %v want %v", got, test.exitNodeIDWant)
|
||||
}
|
||||
}
|
||||
if test.exitNodeIPKey {
|
||||
got := b.pm.prefs.ExitNodeIP()
|
||||
if test.exitNodeIPWant == "" {
|
||||
if got.String() != "invalid IP" {
|
||||
t.Errorf("got %v want invalid IP", got)
|
||||
}
|
||||
} else if got.String() != test.exitNodeIPWant {
|
||||
t.Errorf("got %v want %v", got, test.exitNodeIPWant)
|
||||
}
|
||||
}
|
||||
|
||||
if changed != test.prefsChanged {
|
||||
t.Errorf("wanted prefs changed %v, got prefs changed %v", test.prefsChanged, changed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@
|
||||
import (
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var (
|
||||
@ -56,3 +57,10 @@ func RegisterHandler(h Handler) {
|
||||
panic("handler was already used before registration")
|
||||
}
|
||||
}
|
||||
|
||||
func SetHandlerForTest(tb testing.TB, h Handler) {
|
||||
tb.Helper()
|
||||
oldHandler := handler
|
||||
handler = h
|
||||
tb.Cleanup(func() { handler = oldHandler })
|
||||
}
|
||||
|
@ -10,6 +10,11 @@
|
||||
ControlURL Key = "LoginURL" // default ""; if blank, ipn uses ipn.DefaultControlURL.
|
||||
LogTarget Key = "LogTarget" // default ""; if blank logging uses logtail.DefaultHost.
|
||||
Tailnet Key = "Tailnet" // default ""; if blank, no tailnet name is sent to the server.
|
||||
// ExitNodeID is the exit node's node id. default ""; if blank, no exit node is forced.
|
||||
// Exit node ID takes precedence over exit node IP.
|
||||
// To find the node ID, go to /api.md#device.
|
||||
ExitNodeID Key = "ExitNodeID"
|
||||
ExitNodeIP Key = "ExitNodeIP" // default ""; if blank, no exit node is forced. Value is exit node IP.
|
||||
|
||||
// Keys with a string value that specifies an option: "always", "never", "user-decides".
|
||||
// The default is "user-decides" unless otherwise stated.
|
||||
|
@ -24,13 +24,6 @@ type testHandler struct {
|
||||
|
||||
var someOtherError = errors.New("error other than not found")
|
||||
|
||||
func setHandlerForTest(tb testing.TB, h Handler) {
|
||||
tb.Helper()
|
||||
oldHandler := handler
|
||||
handler = h
|
||||
tb.Cleanup(func() { handler = oldHandler })
|
||||
}
|
||||
|
||||
func (th *testHandler) ReadString(key string) (string, error) {
|
||||
if key != string(th.key) {
|
||||
th.t.Errorf("ReadString(%q) want %q", key, th.key)
|
||||
@ -95,7 +88,7 @@ func TestGetString(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setHandlerForTest(t, &testHandler{
|
||||
SetHandlerForTest(t, &testHandler{
|
||||
t: t,
|
||||
key: tt.key,
|
||||
s: tt.handlerValue,
|
||||
@ -152,7 +145,7 @@ func TestGetUint64(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setHandlerForTest(t, &testHandler{
|
||||
SetHandlerForTest(t, &testHandler{
|
||||
t: t,
|
||||
key: tt.key,
|
||||
u64: tt.handlerValue,
|
||||
@ -204,7 +197,7 @@ func TestGetBoolean(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setHandlerForTest(t, &testHandler{
|
||||
SetHandlerForTest(t, &testHandler{
|
||||
t: t,
|
||||
key: tt.key,
|
||||
b: tt.handlerValue,
|
||||
@ -265,7 +258,7 @@ func TestGetPreferenceOption(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setHandlerForTest(t, &testHandler{
|
||||
SetHandlerForTest(t, &testHandler{
|
||||
t: t,
|
||||
key: tt.key,
|
||||
s: tt.handlerValue,
|
||||
@ -322,7 +315,7 @@ func TestGetVisibility(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setHandlerForTest(t, &testHandler{
|
||||
SetHandlerForTest(t, &testHandler{
|
||||
t: t,
|
||||
key: tt.key,
|
||||
s: tt.handlerValue,
|
||||
@ -389,7 +382,7 @@ func TestGetDuration(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
setHandlerForTest(t, &testHandler{
|
||||
SetHandlerForTest(t, &testHandler{
|
||||
t: t,
|
||||
key: tt.key,
|
||||
s: tt.handlerValue,
|
||||
|
Loading…
x
Reference in New Issue
Block a user