From 00699abdfbbe0972daa71290203878d828bf4037 Mon Sep 17 00:00:00 2001 From: Simon Law Date: Fri, 7 Aug 2026 16:30:35 -0700 Subject: [PATCH] tailcfg,tailcfg/{nodecap,selfcap}: split capability constants to their own packages (#20639) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Package tailcfg defines the types and constants used by the Tailscale protocol, but since everything is all in one package, it’s difficult to sift through the docs: https://pkg.go.dev/tailscale.com/tailcfg We define and enumerate capabilities as string constants for tailcfg.NodeCapability and tailcfg.PeerCapability. This PR extracts them into their own packages: - tailcfg.CapabilityFileSharing becomes nodecap.FileSharing - tailcfg.NodeAttrOnlyTCP443 becomes nodecap.OnlyTCP443 - tailcfg.PeerCapabilityTaildrive becomes peercap.Taildrive We originally intended for CapabilityFoo to grant an entitlement or permission for Foo, and for NodeAttrBar to configure Bar in the nodeAttrs section of the policy file. However, there was no technical enforcement of this convention, so new capabilities have used the NodeAttr prefix regardless of meaning. Therefore, this PR unifies tailcfg.CapabilityFoo and tailcfg.NodeAttrBar into a single package as nodecap.Foo and nodecap.Bar. Ran `go fix -inline ./...` and committed the changes that replaced uses of the tailcfg aliases with the authoritative ones. Updates #20259 Change-Id: Ieb7e7e6c8247c39faf42fdf15c68cdc7c621c730 Signed-off-by: Simon Law --- appc/conn25.go | 3 +- appc/conn25_test.go | 5 +- client/systray/systray.go | 3 +- client/web/auth.go | 3 +- client/web/web_test.go | 21 +- cmd/derper/depaware.txt | 2 + cmd/hello/helloserver/helloserver.go | 3 +- cmd/k8s-operator/depaware.txt | 2 + cmd/proxy-to-grafana/proxy-to-grafana.go | 3 +- cmd/stund/depaware.txt | 2 + cmd/tailscale/cli/exitnode.go | 3 +- cmd/tailscale/cli/funnel.go | 4 +- cmd/tailscale/cli/serve_legacy.go | 32 +- cmd/tailscale/cli/serve_legacy_test.go | 11 +- cmd/tailscale/cli/serve_v2.go | 14 +- cmd/tailscale/cli/serve_v2_test.go | 40 +- cmd/tailscale/depaware.txt | 2 + cmd/tailscaled/depaware-min.txt | 2 + cmd/tailscaled/depaware-minbox.txt | 2 + cmd/tailscaled/depaware.txt | 2 + cmd/tsidp/depaware.txt | 2 + cmd/tsidp/tsidp.go | 5 +- cmd/tsidp/tsidp_test.go | 15 +- control/controlclient/direct.go | 5 +- control/controlclient/map.go | 5 +- control/controlclient/map_test.go | 25 +- control/controlknobs/controlknobs.go | 57 +- feature/acme/cert.go | 14 +- feature/acme/cert_test.go | 5 +- feature/conn25/conn25_test.go | 13 +- feature/relayserver/relayserver.go | 3 +- feature/routecheck/routertracker_test.go | 5 +- feature/taildrop/ext.go | 8 +- feature/taildrop/peerapi.go | 4 +- feature/taildrop/peerapi_test.go | 3 +- feature/wakeonlan/wakeonlan.go | 3 +- ipn/ipn_clone.go | 3 +- ipn/ipn_view.go | 5 +- ipn/ipnext/ipnext.go | 3 +- ipn/ipnlocal/dnsconfig_test.go | 9 +- ipn/ipnlocal/drive.go | 4 +- ipn/ipnlocal/drive_test.go | 10 +- ipn/ipnlocal/drive_tomove.go | 6 +- ipn/ipnlocal/local.go | 59 +- ipn/ipnlocal/local_test.go | 59 +- ipn/ipnlocal/netmapcache/netmapcache.go | 3 +- ipn/ipnlocal/netmapcache/netmapcache_test.go | 11 +- ipn/ipnlocal/node_backend.go | 22 +- ipn/ipnlocal/node_backend_test.go | 7 +- ipn/ipnlocal/peerapi.go | 10 +- ipn/ipnlocal/peerapi_drive.go | 4 +- ipn/ipnlocal/peerapi_test.go | 3 +- ipn/ipnlocal/serve.go | 5 +- ipn/ipnlocal/serve_test.go | 6 +- ipn/ipnstate/ipnstate.go | 5 +- ipn/localapi/localapi_test.go | 3 +- ipn/serve.go | 14 +- ipn/serve_test.go | 21 +- k8s-operator/api-proxy/proxy.go | 7 +- k8s-operator/api-proxy/proxy_events_test.go | 5 +- k8s-operator/api-proxy/proxy_test.go | 19 +- net/dns/resolver/forwarder.go | 2 +- net/routecheck/routecheck.go | 5 +- ssh/tailssh/incubator.go | 6 +- tailcfg/nodecap/nodecap.go | 442 ++++++++++++ tailcfg/peercap/peercap.go | 56 ++ tailcfg/tailcfg.go | 629 ++++-------------- tailcfg/tailcfg_clone.go | 5 +- tailcfg/tailcfg_test.go | 9 +- tailcfg/tailcfg_view.go | 9 +- tsnet/depaware.txt | 2 + tsnet/tailnetlock_test.go | 3 +- tsnet/tsnet.go | 9 +- tsnet/tsnet_test.go | 9 +- tstest/integration/integration_test.go | 5 +- tstest/integration/testcontrol/testcontrol.go | 22 +- tstest/largetailnet/largetailnet.go | 3 +- tstest/natlab/vmtest/connectivity_test.go | 3 +- tstest/natlab/vmtest/vmtest_test.go | 13 +- types/netmap/netmap.go | 11 +- util/deephash/deephash_test.go | 3 +- wgengine/filter/filter_test.go | 30 +- wgengine/filter/filtertype/filtertype.go | 6 +- .../filter/filtertype/filtertype_clone.go | 6 +- wgengine/filter/match.go | 4 +- wgengine/filter/tailcfg.go | 8 +- wgengine/magicsock/magicsock.go | 14 +- wgengine/magicsock/magicsock_test.go | 46 +- 88 files changed, 1121 insertions(+), 873 deletions(-) create mode 100644 tailcfg/nodecap/nodecap.go create mode 100644 tailcfg/peercap/peercap.go diff --git a/appc/conn25.go b/appc/conn25.go index e4c999a7a..33a9552bb 100644 --- a/appc/conn25.go +++ b/appc/conn25.go @@ -11,6 +11,7 @@ "tailscale.com/ipn/ipnext" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/appctype" "tailscale.com/types/dnstype" "tailscale.com/util/set" @@ -60,7 +61,7 @@ func PickConnector(nb ipnext.NodeBackend, app appctype.Conn25Attr) []tailcfg.Nod // time. const DNSAddrScheme = "tailscale-app" -func AppDNSRoutes(hasCap func(c tailcfg.NodeCapability) bool, self tailcfg.NodeView) map[string][]*dnstype.Resolver { +func AppDNSRoutes(hasCap func(c nodecap.Cap) bool, self tailcfg.NodeView) map[string][]*dnstype.Resolver { if !hasCap(AppConnectorsExperimentalAttrName) { return nil } diff --git a/appc/conn25_test.go b/appc/conn25_test.go index 7194a67fe..1cca32950 100644 --- a/appc/conn25_test.go +++ b/appc/conn25_test.go @@ -11,6 +11,7 @@ "github.com/google/go-cmp/cmp" "tailscale.com/ipn/ipnext" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/appctype" "tailscale.com/types/dnstype" "tailscale.com/types/opt" @@ -139,11 +140,11 @@ func TestAppDNSRoutes(t *testing.T) { selfNode := &tailcfg.Node{} if tt.config != nil { selfNode.CapMap = tailcfg.NodeCapMap{ - tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): tt.config, + nodecap.Cap(AppConnectorsExperimentalAttrName): tt.config, } } selfView := selfNode.View() - got := AppDNSRoutes(func(_ tailcfg.NodeCapability) bool { + got := AppDNSRoutes(func(_ nodecap.Cap) bool { return tt.hasCap }, selfView) if diff := cmp.Diff(tt.want, got); diff != "" { diff --git a/client/systray/systray.go b/client/systray/systray.go index 706cccc03..29cb89c98 100644 --- a/client/systray/systray.go +++ b/client/systray/systray.go @@ -33,6 +33,7 @@ "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/util/slicesx" "tailscale.com/util/stringsx" ) @@ -624,7 +625,7 @@ func (menu *Menu) rebuildExitNodeMenu(ctx context.Context) { setExitNodeOnClick(noExitNodeMenu, "") // Show recommended exit node if available. - if status.Self.CapMap.Contains(tailcfg.NodeAttrSuggestExitNodeUI) { + if status.Self.CapMap.Contains(nodecap.SuggestExitNodeUI) { sugg, err := menu.lc.SuggestExitNode(ctx) if err == nil { // Location is invalid for suggested exit nodes that have diff --git a/client/web/auth.go b/client/web/auth.go index 1281695a9..cc4c902da 100644 --- a/client/web/auth.go +++ b/client/web/auth.go @@ -18,6 +18,7 @@ "tailscale.com/client/tailscale/apitype" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" ) const ( @@ -335,7 +336,7 @@ func toPeerCapabilities(status *ipnstate.Status, whois *apitype.WhoIsResponse) ( // For tagged nodes, we actually look at the granted capabilities. caps := peerCapabilities{} - rules, err := tailcfg.UnmarshalCapJSON[capRule](whois.CapMap, tailcfg.PeerCapabilityWebUI) + rules, err := tailcfg.UnmarshalCapJSON[capRule](whois.CapMap, peercap.WebUI) if err != nil { return nil, fmt.Errorf("failed to unmarshal capability: %v", err) } diff --git a/client/web/web_test.go b/client/web/web_test.go index 51b6a8ac5..7bf2d8d2a 100644 --- a/client/web/web_test.go +++ b/client/web/web_test.go @@ -26,6 +26,7 @@ "tailscale.com/ipn/ipnstate" "tailscale.com/net/memnet" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/views" "tailscale.com/util/httpm" "tailscale.com/util/syspolicy/policyclient" @@ -105,7 +106,7 @@ func TestServeAPI(t *testing.T) { remoteIPWithAllCapabilities: { Node: &tailcfg.Node{StableID: "node1"}, UserProfile: remoteUser, - CapMap: tailcfg.PeerCapMap{tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{"{\"canEdit\":[\"*\"]}"}}, + CapMap: tailcfg.PeerCapMap{peercap.WebUI: []tailcfg.RawMessage{"{\"canEdit\":[\"*\"]}"}}, }, remoteIPWithNoCapabilities: { Node: &tailcfg.Node{StableID: "node2"}, @@ -1248,7 +1249,7 @@ func TestPeerCapabilities(t *testing.T) { UserProfile: &tailcfg.UserProfile{ID: tailcfg.UserID(2)}, Node: &tailcfg.Node{ID: tailcfg.NodeID(1)}, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{ + peercap.WebUI: []tailcfg.RawMessage{ "{\"canEdit\":[\"ssh\",\"subnets\"]}", }, }, @@ -1262,7 +1263,7 @@ func TestPeerCapabilities(t *testing.T) { UserProfile: &tailcfg.UserProfile{ID: tailcfg.UserID(1)}, Node: &tailcfg.Node{ID: tailcfg.NodeID(1)}, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{ + peercap.WebUI: []tailcfg.RawMessage{ "{\"canEdit\":[\"ssh\",\"subnets\"]}", }, }, @@ -1275,7 +1276,7 @@ func TestPeerCapabilities(t *testing.T) { whois: &apitype.WhoIsResponse{ Node: &tailcfg.Node{ID: tailcfg.NodeID(1)}, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityDebugPeer: []tailcfg.RawMessage{}, + peercap.DebugPeer: []tailcfg.RawMessage{}, }, }, wantCaps: peerCapabilities{}, @@ -1286,7 +1287,7 @@ func TestPeerCapabilities(t *testing.T) { whois: &apitype.WhoIsResponse{ Node: &tailcfg.Node{ID: tailcfg.NodeID(1)}, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{ + peercap.WebUI: []tailcfg.RawMessage{ "{\"canEdit\":[\"ssh\",\"subnets\"]}", }, }, @@ -1302,7 +1303,7 @@ func TestPeerCapabilities(t *testing.T) { whois: &apitype.WhoIsResponse{ Node: &tailcfg.Node{ID: tailcfg.NodeID(1)}, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{ + peercap.WebUI: []tailcfg.RawMessage{ "{\"canEdit\":[\"ssh\",\"subnets\"]}", "{\"canEdit\":[\"subnets\",\"exitnodes\",\"*\"]}", }, @@ -1321,7 +1322,7 @@ func TestPeerCapabilities(t *testing.T) { whois: &apitype.WhoIsResponse{ Node: &tailcfg.Node{ID: tailcfg.NodeID(1)}, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{ + peercap.WebUI: []tailcfg.RawMessage{ "{\"canEdit\":[\"SSH\",\"sUBnets\"]}", }, }, @@ -1337,7 +1338,7 @@ func TestPeerCapabilities(t *testing.T) { whois: &apitype.WhoIsResponse{ Node: &tailcfg.Node{ID: tailcfg.NodeID(1)}, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{ + peercap.WebUI: []tailcfg.RawMessage{ "{\"canEdit\":[\"unknown-feature\"]}", }, }, @@ -1350,7 +1351,7 @@ func TestPeerCapabilities(t *testing.T) { whois: &apitype.WhoIsResponse{ Node: &tailcfg.Node{ID: tailcfg.NodeID(1)}, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{ + peercap.WebUI: []tailcfg.RawMessage{ "{\"canDoSomething\":[\"*\"]}", }, }, @@ -1363,7 +1364,7 @@ func TestPeerCapabilities(t *testing.T) { whois: &apitype.WhoIsResponse{ Node: &tailcfg.Node{ID: tailcfg.NodeID(1), Tags: tags.AsSlice()}, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{ + peercap.WebUI: []tailcfg.RawMessage{ "{\"canEdit\":[\"ssh\",\"subnets\"]}", }, }, diff --git a/cmd/derper/depaware.txt b/cmd/derper/depaware.txt index 865dbab42..2e12aed9c 100644 --- a/cmd/derper/depaware.txt +++ b/cmd/derper/depaware.txt @@ -124,6 +124,8 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa 💣 tailscale.com/safesocket from tailscale.com/client/local tailscale.com/syncs from tailscale.com/cmd/derper+ tailscale.com/tailcfg from tailscale.com/client/local+ + tailscale.com/tailcfg/nodecap from tailscale.com/tailcfg+ + tailscale.com/tailcfg/peercap from tailscale.com/tailcfg+ tailscale.com/tempfork/acme from tailscale.com/cmd/derper tailscale.com/tka from tailscale.com/client/local+ tailscale.com/tsconst from tailscale.com/net/netmon+ diff --git a/cmd/hello/helloserver/helloserver.go b/cmd/hello/helloserver/helloserver.go index 41e7dbce2..62b000f0a 100644 --- a/cmd/hello/helloserver/helloserver.go +++ b/cmd/hello/helloserver/helloserver.go @@ -16,6 +16,7 @@ "tailscale.com/client/local" "tailscale.com/client/tailscale/apitype" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" ) //go:embed hello.tmpl.html @@ -94,7 +95,7 @@ func tailscaleIP(who *apitype.WhoIsResponse) string { if who == nil { return "" } - vals, err := tailcfg.UnmarshalNodeCapJSON[string](who.Node.CapMap, tailcfg.NodeAttrNativeIPV4) + vals, err := tailcfg.UnmarshalNodeCapJSON[string](who.Node.CapMap, nodecap.NativeIPV4) if err == nil && len(vals) > 0 { return vals[0] } diff --git a/cmd/k8s-operator/depaware.txt b/cmd/k8s-operator/depaware.txt index 3e5117f15..25d7a91fb 100644 --- a/cmd/k8s-operator/depaware.txt +++ b/cmd/k8s-operator/depaware.txt @@ -840,6 +840,8 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/ tailscale.com/sessionrecording from tailscale.com/k8s-operator/sessionrecording+ tailscale.com/syncs from tailscale.com/control/controlknobs+ tailscale.com/tailcfg from tailscale.com/client/local+ + tailscale.com/tailcfg/nodecap from tailscale.com/tailcfg+ + tailscale.com/tailcfg/peercap from tailscale.com/tailcfg+ tailscale.com/tempfork/acme from tailscale.com/feature/acme tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock tailscale.com/tempfork/httprec from tailscale.com/feature/c2n diff --git a/cmd/proxy-to-grafana/proxy-to-grafana.go b/cmd/proxy-to-grafana/proxy-to-grafana.go index 23f264059..573a640d9 100644 --- a/cmd/proxy-to-grafana/proxy-to-grafana.go +++ b/cmd/proxy-to-grafana/proxy-to-grafana.go @@ -55,6 +55,7 @@ "tailscale.com/client/tailscale/apitype" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/tsnet" ) @@ -67,7 +68,7 @@ ) // aclCap is the Tailscale ACL capability used to configure proxy-to-grafana. -const aclCap tailcfg.PeerCapability = "tailscale.com/cap/proxy-to-grafana" +const aclCap peercap.Cap = "tailscale.com/cap/proxy-to-grafana" // aclGrant is an access control rule that assigns Grafana permissions // while provisioning a user. diff --git a/cmd/stund/depaware.txt b/cmd/stund/depaware.txt index 50538b6c3..65e5361f5 100644 --- a/cmd/stund/depaware.txt +++ b/cmd/stund/depaware.txt @@ -62,6 +62,8 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar tailscale.com/net/tsaddr from tailscale.com/tsweb tailscale.com/syncs from tailscale.com/metrics+ tailscale.com/tailcfg from tailscale.com/version+ + tailscale.com/tailcfg/nodecap from tailscale.com/tailcfg + tailscale.com/tailcfg/peercap from tailscale.com/tailcfg tailscale.com/tstime from tailscale.com/tsweb tailscale.com/tsweb from tailscale.com/cmd/stund+ tailscale.com/tsweb/promvarz from tailscale.com/cmd/stund diff --git a/cmd/tailscale/cli/exitnode.go b/cmd/tailscale/cli/exitnode.go index 8b8333121..65daa9ec9 100644 --- a/cmd/tailscale/cli/exitnode.go +++ b/cmd/tailscale/cli/exitnode.go @@ -19,6 +19,7 @@ "tailscale.com/feature/buildfeatures" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/util/slicesx" ) @@ -175,7 +176,7 @@ func runExitNodeSuggest(ctx context.Context, args []string) error { func hasAnyExitNodeSuggestions(peers []*ipnstate.PeerStatus) bool { for _, peer := range peers { - if peer.HasCap(tailcfg.NodeAttrSuggestExitNode) { + if peer.HasCap(nodecap.SuggestExitNode) { return true } } diff --git a/cmd/tailscale/cli/funnel.go b/cmd/tailscale/cli/funnel.go index f16f571e0..694f7bfaa 100644 --- a/cmd/tailscale/cli/funnel.go +++ b/cmd/tailscale/cli/funnel.go @@ -15,7 +15,7 @@ "github.com/peterbourgon/ff/v3/ffcli" "tailscale.com/ipn" - "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" ) func init() { @@ -137,7 +137,7 @@ func (e *serveEnv) runFunnel(ctx context.Context, args []string) error { // // verifyFunnelEnabled may refresh the local state and modify the st input. func (e *serveEnv) verifyFunnelEnabled(ctx context.Context, port uint16) error { - enableErr := e.enableFeatureInteractive(ctx, "funnel", tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel) + enableErr := e.enableFeatureInteractive(ctx, "funnel", nodecap.HTTPS, nodecap.Funnel) st, statusErr := e.getLocalClientStatusWithoutPeers(ctx) // get updated status; interactive flow may block switch { case statusErr != nil: diff --git a/cmd/tailscale/cli/serve_legacy.go b/cmd/tailscale/cli/serve_legacy.go index b8d9e2766..002ede4a1 100644 --- a/cmd/tailscale/cli/serve_legacy.go +++ b/cmd/tailscale/cli/serve_legacy.go @@ -29,6 +29,8 @@ "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/util/slicesx" "tailscale.com/version" ) @@ -163,19 +165,19 @@ type serveEnv struct { json bool // output JSON (status only for now) // v2 specific flags - bg bgBoolFlag // background mode - setPath string // serve path - https uint // HTTP port - http uint // HTTP port - tcp uint // TCP port - tlsTerminatedTCP uint // a TLS terminated TCP port - proxyProtocol uint // PROXY protocol version (1 or 2) - subcmd serveMode // subcommand - yes bool // update without prompt - service tailcfg.ServiceName // service name - tun bool // redirect traffic to OS for service - allServices bool // apply config file to all services - acceptAppCaps []tailcfg.PeerCapability // app capabilities to forward + bg bgBoolFlag // background mode + setPath string // serve path + https uint // HTTP port + http uint // HTTP port + tcp uint // TCP port + tlsTerminatedTCP uint // a TLS terminated TCP port + proxyProtocol uint // PROXY protocol version (1 or 2) + subcmd serveMode // subcommand + yes bool // update without prompt + service tailcfg.ServiceName // service name + tun bool // redirect traffic to OS for service + allServices bool // apply config file to all services + acceptAppCaps []peercap.Cap // app capabilities to forward lc localServeClient // localClient interface, specific to serve // optional stuff for tests: @@ -276,7 +278,7 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error { // on, enableFeatureInteractive will error. For now, we hide that // error and maintain the previous behavior (prior to 2023-08-15) // of letting them edit the serve config before enabling certs. - e.enableFeatureInteractive(ctx, "serve", tailcfg.CapabilityHTTPS) + e.enableFeatureInteractive(ctx, "serve", nodecap.HTTPS) } srcPort, err := parseServePort(srcPortStr) @@ -790,7 +792,7 @@ func parseServePort(s string) (uint16, error) { // // 2023-08-09: The only valid feature values are "serve" and "funnel". // This can be moved to some CLI lib when expanded past serve/funnel. -func (e *serveEnv) enableFeatureInteractive(ctx context.Context, feature string, caps ...tailcfg.NodeCapability) (err error) { +func (e *serveEnv) enableFeatureInteractive(ctx context.Context, feature string, caps ...nodecap.Cap) (err error) { st, err := e.getLocalClientStatusWithoutPeers(ctx) if err != nil { return fmt.Errorf("getting client status: %w", err) diff --git a/cmd/tailscale/cli/serve_legacy_test.go b/cmd/tailscale/cli/serve_legacy_test.go index 27cbb5712..a5b0f9ece 100644 --- a/cmd/tailscale/cli/serve_legacy_test.go +++ b/cmd/tailscale/cli/serve_legacy_test.go @@ -22,6 +22,7 @@ "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstest" "tailscale.com/types/logger" ) @@ -777,7 +778,7 @@ func TestVerifyFunnelEnabled(t *testing.T) { // queryFeatureResponse is the mock response desired from the // call made to lc.QueryFeature by verifyFunnelEnabled. queryFeatureResponse mockQueryFeatureResponse - caps []tailcfg.NodeCapability // optionally set at fakeStatus.Capabilities + caps []nodecap.Cap // optionally set at fakeStatus.Capabilities wantErr string wantPanic string }{ @@ -794,13 +795,13 @@ func TestVerifyFunnelEnabled(t *testing.T) { { name: "fallback-flow-missing-acl-rule", queryFeatureResponse: mockQueryFeatureResponse{resp: nil, err: errors.New("not-allowed")}, - caps: []tailcfg.NodeCapability{tailcfg.CapabilityHTTPS}, + caps: []nodecap.Cap{nodecap.HTTPS}, wantErr: `Funnel not available; "funnel" node attribute not set. See https://tailscale.com/s/no-funnel.`, }, { name: "fallback-flow-enabled", queryFeatureResponse: mockQueryFeatureResponse{resp: nil, err: errors.New("not-allowed")}, - caps: []tailcfg.NodeCapability{tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel, "https://tailscale.com/cap/funnel-ports?ports=80,443,8080-8090"}, + caps: []nodecap.Cap{nodecap.HTTPS, nodecap.Funnel, "https://tailscale.com/cap/funnel-ports?ports=80,443,8080-8090"}, wantErr: "", // no error, success }, { @@ -874,8 +875,8 @@ type fakeLocalServeClient struct { Self: &ipnstate.PeerStatus{ DNSName: "foo.test.ts.net", CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrFunnel: nil, - tailcfg.CapabilityFunnelPorts + "?ports=443,8443": nil, + nodecap.Funnel: nil, + nodecap.FunnelPorts + "?ports=443,8443": nil, }, }, CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"}, diff --git a/cmd/tailscale/cli/serve_v2.go b/cmd/tailscale/cli/serve_v2.go index 6a41f7156..eb2eef538 100644 --- a/cmd/tailscale/cli/serve_v2.go +++ b/cmd/tailscale/cli/serve_v2.go @@ -33,6 +33,8 @@ "tailscale.com/ipn/conffile" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/ipproto" "tailscale.com/util/dnsname" "tailscale.com/util/mak" @@ -100,7 +102,7 @@ func (b *bgBoolFlag) String() string { } type acceptAppCapsFlag struct { - Value *[]tailcfg.PeerCapability + Value *[]peercap.Cap } // An application capability name has the form {domain}/{name}. @@ -120,7 +122,7 @@ func (u *acceptAppCapsFlag) Set(s string) error { if !validAppCap.MatchString(appCap) { return fmt.Errorf("%q does not match the form {domain}/{name}, where domain must be a fully qualified domain name", appCap) } - *u.Value = append(*u.Value, tailcfg.PeerCapability(appCap)) + *u.Value = append(*u.Value, peercap.Cap(appCap)) } return nil } @@ -468,7 +470,7 @@ func (e *serveEnv) runServeCombined(subcmd serveMode) execFunc { // on, enableFeatureInteractive will error. For now, we hide that // error and maintain the previous behavior (prior to 2023-08-15) // of letting them edit the serve config before enabling certs. - if err := e.enableFeatureInteractive(ctx, "serve", tailcfg.CapabilityHTTPS); err != nil { + if err := e.enableFeatureInteractive(ctx, "serve", nodecap.HTTPS); err != nil { return fmt.Errorf("error enabling https feature: %w", err) } } @@ -990,7 +992,7 @@ func (e *serveEnv) runServeSetConfig(ctx context.Context, args []string) (err er return e.lc.SetServeConfig(ctx, sc) } -func (e *serveEnv) setServe(sc *ipn.ServeConfig, dnsName string, srvType serveType, srvPort uint16, mount string, target string, allowFunnel bool, mds string, caps []tailcfg.PeerCapability, proxyProtocol int) error { +func (e *serveEnv) setServe(sc *ipn.ServeConfig, dnsName string, srvType serveType, srvPort uint16, mount string, target string, allowFunnel bool, mds string, caps []peercap.Cap, proxyProtocol int) error { // update serve config based on the type switch srvType { case serveTypeHTTPS, serveTypeHTTP: @@ -1079,7 +1081,7 @@ func (e *serveEnv) messageForPort(sc *ipn.ServeConfig, st *ipnstate.Status, dnsN return "", "" } if forService { - serviceIPMaps, err := tailcfg.UnmarshalNodeCapJSON[tailcfg.ServiceIPMappings](st.Self.CapMap, tailcfg.NodeAttrServiceHost) + serviceIPMaps, err := tailcfg.UnmarshalNodeCapJSON[tailcfg.ServiceIPMappings](st.Self.CapMap, nodecap.ServiceHost) if err != nil || len(serviceIPMaps) == 0 || serviceIPMaps[0][svcName] == nil { // The capmap does not contain IPs for this service yet. Usually this means // the service hasn't been added to prefs and sent to control yet. @@ -1241,7 +1243,7 @@ func (e *serveEnv) shouldWarnRemoteDestCompatibility(ctx context.Context, target return nil } -func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort uint16, useTLS bool, mount, target, mds string, caps []tailcfg.PeerCapability) error { +func (e *serveEnv) applyWebServe(sc *ipn.ServeConfig, dnsName string, srvPort uint16, useTLS bool, mount, target, mds string, caps []peercap.Cap) error { h := new(ipn.HTTPHandler) switch { case strings.HasPrefix(target, "text:"): diff --git a/cmd/tailscale/cli/serve_v2_test.go b/cmd/tailscale/cli/serve_v2_test.go index b86c40139..37f29c02e 100644 --- a/cmd/tailscale/cli/serve_v2_test.go +++ b/cmd/tailscale/cli/serve_v2_test.go @@ -24,6 +24,8 @@ "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/views" ) @@ -886,8 +888,8 @@ type group struct { Self: &ipnstate.PeerStatus{ DNSName: "foo.test.ts.net", CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrFunnel: nil, - tailcfg.CapabilityFunnelPorts + "?ports=443,8443": nil, + nodecap.Funnel: nil, + nodecap.FunnelPorts + "?ports=443,8443": nil, }, Tags: ptrToReadOnlySlice([]string{"some-tag"}), }, @@ -931,7 +933,7 @@ type group struct { "foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{ "/": { Proxy: "http://127.0.0.1:3000", - AcceptAppCaps: []tailcfg.PeerCapability{"example.com/cap/foo"}, + AcceptAppCaps: []peercap.Cap{"example.com/cap/foo"}, }, }}, }, @@ -945,7 +947,7 @@ type group struct { "foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{ "/": { Proxy: "http://127.0.0.1:3000", - AcceptAppCaps: []tailcfg.PeerCapability{"example.com/cap/foo", "example.com/cap/bar"}, + AcceptAppCaps: []peercap.Cap{"example.com/cap/foo", "example.com/cap/bar"}, }, }}, }, @@ -959,7 +961,7 @@ type group struct { "foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{ "/": { Proxy: "http://127.0.0.1:3000", - AcceptAppCaps: []tailcfg.PeerCapability{"example.com/cap/bar"}, + AcceptAppCaps: []peercap.Cap{"example.com/cap/bar"}, }, }}, }, @@ -1189,37 +1191,37 @@ func TestAcceptSetAppCapsFlag(t *testing.T) { inputs []string expectErr bool expectErrToMatch *regexp.Regexp - expectedValue []tailcfg.PeerCapability + expectedValue []peercap.Cap }{ { name: "valid_simple", inputs: []string{"example.com/name"}, expectErr: false, - expectedValue: []tailcfg.PeerCapability{"example.com/name"}, + expectedValue: []peercap.Cap{"example.com/name"}, }, { name: "valid_unicode", inputs: []string{"bücher.de/something"}, expectErr: false, - expectedValue: []tailcfg.PeerCapability{"bücher.de/something"}, + expectedValue: []peercap.Cap{"bücher.de/something"}, }, { name: "more_valid_unicode", inputs: []string{"example.tw/某某某"}, expectErr: false, - expectedValue: []tailcfg.PeerCapability{"example.tw/某某某"}, + expectedValue: []peercap.Cap{"example.tw/某某某"}, }, { name: "valid_path_slashes", inputs: []string{"domain.com/path/to/name"}, expectErr: false, - expectedValue: []tailcfg.PeerCapability{"domain.com/path/to/name"}, + expectedValue: []peercap.Cap{"domain.com/path/to/name"}, }, { name: "valid_multiple_sets", inputs: []string{"one.com/foo,two.com/bar"}, expectErr: false, - expectedValue: []tailcfg.PeerCapability{"one.com/foo", "two.com/bar"}, + expectedValue: []peercap.Cap{"one.com/foo", "two.com/bar"}, }, { name: "valid_empty_string", @@ -1238,7 +1240,7 @@ func TestAcceptSetAppCapsFlag(t *testing.T) { name: "valid_subdomain", inputs: []string{"sub.domain.com/name"}, expectErr: false, - expectedValue: []tailcfg.PeerCapability{"sub.domain.com/name"}, + expectedValue: []peercap.Cap{"sub.domain.com/name"}, }, { name: "invalid_no_path", @@ -1259,13 +1261,13 @@ func TestAcceptSetAppCapsFlag(t *testing.T) { inputs: []string{"one.com/foo,bad/bar,two.com/baz"}, expectErr: true, expectErrToMatch: regexp.MustCompile(`"bad/bar"`), - expectedValue: []tailcfg.PeerCapability{"one.com/foo"}, // Parsing will stop after first error + expectedValue: []peercap.Cap{"one.com/foo"}, // Parsing will stop after first error }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - var v []tailcfg.PeerCapability + var v []peercap.Cap flag := &acceptAppCapsFlag{Value: &v} var err error @@ -1531,7 +1533,7 @@ func TestMessageForPort(t *testing.T) { CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"}, Self: &ipnstate.PeerStatus{ CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG}, + nodecap.ServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG}, }, }, }, @@ -1575,7 +1577,7 @@ func TestMessageForPort(t *testing.T) { CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"}, Self: &ipnstate.PeerStatus{ CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG}, + nodecap.ServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG}, }, }, }, @@ -1619,7 +1621,7 @@ func TestMessageForPort(t *testing.T) { CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"}, Self: &ipnstate.PeerStatus{ CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG}, + nodecap.ServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG}, }, }, }, @@ -1654,7 +1656,7 @@ func TestMessageForPort(t *testing.T) { CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"}, Self: &ipnstate.PeerStatus{ CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG}, + nodecap.ServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG}, }, }, }, @@ -1689,7 +1691,7 @@ func TestMessageForPort(t *testing.T) { CurrentTailnet: &ipnstate.TailnetStatus{MagicDNSSuffix: "test.ts.net"}, Self: &ipnstate.PeerStatus{ CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG}, + nodecap.ServiceHost: []tailcfg.RawMessage{svcIPMapJSONRawMSG}, }, }, }, diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index 69a287eb5..465159b08 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -260,6 +260,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep 💣 tailscale.com/safesocket from tailscale.com/client/local+ tailscale.com/syncs from tailscale.com/control/controlhttp+ tailscale.com/tailcfg from tailscale.com/client/local+ + tailscale.com/tailcfg/nodecap from tailscale.com/tailcfg+ + tailscale.com/tailcfg/peercap from tailscale.com/tailcfg+ tailscale.com/tempfork/spf13/cobra from tailscale.com/cmd/tailscale/cli/ffcomplete+ tailscale.com/tka from tailscale.com/client/local+ tailscale.com/tsconst from tailscale.com/net/netmon+ diff --git a/cmd/tailscaled/depaware-min.txt b/cmd/tailscaled/depaware-min.txt index 395dcd40f..0c44c30ae 100644 --- a/cmd/tailscaled/depaware-min.txt +++ b/cmd/tailscaled/depaware-min.txt @@ -127,6 +127,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/safesocket from tailscale.com/cmd/tailscaled+ tailscale.com/syncs from tailscale.com/cmd/tailscaled+ tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+ + tailscale.com/tailcfg/nodecap from tailscale.com/tailcfg+ + tailscale.com/tailcfg/peercap from tailscale.com/tailcfg+ tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock tailscale.com/tka from tailscale.com/control/controlclient+ tailscale.com/tsconst from tailscale.com/net/netns+ diff --git a/cmd/tailscaled/depaware-minbox.txt b/cmd/tailscaled/depaware-minbox.txt index 8edf656b1..397e092dc 100644 --- a/cmd/tailscaled/depaware-minbox.txt +++ b/cmd/tailscaled/depaware-minbox.txt @@ -145,6 +145,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/safesocket from tailscale.com/cmd/tailscaled+ tailscale.com/syncs from tailscale.com/cmd/tailscaled+ tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+ + tailscale.com/tailcfg/nodecap from tailscale.com/tailcfg+ + tailscale.com/tailcfg/peercap from tailscale.com/tailcfg+ tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock tailscale.com/tempfork/spf13/cobra from tailscale.com/cmd/tailscale/cli/ffcomplete+ tailscale.com/tka from tailscale.com/control/controlclient+ diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index 9bb37a049..e2d17f7b7 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -421,6 +421,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de LD 💣 tailscale.com/ssh/tailssh from tailscale.com/feature/ssh tailscale.com/syncs from tailscale.com/cmd/tailscaled+ tailscale.com/tailcfg from tailscale.com/client/local+ + tailscale.com/tailcfg/nodecap from tailscale.com/tailcfg+ + tailscale.com/tailcfg/peercap from tailscale.com/tailcfg+ tailscale.com/tempfork/acme from tailscale.com/feature/acme tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock tailscale.com/tempfork/httprec from tailscale.com/feature/c2n diff --git a/cmd/tsidp/depaware.txt b/cmd/tsidp/depaware.txt index af0ed1218..bd8e92ad9 100644 --- a/cmd/tsidp/depaware.txt +++ b/cmd/tsidp/depaware.txt @@ -230,6 +230,8 @@ tailscale.com/cmd/tsidp dependencies: (generated by github.com/tailscale/depawar 💣 tailscale.com/safesocket from tailscale.com/client/local+ tailscale.com/syncs from tailscale.com/control/controlhttp+ tailscale.com/tailcfg from tailscale.com/client/local+ + tailscale.com/tailcfg/nodecap from tailscale.com/tailcfg+ + tailscale.com/tailcfg/peercap from tailscale.com/tailcfg+ tailscale.com/tempfork/acme from tailscale.com/feature/acme tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock tailscale.com/tempfork/httprec from tailscale.com/feature/c2n diff --git a/cmd/tsidp/tsidp.go b/cmd/tsidp/tsidp.go index 24c823d71..13e4bf2be 100644 --- a/cmd/tsidp/tsidp.go +++ b/cmd/tsidp/tsidp.go @@ -44,6 +44,7 @@ "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/tsnet" "tailscale.com/types/key" "tailscale.com/types/lazy" @@ -672,7 +673,7 @@ func (s *idpServer) serveUserInfo(w http.ResponseWriter, r *http.Request) { // TODO(maisem): not sure if this is the right thing to do ui.UserName, _, _ = strings.Cut(ar.remoteUser.UserProfile.LoginName, "@") - rules, err := tailcfg.UnmarshalCapJSON[capRule](ar.remoteUser.CapMap, tailcfg.PeerCapabilityTsIDP) + rules, err := tailcfg.UnmarshalCapJSON[capRule](ar.remoteUser.CapMap, peercap.TsIDP) if err != nil { http.Error(w, "tsidp: failed to unmarshal capability: %v", http.StatusBadRequest) return @@ -964,7 +965,7 @@ func (s *idpServer) serveToken(w http.ResponseWriter, r *http.Request) { tsClaims.Issuer = s.loopbackURL } - rules, err := tailcfg.UnmarshalCapJSON[capRule](who.CapMap, tailcfg.PeerCapabilityTsIDP) + rules, err := tailcfg.UnmarshalCapJSON[capRule](who.CapMap, peercap.TsIDP) if err != nil { log.Printf("tsidp: failed to unmarshal capability: %v", err) http.Error(w, err.Error(), http.StatusBadRequest) diff --git a/cmd/tsidp/tsidp_test.go b/cmd/tsidp/tsidp_test.go index baf5aaee1..1f3811b02 100644 --- a/cmd/tsidp/tsidp_test.go +++ b/cmd/tsidp/tsidp_test.go @@ -42,6 +42,7 @@ "tailscale.com/client/local" "tailscale.com/client/tailscale/apitype" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/key" "tailscale.com/types/opt" "tailscale.com/types/views" @@ -556,7 +557,7 @@ func TestServeToken(t *testing.T) { remoteAddr: "127.0.0.1:12345", strictMode: false, caps: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityTsIDP: { + peercap.TsIDP: { mustMarshalJSON(t, capRule{ IncludeInUserInfo: true, ExtraClaims: map[string]any{ @@ -578,7 +579,7 @@ func TestServeToken(t *testing.T) { code: "valid-code", strictMode: false, caps: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityTsIDP: { + peercap.TsIDP: { mustMarshalJSON(t, capRule{ IncludeInUserInfo: true, ExtraClaims: map[string]any{ @@ -714,7 +715,7 @@ func TestExtraUserInfo(t *testing.T) { name: "extra-claim", tokenValidTill: time.Now().Add(1 * time.Minute), caps: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityTsIDP: { + peercap.TsIDP: { mustMarshalJSON(t, capRule{ IncludeInUserInfo: true, ExtraClaims: map[string]any{ @@ -731,7 +732,7 @@ func TestExtraUserInfo(t *testing.T) { name: "duplicate-claim-distinct-values", tokenValidTill: time.Now().Add(1 * time.Minute), caps: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityTsIDP: { + peercap.TsIDP: { mustMarshalJSON(t, capRule{ IncludeInUserInfo: true, ExtraClaims: map[string]any{ @@ -748,7 +749,7 @@ func TestExtraUserInfo(t *testing.T) { name: "multiple-extra-claims", tokenValidTill: time.Now().Add(1 * time.Minute), caps: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityTsIDP: { + peercap.TsIDP: { mustMarshalJSON(t, capRule{ IncludeInUserInfo: true, ExtraClaims: map[string]any{ @@ -773,7 +774,7 @@ func TestExtraUserInfo(t *testing.T) { name: "overwrite-protected-claim", tokenValidTill: time.Now().Add(1 * time.Minute), caps: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityTsIDP: { + peercap.TsIDP: { mustMarshalJSON(t, capRule{ IncludeInUserInfo: true, ExtraClaims: map[string]any{ @@ -789,7 +790,7 @@ func TestExtraUserInfo(t *testing.T) { name: "extra-claim-omitted", tokenValidTill: time.Now().Add(1 * time.Minute), caps: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityTsIDP: { + peercap.TsIDP: { mustMarshalJSON(t, capRule{ IncludeInUserInfo: false, ExtraClaims: map[string]any{ diff --git a/control/controlclient/direct.go b/control/controlclient/direct.go index 996f51ee1..bfc32b2e1 100644 --- a/control/controlclient/direct.go +++ b/control/controlclient/direct.go @@ -49,6 +49,7 @@ "tailscale.com/net/tsdial" "tailscale.com/syncs" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tka" "tailscale.com/tstime" "tailscale.com/types/events" @@ -1367,12 +1368,12 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap // DefaultAutoUpdate in its CapMap and deprecated top-level field forms. if self := resp.Node; self != nil { - for _, v := range self.CapMap[tailcfg.NodeAttrDefaultAutoUpdate] { + for _, v := range self.CapMap[nodecap.DefaultAutoUpdate] { switch v { case "true", "false": c.autoUpdatePub.Publish(AutoUpdate{c.controlClientID, v == "true"}) default: - c.logf("netmap: [unexpected] unknown %s in CapMap: %q", tailcfg.NodeAttrDefaultAutoUpdate, v) + c.logf("netmap: [unexpected] unknown %s in CapMap: %q", nodecap.DefaultAutoUpdate, v) } } } diff --git a/control/controlclient/map.go b/control/controlclient/map.go index 77ca0e68d..7e23b3f34 100644 --- a/control/controlclient/map.go +++ b/control/controlclient/map.go @@ -26,6 +26,7 @@ "tailscale.com/envknob" "tailscale.com/hostinfo" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstime" "tailscale.com/types/key" "tailscale.com/types/logger" @@ -86,7 +87,7 @@ type mapSession struct { // Fields storing state over the course of multiple MapResponses. lastPrintMap time.Time lastNode tailcfg.NodeView - lastCapSet set.Set[tailcfg.NodeCapability] + lastCapSet set.Set[nodecap.Cap] lastDNSConfig *tailcfg.DNSConfig lastDERPMap *tailcfg.DERPMap lastUserProfile map[tailcfg.UserID]tailcfg.UserProfileView @@ -603,7 +604,7 @@ func (ms *mapSession) updateStateFromResponse(resp *tailcfg.MapResponse) { if resp.Node != nil { ms.lastNode = resp.Node.View() - capSet := set.Set[tailcfg.NodeCapability]{} + capSet := set.Set[nodecap.Cap]{} for _, c := range resp.Node.Capabilities { capSet.Add(c) } diff --git a/control/controlclient/map_test.go b/control/controlclient/map_test.go index fb4307c39..3ae7d66f5 100644 --- a/control/controlclient/map_test.go +++ b/control/controlclient/map_test.go @@ -24,6 +24,7 @@ "tailscale.com/health" "tailscale.com/ipn" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstest" "tailscale.com/tstime" "tailscale.com/types/dnstype" @@ -1250,38 +1251,38 @@ func TestPeerChangeDiff(t *testing.T) { }, { name: "patch-capmap-add-value-to-existing-key", - a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}}, - b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: []tailcfg.RawMessage{"true"}}}, - want: &tailcfg.PeerChange{NodeID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: []tailcfg.RawMessage{"true"}}}, + a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: nil}}, + b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: []tailcfg.RawMessage{"true"}}}, + want: &tailcfg.PeerChange{NodeID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: []tailcfg.RawMessage{"true"}}}, }, { name: "patch-capmap-add-new-key", - a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}}, - b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil, tailcfg.CapabilityDebug: nil}}, - want: &tailcfg.PeerChange{NodeID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil, tailcfg.CapabilityDebug: nil}}, + a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: nil}}, + b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: nil, nodecap.Debug: nil}}, + want: &tailcfg.PeerChange{NodeID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: nil, nodecap.Debug: nil}}, }, { name: "patch-capmap-remove-key", - a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}}, + a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: nil}}, b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{}}, want: &tailcfg.PeerChange{NodeID: 1, CapMap: tailcfg.NodeCapMap{}}, }, { name: "patch-capmap-remove-as-nil", - a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}}, + a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: nil}}, b: &tailcfg.Node{ID: 1}, want: &tailcfg.PeerChange{NodeID: 1, CapMap: tailcfg.NodeCapMap{}}, }, { name: "patch-capmap-add-key-to-empty-map", a: &tailcfg.Node{ID: 1}, - b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}}, - want: &tailcfg.PeerChange{NodeID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}}, + b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: nil}}, + want: &tailcfg.PeerChange{NodeID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: nil}}, }, { name: "patch-capmap-no-change", - a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}}, - b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{tailcfg.CapabilityAdmin: nil}}, + a: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: nil}}, + b: &tailcfg.Node{ID: 1, CapMap: tailcfg.NodeCapMap{nodecap.Admin: nil}}, wantEqual: true, }, } diff --git a/control/controlknobs/controlknobs.go b/control/controlknobs/controlknobs.go index 45bfde0fa..2c8fbe2ff 100644 --- a/control/controlknobs/controlknobs.go +++ b/control/controlknobs/controlknobs.go @@ -12,6 +12,7 @@ "tailscale.com/syncs" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/opt" ) @@ -158,38 +159,38 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) { } has := capMap.Contains var ( - disableUPnP = has(tailcfg.NodeAttrDisableUPnP) - randomizeClientPort = has(tailcfg.NodeAttrRandomizeClientPort) - disableDeltaUpdates = has(tailcfg.NodeAttrDisableDeltaUpdates) + disableUPnP = has(nodecap.DisableUPnP) + randomizeClientPort = has(nodecap.RandomizeClientPort) + disableDeltaUpdates = has(nodecap.DisableDeltaUpdates) oneCGNAT opt.Bool - forceBackgroundSTUN = has(tailcfg.NodeAttrDebugForceBackgroundSTUN) - peerMTUEnable = has(tailcfg.NodeAttrPeerMTUEnable) - dnsForwarderDisableTCPRetries = has(tailcfg.NodeAttrDNSForwarderDisableTCPRetries) - silentDisco = has(tailcfg.NodeAttrSilentDisco) - forceIPTables = has(tailcfg.NodeAttrLinuxMustUseIPTables) - forceNfTables = has(tailcfg.NodeAttrLinuxMustUseNfTables) - probeUDPLifetime = has(tailcfg.NodeAttrProbeUDPLifetime) - appCStoreRoutes = has(tailcfg.NodeAttrStoreAppCRoutes) - userDialUseRoutes = has(tailcfg.NodeAttrUserDialUseRoutes) - disableSplitDNSWhenNoCustomResolvers = has(tailcfg.NodeAttrDisableSplitDNSWhenNoCustomResolvers) - disableLocalDNSOverrideViaNRPT = has(tailcfg.NodeAttrDisableLocalDNSOverrideViaNRPT) - disableCaptivePortalDetection = has(tailcfg.NodeAttrDisableCaptivePortalDetection) - disableSkipStatusQueue = has(tailcfg.NodeAttrDisableSkipStatusQueue) - disableHostsFileUpdates = has(tailcfg.NodeAttrDisableHostsFileUpdates) - forceRegisterMagicDNSIPv4Only = has(tailcfg.NodeAttrForceRegisterMagicDNSIPv4Only) - emitRuntimeMetrics = has(tailcfg.NodeAttrEmitRuntimeMetrics) - disableUDPGRO = has(tailcfg.NodeAttrDisableUDPGRO) - disableUDPGSO = has(tailcfg.NodeAttrDisableUDPGSO) - disableTUNUDPGRO = has(tailcfg.NodeAttrDisableTUNUDPGRO) - disableTUNTCPGRO = has(tailcfg.NodeAttrDisableTUNTCPGRO) - neverGSOEqualTail = has(tailcfg.NodeAttrNeverGSOEqualTail) - cacheNetworkMaps = has(tailcfg.NodeAttrCacheNetworkMaps) - scopeQuad100OnMacOS = has(tailcfg.NodeAttrScopeQuad100OnMacOS) + forceBackgroundSTUN = has(nodecap.DebugForceBackgroundSTUN) + peerMTUEnable = has(nodecap.PeerMTUEnable) + dnsForwarderDisableTCPRetries = has(nodecap.DNSForwarderDisableTCPRetries) + silentDisco = has(nodecap.SilentDisco) + forceIPTables = has(nodecap.LinuxMustUseIPTables) + forceNfTables = has(nodecap.LinuxMustUseNfTables) + probeUDPLifetime = has(nodecap.ProbeUDPLifetime) + appCStoreRoutes = has(nodecap.StoreAppCRoutes) + userDialUseRoutes = has(nodecap.UserDialUseRoutes) + disableSplitDNSWhenNoCustomResolvers = has(nodecap.DisableSplitDNSWhenNoCustomResolvers) + disableLocalDNSOverrideViaNRPT = has(nodecap.DisableLocalDNSOverrideViaNRPT) + disableCaptivePortalDetection = has(nodecap.DisableCaptivePortalDetection) + disableSkipStatusQueue = has(nodecap.DisableSkipStatusQueue) + disableHostsFileUpdates = has(nodecap.DisableHostsFileUpdates) + forceRegisterMagicDNSIPv4Only = has(nodecap.ForceRegisterMagicDNSIPv4Only) + emitRuntimeMetrics = has(nodecap.EmitRuntimeMetrics) + disableUDPGRO = has(nodecap.DisableUDPGRO) + disableUDPGSO = has(nodecap.DisableUDPGSO) + disableTUNUDPGRO = has(nodecap.DisableTUNUDPGRO) + disableTUNTCPGRO = has(nodecap.DisableTUNTCPGRO) + neverGSOEqualTail = has(nodecap.NeverGSOEqualTail) + cacheNetworkMaps = has(nodecap.CacheNetworkMaps) + scopeQuad100OnMacOS = has(nodecap.ScopeQuad100OnMacOS) ) - if has(tailcfg.NodeAttrOneCGNATEnable) { + if has(nodecap.OneCGNATEnable) { oneCGNAT.Set(true) - } else if has(tailcfg.NodeAttrOneCGNATDisable) { + } else if has(nodecap.OneCGNATDisable) { oneCGNAT.Set(false) } diff --git a/feature/acme/cert.go b/feature/acme/cert.go index 0d9b7408e..5c4671ec9 100644 --- a/feature/acme/cert.go +++ b/feature/acme/cert.go @@ -26,7 +26,7 @@ "tailscale.com/health" "tailscale.com/ipn" "tailscale.com/ipn/ipnlocal" - "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" xacme "tailscale.com/tempfork/acme" "tailscale.com/types/logger" "tailscale.com/util/mak" @@ -442,12 +442,12 @@ func (e *extension) ensureAccount(ctx context.Context, ac *xacme.Client, logf lo } type acmeCertIssueArgs struct { - cs certStore // certificate and ACME account storage - logf logger.Logf // logs ACME progress and failures - traceACME func(any) // optional hook for logging ACME messages - domain string // certificate domain being issued + cs certStore // certificate and ACME account storage + logf logger.Logf // logs ACME progress and failures + traceACME func(any) // optional hook for logging ACME messages + domain string // certificate domain being issued opts []xacme.OrderOption // ACME order options - challengeType acmeChallengeType // challenge type to fulfill + challengeType acmeChallengeType // challenge type to fulfill } func (args acmeCertIssueArgs) baseDomain() string { return strings.TrimPrefix(args.domain, "*.") } @@ -672,7 +672,7 @@ func (e *extension) resolveCertDomain(b *ipnlocal.LocalBackend, domain string) ( // Wildcard request like "*.node.ts.net". if base, ok := strings.CutPrefix(domain, "*."); ok { - if !nm.AllCaps.Contains(tailcfg.NodeAttrDNSSubdomainResolve) { + if !nm.AllCaps.Contains(nodecap.DNSSubdomainResolve) { return "", fmt.Errorf("wildcard certificates are not enabled for this node") } if !slices.Contains(certDomains, base) { diff --git a/feature/acme/cert_test.go b/feature/acme/cert_test.go index 2ed4f28e5..0213136ed 100644 --- a/feature/acme/cert_test.go +++ b/feature/acme/cert_test.go @@ -34,6 +34,7 @@ "tailscale.com/ipn/ipnlocal/ipnlocaltest" "tailscale.com/ipn/store/mem" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" xacme "tailscale.com/tempfork/acme" "tailscale.com/tsconst" "tailscale.com/tstest" @@ -194,9 +195,9 @@ func TestResolveCertDomain(t *testing.T) { e := extOf(t, b) if !tt.skipNetmap { - var allCaps set.Set[tailcfg.NodeCapability] + var allCaps set.Set[nodecap.Cap] if tt.hasCap { - allCaps = set.Of(tailcfg.NodeAttrDNSSubdomainResolve) + allCaps = set.Of(nodecap.DNSSubdomainResolve) } b.ForTest().SetNetMap(&netmap.NetworkMap{ SelfNode: (&tailcfg.Node{}).View(), diff --git a/feature/conn25/conn25_test.go b/feature/conn25/conn25_test.go index 779864618..10f894304 100644 --- a/feature/conn25/conn25_test.go +++ b/feature/conn25/conn25_test.go @@ -26,6 +26,7 @@ "tailscale.com/net/tsdial" "tailscale.com/net/tstun" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tsd" "tailscale.com/tstest" "tailscale.com/types/appctype" @@ -457,10 +458,10 @@ func TestReserveIPs(t *testing.T) { func TestReconfig(t *testing.T) { rawCfg := `{"name":"app1","connectors":["tag:woo"],"domains":["example.com"]}` capMap := tailcfg.NodeCapMap{ - tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): []tailcfg.RawMessage{ + nodecap.Cap(AppConnectorsExperimentalAttrName): []tailcfg.RawMessage{ tailcfg.RawMessage(rawCfg), }, - tailcfg.NodeCapability(AppConnectorsExperimentalIPPoolsAttrName): []tailcfg.RawMessage{ + nodecap.Cap(AppConnectorsExperimentalIPPoolsAttrName): []tailcfg.RawMessage{ tailcfg.RawMessage("{}"), }, } @@ -648,8 +649,8 @@ func TestConfigFromNodeView(t *testing.T) { poolsCfg = getRawMessages(t, tt.poolsCfg) } capMap := tailcfg.NodeCapMap{ - tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): appCfg, - tailcfg.NodeCapability(AppConnectorsExperimentalIPPoolsAttrName): poolsCfg, + nodecap.Cap(AppConnectorsExperimentalAttrName): appCfg, + nodecap.Cap(AppConnectorsExperimentalIPPoolsAttrName): poolsCfg, } sn := (&tailcfg.Node{ CapMap: capMap, @@ -792,8 +793,8 @@ func makeSelfNode(t *testing.T, attrs []appctype.Conn25Attr, pools appctype.Conn t.Fatalf("unexpected error marshaling pools in test setup: %v", err) } capMap := tailcfg.NodeCapMap{ - tailcfg.NodeCapability(AppConnectorsExperimentalAttrName): cfg, - tailcfg.NodeCapability(AppConnectorsExperimentalIPPoolsAttrName): {tailcfg.RawMessage(poolsBytes)}, + nodecap.Cap(AppConnectorsExperimentalAttrName): cfg, + nodecap.Cap(AppConnectorsExperimentalIPPoolsAttrName): {tailcfg.RawMessage(poolsBytes)}, } return (&tailcfg.Node{ diff --git a/feature/relayserver/relayserver.go b/feature/relayserver/relayserver.go index 2de5654c7..4ed84e4c6 100644 --- a/feature/relayserver/relayserver.go +++ b/feature/relayserver/relayserver.go @@ -21,6 +21,7 @@ "tailscale.com/net/udprelay/status" "tailscale.com/syncs" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/key" "tailscale.com/types/logger" "tailscale.com/types/views" @@ -209,7 +210,7 @@ func (e *extension) handleRelayServerStaticAddrPortsLocked() { func (e *extension) selfNodeViewChanged(nodeView tailcfg.NodeView) { e.mu.Lock() defer e.mu.Unlock() - e.hasNodeAttrDisableRelayServer = nodeView.HasCap(tailcfg.NodeAttrDisableRelayServer) + e.hasNodeAttrDisableRelayServer = nodeView.HasCap(nodecap.DisableRelayServer) e.handleRelayServerLifetimeLocked() } diff --git a/feature/routecheck/routertracker_test.go b/feature/routecheck/routertracker_test.go index 753fb6bf4..8c6936c78 100644 --- a/feature/routecheck/routertracker_test.go +++ b/feature/routecheck/routertracker_test.go @@ -17,6 +17,7 @@ "tailscale.com/ipn/ipnstate" netroutecheck "tailscale.com/net/routecheck" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/opt" ) @@ -210,8 +211,8 @@ func makeSelfNodeWithRouteCheckEnabled(t *testing.T) tailcfg.NodeView { t.Helper() self := (&tailcfg.Node{ CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrClientSideReachability: nil, - tailcfg.NodeAttrClientSideReachabilityRouteCheck: nil, + nodecap.ClientSideReachability: nil, + nodecap.ClientSideReachabilityRouteCheck: nil, }, }).View() if !netroutecheck.IsEnabled(self) { diff --git a/feature/taildrop/ext.go b/feature/taildrop/ext.go index 5840c9d95..c95832cec 100644 --- a/feature/taildrop/ext.go +++ b/feature/taildrop/ext.go @@ -22,6 +22,8 @@ "tailscale.com/ipn/ipnext" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/tstime" "tailscale.com/types/empty" "tailscale.com/types/logger" @@ -124,7 +126,7 @@ func (e *Extension) onSelfChange(self tailcfg.NodeView) { if self.Valid() { e.selfUID = self.User() } - e.capFileSharing = self.Valid() && self.CapMap().Contains(tailcfg.CapabilityFileSharing) + e.capFileSharing = self.Valid() && self.CapMap().Contains(nodecap.FileSharing) osshare.SetFileSharingEnabled(e.capFileSharing, e.logf) } @@ -353,7 +355,7 @@ func (e *Extension) FileTargets() ([]*apitype.FileTarget, error) { if self == p.User() { return true } - if nb.PeerHasCap(p, tailcfg.PeerCapabilityFileSharingTarget) { + if nb.PeerHasCap(p, peercap.FileSharingTarget) { // Explicitly noted in the netmap ACL caps as a target. return true } @@ -400,7 +402,7 @@ func (e *Extension) taildropTargetStatus(p tailcfg.NodeView, nb ipnext.NodeBacke } if selfUID != p.User() { // Different user must have the explicit file sharing target capability - if !nb.PeerHasCap(p, tailcfg.PeerCapabilityFileSharingTarget) { + if !nb.PeerHasCap(p, peercap.FileSharingTarget) { return ipnstate.TaildropTargetOwnedByOtherUser } } diff --git a/feature/taildrop/peerapi.go b/feature/taildrop/peerapi.go index 8b92c8c85..53ea10dd9 100644 --- a/feature/taildrop/peerapi.go +++ b/feature/taildrop/peerapi.go @@ -13,7 +13,7 @@ "time" "tailscale.com/ipn/ipnlocal" - "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/tstime" "tailscale.com/util/clientmetric" "tailscale.com/util/httphdr" @@ -33,7 +33,7 @@ func canPutFile(h ipnlocal.PeerAPIHandler) bool { // Unsigned peers can't send files. return false } - return h.IsSelfUntagged() || h.PeerCaps().HasCapability(tailcfg.PeerCapabilityFileSharingSend) + return h.IsSelfUntagged() || h.PeerCaps().HasCapability(peercap.FileSharingSend) } func handlePeerPut(h ipnlocal.PeerAPIHandler, w http.ResponseWriter, r *http.Request) { diff --git a/feature/taildrop/peerapi_test.go b/feature/taildrop/peerapi_test.go index 65f881be9..0aa6fa56d 100644 --- a/feature/taildrop/peerapi_test.go +++ b/feature/taildrop/peerapi_test.go @@ -21,6 +21,7 @@ "tailscale.com/client/tailscale/apitype" "tailscale.com/ipn/ipnlocal" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstest" "tailscale.com/tstime" "tailscale.com/types/logger" @@ -481,7 +482,7 @@ func(t *testing.T, env *peerAPITestEnv) { }, } if tt.debugCap { - selfNode.CapMap = tailcfg.NodeCapMap{tailcfg.CapabilityDebug: nil} + selfNode.CapMap = tailcfg.NodeCapMap{nodecap.Debug: nil} } var rootDir string var fo FileOps diff --git a/feature/wakeonlan/wakeonlan.go b/feature/wakeonlan/wakeonlan.go index 5a567ad44..52c961eed 100644 --- a/feature/wakeonlan/wakeonlan.go +++ b/feature/wakeonlan/wakeonlan.go @@ -20,6 +20,7 @@ "tailscale.com/hostinfo" "tailscale.com/ipn/ipnlocal" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/util/clientmetric" ) @@ -90,7 +91,7 @@ func canWakeOnLAN(h ipnlocal.PeerAPIHandler) bool { if h.Peer().UnsignedPeerAPIOnly() { return false } - return h.IsSelfUntagged() || h.PeerCaps().HasCapability(tailcfg.PeerCapabilityWakeOnLAN) + return h.IsSelfUntagged() || h.PeerCaps().HasCapability(peercap.WakeOnLAN) } var metricWakeOnLANCalls = clientmetric.NewCounter("peerapi_wol") diff --git a/ipn/ipn_clone.go b/ipn/ipn_clone.go index 4b1f84e1e..6a5353aed 100644 --- a/ipn/ipn_clone.go +++ b/ipn/ipn_clone.go @@ -12,6 +12,7 @@ "tailscale.com/drive" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/opt" "tailscale.com/types/persist" "tailscale.com/types/preftype" @@ -247,7 +248,7 @@ func (src *HTTPHandler) Clone() *HTTPHandler { Path string Proxy string Text string - AcceptAppCaps []tailcfg.PeerCapability + AcceptAppCaps []peercap.Cap Redirect string }{}) diff --git a/ipn/ipn_view.go b/ipn/ipn_view.go index 67ff02f6b..38695057b 100644 --- a/ipn/ipn_view.go +++ b/ipn/ipn_view.go @@ -15,6 +15,7 @@ "github.com/go-json-experiment/json/jsontext" "tailscale.com/drive" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/opt" "tailscale.com/types/persist" "tailscale.com/types/preftype" @@ -932,7 +933,7 @@ func (v HTTPHandlerView) Proxy() string { return v.ж.Proxy } func (v HTTPHandlerView) Text() string { return v.ж.Text } // peer capabilities to forward in grant header, e.g. example.com/cap/mon -func (v HTTPHandlerView) AcceptAppCaps() views.Slice[tailcfg.PeerCapability] { +func (v HTTPHandlerView) AcceptAppCaps() views.Slice[peercap.Cap] { return views.SliceOf(v.ж.AcceptAppCaps) } @@ -950,7 +951,7 @@ func (v HTTPHandlerView) Redirect() string { return v.ж.Redirect } Path string Proxy string Text string - AcceptAppCaps []tailcfg.PeerCapability + AcceptAppCaps []peercap.Cap Redirect string }{}) diff --git a/ipn/ipnext/ipnext.go b/ipn/ipnext/ipnext.go index 261428721..1e817c084 100644 --- a/ipn/ipnext/ipnext.go +++ b/ipn/ipnext/ipnext.go @@ -18,6 +18,7 @@ "tailscale.com/ipn/ipnauth" "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/tsd" "tailscale.com/tstime" "tailscale.com/types/key" @@ -526,7 +527,7 @@ type NodeBackend interface { PeerCaps(src netip.Addr) tailcfg.PeerCapMap // PeerHasCap reports whether the peer has the specified peer capability. - PeerHasCap(peer tailcfg.NodeView, cap tailcfg.PeerCapability) bool + PeerHasCap(peer tailcfg.NodeView, cap peercap.Cap) bool // PeerAPIBase returns the "http://ip:port" URL base to reach peer's // PeerAPI, or the empty string if the peer is invalid or doesn't support diff --git a/ipn/ipnlocal/dnsconfig_test.go b/ipn/ipnlocal/dnsconfig_test.go index 3198222f1..712ed9901 100644 --- a/ipn/ipnlocal/dnsconfig_test.go +++ b/ipn/ipnlocal/dnsconfig_test.go @@ -14,6 +14,7 @@ "tailscale.com/ipn" "tailscale.com/net/dns" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstest" "tailscale.com/types/dnstype" "tailscale.com/types/netmap" @@ -115,14 +116,14 @@ func TestDNSConfigForNetmap(t *testing.T) { Name: "myname.net.", Addresses: ipps("100.101.101.101"), }).View(), - AllCaps: set.SetOf([]tailcfg.NodeCapability{tailcfg.NodeAttrDNSSubdomainResolve}), + AllCaps: set.SetOf([]nodecap.Cap{nodecap.DNSSubdomainResolve}), }, peers: nodeViews([]*tailcfg.Node{ { ID: 1, Name: "peer-with-cap.net.", Addresses: ipps("100.102.0.1"), - CapMap: tailcfg.NodeCapMap{tailcfg.NodeAttrDNSSubdomainResolve: nil}, + CapMap: tailcfg.NodeCapMap{nodecap.DNSSubdomainResolve: nil}, }, { ID: 2, @@ -397,12 +398,12 @@ func TestDNSConfigForNetmap(t *testing.T) { Name: "a", Addresses: ipps("100.101.101.101"), CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeCapability(appc.AppConnectorsExperimentalAttrName): []tailcfg.RawMessage{ + nodecap.Cap(appc.AppConnectorsExperimentalAttrName): []tailcfg.RawMessage{ tailcfg.RawMessage(`{"name":"app1","connectors":["tag:woo"],"domains":["example.com"]}`), }, }, }).View(), - AllCaps: set.Of(tailcfg.NodeCapability(appc.AppConnectorsExperimentalAttrName)), + AllCaps: set.Of(nodecap.Cap(appc.AppConnectorsExperimentalAttrName)), }, peers: nodeViews([]*tailcfg.Node{ { diff --git a/ipn/ipnlocal/drive.go b/ipn/ipnlocal/drive.go index 70a52cb1d..81c328a84 100644 --- a/ipn/ipnlocal/drive.go +++ b/ipn/ipnlocal/drive.go @@ -17,7 +17,7 @@ "tailscale.com/drive" "tailscale.com/ipn" - "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/logger" "tailscale.com/types/views" "tailscale.com/util/httpm" @@ -381,7 +381,7 @@ func (s driveRemoteSource) Remotes() iter.Seq[*drive.Remote] { b.logf("[v2] taildrive: peer %s (%s, id=%v) PeerAPI unreachable", peerKey, peerName, peerID) return false } - if cn.PeerHasCap(peer, tailcfg.PeerCapabilityTaildriveSharer) { + if cn.PeerHasCap(peer, peercap.TaildriveSharer) { b.logf("[v2] taildrive: peer %s (%s, id=%v) available", peerKey, peerName, peerID) return true } diff --git a/ipn/ipnlocal/drive_test.go b/ipn/ipnlocal/drive_test.go index faa32f3d1..9d51d6516 100644 --- a/ipn/ipnlocal/drive_test.go +++ b/ipn/ipnlocal/drive_test.go @@ -24,6 +24,8 @@ "tailscale.com/ipn" "tailscale.com/ipn/store/mem" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/tsd" "tailscale.com/tstest" "tailscale.com/types/ipproto" @@ -157,7 +159,7 @@ func TestDriveRemoteSourceAccessGate(t *testing.T) { }).View(), } - install := func(allCaps set.Set[tailcfg.NodeCapability]) { + install := func(allCaps set.Set[nodecap.Cap]) { b.mu.Lock() defer b.mu.Unlock() b.setNetMapLocked(&netmap.NetworkMap{ @@ -181,7 +183,7 @@ func TestDriveRemoteSourceAccessGate(t *testing.T) { t.Errorf("Remotes without DriveAccess cap: got %d entries, want 0", len(got)) } - install(set.Of(tailcfg.NodeAttrsTaildriveAccess)) + install(set.Of(nodecap.TaildriveAccess)) if got := collect(); len(got) != len(peers) { t.Errorf("Remotes with DriveAccess cap: got %d entries, want %d", len(got), len(peers)) } @@ -376,7 +378,7 @@ func (h *driveEndToEndHarness) filterMatchesFor(specs []peerSpec) []filtertype.M Srcs: []netip.Prefix{netip.PrefixFrom(s.addr, s.addr.BitLen())}, Caps: []filtertype.CapMatch{{ Dst: netip.PrefixFrom(h.selfAddr, h.selfAddr.BitLen()), - Cap: tailcfg.PeerCapabilityTaildriveSharer, + Cap: peercap.TaildriveSharer, }}, }) } @@ -403,7 +405,7 @@ func (h *driveEndToEndHarness) installNetMap(specs []peerSpec) { }).View(), Domain: h.domain, Peers: peers, - AllCaps: set.Of(tailcfg.NodeAttrsTaildriveAccess), + AllCaps: set.Of(nodecap.TaildriveAccess), PacketFilter: h.filterMatchesFor(specs), } h.cc.send(sendOpt{loginFinished: true, nm: nm}) diff --git a/ipn/ipnlocal/drive_tomove.go b/ipn/ipnlocal/drive_tomove.go index ccea48f7a..86f7fa883 100644 --- a/ipn/ipnlocal/drive_tomove.go +++ b/ipn/ipnlocal/drive_tomove.go @@ -7,7 +7,7 @@ package ipnlocal -import "tailscale.com/tailcfg" +import "tailscale.com/tailcfg/nodecap" const ( // DriveLocalPort is the port on which the Taildrive listens for location @@ -19,12 +19,12 @@ // enabled. This is currently based on checking for the drive:share node // attribute. func (b *LocalBackend) DriveSharingEnabled() bool { - return b.currentNode().SelfHasCap(tailcfg.NodeAttrsTaildriveShare) + return b.currentNode().SelfHasCap(nodecap.TaildriveShare) } // DriveAccessEnabled reports whether accessing Taildrive shares on remote nodes // is enabled. This is currently based on checking for the drive:access node // attribute. func (b *LocalBackend) DriveAccessEnabled() bool { - return b.currentNode().SelfHasCap(tailcfg.NodeAttrsTaildriveAccess) + return b.currentNode().SelfHasCap(nodecap.TaildriveAccess) } diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 3f9e45310..57c91bd90 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -71,6 +71,7 @@ "tailscale.com/paths" "tailscale.com/syncs" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tsd" "tailscale.com/tstime" "tailscale.com/types/appctype" @@ -1535,7 +1536,7 @@ func (b *LocalBackend) updateStatusLocked(sb *ipnstate.StatusBuilder) { if sn := nm.SelfNode; sn.Valid() { peerStatusFromNode(ss, sn) if cm := sn.CapMap(); cm.Len() > 0 { - ss.Capabilities = make([]tailcfg.NodeCapability, 1, cm.Len()+1) + ss.Capabilities = make([]nodecap.Cap, 1, cm.Len()+1) ss.Capabilities[0] = "HTTPS://TAILSCALE.COM/s/DEPRECATED-NODE-CAPS#see-https://github.com/tailscale/tailscale/issues/11508" ss.CapMap = make(tailcfg.NodeCapMap, sn.CapMap().Len()) for k, v := range cm.All() { @@ -1991,7 +1992,7 @@ func (b *LocalBackend) setControlClientStatusLocked(c controlclient.Client, st c // Perform all reconfiguration based on the netmap here. if st.NetMap != nil { - b.capTailnetLock = st.NetMap.HasCap(tailcfg.CapabilityTailnetLock) + b.capTailnetLock = st.NetMap.HasCap(nodecap.TailnetLock) b.setWebClientAtomicBoolLocked(st.NetMap.AllCaps) b.mu.Unlock() // respect locking rules for tkaSyncIfNeeded @@ -2023,7 +2024,7 @@ func (b *LocalBackend) setControlClientStatusLocked(c controlclient.Client, st c // Now complete the lock-free parts of what we started while locked. if st.NetMap != nil { - if envknob.NoLogsNoSupport() && st.NetMap.HasCap(tailcfg.CapabilityDataPlaneAuditLogs) { + if envknob.NoLogsNoSupport() && st.NetMap.HasCap(nodecap.DataPlaneAuditLogs) { msg := "tailnet requires logging to be enabled. Remove --no-logs-no-support from tailscaled command line." b.health.SetLocalLogConfigHealth(errors.New(msg)) // Get the current prefs again, since we unlocked above. @@ -2068,7 +2069,7 @@ func (b *LocalBackend) setControlClientStatusLocked(c controlclient.Client, st c b.MagicConn().SetDERPMap(st.NetMap.DERPMap) } - b.MagicConn().SetOnlyTCP443(st.NetMap.HasCap(tailcfg.NodeAttrOnlyTCP443)) + b.MagicConn().SetOnlyTCP443(st.NetMap.HasCap(nodecap.OnlyTCP443)) // Update our cached DERP map dnsfallback.UpdateCache(st.NetMap.DERPMap, b.logf) @@ -2552,7 +2553,7 @@ func (b *LocalBackend) UpdateNetmapDelta(muts []netmap.NodeMutation) (handled bo // Note we do this AFTER the updates are applied in the nodeBackend, so that // we can get its updated views to put back into the cache. if buildfeatures.HasCacheNetMap && - cn.SelfHasCap(tailcfg.NodeAttrCacheNetworkMaps) && + cn.SelfHasCap(nodecap.CacheNetworkMaps) && envknob.BoolDefaultTrue("TS_USE_CACHED_NETMAP") { var peersToUpdate []tailcfg.NodeView @@ -3297,7 +3298,7 @@ func addServiceIPs(localNetsB *netipx.IPSetBuilder, selfNode tailcfg.NodeView) e return nil } - serviceMap, err := tailcfg.UnmarshalNodeCapViewJSON[tailcfg.ServiceIPMappings](selfNode.CapMap(), tailcfg.NodeAttrServiceHost) + serviceMap, err := tailcfg.UnmarshalNodeCapViewJSON[tailcfg.ServiceIPMappings](selfNode.CapMap(), nodecap.ServiceHost) if err != nil { return err } @@ -4996,7 +4997,7 @@ func (b *LocalBackend) checkSSHPrefsLocked(p *ipn.Prefs) error { return nil } // Assume that we do have the SSH capability if don't have a netmap yet. - if !b.currentNode().SelfHasCapOr(tailcfg.CapabilitySSH, true) { + if !b.currentNode().SelfHasCapOr(nodecap.SSH, true) { if b.isDefaultServerLocked() { return errors.New("Unable to enable local Tailscale SSH server; not enabled on Tailnet. See https://tailscale.com/s/ssh") } @@ -5021,7 +5022,7 @@ func (b *LocalBackend) sshOnButUnusableHealthCheckMessageLocked() (healthMessage } isDefault := b.isDefaultServerLocked() - if !nm.HasCap(tailcfg.CapabilityAdmin) { + if !nm.HasCap(nodecap.Admin) { return healthmsg.TailscaleSSHOnBut + "access controls don't allow anyone to access this device. Ask your admin to update your tailnet's ACLs to allow access." } if !isDefault { @@ -6071,7 +6072,7 @@ func (b *LocalBackend) authReconfigLocked() { prefs := b.pm.CurrentPrefs() hasPAC := b.interfaceState.HasPAC() - disableSubnetsIfPAC := cn.SelfHasCap(tailcfg.NodeAttrDisableSubnetsIfPAC) + disableSubnetsIfPAC := cn.SelfHasCap(nodecap.DisableSubnetsIfPAC) dohURL, dohURLOK := cn.exitNodeCanProxyDNS(prefs.ExitNodeID()) dcfg := cn.dnsConfigForNetmap(prefs, b.keyExpired, cmp.Or(b.goos, runtime.GOOS)) // If the current node is an app connector, ensure the app connector machine is started @@ -6537,7 +6538,7 @@ func (b *LocalBackend) routerConfigLocked(cfg *wgcfg.Config, prefs ipn.PrefsView NetfilterMode: prefs.NetfilterMode(), Routes: b.currentNode().osRoutes(), NetfilterKind: netfilterKind, - RemoveCGNATDropRule: nm.HasCap(tailcfg.NodeAttrDisableLinuxCGNATDropRule), + RemoveCGNATDropRule: nm.HasCap(nodecap.DisableLinuxCGNATDropRule), } if buildfeatures.HasSynology && distro.Get() == distro.Synology { @@ -7027,10 +7028,10 @@ func (b *LocalBackend) ShouldExposeRemoteWebClient() bool { // if the caller has no netmap. // // b.mu must be held. -func (b *LocalBackend) setWebClientAtomicBoolLocked(caps set.Set[tailcfg.NodeCapability]) { +func (b *LocalBackend) setWebClientAtomicBoolLocked(caps set.Set[nodecap.Cap]) { syncs.RequiresMutex(&b.mu) - shouldRun := !caps.Contains(tailcfg.NodeAttrDisableWebClient) + shouldRun := !caps.Contains(nodecap.DisableWebClient) wasRunning := b.webClientAtomicBool.Swap(shouldRun) if wasRunning && !shouldRun { b.goTracker.Go(b.webClientShutdown) // stop web client @@ -7341,9 +7342,9 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) { } if runtime.GOOS == "linux" && buildfeatures.HasOSRouter { - if nm.HasCap(tailcfg.NodeAttrLinuxMustUseIPTables) { + if nm.HasCap(nodecap.LinuxMustUseIPTables) { b.capForcedNetfilter = "iptables" - } else if nm.HasCap(tailcfg.NodeAttrLinuxMustUseNfTables) { + } else if nm.HasCap(nodecap.LinuxMustUseNfTables) { b.capForcedNetfilter = "nftables" } else { b.capForcedNetfilter = "" // empty string means client can auto-detect @@ -7359,7 +7360,7 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) { } if buildfeatures.HasDebug { - var caps set.Set[tailcfg.NodeCapability] + var caps set.Set[nodecap.Cap] if nm != nil { caps = nm.AllCaps } @@ -7367,12 +7368,12 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) { } // See the netns package for documentation on what these capability do. - netns.SetBindToInterfaceByRoute(b.logf, nm.HasCap(tailcfg.CapabilityBindToInterfaceByRoute)) + netns.SetBindToInterfaceByRoute(b.logf, nm.HasCap(nodecap.BindToInterfaceByRoute)) if runtime.GOOS == "android" { - netns.SetDisableAndroidBindToActiveNetwork(b.logf, nm.HasCap(tailcfg.NodeAttrDisableAndroidBindToActiveNetwork)) + netns.SetDisableAndroidBindToActiveNetwork(b.logf, nm.HasCap(nodecap.DisableAndroidBindToActiveNetwork)) } - netns.SetDisableBindConnToInterface(b.logf, nm.HasCap(tailcfg.CapabilityDebugDisableBindConnToInterface)) - netns.SetDisableBindConnToInterfaceAppleExt(b.logf, nm.HasCap(tailcfg.CapabilityDebugDisableBindConnToInterfaceAppleExt)) + netns.SetDisableBindConnToInterface(b.logf, nm.HasCap(nodecap.DebugDisableBindConnToInterface)) + netns.SetDisableBindConnToInterfaceAppleExt(b.logf, nm.HasCap(nodecap.DebugDisableBindConnToInterfaceAppleExt)) b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs()) if buildfeatures.HasServe { @@ -7443,7 +7444,7 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) { // not being updated (because of the envknob) and could be read back when // the node starts up. if nm != nil { - if b.currentNode().SelfHasCap(tailcfg.NodeAttrCacheNetworkMaps) && envknob.BoolDefaultTrue("TS_USE_CACHED_NETMAP") { + if b.currentNode().SelfHasCap(nodecap.CacheNetworkMaps) && envknob.BoolDefaultTrue("TS_USE_CACHED_NETMAP") { if err := b.writeNetmapToDiskLockedWithPeers(nm); err != nil { b.logf("write netmap to cache: %v", err) } @@ -7500,10 +7501,10 @@ func roundTraffic(bytes int64) float64 { // setDebugLogsByCapabilityLocked sets debug logging based on the self node's // capabilities. caps may be nil if the caller has no netmap. -func (b *LocalBackend) setDebugLogsByCapabilityLocked(caps set.Set[tailcfg.NodeCapability]) { +func (b *LocalBackend) setDebugLogsByCapabilityLocked(caps set.Set[nodecap.Cap]) { // These are sufficiently cheap (atomic bools) that we don't need to // store state and compare. - if caps.Contains(tailcfg.CapabilityDebugTSDNSResolution) { + if caps.Contains(nodecap.DebugTSDNSResolution) { dnscache.SetDebugLoggingEnabled(true) } else { dnscache.SetDebugLoggingEnabled(false) @@ -8069,7 +8070,7 @@ func (s netLogNodeSource) NetLogIDs() (nodeID, domainID logid.PrivateID, logExit if nm == nil || !nm.SelfNode.Valid() { return } - if !nm.SelfNode.HasCap(tailcfg.CapabilityDataPlaneAuditLogs) { + if !nm.SelfNode.HasCap(nodecap.DataPlaneAuditLogs) { return } if nm.SelfNode.DataPlaneAuditLogID() == "" || nm.DomainAuditLogID == "" { @@ -8086,7 +8087,7 @@ func (s netLogNodeSource) NetLogIDs() (nodeID, domainID logid.PrivateID, logExit if errNode != nil || errDomain != nil { return logid.PrivateID{}, logid.PrivateID{}, false, false } - return nodeID, domainID, nm.SelfNode.HasCap(tailcfg.NodeAttrLogExitFlows), true + return nodeID, domainID, nm.SelfNode.HasCap(nodecap.LogExitFlows), true } // Compile-time assertion that netLogNodeSource implements @@ -8784,7 +8785,7 @@ func fillAllowedSuggestions(polc policyclient.Client) (set.Set[tailcfg.StableNod // Errors are always logged. Suggestions are logged if they defer from prevSuggestion. func suggestExitNode(preferredDERP int, regionLatency map[int]time.Duration, rp RouteCheckReport, nb *nodeBackend, prevSuggestion tailcfg.StableNodeID, selectRegion selectRegionFunc, selectNode selectNodeFunc, allowList set.Set[tailcfg.StableNodeID]) (res apitype.ExitNodeSuggestionResponse, err error) { switch { - case nb.SelfHasCap(tailcfg.NodeAttrTrafficSteering): + case nb.SelfHasCap(nodecap.TrafficSteering): // The traffic-steering feature flag is enabled on this tailnet. res, err = suggestExitNodeUsingTrafficSteering(rp, nb, allowList) default: @@ -8833,7 +8834,7 @@ func suggestExitNodeUsingDERP(preferredRegionID int, regionLatency map[int]time. if allowList != nil && !allowList.Contains(peer.StableID()) { return false } - return peer.CapMap().Contains(tailcfg.NodeAttrSuggestExitNode) && tsaddr.ContainsExitRoutes(peer.AllowedIPs()) + return peer.CapMap().Contains(nodecap.SuggestExitNode) && tsaddr.ContainsExitRoutes(peer.AllowedIPs()) }) if len(candidates) == 0 { return res, nil @@ -8966,7 +8967,7 @@ func suggestExitNodeUsingTrafficSteering(rp RouteCheckReport, nb *nodeBackend, a return apitype.ExitNodeSuggestionResponse{}, ErrNoNetMap } - if !nb.SelfHasCap(tailcfg.NodeAttrTrafficSteering) { + if !nb.SelfHasCap(nodecap.TrafficSteering) { panic("missing traffic-steering capability") } @@ -8980,7 +8981,7 @@ func suggestExitNodeUsingTrafficSteering(rp RouteCheckReport, nb *nodeBackend, a if allowed != nil && !allowed.Contains(p.StableID()) { return false } - if !p.CapMap().Contains(tailcfg.NodeAttrSuggestExitNode) { + if !p.CapMap().Contains(nodecap.SuggestExitNode) { return false } if !tsaddr.ContainsExitRoutes(p.AllowedIPs()) { @@ -9128,7 +9129,7 @@ func isAllowedAutoExitNodeID(polc policyclient.Client, exitNodeID tailcfg.Stable // // TODO(bradfitz): optimize this later if/when it matters. // TODO(nickkhyl): move this into [nodeBackend] along with [LocalBackend.updateFilterLocked]. -func (b *LocalBackend) srcIPHasCapForFilter(srcIP netip.Addr, cap tailcfg.NodeCapability) bool { +func (b *LocalBackend) srcIPHasCapForFilter(srcIP netip.Addr, cap nodecap.Cap) bool { if cap == "" { // Shouldn't happen, but just in case. // But the empty cap also shouldn't be found in Node.CapMap. diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index b00759563..a1721fadc 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -49,6 +49,7 @@ "tailscale.com/net/tsaddr" "tailscale.com/net/tsdial" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tsd" "tailscale.com/tstest" "tailscale.com/tstest/deptest" @@ -672,7 +673,7 @@ func TestUpdateNetMapCache(t *testing.T) { // Now enable the netmap caching attribute, and send another update. // After doing so, the cache should have real data in it. - testMap.AllCaps = set.Of(tailcfg.NodeAttrCacheNetworkMaps) + testMap.AllCaps = set.Of(nodecap.CacheNetworkMaps) clb.mu.Lock() clb.setNetMapLocked(testMap) @@ -1803,7 +1804,7 @@ func TestStatusPeerCapabilities(t *testing.T) { tests := []struct { name string peers []tailcfg.NodeView - expectedPeerCapabilities map[tailcfg.StableNodeID][]tailcfg.NodeCapability + expectedPeerCapabilities map[tailcfg.StableNodeID][]nodecap.Cap expectedPeerCapMap map[tailcfg.StableNodeID]tailcfg.NodeCapMap }{ { @@ -1815,9 +1816,9 @@ func TestStatusPeerCapabilities(t *testing.T) { Key: makeNodeKeyFromID(1), IsWireGuardOnly: true, Hostinfo: (&tailcfg.Hostinfo{}).View(), - Capabilities: []tailcfg.NodeCapability{tailcfg.CapabilitySSH}, - CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{ - tailcfg.CapabilitySSH: nil, + Capabilities: []nodecap.Cap{nodecap.SSH}, + CapMap: (tailcfg.NodeCapMap)(map[nodecap.Cap][]tailcfg.RawMessage{ + nodecap.SSH: nil, }), }).View(), (&tailcfg.Node{ @@ -1825,9 +1826,9 @@ func TestStatusPeerCapabilities(t *testing.T) { StableID: "bar", Key: makeNodeKeyFromID(2), Hostinfo: (&tailcfg.Hostinfo{}).View(), - Capabilities: []tailcfg.NodeCapability{tailcfg.CapabilityAdmin}, - CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{ - tailcfg.CapabilityAdmin: {`{"test": "true}`}, + Capabilities: []nodecap.Cap{nodecap.Admin}, + CapMap: (tailcfg.NodeCapMap)(map[nodecap.Cap][]tailcfg.RawMessage{ + nodecap.Admin: {`{"test": "true}`}, }), }).View(), (&tailcfg.Node{ @@ -1835,26 +1836,26 @@ func TestStatusPeerCapabilities(t *testing.T) { StableID: "baz", Key: makeNodeKeyFromID(3), Hostinfo: (&tailcfg.Hostinfo{}).View(), - Capabilities: []tailcfg.NodeCapability{tailcfg.CapabilityOwner}, - CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{ - tailcfg.CapabilityOwner: nil, + Capabilities: []nodecap.Cap{nodecap.Owner}, + CapMap: (tailcfg.NodeCapMap)(map[nodecap.Cap][]tailcfg.RawMessage{ + nodecap.Owner: nil, }), }).View(), }, - expectedPeerCapabilities: map[tailcfg.StableNodeID][]tailcfg.NodeCapability{ - tailcfg.StableNodeID("foo"): {tailcfg.CapabilitySSH}, - tailcfg.StableNodeID("bar"): {tailcfg.CapabilityAdmin}, - tailcfg.StableNodeID("baz"): {tailcfg.CapabilityOwner}, + expectedPeerCapabilities: map[tailcfg.StableNodeID][]nodecap.Cap{ + tailcfg.StableNodeID("foo"): {nodecap.SSH}, + tailcfg.StableNodeID("bar"): {nodecap.Admin}, + tailcfg.StableNodeID("baz"): {nodecap.Owner}, }, expectedPeerCapMap: map[tailcfg.StableNodeID]tailcfg.NodeCapMap{ - tailcfg.StableNodeID("foo"): (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{ - tailcfg.CapabilitySSH: nil, + tailcfg.StableNodeID("foo"): (tailcfg.NodeCapMap)(map[nodecap.Cap][]tailcfg.RawMessage{ + nodecap.SSH: nil, }), - tailcfg.StableNodeID("bar"): (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{ - tailcfg.CapabilityAdmin: {`{"test": "true}`}, + tailcfg.StableNodeID("bar"): (tailcfg.NodeCapMap)(map[nodecap.Cap][]tailcfg.RawMessage{ + nodecap.Admin: {`{"test": "true}`}, }), - tailcfg.StableNodeID("baz"): (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{ - tailcfg.CapabilityOwner: nil, + tailcfg.StableNodeID("baz"): (tailcfg.NodeCapMap)(map[nodecap.Cap][]tailcfg.RawMessage{ + nodecap.Owner: nil, }), }, }, @@ -3395,7 +3396,7 @@ func TestReconfigureAppConnector(t *testing.T) { SelfNode: (&tailcfg.Node{ Name: "example.ts.net", Tags: []string{"tag:example"}, - CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{ + CapMap: (tailcfg.NodeCapMap)(map[nodecap.Cap][]tailcfg.RawMessage{ "tailscale.com/app-connectors": {tailcfg.RawMessage(appCfg)}, }), }).View(), @@ -4753,7 +4754,7 @@ func TestTCPHandlerForDstWithVIPService(t *testing.T) { SelfNode: (&tailcfg.Node{ Name: "example.ts.net", CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{tailcfg.RawMessage(svcIPMapJSON)}, + nodecap.ServiceHost: []tailcfg.RawMessage{tailcfg.RawMessage(svcIPMapJSON)}, }, }).View(), UserProfiles: map[tailcfg.UserID]tailcfg.UserProfileView{ @@ -5133,7 +5134,7 @@ func TestDriveManageShares(t *testing.T) { if !tt.disabled { nm := new(*b.currentNode().NetMap()) self := nm.SelfNode.AsStruct() - self.CapMap = tailcfg.NodeCapMap{tailcfg.NodeAttrsTaildriveShare: nil} + self.CapMap = tailcfg.NodeCapMap{nodecap.TaildriveShare: nil} nm.SelfNode = self.View() b.currentNode().SetNetMap(nm) b.sys.Set(driveimpl.NewFileSystemForRemote(b.logf)) @@ -5339,7 +5340,7 @@ func withExitRoutes() peerOptFunc { func withSuggest() peerOptFunc { return func(n *tailcfg.Node) { - mak.Set(&n.CapMap, tailcfg.NodeAttrSuggestExitNode, []tailcfg.RawMessage{}) + mak.Set(&n.CapMap, nodecap.SuggestExitNode, []tailcfg.RawMessage{}) } } @@ -6173,7 +6174,7 @@ func TestSuggestExitNodeTrafficSteering(t *testing.T) { netip.MustParsePrefix("fe70::1/128"), }, CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrTrafficSteering: []tailcfg.RawMessage{}, + nodecap.TrafficSteering: []tailcfg.RawMessage{}, }, } @@ -8014,7 +8015,7 @@ func TestSrcCapPacketFilter(t *testing.T) { }, PacketFilter: []filtertype.Match{{ IPProto: views.SliceOf([]ipproto.Proto{ipproto.TCP}), - SrcCaps: []tailcfg.NodeCapability{"cap-X"}, // cap in packet filter rule + SrcCaps: []nodecap.Cap{"cap-X"}, // cap in packet filter rule Dsts: []filtertype.NetPortRange{{ Net: netip.MustParsePrefix("1.1.1.1/32"), Ports: filtertype.PortRange{ @@ -8075,7 +8076,7 @@ func TestSrcCapPacketFilterUnsignedPeer(t *testing.T) { }, PacketFilter: []filtertype.Match{{ IPProto: views.SliceOf([]ipproto.Proto{ipproto.TCP}), - SrcCaps: []tailcfg.NodeCapability{"cap-X"}, + SrcCaps: []nodecap.Cap{"cap-X"}, Dsts: []filtertype.NetPortRange{{ Net: netip.MustParsePrefix("1.1.1.1/32"), Ports: filtertype.PortRange{ @@ -8897,7 +8898,7 @@ func TestRouteAllDisabled(t *testing.T) { pp("100.64.1.1/32"), }, CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{ + nodecap.ServiceHost: []tailcfg.RawMessage{ tailcfg.RawMessage(svcIPMapJSON), }, }, diff --git a/ipn/ipnlocal/netmapcache/netmapcache.go b/ipn/ipnlocal/netmapcache/netmapcache.go index 3c4b01cf3..531208a44 100644 --- a/ipn/ipnlocal/netmapcache/netmapcache.go +++ b/ipn/ipnlocal/netmapcache/netmapcache.go @@ -24,6 +24,7 @@ "tailscale.com/feature/buildfeatures" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/netmap" "tailscale.com/util/mak" "tailscale.com/util/set" @@ -339,7 +340,7 @@ func (c *Cache) Load(ctx context.Context) (*netmap.NetworkMap, error) { // If we successfully recovered a SelfNode, pull out its related fields. if s := nm.SelfNode; s.Valid() { nm.NodeKey = s.Key() - nm.AllCaps = make(set.Set[tailcfg.NodeCapability]) + nm.AllCaps = make(set.Set[nodecap.Cap]) for _, c := range s.Capabilities().All() { nm.AllCaps.Add(c) } diff --git a/ipn/ipnlocal/netmapcache/netmapcache_test.go b/ipn/ipnlocal/netmapcache/netmapcache_test.go index 884e450e4..befedccc1 100644 --- a/ipn/ipnlocal/netmapcache/netmapcache_test.go +++ b/ipn/ipnlocal/netmapcache/netmapcache_test.go @@ -23,6 +23,7 @@ "github.com/google/go-cmp/cmp/cmpopts" "tailscale.com/ipn/ipnlocal/netmapcache" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tka" "tailscale.com/types/ipproto" "tailscale.com/types/key" @@ -119,12 +120,12 @@ func init() { User: 30337, Name: "test.example.com.", Key: testNodeKey, - Capabilities: []tailcfg.NodeCapability{"cap1"}, - CapMap: map[tailcfg.NodeCapability][]tailcfg.RawMessage{ + Capabilities: []nodecap.Cap{"cap1"}, + CapMap: map[nodecap.Cap][]tailcfg.RawMessage{ "cap2": nil, }, }).View(), - AllCaps: set.Of[tailcfg.NodeCapability]("cap1", "cap2"), + AllCaps: set.Of[nodecap.Cap]("cap1", "cap2"), NodeKey: testNodeKey, DNS: tailcfg.DNSConfig{Domains: []string{"example1.com", "example2.ac.uk"}}, // "dns" @@ -276,11 +277,11 @@ func TestUpdateSelfOnly(t *testing.T) { Name: "alt.example.com.", Key: testNodeKey, HomeDERP: 6174, - Capabilities: []tailcfg.NodeCapability{"cap1", "cap3"}, + Capabilities: []nodecap.Cap{"cap1", "cap3"}, } updated := *testMap // shallow copy updated.SelfNode = newSelf.View() - updated.AllCaps = set.Of[tailcfg.NodeCapability]("cap1", "cap3") + updated.AllCaps = set.Of[nodecap.Cap]("cap1", "cap3") updated.DNS = tailcfg.DNSConfig{Domains: []string{"example3.org", "example4.horse"}} // Empty the peers and profiles so that we can verify the update does not diff --git a/ipn/ipnlocal/node_backend.go b/ipn/ipnlocal/node_backend.go index bf79600b0..68e065880 100644 --- a/ipn/ipnlocal/node_backend.go +++ b/ipn/ipnlocal/node_backend.go @@ -24,6 +24,8 @@ "tailscale.com/net/tsaddr" "tailscale.com/syncs" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/dnstype" "tailscale.com/types/key" "tailscale.com/types/logger" @@ -227,13 +229,13 @@ func (nb *nodeBackend) SelfUserID() tailcfg.UserID { } // SelfHasCap reports whether the specified capability was granted to the self node in the most recent netmap. -func (nb *nodeBackend) SelfHasCap(wantCap tailcfg.NodeCapability) bool { +func (nb *nodeBackend) SelfHasCap(wantCap nodecap.Cap) bool { return nb.SelfHasCapOr(wantCap, false) } // SelfHasCapOr is like [nodeBackend.SelfHasCap], but returns the specified default value // if the netmap is not available yet. -func (nb *nodeBackend) SelfHasCapOr(wantCap tailcfg.NodeCapability, def bool) bool { +func (nb *nodeBackend) SelfHasCapOr(wantCap nodecap.Cap, def bool) bool { nb.mu.Lock() defer nb.mu.Unlock() if nb.netMap == nil { @@ -521,7 +523,7 @@ func (nb *nodeBackend) PeerCapsForService(src netip.Addr, svcName tailcfg.Servic // PeerHasCap reports whether the peer contains the given capability string, // with any value(s). -func (nb *nodeBackend) PeerHasCap(peer tailcfg.NodeView, wantCap tailcfg.PeerCapability) bool { +func (nb *nodeBackend) PeerHasCap(peer tailcfg.NodeView, wantCap peercap.Cap) bool { if !peer.Valid() { return false } @@ -536,7 +538,7 @@ func (nb *nodeBackend) PeerHasCap(peer tailcfg.NodeView, wantCap tailcfg.PeerCap return false } -func (nb *nodeBackend) peerHasCapLocked(addr netip.Addr, wantCap tailcfg.PeerCapability) bool { +func (nb *nodeBackend) peerHasCapLocked(addr netip.Addr, wantCap peercap.Cap) bool { return nb.peerCapsLocked(addr).HasCapability(wantCap) } @@ -583,7 +585,7 @@ func (nb *nodeBackend) PeerIsReachable(rp RouteCheckReport, p tailcfg.NodeView) self := nm.SelfNode useRouteCheck := isRouteCheckEnabled(self) - if !useRouteCheck && !self.HasCap(tailcfg.NodeAttrClientSideReachability) { + if !useRouteCheck && !self.HasCap(nodecap.ClientSideReachability) { // Legacy behavior is to always trust the control plane, which // isn’t always correct because the peer could be slow to check // in so that control marks it as offline. @@ -596,7 +598,7 @@ func (nb *nodeBackend) PeerIsReachable(rp RouteCheckReport, p tailcfg.NodeView) return true } - if !useRouteCheck && !self.HasCap(tailcfg.NodeAttrClientSideReachabilityRouteCheck) { + if !useRouteCheck && !self.HasCap(nodecap.ClientSideReachabilityRouteCheck) { // TODO(sfllaw): The following does not actually test for client-side // reachability. This would require a mechanism that tracks whether the // current node can actually reach this peer, either because they are @@ -1231,7 +1233,7 @@ func (nb *nodeBackend) magicDNSHostAddrs(fqdn dnsname.FQDN) (ips []netip.Addr, o !nm.GetAddresses().ContainsFunc(tsaddr.PrefixIs4) { flags |= selfV6Only } - if nm.AllCaps.Contains(tailcfg.NodeAttrMagicDNSPeerAAAA) { + if nm.AllCaps.Contains(nodecap.MagicDNSPeerAAAA) { flags |= wantAAAA } return magicDNSAddrs(n.Addresses(), flags), true @@ -1274,9 +1276,9 @@ func (nb *nodeBackend) magicDNSSubdomainHost(fqdn dnsname.FQDN) bool { return false } if nm := nb.netMap; nm != nil && nm.SelfNode.Valid() && nm.SelfNode.ID() == n.ID() { - return nm.AllCaps.Contains(tailcfg.NodeAttrDNSSubdomainResolve) + return nm.AllCaps.Contains(nodecap.DNSSubdomainResolve) } - return n.CapMap().Contains(tailcfg.NodeAttrDNSSubdomainResolve) + return n.CapMap().Contains(nodecap.DNSSubdomainResolve) } // nodeByFQDNLocked returns the node (peer or self) with the given @@ -1474,7 +1476,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg. addrFlags |= selfV6Only dcfg.OnlyIPv6 = true } - if nm.AllCaps.Contains(tailcfg.NodeAttrMagicDNSPeerAAAA) { + if nm.AllCaps.Contains(nodecap.MagicDNSPeerAAAA) { addrFlags |= wantAAAA } diff --git a/ipn/ipnlocal/node_backend_test.go b/ipn/ipnlocal/node_backend_test.go index 991ccf299..3548ccb4f 100644 --- a/ipn/ipnlocal/node_backend_test.go +++ b/ipn/ipnlocal/node_backend_test.go @@ -15,6 +15,7 @@ "tailscale.com/net/routecheck/peernode" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstest" "tailscale.com/types/key" "tailscale.com/types/netmap" @@ -239,10 +240,10 @@ func TestNodeBackendReachability(t *testing.T) { Name: "self", } if tc.cap { - mak.Set(&self.CapMap, tailcfg.NodeAttrClientSideReachability, nil) + mak.Set(&self.CapMap, nodecap.ClientSideReachability, nil) } if tc.rchk { - mak.Set(&self.CapMap, tailcfg.NodeAttrClientSideReachabilityRouteCheck, nil) + mak.Set(&self.CapMap, nodecap.ClientSideReachabilityRouteCheck, nil) } peer := &tailcfg.Node{ @@ -584,7 +585,7 @@ func TestNodeBackendMagicDNSHosts(t *testing.T) { netip.MustParsePrefix("100.64.0.2/32"), netip.MustParsePrefix("fd7a:115c:a1e0::2/128"), }, - CapMap: tailcfg.NodeCapMap{tailcfg.NodeAttrDNSSubdomainResolve: nil}, + CapMap: tailcfg.NodeCapMap{nodecap.DNSSubdomainResolve: nil}, } nb.SetNetMap(&netmap.NetworkMap{ SelfNode: self.View(), diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index 53d1e6d35..6d4004392 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -35,6 +35,8 @@ "tailscale.com/net/netutil" "tailscale.com/net/sockstats" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/netmap" "tailscale.com/types/views" "tailscale.com/util/clientmetric" @@ -578,7 +580,7 @@ func (h *peerAPIHandler) CanDebug() bool { return h.canDebug() } // canDebug reports whether h can debug this node (goroutines, metrics, // magicsock internal state, etc). func (h *peerAPIHandler) canDebug() bool { - if !h.selfNode.HasCap(tailcfg.CapabilityDebug) { + if !h.selfNode.HasCap(nodecap.Debug) { // This node does not expose debug info. return false } @@ -586,17 +588,17 @@ func (h *peerAPIHandler) canDebug() bool { // Unsigned peers can't debug. return false } - return h.isSelf || h.peerHasCap(tailcfg.PeerCapabilityDebugPeer) + return h.isSelf || h.peerHasCap(peercap.DebugPeer) } var allowSelfIngress = envknob.RegisterBool("TS_ALLOW_SELF_INGRESS") // canIngress reports whether h can send ingress requests to this node. func (h *peerAPIHandler) canIngress() bool { - return h.peerHasCap(tailcfg.PeerCapabilityIngress) || (allowSelfIngress() && h.isSelf) + return h.peerHasCap(peercap.Ingress) || (allowSelfIngress() && h.isSelf) } -func (h *peerAPIHandler) peerHasCap(wantCap tailcfg.PeerCapability) bool { +func (h *peerAPIHandler) peerHasCap(wantCap peercap.Cap) bool { return h.PeerCaps().HasCapability(wantCap) } diff --git a/ipn/ipnlocal/peerapi_drive.go b/ipn/ipnlocal/peerapi_drive.go index d42843577..6bdae3d9d 100644 --- a/ipn/ipnlocal/peerapi_drive.go +++ b/ipn/ipnlocal/peerapi_drive.go @@ -11,7 +11,7 @@ "strings" "tailscale.com/drive" - "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/util/httpm" ) @@ -34,7 +34,7 @@ func handleServeDrive(hi PeerAPIHandler, w http.ResponseWriter, r *http.Request) } capsMap := h.PeerCaps() - driveCaps, ok := capsMap[tailcfg.PeerCapabilityTaildrive] + driveCaps, ok := capsMap[peercap.Taildrive] if !ok { h.logf("taildrive: not permitted") http.Error(w, "taildrive not permitted", http.StatusForbidden) diff --git a/ipn/ipnlocal/peerapi_test.go b/ipn/ipnlocal/peerapi_test.go index 63abf089c..fc31ed7a5 100644 --- a/ipn/ipnlocal/peerapi_test.go +++ b/ipn/ipnlocal/peerapi_test.go @@ -21,6 +21,7 @@ "tailscale.com/ipn" "tailscale.com/ipn/store/mem" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tsd" "tailscale.com/tstest" "tailscale.com/types/appctype" @@ -155,7 +156,7 @@ func TestHandlePeerAPI(t *testing.T) { }, } if tt.debugCap { - selfNode.CapMap = tailcfg.NodeCapMap{tailcfg.CapabilityDebug: nil} + selfNode.CapMap = tailcfg.NodeCapMap{nodecap.Debug: nil} } var e peerAPITestEnv lb := newTestLocalBackend(t) diff --git a/ipn/ipnlocal/serve.go b/ipn/ipnlocal/serve.go index 0012a214b..3730123a7 100644 --- a/ipn/ipnlocal/serve.go +++ b/ipn/ipnlocal/serve.go @@ -40,6 +40,7 @@ "tailscale.com/net/netutil" "tailscale.com/syncs" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/lazy" "tailscale.com/types/logger" "tailscale.com/types/views" @@ -92,7 +93,7 @@ type serveHTTPContext struct { // provides funnel-specific context, nil if not funneled Funnel *funnelFlow // AppCapabilities lists all PeerCapabilities that should be forwarded by serve - AppCapabilities views.Slice[tailcfg.PeerCapability] + AppCapabilities views.Slice[peercap.Cap] } // funnelFlow represents a funneled connection initiated via IngressPeer @@ -1137,7 +1138,7 @@ func (b *LocalBackend) addAppCapabilitiesHeader(r *httputil.ProxyRequest) error return nil } - peerCapsFiltered := make(map[tailcfg.PeerCapability][]tailcfg.RawMessage, acceptCaps.Len()) + peerCapsFiltered := make(map[peercap.Cap][]tailcfg.RawMessage, acceptCaps.Len()) for _, cap := range acceptCaps.AsSlice() { if peerCaps.HasCapability(cap) { peerCapsFiltered[cap] = peerCaps[cap] diff --git a/ipn/ipnlocal/serve_test.go b/ipn/ipnlocal/serve_test.go index 205dc674e..84c0b293f 100644 --- a/ipn/ipnlocal/serve_test.go +++ b/ipn/ipnlocal/serve_test.go @@ -33,6 +33,8 @@ "tailscale.com/ipn" "tailscale.com/ipn/store/mem" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/tsd" "tailscale.com/tstest" "tailscale.com/types/logger" @@ -395,7 +397,7 @@ func TestServeConfigServices(t *testing.T) { SelfNode: (&tailcfg.Node{ Name: "example.ts.net", CapMap: tailcfg.NodeCapMap{ - tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{tailcfg.RawMessage(svcIPMapJSON)}, + nodecap.ServiceHost: []tailcfg.RawMessage{tailcfg.RawMessage(svcIPMapJSON)}, }, }).View(), UserProfiles: map[tailcfg.UserID]tailcfg.UserProfileView{ @@ -893,7 +895,7 @@ func(w http.ResponseWriter, r *http.Request) { "example.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{ "/": { Proxy: testServ.URL, - AcceptAppCaps: []tailcfg.PeerCapability{"example.com/cap/interesting", "example.com/cap/boring"}, + AcceptAppCaps: []peercap.Cap{"example.com/cap/interesting", "example.com/cap/boring"}, }, }}, }, diff --git a/ipn/ipnstate/ipnstate.go b/ipn/ipnstate/ipnstate.go index 3e2c79151..25a7e6e03 100644 --- a/ipn/ipnstate/ipnstate.go +++ b/ipn/ipnstate/ipnstate.go @@ -18,6 +18,7 @@ "time" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tka" "tailscale.com/types/key" "tailscale.com/types/views" @@ -302,7 +303,7 @@ type PeerStatus struct { // Deprecated: use CapMap instead. See https://github.com/tailscale/tailscale/issues/11508 // Every value is Capabilities is also a key in CapMap, even if it // has no values in that map. - Capabilities []tailcfg.NodeCapability `json:",omitempty"` + Capabilities []nodecap.Cap `json:",omitempty"` // CapMap is a map of capabilities to their values. CapMap tailcfg.NodeCapMap `json:",omitempty"` @@ -356,7 +357,7 @@ type PeerStatus struct { ) // HasCap reports whether ps has the given capability. -func (ps *PeerStatus) HasCap(cap tailcfg.NodeCapability) bool { +func (ps *PeerStatus) HasCap(cap nodecap.Cap) bool { return ps.CapMap.Contains(cap) } diff --git a/ipn/localapi/localapi_test.go b/ipn/localapi/localapi_test.go index cc5a6b6a7..2a5db08db 100644 --- a/ipn/localapi/localapi_test.go +++ b/ipn/localapi/localapi_test.go @@ -32,6 +32,7 @@ "tailscale.com/ipn/ipnstate" "tailscale.com/ipn/store/mem" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/tsd" "tailscale.com/tstest" "tailscale.com/types/key" @@ -191,7 +192,7 @@ func TestWhoIsArgTypes(t *testing.T) { return match() }, peerCaps: map[netip.Addr]tailcfg.PeerCapMap{ - netip.MustParseAddr("100.101.102.103"): map[tailcfg.PeerCapability][]tailcfg.RawMessage{ + netip.MustParseAddr("100.101.102.103"): map[peercap.Cap][]tailcfg.RawMessage{ "foo": {`"bar"`}, }, }, diff --git a/ipn/serve.go b/ipn/serve.go index 5c1fa2975..57ada0d26 100644 --- a/ipn/serve.go +++ b/ipn/serve.go @@ -17,6 +17,8 @@ "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/ipproto" "tailscale.com/util/dnsname" "tailscale.com/util/mak" @@ -171,7 +173,7 @@ type HTTPHandler struct { Text string `json:",omitempty"` // plaintext to serve (primarily for testing) - AcceptAppCaps []tailcfg.PeerCapability `json:",omitempty"` // peer capabilities to forward in grant header, e.g. example.com/cap/mon + AcceptAppCaps []peercap.Cap `json:",omitempty"` // peer capabilities to forward in grant header, e.g. example.com/cap/mon // Redirect, if not empty, is the target URL to redirect requests to. // By default, we redirect with HTTP 302 (Found) status. @@ -652,10 +654,10 @@ func CheckFunnelAccess(port uint16, node *ipnstate.PeerStatus) error { // NodeCanFunnel returns an error if the given node is not configured to allow // for Tailscale Funnel usage. func NodeCanFunnel(node *ipnstate.PeerStatus) error { - if !node.HasCap(tailcfg.CapabilityHTTPS) { + if !node.HasCap(nodecap.HTTPS) { return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/s/https.") } - if !node.HasCap(tailcfg.NodeAttrFunnel) { + if !node.HasCap(nodecap.Funnel) { return errors.New("Funnel not available; \"funnel\" node attribute not set. See https://tailscale.com/s/no-funnel.") } return nil @@ -682,14 +684,14 @@ func CheckFunnelPort(wantedPort uint16, node *ipnstate.PeerStatus) error { return "", deny("") } u.RawQuery = "" - if u.String() != string(tailcfg.CapabilityFunnelPorts) { + if u.String() != string(nodecap.FunnelPorts) { return "", deny("") } return portsStr, nil } for attr := range node.CapMap { attr := string(attr) - if !strings.HasPrefix(attr, string(tailcfg.CapabilityFunnelPorts)) { + if !strings.HasPrefix(attr, string(nodecap.FunnelPorts)) { continue } var err error @@ -702,7 +704,7 @@ func CheckFunnelPort(wantedPort uint16, node *ipnstate.PeerStatus) error { if portsStr == "" { for attr := range node.CapMap { attr := string(attr) - if !strings.HasPrefix(attr, string(tailcfg.CapabilityFunnelPorts)) { + if !strings.HasPrefix(attr, string(nodecap.FunnelPorts)) { continue } var err error diff --git a/ipn/serve_test.go b/ipn/serve_test.go index b8a7aee79..7b19e65bc 100644 --- a/ipn/serve_test.go +++ b/ipn/serve_test.go @@ -8,24 +8,25 @@ "tailscale.com/ipn/ipnstate" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" ) func TestCheckFunnelAccess(t *testing.T) { - caps := func(c ...tailcfg.NodeCapability) []tailcfg.NodeCapability { return c } - const portAttr tailcfg.NodeCapability = "https://tailscale.com/cap/funnel-ports?ports=443,8080-8090,8443," + caps := func(c ...nodecap.Cap) []nodecap.Cap { return c } + const portAttr nodecap.Cap = "https://tailscale.com/cap/funnel-ports?ports=443,8080-8090,8443," tests := []struct { port uint16 - caps []tailcfg.NodeCapability + caps []nodecap.Cap wantErr bool }{ {443, caps(portAttr), true}, // No "funnel" attribute - {443, caps(portAttr, tailcfg.NodeAttrFunnel), true}, - {443, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), false}, - {8443, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), false}, - {8321, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true}, - {8083, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), false}, - {8091, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true}, - {3000, caps(portAttr, tailcfg.CapabilityHTTPS, tailcfg.NodeAttrFunnel), true}, + {443, caps(portAttr, nodecap.Funnel), true}, + {443, caps(portAttr, nodecap.HTTPS, nodecap.Funnel), false}, + {8443, caps(portAttr, nodecap.HTTPS, nodecap.Funnel), false}, + {8321, caps(portAttr, nodecap.HTTPS, nodecap.Funnel), true}, + {8083, caps(portAttr, nodecap.HTTPS, nodecap.Funnel), false}, + {8091, caps(portAttr, nodecap.HTTPS, nodecap.Funnel), true}, + {3000, caps(portAttr, nodecap.HTTPS, nodecap.Funnel), true}, } for _, tt := range tests { cm := tailcfg.NodeCapMap{} diff --git a/k8s-operator/api-proxy/proxy.go b/k8s-operator/api-proxy/proxy.go index 73a9884b2..47a65d17b 100644 --- a/k8s-operator/api-proxy/proxy.go +++ b/k8s-operator/api-proxy/proxy.go @@ -35,6 +35,7 @@ "tailscale.com/net/netx" "tailscale.com/sessionrecording" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/tsnet" "tailscale.com/util/clientmetric" "tailscale.com/util/ctxkey" @@ -501,7 +502,7 @@ func (ap *APIServerProxy) authError(w http.ResponseWriter, err error) { // that is respected for this form is group impersonation - for // backwards compatibility reasons. // TODO (irbekrm): determine if anyone uses this and remove if possible. - oldCapabilityName = "https://" + tailcfg.PeerCapabilityKubernetes + oldCapabilityName = "https://" + peercap.Kubernetes ) // addImpersonationHeaders adds the appropriate headers to r to impersonate the @@ -510,7 +511,7 @@ func (ap *APIServerProxy) authError(w http.ResponseWriter, err error) { func addImpersonationHeaders(r *http.Request, log *zap.SugaredLogger) error { log = log.With("remote", r.RemoteAddr) who := whoIsKey.Value(r.Context()) - rules, err := tailcfg.UnmarshalCapJSON[kubetypes.KubernetesCapRule](who.CapMap, tailcfg.PeerCapabilityKubernetes) + rules, err := tailcfg.UnmarshalCapJSON[kubetypes.KubernetesCapRule](who.CapMap, peercap.Kubernetes) if len(rules) == 0 && err == nil { // Try the old capability name for backwards compatibility. rules, err = tailcfg.UnmarshalCapJSON[kubetypes.KubernetesCapRule](who.CapMap, oldCapabilityName) @@ -571,7 +572,7 @@ func determineRecorderConfig(who *apitype.WhoIsResponse) (c recorderConfig, _ er c.failOpen = true c.enableEvents = false - rules, err := tailcfg.UnmarshalCapJSON[kubetypes.KubernetesCapRule](who.CapMap, tailcfg.PeerCapabilityKubernetes) + rules, err := tailcfg.UnmarshalCapJSON[kubetypes.KubernetesCapRule](who.CapMap, peercap.Kubernetes) if err != nil { return c, fmt.Errorf("failed to unmarshal Kubernetes capability: %w", err) } diff --git a/k8s-operator/api-proxy/proxy_events_test.go b/k8s-operator/api-proxy/proxy_events_test.go index 1426f170c..7def9d303 100644 --- a/k8s-operator/api-proxy/proxy_events_test.go +++ b/k8s-operator/api-proxy/proxy_events_test.go @@ -22,6 +22,7 @@ "tailscale.com/net/netx" "tailscale.com/sessionrecording" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/tsnet" ) @@ -73,7 +74,7 @@ func TestRecordRequestAsEvent(t *testing.T) { LoginName: "user@example.com", }, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityKubernetes: []tailcfg.RawMessage{ + peercap.Kubernetes: []tailcfg.RawMessage{ tailcfg.RawMessage(`{"recorderAddrs":["127.0.0.1:1234"]}`), tailcfg.RawMessage(`{"enforceRecorder": true, "enableEvents": true}`), }, @@ -307,7 +308,7 @@ func TestRecordRequestAsEvent(t *testing.T) { Node: defaultWho.Node, UserProfile: defaultWho.UserProfile, CapMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityKubernetes: []tailcfg.RawMessage{ + peercap.Kubernetes: []tailcfg.RawMessage{ tailcfg.RawMessage(`{"recorderAddrs":["127.0.0.1:1234", "127.0.0.1:5678"]}`), tailcfg.RawMessage(`{"enforceRecorder": true, "enableEvents": true}`), }, diff --git a/k8s-operator/api-proxy/proxy_test.go b/k8s-operator/api-proxy/proxy_test.go index 5d1606d76..c5b39a6fd 100644 --- a/k8s-operator/api-proxy/proxy_test.go +++ b/k8s-operator/api-proxy/proxy_test.go @@ -15,6 +15,7 @@ "go.uber.org/zap" "tailscale.com/client/tailscale/apitype" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/util/must" ) @@ -51,7 +52,7 @@ func TestImpersonationHeaders(t *testing.T) { name: "user-with-cap", emailish: "foo@example.com", capMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityKubernetes: { + peercap.Kubernetes: { tailcfg.RawMessage(`{"impersonate":{"groups":["group1","group2"]}}`), tailcfg.RawMessage(`{"impersonate":{"groups":["group1","group3"]}}`), // One group is duplicated. tailcfg.RawMessage(`{"impersonate":{"groups":["group4"]}}`), @@ -73,7 +74,7 @@ func TestImpersonationHeaders(t *testing.T) { emailish: "tagged-device", tags: []string{"tag:foo", "tag:bar"}, capMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityKubernetes: { + peercap.Kubernetes: { tailcfg.RawMessage(`{"impersonate":{"groups":["group1"]}}`), }, }, @@ -87,7 +88,7 @@ func TestImpersonationHeaders(t *testing.T) { emailish: "tagged-device", tags: []string{"tag:foo", "tag:bar"}, capMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityKubernetes: { + peercap.Kubernetes: { tailcfg.RawMessage(`{"impersonate":{"groups":["group1"]},"recorder":["tag:foo"],"enforceRecorder":true}`), }, }, @@ -101,7 +102,7 @@ func TestImpersonationHeaders(t *testing.T) { emailish: "tagged-device", tags: []string{"tag:foo", "tag:bar"}, capMap: tailcfg.PeerCapMap{ - tailcfg.PeerCapabilityKubernetes: { + peercap.Kubernetes: { tailcfg.RawMessage(`[]`), }, }, @@ -139,18 +140,18 @@ func Test_determineRecorderConfig(t *testing.T) { }{ { name: "two_ips_fail_closed", - who: whoResp(map[string][]string{string(tailcfg.PeerCapabilityKubernetes): {`{"recorderAddrs":["[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80","100.99.99.99:80"],"enforceRecorder":true}`}}), + who: whoResp(map[string][]string{string(peercap.Kubernetes): {`{"recorderAddrs":["[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80","100.99.99.99:80"],"enforceRecorder":true}`}}), wantRecorderAddresses: []netip.AddrPort{addr1, addr2}, }, { name: "two_ips_fail_open", - who: whoResp(map[string][]string{string(tailcfg.PeerCapabilityKubernetes): {`{"recorderAddrs":["[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80","100.99.99.99:80"]}`}}), + who: whoResp(map[string][]string{string(peercap.Kubernetes): {`{"recorderAddrs":["[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80","100.99.99.99:80"]}`}}), wantRecorderAddresses: []netip.AddrPort{addr1, addr2}, wantFailOpen: true, }, { name: "odd_rule_combination_fail_closed", - who: whoResp(map[string][]string{string(tailcfg.PeerCapabilityKubernetes): {`{"recorderAddrs":["100.99.99.99:80"],"enforceRecorder":false}`, `{"recorderAddrs":["[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80"]}`, `{"enforceRecorder":true,"impersonate":{"groups":["system:masters"]}}`}}), + who: whoResp(map[string][]string{string(peercap.Kubernetes): {`{"recorderAddrs":["100.99.99.99:80"],"enforceRecorder":false}`, `{"recorderAddrs":["[fd7a:115c:a1e0:ab12:4843:cd96:626b:628b]:80"]}`, `{"enforceRecorder":true,"impersonate":{"groups":["system:masters"]}}`}}), wantRecorderAddresses: []netip.AddrPort{addr2, addr1}, }, { @@ -160,7 +161,7 @@ func Test_determineRecorderConfig(t *testing.T) { }, { name: "no_recorder_caps", - who: whoResp(map[string][]string{"foo": {`{"x":"y"}`}, string(tailcfg.PeerCapabilityKubernetes): {`{"impersonate":{"groups":["system:masters"]}}`}}), + who: whoResp(map[string][]string{"foo": {`{"x":"y"}`}, string(peercap.Kubernetes): {`{"impersonate":{"groups":["system:masters"]}}`}}), wantFailOpen: true, }, } @@ -185,7 +186,7 @@ func whoResp(capMap map[string][]string) *apitype.WhoIsResponse { CapMap: tailcfg.PeerCapMap{}, } for cap, rules := range capMap { - resp.CapMap[tailcfg.PeerCapability(cap)] = raw(rules...) + resp.CapMap[peercap.Cap(cap)] = raw(rules...) } return resp } diff --git a/net/dns/resolver/forwarder.go b/net/dns/resolver/forwarder.go index bd1245ad1..ef609cae9 100644 --- a/net/dns/resolver/forwarder.go +++ b/net/dns/resolver/forwarder.go @@ -908,7 +908,7 @@ func ShouldUseRoutes(knobs *controlknobs.Knobs) bool { switch runtime.GOOS { case "android", "ios": // On mobile platforms with lower memory limits (e.g., 50MB on iOS), - // this behavior is still gated by the "user-dial-routes" nodeAttr. + // this behavior is still gated by the "user-dial-routes" nodecap. return knobs != nil && knobs.UserDialUseRoutes.Load() default: // On all other platforms, it is the default behavior, diff --git a/net/routecheck/routecheck.go b/net/routecheck/routecheck.go index 5195b12b7..cf0d11320 100644 --- a/net/routecheck/routecheck.go +++ b/net/routecheck/routecheck.go @@ -16,6 +16,7 @@ "tailscale.com/ipn/ipnstate" "tailscale.com/net/netmon" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/logger" "tailscale.com/types/netmap" "tailscale.com/util/clientmetric" @@ -43,8 +44,8 @@ func IsEnabled(self tailcfg.NodeView) bool { return false } // TODO(sfllaw): We intend to eventually enable this behaviour by default. - return self.HasCap(tailcfg.NodeAttrClientSideReachability) && - self.HasCap(tailcfg.NodeAttrClientSideReachabilityRouteCheck) + return self.HasCap(nodecap.ClientSideReachability) && + self.HasCap(nodecap.ClientSideReachabilityRouteCheck) } // Client generates Reports describing the result of both passive and active diff --git a/ssh/tailssh/incubator.go b/ssh/tailssh/incubator.go index faf70af90..5eb409a5f 100644 --- a/ssh/tailssh/incubator.go +++ b/ssh/tailssh/incubator.go @@ -41,7 +41,7 @@ "golang.org/x/sys/unix" "tailscale.com/cmd/tailscaled/childproc" "tailscale.com/hostinfo" - "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/logger" "tailscale.com/version/distro" ) @@ -209,7 +209,7 @@ func (ss *sshSession) newIncubatorCommand(logf logger.Logf) (cmd *exec.Cmd, forw } nm := ss.conn.srv.lb.NetMapNoPeers() - forceV1Behavior := nm.HasCap(tailcfg.NodeAttrSSHBehaviorV1) && !nm.HasCap(tailcfg.NodeAttrSSHBehaviorV2) + forceV1Behavior := nm.HasCap(nodecap.SSHBehaviorV1) && !nm.HasCap(nodecap.SSHBehaviorV2) if forceV1Behavior { incubatorArgs = append(incubatorArgs, "--force-v1-behavior") } @@ -233,7 +233,7 @@ func (ss *sshSession) newIncubatorCommand(logf logger.Logf) (cmd *exec.Cmd, forw incubatorArgs = append(incubatorArgs, "--cmd="+ss.RawCommand()) } - allowSendEnv := nm.HasCap(tailcfg.NodeAttrSSHEnvironmentVariables) + allowSendEnv := nm.HasCap(nodecap.SSHEnvironmentVariables) if allowSendEnv { env, err := filterEnv(ss.conn.acceptEnv, ss.Session.Environ()) if err != nil { diff --git a/tailcfg/nodecap/nodecap.go b/tailcfg/nodecap/nodecap.go new file mode 100644 index 000000000..387c8c6a7 --- /dev/null +++ b/tailcfg/nodecap/nodecap.go @@ -0,0 +1,442 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +// Package nodecap defines the types of capabilities granted to tailnet nodes. +package nodecap + +// Cap represents a capability granted to the self node +// as listed in [tailscale.com/tailcfg/MapResponse.Node.Capabilities]. +// +// It must be a URL like "https://tailscale.com/cap/file-sharing" or "tailscale.com/cap/webui", +// or a well-known capability name like "funnel". +// The latter is only allowed for Tailscale-defined capabilities. +// +// Unlike [tailscale.com/tailcfg/peercap.Cap], +// Cap is not in context of a peer and is granted to the node itself. +// +// These are also referred to as "Node Attributes" in the ACL policy file. +type Cap string + +// Prefix is a prefix for [tailscale.com/tailcfg.NodeCapMap] keys that share a common namespace, +// where each entry represents a distinct named instance (e.g. one per service). +// The full key is formed by concatenating the prefix with the instance name. +type Prefix string + +// ToAttribute returns the full [Cap] key for the given value under this prefix, +// of the form prefix+value. +func (p Prefix) ToAttribute(value string) Cap { + return Cap(string(p) + value) +} + +const ( + FileSharing Cap = "https://tailscale.com/cap/file-sharing" + Admin Cap = "https://tailscale.com/cap/is-admin" + Owner Cap = "https://tailscale.com/cap/is-owner" + SSH Cap = "https://tailscale.com/cap/ssh" // feature enabled/available + SSHRuleIn Cap = "https://tailscale.com/cap/ssh-rule-in" // some SSH rule reach this node + DataPlaneAuditLogs Cap = "https://tailscale.com/cap/data-plane-audit-logs" // feature enabled + Debug Cap = "https://tailscale.com/cap/debug" // exposes debug endpoints over the PeerAPI + HTTPS Cap = "https" + + // MacUIV2 makes the macOS GUI enable its v2 mode. + MacUIV2 Cap = "https://tailscale.com/cap/mac-ui-v2" + + // ServicesInDesktopClients enables services list/menu/section in desktop clients. + // If this capability is not present, desktop clients should not show services. + ServicesInDesktopClients Cap = "https://tailscale.com/cap/services-in-desktop-clients" + + // BindToInterfaceByRoute changes how Darwin nodes create + // sockets (in the net/netns package). See that package for more + // details on the behaviour of this capability. + BindToInterfaceByRoute Cap = "https://tailscale.com/cap/bind-to-interface-by-route" + + // DisableAndroidBindToActiveNetwork disables binding sockets to the + // currently active network on Android, which is enabled by default. + // This allows the control plane to turn off the behavior if it causes + // problems. + DisableAndroidBindToActiveNetwork Cap = "disable-android-bind-to-active-network" + + // DebugDisableAlternateDefaultRouteInterface changes how Darwin + // nodes get the default interface. There is an optional hook (used by the + // macOS and iOS clients) to override the default interface, this capability + // disables that and uses the default behavior (of parsing the routing + // table). + DebugDisableAlternateDefaultRouteInterface Cap = "https://tailscale.com/cap/debug-disable-alternate-default-route-interface" + + // DebugDisableBindConnToInterface disables the automatic binding + // of connections to the default network interface on Darwin nodes. + DebugDisableBindConnToInterface Cap = "https://tailscale.com/cap/debug-disable-bind-conn-to-interface" + + // DebugDisableBindConnToInterface disables the automatic binding + // of connections to the default network interface on Darwin nodes using network extensions + DebugDisableBindConnToInterfaceAppleExt Cap = "https://tailscale.com/cap/debug-disable-bind-conn-to-interface-apple-ext" + + // TailnetLock indicates the node may initialize tailnet lock. + TailnetLock Cap = "https://tailscale.com/cap/tailnet-lock" + + //// Funnel warning capabilities used for reporting errors to the user. + + // WarnFunnelNoInvite indicates whether Funnel is enabled for the tailnet. + // This cap is no longer used 2023-08-09 onwards. + WarnFunnelNoInvite Cap = "https://tailscale.com/cap/warn-funnel-no-invite" + + // WarnFunnelNoHTTPS indicates HTTPS has not been enabled for the tailnet. + // This cap is no longer used 2023-08-09 onwards. + WarnFunnelNoHTTPS Cap = "https://tailscale.com/cap/warn-funnel-no-https" + + //// Debug logging capabilities + + // DebugTSDNSResolution enables verbose debug logging for DNS + // resolution for Tailscale-controlled domains (the control server, log + // server, DERP servers, etc.) + DebugTSDNSResolution Cap = "https://tailscale.com/cap/debug-ts-dns-resolution" + + // FunnelPorts specifies the ports that the Funnel is available on. + // The ports are specified as a comma-separated list of port numbers or port + // ranges (e.g. "80,443,8080-8090") in the ports query parameter. + // e.g. https://tailscale.com/cap/funnel-ports?ports=80,443,8080-8090 + FunnelPorts Cap = "https://tailscale.com/cap/funnel-ports" + + // OnlyTCP443 specifies that the client should not attempt to generate + // any outbound traffic that isn't TCP on port 443 (HTTPS). This is used for + // clients in restricted environments where only HTTPS traffic is allowed + // other types of traffic trips outbound firewall alarms. This thus implies + // all traffic is over DERP. + OnlyTCP443 Cap = "only-tcp-443" + + // Funnel grants the ability for a node to host ingress traffic. + Funnel Cap = "funnel" + // SSHAggregator grants the ability for a node to collect SSH sessions. + SSHAggregator Cap = "ssh-aggregator" + + // DebugForceBackgroundSTUN forces a node to always do background + // STUN queries regardless of inactivity. + DebugForceBackgroundSTUN Cap = "debug-always-stun" + + // DebugDisableWGTrim disables the lazy WireGuard configuration, + // always giving WireGuard the full netmap, even for idle peers. + DebugDisableWGTrim Cap = "debug-no-wg-trim" + + // DisableSubnetsIfPAC controls whether subnet routers should be + // disabled if WPAD is present on the network. + DisableSubnetsIfPAC Cap = "debug-disable-subnets-if-pac" + + // DisableUPnP makes the client not perform a UPnP portmapping. + // By default, we want to enable it to see if it works on more clients. + // + // If UPnP catastrophically fails for people, this should be set kill + // new attempts at UPnP connections. + DisableUPnP Cap = "debug-disable-upnp" + + // DisableDeltaUpdates makes the client not process updates via the + // delta update mechanism and should instead treat all netmap changes as + // "full" ones as tailscaled did in 1.48.x and earlier. + DisableDeltaUpdates Cap = "disable-delta-updates" + + // RandomizeClientPort makes magicsock UDP bind to + // :0 to get a random local port, ignoring any configured + // fixed port. + RandomizeClientPort Cap = "randomize-client-port" + + // SilentDisco makes the client suppress disco heartbeats to its + // peers. + SilentDisco Cap = "silent-disco" + + // OneCGNATEnable makes the client prefer one big CGNAT /10 route + // rather than a /32 per peer. At most one of this or + // [OneCGNATDisable] may be set; if neither are, it's automatic. + OneCGNATEnable Cap = "one-cgnat?v=true" + + // OneCGNATDisable makes the client prefer a /32 route per peer + // rather than one big /10 CGNAT route. At most one of this or + // [OneCGNATEnable] may be set; if neither are, it's automatic. + OneCGNATDisable Cap = "one-cgnat?v=false" + + // PeerMTUEnable makes the client do path MTU discovery to its + // peers. If it isn't set, it defaults to the client default. + PeerMTUEnable Cap = "peer-mtu-enable" + + // DNSForwarderDisableTCPRetries disables retrying truncated + // DNS queries over TCP if the response is truncated. + DNSForwarderDisableTCPRetries Cap = "dns-forwarder-disable-tcp-retries" + + // LinuxMustUseIPTables forces Linux clients to use iptables for + // netfilter management. + // This cannot be set simultaneously with [LinuxMustUseNfTables]. + LinuxMustUseIPTables Cap = "linux-netfilter?v=iptables" + + // LinuxMustUseNfTables forces Linux clients to use nftables for + // netfilter management. + // This cannot be set simultaneously with [LinuxMustUseIPTables]. + LinuxMustUseNfTables Cap = "linux-netfilter?v=nftables" + + // ProbeUDPLifetime makes the client probe UDP path lifetime at the + // tail end of an active direct connection in magicsock. + ProbeUDPLifetime Cap = "probe-udp-lifetime" + + // TaildriveShare enables sharing via Taildrive. + TaildriveShare Cap = "drive:share" + + // TaildriveAccess enables accessing shares via Taildrive. + TaildriveAccess Cap = "drive:access" + + // SuggestExitNode is applied to each exit node which the control plane has determined + // is a recommended exit node. + SuggestExitNode Cap = "suggest-exit-node" + + // DisableWebClient disables using the web client. + DisableWebClient Cap = "disable-web-client" + + // LogExitFlows enables exit node destinations in network flow logs. + LogExitFlows Cap = "log-exit-flows" + + // AutoExitNode permits the automatic exit nodes feature. + AutoExitNode Cap = "auto-exit-node" + + // StoreAppCRoutes configures the node to store app connector routes persistently. + StoreAppCRoutes Cap = "store-appc-routes" + + // SuggestExitNodeUI allows the currently suggested exit node to appear in the client GUI. + SuggestExitNodeUI Cap = "suggest-exit-node-ui" + + // UserDialUseRoutes makes UserDial use either the peer dialer or the system dialer, + // depending on the destination address and the configured routes. When present, it also makes + // the DNS forwarder use UserDial instead of SystemDial when dialing resolvers. + UserDialUseRoutes Cap = "user-dial-routes" + + // SSHBehaviorV1 forces SSH to use the V1 behavior (no su, run SFTP in-process) + // Added 2024-05-29 in Tailscale version 1.68. + SSHBehaviorV1 Cap = "ssh-behavior-v1" + + // SSHBehaviorV2 forces SSH to use the V2 behavior (use su, run SFTP in child process). + // This overrides [SSHBehaviorV1] if set. + // See forceV1Behavior in ssh/tailssh/incubator.go for distinction between + // V1 and V2 behavior. + // Added 2024-08-06 in Tailscale version 1.72. + SSHBehaviorV2 Cap = "ssh-behavior-v2" + + // DisableSplitDNSWhenNoCustomResolvers indicates that the node's + // DNS manager should not adopt a split DNS configuration even though the + // Config of the resolver only contains routes that do not specify custom + // resolver(s), hence all DNS queries can be safely sent to the upstream + // DNS resolver and the node's DNS forwarder doesn't need to handle all + // DNS traffic. + // This is for now (2024-06-06) an iOS-specific battery life optimization, + // and this node attribute allows us to disable the optimization remotely + // if needed. + DisableSplitDNSWhenNoCustomResolvers Cap = "disable-split-dns-when-no-custom-resolvers" + + // ScopeQuad100OnMacOS makes sandboxed macOS clients scope quad-100 + // to its match domains instead of installing it as the OS's primary + // (catch-all) resolver, so that public names fall through to the OS + // resolver -- e.g. a user's DoH system profile -- rather than being + // shadowed. It has no effect on any other platform. Without this attribute, + // sandboxed macOS keeps the older behavior of making quad-100 the default + // resolver, as iOS still does. See tailscale/corp#45534. + ScopeQuad100OnMacOS Cap = "scope-quad100-macos" + + // DisableLocalDNSOverrideViaNRPT indicates that the node's DNS manager should not + // create a default (catch-all) Windows NRPT rule when "Override local DNS" is enabled. + // Without this rule, Windows 8.1 and newer devices issue parallel DNS requests to DNS servers + // associated with all network adapters, even when "Override local DNS" is enabled and/or + // a Mullvad exit node is being used, resulting in DNS leaks. + // We began creating this rule on 2024-06-14, and this node attribute + // allows us to disable the new behavior remotely if needed. + DisableLocalDNSOverrideViaNRPT Cap = "disable-local-dns-override-via-nrpt" + + // DisableMagicSockCryptoRouting disables the use of the + // magicsock cryptorouting hook. See tailscale/corp#20732. + // + // Deprecated: [DisableMagicSockCryptoRouting] is deprecated as of + // [tailscale.com/tailcfg.CapabilityVersion] 124, + // CryptoRouting is now mandatory. See tailscale/corp#31083. + DisableMagicSockCryptoRouting Cap = "disable-magicsock-crypto-routing" + + // DisableCaptivePortalDetection instructs the client to not perform captive portal detection + // automatically when the network state changes. + DisableCaptivePortalDetection Cap = "disable-captive-portal-detection" + + // DisableSkipStatusQueue is set when the node should disable skipping + // of queued netmap.NetworkMap between the controlclient and LocalBackend. + // See tailscale/tailscale#14768. + DisableSkipStatusQueue Cap = "disable-skip-status-queue" + + // SSHEnvironmentVariables enables logic for handling environment variables sent + // via SendEnv in the SSH server and applying them to the SSH session. + SSHEnvironmentVariables Cap = "ssh-env-vars" + + // ServiceHost indicates the VIP Services for which the client is + // approved to act as a service host, and which IP addresses are assigned + // to those VIP Services. Any VIP Services that the client is not + // advertising can be ignored. + // Each value of this key in [NodeCapMap] is of type [ServiceIPMappings]. + // If multiple values of this key exist, they should be merged in sequence + // (replace conflicting keys). + ServiceHost Cap = "service-host" + + // MaxKeyDuration represents the MaxKeyDuration setting on the + // tailnet. The value of this key in [NodeCapMap] will be only one entry of + // type float64 representing the duration in seconds. This cap will be + // omitted if the tailnet's MaxKeyDuration is the default. + MaxKeyDuration Cap = "tailnet.maxKeyDuration" + + // NativeIPV4 contains the IPV4 address of the node in its + // native tailnet. This is currently only sent to Hello, in its + // peer node list. + NativeIPV4 Cap = "native-ipv4" + + // DisableRelayServer prevents the node from acting as an underlay + // UDP relay server. There are no expected values for this key; the key + // only needs to be present in [NodeCapMap] to take effect. + DisableRelayServer Cap = "disable-relay-server" + + // DisableRelayClient prevents the node from both allocating UDP + // relay server endpoints itself, and from using endpoints allocated by + // its peers. This attribute can be added to the node dynamically; if added + // while the node is already running, the node will be unable to allocate + // endpoints after it next updates its network map, and will be immediately + // unable to use new paths via a UDP relay server. Setting this attribute + // dynamically does not remove any existing paths, including paths that + // traverse a UDP relay server. There are no expected values for this key + // in [NodeCapMap]; the key only needs to be present in [NodeCapMap] to + // take effect. + DisableRelayClient Cap = "disable-relay-client" + + // MagicDNSPeerAAAA is a capability that tells the node's MagicDNS + // server to answer AAAA queries about its peers. See tailscale/tailscale#1152. + MagicDNSPeerAAAA Cap = "magicdns-aaaa" + + // DNSSubdomainResolve, when set on Self or a Peer node, indicates + // that the subdomains of that node's MagicDNS name should resolve to the + // same IP addresses as the node itself. + // For example, if node "myserver.tailnet.ts.net" has this capability, + // then "anything.myserver.tailnet.ts.net" will resolve to myserver's IPs. + DNSSubdomainResolve Cap = "dns-subdomain-resolve" + + // TrafficSteering configures the node to use the traffic + // steering subsystem for via routes. See tailscale/corp#29966. + TrafficSteering Cap = "traffic-steering" + + // TailnetDisplayName is an optional alternate name for the tailnet + // to be displayed to the user. + // If empty or absent, a default is used. + // If this value is present and set by a user this will only include letters, + // numbers, apostrophe, spaces, and hyphens. This may not be true for the default. + // Values can look like "foo.com" or "Foo's Test Tailnet - Staging". + TailnetDisplayName Cap = "tailnet-display-name" + + // ClientSideReachability configures the node to determine + // reachability itself when choosing connectors. When absent, the + // default behavior is to trust the control plane when it claims that a + // node is no longer online, but that is not a reliable signal. + // + // It is temporary and will be ignored once its behaviour becomes the default. + ClientSideReachability Cap = "client-side-reachability" + + // ClientSideReachabilityRouteCheck configures the node to use + // the routecheck subsystem to determine reachability when choosing + // connectors. This relies on [ClientSideReachability] being set. + // See tailscale/tailscale#17367. + // + // It is temporary and will be ignored once its behaviour becomes the default. + ClientSideReachabilityRouteCheck Cap = "client-side-reachability-routecheck" + + // DefaultAutoUpdate advertises the default node auto-update setting + // for this tailnet. The node is free to opt-in or out locally regardless of + // this value. Once this has been set and stored in the client, future + // changes from the control plane are ignored. + // + // The value of the key in [NodeCapMap] is a JSON boolean. + DefaultAutoUpdate Cap = "default-auto-update" + + // DisableHostsFileUpdates indicates that the node's DNS manager should + // not create hosts file entries when it normally would, such as when we're not + // the primary resolver on Windows or when the host is domain-joined and its + // primary domain takes precedence over MagicDNS. As of 2026-02-12, it is only + // used on Windows. + DisableHostsFileUpdates Cap = "disable-hosts-file-updates" + + // ForceRegisterMagicDNSIPv4Only forces the client to only register + // its MagicDNS IPv4 address with systemd/etc, and not both its IPv4 and IPv6 addresses. + // See https://github.com/tailscale/tailscale/issues/15404. + // TODO(bradfitz): remove this a few releases after 2026-02-16. + ForceRegisterMagicDNSIPv4Only Cap = "force-register-magicdns-ipv4-only" + + // CacheNetworkMaps instructs the node to persistently cache network + // maps and use them to establish peer connectivity on start, if doing so is + // supported by the client and storage is available. When this attribute is + // absent (or removed), a node that supports netmap caching will ignore and + // discard existing cached maps, and will not store any. + CacheNetworkMaps Cap = "cache-network-maps" + + // DisableCacheNetworkMaps indicates that the node should not cache + // network maps (as per [CacheNetworkMaps]) when it normally would. + // This attribute exists to allow the policy document to override the default. + // When set, it takes precedence over [CacheNetworkMaps]. + DisableCacheNetworkMaps Cap = "disable-cache-network-maps" + + // DisableLinuxCGNATDropRule tells Linux clients to not insert a + // blanket firewall DROP rule for inbound traffic from the CGNAT IP range + // that does not originate from the Tailscale network interface. + // This enables access to off-tailnet endpoints within that IP range. + DisableLinuxCGNATDropRule Cap = "disable-linux-cgnat-drop-rule" + + // EmitRuntimeMetrics enables emission of [runtime/metrics] as + // [tailscale.com/util/clientmetric]'s. + EmitRuntimeMetrics Cap = "emit-runtime-metrics" + + // DisableUDPGRO disables UDP GRO (UDP_GRO socket option on Linux) + // on the magicsock UDP socket. It exists so control can mitigate kernel + // regressions that cause throughput or correctness issues with UDP GRO on + // specific OS/kernel versions, without requiring a client release. See + // https://github.com/tailscale/tailscale/issues/19777 for example. + // Currently only consulted on Linux; may apply to other platforms as they + // gain UDP GRO support. + DisableUDPGRO Cap = "disable-udp-gro" + + // DisableUDPGSO disables UDP GSO (UDP_SEGMENT socket option on + // Linux) on the magicsock UDP socket. It exists so control can mitigate + // kernel regressions that cause throughput or correctness issues with UDP + // GSO on specific OS/kernel versions, without requiring a client release. + // See https://github.com/tailscale/tailscale/issues/19777 for example. + // Currently only consulted on Linux; may apply to other platforms as they + // gain UDP GSO support. + DisableUDPGSO Cap = "disable-udp-gso" + + // DisableTUNUDPGRO disables UDP GRO on the Tailscale TUN device. + // It exists so control can mitigate kernel regressions that cause + // throughput or correctness issues with TUN UDP GRO on specific OS/kernel + // versions, without requiring a client release. See + // https://github.com/tailscale/tailscale/issues/13041 for example. + // Currently only consulted on Linux; may apply to other platforms as they + // gain TUN UDP GRO support. + DisableTUNUDPGRO Cap = "disable-tun-udp-gro" + + // DisableTUNTCPGRO disables TCP GRO on the Tailscale TUN device. + // It exists so control can mitigate kernel regressions that cause + // throughput or correctness issues with TUN TCP GRO on specific OS/kernel + // versions, without requiring a client release. See + // https://github.com/tailscale/tailscale/issues/13041 for example. + // Currently only consulted on Linux; may apply to other platforms as they + // gain TUN TCP GRO support. + DisableTUNTCPGRO Cap = "disable-tun-tcp-gro" + + // NeverGSOEqualTail enables a sentinel-tail workaround in the + // underlay UDP packet TX path on Linux. Applies to magicsock and peer relay + // UDP sockets. The workaround avoids emitting UDP GSO batches whose + // fragments are all equal in length, at a small payload and packet overhead + // cost. It exists so control can mitigate kernel regressions that mangle + // UDP headers or checksums for equal-length GSO batches, without requiring + // a client release. See https://github.com/tailscale/tailscale/issues/19777. + NeverGSOEqualTail Cap = "never-gso-equal-tail" +) + +const ( + // ServicesPrefix is the prefix for per-service [NodeCapMap] + // entries describing Services visible (accessible) to this node. + // Each value under such a key is of type [ServiceDetails]. + // The suffix after the prefix is an opaque server-chosen identifier; + // consumers must use [ServiceDetails.Name] as the canonical service name + // rather than parsing it from the map key. + ServicesPrefix Prefix = "services/" +) diff --git a/tailcfg/peercap/peercap.go b/tailcfg/peercap/peercap.go new file mode 100644 index 000000000..3890b4d78 --- /dev/null +++ b/tailcfg/peercap/peercap.go @@ -0,0 +1,56 @@ +// Copyright (c) Tailscale Inc & contributors +// SPDX-License-Identifier: BSD-3-Clause + +// Package peercap defines the capabilities that can be granted to peer nodes. +package peercap + +// Cap represents a capability granted to a peer by a [tailscale.com/tailcfg.FilterRule] +// when the peer communicates with the node that has this rule. +// Its meaning is application-defined. +// +// It must be a URL like "https://tailscale.com/cap/file-send" +// or "tailscale.com/cap/webui". +type Cap string + +const ( + // FileSharingTarget grants the current node the ability to send + // files to the peer which has this capability. + FileSharingTarget Cap = "https://tailscale.com/cap/file-sharing-target" + // FileSharingSend grants the ability to receive files from a + // node that's owned by a different user. + FileSharingSend Cap = "https://tailscale.com/cap/file-send" + // DebugPeer grants the ability for a peer to read this node's + // goroutines, metrics, magicsock internal state, etc. + DebugPeer Cap = "https://tailscale.com/cap/debug-peer" + // WakeOnLAN grants the ability to send a Wake-On-LAN packet. + WakeOnLAN Cap = "https://tailscale.com/cap/wake-on-lan" + // Ingress grants the ability for a peer to send ingress traffic. + Ingress Cap = "https://tailscale.com/cap/ingress" + // WebUI grants the ability for a peer to edit features from the + // device Web UI. + WebUI Cap = "tailscale.com/cap/webui" + // Taildrive grants the ability for a peer to access Taildrive + // shares. + Taildrive Cap = "tailscale.com/cap/drive" + // TaildriveSharer indicates that a peer has the ability to + // share folders with us. + TaildriveSharer Cap = "tailscale.com/cap/drive-sharer" + + // Kubernetes grants a peer Kubernetes-specific + // capabilities, such as the ability to impersonate specific Tailscale + // user groups as Kubernetes user groups. This capability is read by + // peers that are Tailscale Kubernetes operator instances. + Kubernetes Cap = "tailscale.com/cap/kubernetes" + + // Relay grants the ability for a peer to allocate relay + // endpoints. + Relay Cap = "tailscale.com/cap/relay" + // RelayTarget grants the current node the ability to allocate + // relay endpoints to the peer which has this capability. + RelayTarget Cap = "tailscale.com/cap/relay-target" + + // TsIDP grants a peer tsidp-specific + // capabilities, such as the ability to add user groups to the OIDC + // claim + TsIDP Cap = "tailscale.com/cap/tsidp" +) diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 99276d7ae..d0a7a5418 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -22,6 +22,8 @@ "time" "tailscale.com/feature/buildfeatures" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/dnstype" "tailscale.com/types/key" "tailscale.com/types/opt" @@ -446,7 +448,7 @@ type Node struct { // "https://tailscale.com/cap/file-sharing" // // Deprecated: use CapMap instead. See https://github.com/tailscale/tailscale/issues/11508 - Capabilities []NodeCapability `json:",omitempty"` + Capabilities []nodecap.Cap `json:",omitempty"` // CapMap is a map of capabilities to their optional argument/data values. // @@ -457,7 +459,7 @@ type Node struct { // represented by the Capabilities field, but can now be represented by // CapMap with an empty value. // - // See NodeCapability for more information on keys. + // See [nodecap.Cap] for more information on keys. // // Metadata about nodes can be transmitted in 3 ways: // 1. MapResponse.Node.CapMap describes attributes that affect behavior for @@ -543,13 +545,13 @@ type Node struct { // HasCap reports whether the node has the given capability. // It is safe to call on an invalid NodeView. -func (v NodeView) HasCap(cap NodeCapability) bool { +func (v NodeView) HasCap(cap nodecap.Cap) bool { return v.ж.HasCap(cap) } // HasCap reports whether the node has the given capability. // It is safe to call on a nil Node. -func (v *Node) HasCap(cap NodeCapability) bool { +func (v *Node) HasCap(cap nodecap.Cap) bool { return v != nil && v.CapMap.Contains(cap) } @@ -1565,7 +1567,7 @@ type CapGrant struct { // FilterRule.SrcIPs are granted to the destination IP, // matched by Dsts. // Deprecated: use CapMap instead. - Caps []PeerCapability `json:",omitempty"` + Caps []peercap.Cap `json:",omitempty"` // CapMap is a map of capabilities to their values. // The key is the capability name, and the value is a list of @@ -1573,62 +1575,40 @@ type CapGrant struct { CapMap PeerCapMap `json:",omitempty"` } -// PeerCapability represents a capability granted to a peer by a FilterRule when -// the peer communicates with the node that has this rule. Its meaning is -// application-defined. +// PeerCapability is a type alias to [peercap.Cap] +// and peer capabilities are now defined in the peercap package, +// see [PeerCapabilityFileSharingTarget]. // -// It must be a URL like "https://tailscale.com/cap/file-send". -type PeerCapability string +//go:fix inline +type PeerCapability = peercap.Cap +// Deprecated: Peer capabilities are now defined in the peercap package, see [peercap.Cap]. +// These constants are provided for backwards compatibility +// but no new [PeerCapability] aliases will be added to this list. +// +//go:fix inline const ( - // PeerCapabilityFileSharingTarget grants the current node the ability to send - // files to the peer which has this capability. - PeerCapabilityFileSharingTarget PeerCapability = "https://tailscale.com/cap/file-sharing-target" - // PeerCapabilityFileSharingSend grants the ability to receive files from a - // node that's owned by a different user. - PeerCapabilityFileSharingSend PeerCapability = "https://tailscale.com/cap/file-send" - // PeerCapabilityDebugPeer grants the ability for a peer to read this node's - // goroutines, metrics, magicsock internal state, etc. - PeerCapabilityDebugPeer PeerCapability = "https://tailscale.com/cap/debug-peer" - // PeerCapabilityWakeOnLAN grants the ability to send a Wake-On-LAN packet. - PeerCapabilityWakeOnLAN PeerCapability = "https://tailscale.com/cap/wake-on-lan" - // PeerCapabilityIngress grants the ability for a peer to send ingress traffic. - PeerCapabilityIngress PeerCapability = "https://tailscale.com/cap/ingress" - // PeerCapabilityWebUI grants the ability for a peer to edit features from the - // device Web UI. - PeerCapabilityWebUI PeerCapability = "tailscale.com/cap/webui" - // PeerCapabilityTaildrive grants the ability for a peer to access Taildrive - // shares. - PeerCapabilityTaildrive PeerCapability = "tailscale.com/cap/drive" - // PeerCapabilityTaildriveSharer indicates that a peer has the ability to - // share folders with us. - PeerCapabilityTaildriveSharer PeerCapability = "tailscale.com/cap/drive-sharer" - - // PeerCapabilityKubernetes grants a peer Kubernetes-specific - // capabilities, such as the ability to impersonate specific Tailscale - // user groups as Kubernetes user groups. This capability is read by - // peers that are Tailscale Kubernetes operator instances. - PeerCapabilityKubernetes PeerCapability = "tailscale.com/cap/kubernetes" - - // PeerCapabilityRelay grants the ability for a peer to allocate relay - // endpoints. - PeerCapabilityRelay PeerCapability = "tailscale.com/cap/relay" - // PeerCapabilityRelayTarget grants the current node the ability to allocate - // relay endpoints to the peer which has this capability. - PeerCapabilityRelayTarget PeerCapability = "tailscale.com/cap/relay-target" - - // PeerCapabilityTsIDP grants a peer tsidp-specific - // capabilities, such as the ability to add user groups to the OIDC - // claim - PeerCapabilityTsIDP PeerCapability = "tailscale.com/cap/tsidp" + PeerCapabilityFileSharingTarget = peercap.FileSharingTarget + PeerCapabilityFileSharingSend = peercap.FileSharingSend + PeerCapabilityDebugPeer = peercap.DebugPeer + PeerCapabilityWakeOnLAN = peercap.WakeOnLAN + PeerCapabilityIngress = peercap.Ingress + PeerCapabilityWebUI = peercap.WebUI + PeerCapabilityTaildrive = peercap.Taildrive + PeerCapabilityTaildriveSharer = peercap.TaildriveSharer + PeerCapabilityKubernetes = peercap.Kubernetes + PeerCapabilityRelay = peercap.Relay + PeerCapabilityRelayTarget = peercap.RelayTarget + PeerCapabilityTsIDP = peercap.TsIDP + // Deprecated: Do not add any further values here, use [peercap] instead. ) // NodeCapMap is a map of capabilities to their optional values. It is valid for // a capability to have no values (nil slice); such capabilities can be tested // for by using the [NodeCapMap.Contains] method. // -// See [NodeCapability] for more information on keys. -type NodeCapMap map[NodeCapability][]RawMessage +// See [nodecap.Cap] for more information on keys. +type NodeCapMap map[nodecap.Cap][]RawMessage // Equal reports whether c and c2 are equal. func (c NodeCapMap) Equal(c2 NodeCapMap) bool { @@ -1638,14 +1618,14 @@ func (c NodeCapMap) Equal(c2 NodeCapMap) bool { // UnmarshalNodeCapJSON unmarshals each JSON value in cm[cap] as T. // If cap does not exist in cm, it returns (nil, nil). // It returns an error if the values cannot be unmarshaled into the provided type. -func UnmarshalNodeCapJSON[T any](cm NodeCapMap, cap NodeCapability) ([]T, error) { +func UnmarshalNodeCapJSON[T any](cm NodeCapMap, cap nodecap.Cap) ([]T, error) { return UnmarshalNodeCapViewJSON[T](views.MapSliceOf(cm), cap) } // UnmarshalNodeCapViewJSON unmarshals each JSON value in cm.Get(cap) as T. // If cap does not exist in cm, it returns (nil, nil). // It returns an error if the values cannot be unmarshaled into the provided type. -func UnmarshalNodeCapViewJSON[T any](cm views.MapSlice[NodeCapability, RawMessage], cap NodeCapability) ([]T, error) { +func UnmarshalNodeCapViewJSON[T any](cm views.MapSlice[nodecap.Cap, RawMessage], cap nodecap.Cap) ([]T, error) { vals, ok := cm.GetOk(cap) if !ok { return nil, nil @@ -1664,7 +1644,7 @@ func UnmarshalNodeCapViewJSON[T any](cm views.MapSlice[NodeCapability, RawMessag // Contains reports whether c has the capability cap. This is used to test for // the existence of a capability, especially when the capability has no // associated argument/data values. -func (c NodeCapMap) Contains(cap NodeCapability) bool { +func (c NodeCapMap) Contains(cap nodecap.Cap) bool { _, ok := c[cap] return ok } @@ -1675,19 +1655,19 @@ func (c NodeCapMap) Contains(cap NodeCapability) bool { // // The values are opaque to Tailscale, but are passed through from the ACLs to // the application via the WhoIs API. -type PeerCapMap map[PeerCapability][]RawMessage +type PeerCapMap map[peercap.Cap][]RawMessage // UnmarshalCapJSON unmarshals each JSON value in cm[cap] as T. // If cap does not exist in cm, it returns (nil, nil). // It returns an error if the values cannot be unmarshaled into the provided type. -func UnmarshalCapJSON[T any](cm PeerCapMap, cap PeerCapability) ([]T, error) { +func UnmarshalCapJSON[T any](cm PeerCapMap, cap peercap.Cap) ([]T, error) { return UnmarshalCapViewJSON[T](views.MapSliceOf(cm), cap) } // UnmarshalCapViewJSON unmarshals each JSON value in cm.Get(cap) as T. // If cap does not exist in cm, it returns (nil, nil). // It returns an error if the values cannot be unmarshaled into the provided type. -func UnmarshalCapViewJSON[T any](cm views.MapSlice[PeerCapability, RawMessage], cap PeerCapability) ([]T, error) { +func UnmarshalCapViewJSON[T any](cm views.MapSlice[peercap.Cap, RawMessage], cap peercap.Cap) ([]T, error) { vals, ok := cm.GetOk(cap) if !ok { return nil, nil @@ -1706,7 +1686,7 @@ func UnmarshalCapViewJSON[T any](cm views.MapSlice[PeerCapability, RawMessage], // HasCapability reports whether c has the capability cap. This is used to test // for the existence of a capability, especially when the capability has no // associated argument/data values. -func (c PeerCapMap) HasCapability(cap PeerCapability) bool { +func (c PeerCapMap) HasCapability(cap peercap.Cap) bool { _, ok := c[cap] return ok } @@ -2475,441 +2455,110 @@ type Oauth2Token struct { Expiry time.Time `json:"expiry,omitzero"` } -// NodeCapability represents a capability granted to the self node as listed in -// MapResponse.Node.Capabilities. +// NodeCapability is a type alias to [nodecap.Cap] +// and node capabilities are now defined in the [nodecap] package, +// see [CapabilityFileSharing] and [NodeAttrDisableAndroidBindToActiveNetwork] respectively. // -// It must be a URL like "https://tailscale.com/cap/file-sharing", or a -// well-known capability name like "funnel". The latter is only allowed for -// Tailscale-defined capabilities. +//go:fix inline +type NodeCapability = nodecap.Cap + +// NodeCapabilityPrefix is a type alias to [nodecap.Prefix] +// and these prefixes are now defined in the [nodecap] package, +// see [NodeAttrPrefixServices]. // -// Unlike PeerCapability, NodeCapability is not in context of a peer and is -// granted to the node itself. +//go:fix inline +type NodeCapabilityPrefix = nodecap.Prefix + +// Deprecated: Capabilities and NodeAttrs are now defined in the nodecap package, see [nodecap.Cap]. +// These constants are provided for backwards compatibility +// but no new [NodeCapability] aliases will be added to this list. // -// These are also referred to as "Node Attributes" in the ACL policy file. -type NodeCapability string - -// NodeCapabilityPrefix is a prefix for [NodeCapMap] keys that share a common -// namespace, where each entry represents a distinct named instance (e.g. one -// per service). The full key is formed by concatenating the prefix with the -// instance name. -type NodeCapabilityPrefix string - -// ToAttribute returns the full [NodeCapability] key for the given value under -// this prefix, of the form prefix+value. -func (p NodeCapabilityPrefix) ToAttribute(value string) NodeCapability { - return NodeCapability(string(p) + value) -} - +//go:fix inline const ( - CapabilityFileSharing NodeCapability = "https://tailscale.com/cap/file-sharing" - CapabilityAdmin NodeCapability = "https://tailscale.com/cap/is-admin" - CapabilityOwner NodeCapability = "https://tailscale.com/cap/is-owner" - CapabilitySSH NodeCapability = "https://tailscale.com/cap/ssh" // feature enabled/available - CapabilitySSHRuleIn NodeCapability = "https://tailscale.com/cap/ssh-rule-in" // some SSH rule reach this node - CapabilityDataPlaneAuditLogs NodeCapability = "https://tailscale.com/cap/data-plane-audit-logs" // feature enabled - CapabilityDebug NodeCapability = "https://tailscale.com/cap/debug" // exposes debug endpoints over the PeerAPI - CapabilityHTTPS NodeCapability = "https" - - // CapabilityMacUIV2 makes the macOS GUI enable its v2 mode. - CapabilityMacUIV2 NodeCapability = "https://tailscale.com/cap/mac-ui-v2" - - // CapabilityServicesInDesktopClients enables services list/menu/section in desktop clients. - // If this capability is not present, desktop clients should not show services. - CapabilityServicesInDesktopClients NodeCapability = "https://tailscale.com/cap/services-in-desktop-clients" - - // CapabilityBindToInterfaceByRoute changes how Darwin nodes create - // sockets (in the net/netns package). See that package for more - // details on the behaviour of this capability. - CapabilityBindToInterfaceByRoute NodeCapability = "https://tailscale.com/cap/bind-to-interface-by-route" - - // NodeAttrDisableAndroidBindToActiveNetwork disables binding sockets to the - // currently active network on Android, which is enabled by default. - // This allows the control plane to turn off the behavior if it causes - // problems. - NodeAttrDisableAndroidBindToActiveNetwork NodeCapability = "disable-android-bind-to-active-network" - - // CapabilityDebugDisableAlternateDefaultRouteInterface changes how Darwin - // nodes get the default interface. There is an optional hook (used by the - // macOS and iOS clients) to override the default interface, this capability - // disables that and uses the default behavior (of parsing the routing - // table). - CapabilityDebugDisableAlternateDefaultRouteInterface NodeCapability = "https://tailscale.com/cap/debug-disable-alternate-default-route-interface" - - // CapabilityDebugDisableBindConnToInterface disables the automatic binding - // of connections to the default network interface on Darwin nodes. - CapabilityDebugDisableBindConnToInterface NodeCapability = "https://tailscale.com/cap/debug-disable-bind-conn-to-interface" - - // CapabilityDebugDisableBindConnToInterface disables the automatic binding - // of connections to the default network interface on Darwin nodes using network extensions - CapabilityDebugDisableBindConnToInterfaceAppleExt NodeCapability = "https://tailscale.com/cap/debug-disable-bind-conn-to-interface-apple-ext" - - // CapabilityTailnetLock indicates the node may initialize tailnet lock. - CapabilityTailnetLock NodeCapability = "https://tailscale.com/cap/tailnet-lock" - - // Funnel warning capabilities used for reporting errors to the user. - - // CapabilityWarnFunnelNoInvite indicates whether Funnel is enabled for the tailnet. - // This cap is no longer used 2023-08-09 onwards. - CapabilityWarnFunnelNoInvite NodeCapability = "https://tailscale.com/cap/warn-funnel-no-invite" - - // CapabilityWarnFunnelNoHTTPS indicates HTTPS has not been enabled for the tailnet. - // This cap is no longer used 2023-08-09 onwards. - CapabilityWarnFunnelNoHTTPS NodeCapability = "https://tailscale.com/cap/warn-funnel-no-https" - - // Debug logging capabilities - - // CapabilityDebugTSDNSResolution enables verbose debug logging for DNS - // resolution for Tailscale-controlled domains (the control server, log - // server, DERP servers, etc.) - CapabilityDebugTSDNSResolution NodeCapability = "https://tailscale.com/cap/debug-ts-dns-resolution" - - // CapabilityFunnelPorts specifies the ports that the Funnel is available on. - // The ports are specified as a comma-separated list of port numbers or port - // ranges (e.g. "80,443,8080-8090") in the ports query parameter. - // e.g. https://tailscale.com/cap/funnel-ports?ports=80,443,8080-8090 - CapabilityFunnelPorts NodeCapability = "https://tailscale.com/cap/funnel-ports" - - // NodeAttrOnlyTCP443 specifies that the client should not attempt to generate - // any outbound traffic that isn't TCP on port 443 (HTTPS). This is used for - // clients in restricted environments where only HTTPS traffic is allowed - // other types of traffic trips outbound firewall alarms. This thus implies - // all traffic is over DERP. - NodeAttrOnlyTCP443 NodeCapability = "only-tcp-443" - - // NodeAttrFunnel grants the ability for a node to host ingress traffic. - NodeAttrFunnel NodeCapability = "funnel" - // NodeAttrSSHAggregator grants the ability for a node to collect SSH sessions. - NodeAttrSSHAggregator NodeCapability = "ssh-aggregator" - - // NodeAttrDebugForceBackgroundSTUN forces a node to always do background - // STUN queries regardless of inactivity. - NodeAttrDebugForceBackgroundSTUN NodeCapability = "debug-always-stun" - - // NodeAttrDebugDisableWGTrim disables the lazy WireGuard configuration, - // always giving WireGuard the full netmap, even for idle peers. - NodeAttrDebugDisableWGTrim NodeCapability = "debug-no-wg-trim" - - // NodeAttrDisableSubnetsIfPAC controls whether subnet routers should be - // disabled if WPAD is present on the network. - NodeAttrDisableSubnetsIfPAC NodeCapability = "debug-disable-subnets-if-pac" - - // NodeAttrDisableUPnP makes the client not perform a UPnP portmapping. - // By default, we want to enable it to see if it works on more clients. - // - // If UPnP catastrophically fails for people, this should be set kill - // new attempts at UPnP connections. - NodeAttrDisableUPnP NodeCapability = "debug-disable-upnp" - - // NodeAttrDisableDeltaUpdates makes the client not process updates via the - // delta update mechanism and should instead treat all netmap changes as - // "full" ones as tailscaled did in 1.48.x and earlier. - NodeAttrDisableDeltaUpdates NodeCapability = "disable-delta-updates" - - // NodeAttrRandomizeClientPort makes magicsock UDP bind to - // :0 to get a random local port, ignoring any configured - // fixed port. - NodeAttrRandomizeClientPort NodeCapability = "randomize-client-port" - - // NodeAttrSilentDisco makes the client suppress disco heartbeats to its - // peers. - NodeAttrSilentDisco NodeCapability = "silent-disco" - - // NodeAttrOneCGNATEnable makes the client prefer one big CGNAT /10 route - // rather than a /32 per peer. At most one of this or - // NodeAttrOneCGNATDisable may be set; if neither are, it's automatic. - NodeAttrOneCGNATEnable NodeCapability = "one-cgnat?v=true" - - // NodeAttrOneCGNATDisable makes the client prefer a /32 route per peer - // rather than one big /10 CGNAT route. At most one of this or - // NodeAttrOneCGNATEnable may be set; if neither are, it's automatic. - NodeAttrOneCGNATDisable NodeCapability = "one-cgnat?v=false" - - // NodeAttrPeerMTUEnable makes the client do path MTU discovery to its - // peers. If it isn't set, it defaults to the client default. - NodeAttrPeerMTUEnable NodeCapability = "peer-mtu-enable" - - // NodeAttrDNSForwarderDisableTCPRetries disables retrying truncated - // DNS queries over TCP if the response is truncated. - NodeAttrDNSForwarderDisableTCPRetries NodeCapability = "dns-forwarder-disable-tcp-retries" - - // NodeAttrLinuxMustUseIPTables forces Linux clients to use iptables for - // netfilter management. - // This cannot be set simultaneously with NodeAttrLinuxMustUseNfTables. - NodeAttrLinuxMustUseIPTables NodeCapability = "linux-netfilter?v=iptables" - - // NodeAttrLinuxMustUseNfTables forces Linux clients to use nftables for - // netfilter management. - // This cannot be set simultaneously with NodeAttrLinuxMustUseIPTables. - NodeAttrLinuxMustUseNfTables NodeCapability = "linux-netfilter?v=nftables" - - // NodeAttrProbeUDPLifetime makes the client probe UDP path lifetime at the - // tail end of an active direct connection in magicsock. - NodeAttrProbeUDPLifetime NodeCapability = "probe-udp-lifetime" - - // NodeAttrsTaildriveShare enables sharing via Taildrive. - NodeAttrsTaildriveShare NodeCapability = "drive:share" - - // NodeAttrsTaildriveAccess enables accessing shares via Taildrive. - NodeAttrsTaildriveAccess NodeCapability = "drive:access" - - // NodeAttrSuggestExitNode is applied to each exit node which the control plane has determined - // is a recommended exit node. - NodeAttrSuggestExitNode NodeCapability = "suggest-exit-node" - - // NodeAttrDisableWebClient disables using the web client. - NodeAttrDisableWebClient NodeCapability = "disable-web-client" - - // NodeAttrLogExitFlows enables exit node destinations in network flow logs. - NodeAttrLogExitFlows NodeCapability = "log-exit-flows" - - // NodeAttrAutoExitNode permits the automatic exit nodes feature. - NodeAttrAutoExitNode NodeCapability = "auto-exit-node" - - // NodeAttrStoreAppCRoutes configures the node to store app connector routes persistently. - NodeAttrStoreAppCRoutes NodeCapability = "store-appc-routes" - - // NodeAttrSuggestExitNodeUI allows the currently suggested exit node to appear in the client GUI. - NodeAttrSuggestExitNodeUI NodeCapability = "suggest-exit-node-ui" - - // NodeAttrUserDialUseRoutes makes UserDial use either the peer dialer or the system dialer, - // depending on the destination address and the configured routes. When present, it also makes - // the DNS forwarder use UserDial instead of SystemDial when dialing resolvers. - NodeAttrUserDialUseRoutes NodeCapability = "user-dial-routes" - - // NodeAttrSSHBehaviorV1 forces SSH to use the V1 behavior (no su, run SFTP in-process) - // Added 2024-05-29 in Tailscale version 1.68. - NodeAttrSSHBehaviorV1 NodeCapability = "ssh-behavior-v1" - - // NodeAttrSSHBehaviorV2 forces SSH to use the V2 behavior (use su, run SFTP in child process). - // This overrides NodeAttrSSHBehaviorV1 if set. - // See forceV1Behavior in ssh/tailssh/incubator.go for distinction between - // V1 and V2 behavior. - // Added 2024-08-06 in Tailscale version 1.72. - NodeAttrSSHBehaviorV2 NodeCapability = "ssh-behavior-v2" - - // NodeAttrDisableSplitDNSWhenNoCustomResolvers indicates that the node's - // DNS manager should not adopt a split DNS configuration even though the - // Config of the resolver only contains routes that do not specify custom - // resolver(s), hence all DNS queries can be safely sent to the upstream - // DNS resolver and the node's DNS forwarder doesn't need to handle all - // DNS traffic. - // This is for now (2024-06-06) an iOS-specific battery life optimization, - // and this node attribute allows us to disable the optimization remotely - // if needed. - NodeAttrDisableSplitDNSWhenNoCustomResolvers NodeCapability = "disable-split-dns-when-no-custom-resolvers" - - // NodeAttrScopeQuad100OnMacOS makes sandboxed macOS clients scope quad-100 - // to its match domains instead of installing it as the OS's primary - // (catch-all) resolver, so that public names fall through to the OS - // resolver -- e.g. a user's DoH system profile -- rather than being - // shadowed. It has no effect on any other platform. Without this attribute, - // sandboxed macOS keeps the older behavior of making quad-100 the default - // resolver, as iOS still does. See tailscale/corp#45534. - NodeAttrScopeQuad100OnMacOS NodeCapability = "scope-quad100-macos" - - // NodeAttrDisableLocalDNSOverrideViaNRPT indicates that the node's DNS manager should not - // create a default (catch-all) Windows NRPT rule when "Override local DNS" is enabled. - // Without this rule, Windows 8.1 and newer devices issue parallel DNS requests to DNS servers - // associated with all network adapters, even when "Override local DNS" is enabled and/or - // a Mullvad exit node is being used, resulting in DNS leaks. - // We began creating this rule on 2024-06-14, and this node attribute - // allows us to disable the new behavior remotely if needed. - NodeAttrDisableLocalDNSOverrideViaNRPT NodeCapability = "disable-local-dns-override-via-nrpt" - - // NodeAttrDisableMagicSockCryptoRouting disables the use of the - // magicsock cryptorouting hook. See tailscale/corp#20732. - // - // Deprecated: NodeAttrDisableMagicSockCryptoRouting is deprecated as of - // CapabilityVersion 124, CryptoRouting is now mandatory. See tailscale/corp#31083. - NodeAttrDisableMagicSockCryptoRouting NodeCapability = "disable-magicsock-crypto-routing" - - // NodeAttrDisableCaptivePortalDetection instructs the client to not perform captive portal detection - // automatically when the network state changes. - NodeAttrDisableCaptivePortalDetection NodeCapability = "disable-captive-portal-detection" - - // NodeAttrDisableSkipStatusQueue is set when the node should disable skipping - // of queued netmap.NetworkMap between the controlclient and LocalBackend. - // See tailscale/tailscale#14768. - NodeAttrDisableSkipStatusQueue NodeCapability = "disable-skip-status-queue" - - // NodeAttrSSHEnvironmentVariables enables logic for handling environment variables sent - // via SendEnv in the SSH server and applying them to the SSH session. - NodeAttrSSHEnvironmentVariables NodeCapability = "ssh-env-vars" - - // NodeAttrServiceHost indicates the VIP Services for which the client is - // approved to act as a service host, and which IP addresses are assigned - // to those VIP Services. Any VIP Services that the client is not - // advertising can be ignored. - // Each value of this key in [NodeCapMap] is of type [ServiceIPMappings]. - // If multiple values of this key exist, they should be merged in sequence - // (replace conflicting keys). - NodeAttrServiceHost NodeCapability = "service-host" - - // NodeAttrMaxKeyDuration represents the MaxKeyDuration setting on the - // tailnet. The value of this key in [NodeCapMap] will be only one entry of - // type float64 representing the duration in seconds. This cap will be - // omitted if the tailnet's MaxKeyDuration is the default. - NodeAttrMaxKeyDuration NodeCapability = "tailnet.maxKeyDuration" - - // NodeAttrNativeIPV4 contains the IPV4 address of the node in its - // native tailnet. This is currently only sent to Hello, in its - // peer node list. - NodeAttrNativeIPV4 NodeCapability = "native-ipv4" - - // NodeAttrDisableRelayServer prevents the node from acting as an underlay - // UDP relay server. There are no expected values for this key; the key - // only needs to be present in [NodeCapMap] to take effect. - NodeAttrDisableRelayServer NodeCapability = "disable-relay-server" - - // NodeAttrDisableRelayClient prevents the node from both allocating UDP - // relay server endpoints itself, and from using endpoints allocated by - // its peers. This attribute can be added to the node dynamically; if added - // while the node is already running, the node will be unable to allocate - // endpoints after it next updates its network map, and will be immediately - // unable to use new paths via a UDP relay server. Setting this attribute - // dynamically does not remove any existing paths, including paths that - // traverse a UDP relay server. There are no expected values for this key - // in [NodeCapMap]; the key only needs to be present in [NodeCapMap] to - // take effect. - NodeAttrDisableRelayClient NodeCapability = "disable-relay-client" - - // NodeAttrMagicDNSPeerAAAA is a capability that tells the node's MagicDNS - // server to answer AAAA queries about its peers. See tailscale/tailscale#1152. - NodeAttrMagicDNSPeerAAAA NodeCapability = "magicdns-aaaa" - - // NodeAttrDNSSubdomainResolve, when set on Self or a Peer node, indicates - // that the subdomains of that node's MagicDNS name should resolve to the - // same IP addresses as the node itself. - // For example, if node "myserver.tailnet.ts.net" has this capability, - // then "anything.myserver.tailnet.ts.net" will resolve to myserver's IPs. - NodeAttrDNSSubdomainResolve NodeCapability = "dns-subdomain-resolve" - - // NodeAttrTrafficSteering configures the node to use the traffic - // steering subsystem for via routes. See tailscale/corp#29966. - NodeAttrTrafficSteering NodeCapability = "traffic-steering" - - // NodeAttrTailnetDisplayName is an optional alternate name for the tailnet - // to be displayed to the user. - // If empty or absent, a default is used. - // If this value is present and set by a user this will only include letters, - // numbers, apostrophe, spaces, and hyphens. This may not be true for the default. - // Values can look like "foo.com" or "Foo's Test Tailnet - Staging". - NodeAttrTailnetDisplayName NodeCapability = "tailnet-display-name" - - // NodeAttrClientSideReachability configures the node to determine - // reachability itself when choosing connectors. When absent, the - // default behavior is to trust the control plane when it claims that a - // node is no longer online, but that is not a reliable signal. - // - // It is temporary and will be ignored once its behaviour becomes the default. - NodeAttrClientSideReachability NodeCapability = "client-side-reachability" - - // NodeAttrClientSideReachabilityRouteCheck configures the node to use - // the routecheck subsystem to determine reachability when choosing - // connectors. This relies on [NodeAttrClientSideReachability] being set. - // See tailscale/tailscale#17367. - // - // It is temporary and will be ignored once its behaviour becomes the default. - NodeAttrClientSideReachabilityRouteCheck NodeCapability = "client-side-reachability-routecheck" - - // NodeAttrDefaultAutoUpdate advertises the default node auto-update setting - // for this tailnet. The node is free to opt-in or out locally regardless of - // this value. Once this has been set and stored in the client, future - // changes from the control plane are ignored. - // - // The value of the key in [NodeCapMap] is a JSON boolean. - NodeAttrDefaultAutoUpdate NodeCapability = "default-auto-update" - - // NodeAttrDisableHostsFileUpdates indicates that the node's DNS manager should - // not create hosts file entries when it normally would, such as when we're not - // the primary resolver on Windows or when the host is domain-joined and its - // primary domain takes precedence over MagicDNS. As of 2026-02-12, it is only - // used on Windows. - NodeAttrDisableHostsFileUpdates NodeCapability = "disable-hosts-file-updates" - - // NodeAttrForceRegisterMagicDNSIPv4Only forces the client to only register - // its MagicDNS IPv4 address with systemd/etc, and not both its IPv4 and IPv6 addresses. - // See https://github.com/tailscale/tailscale/issues/15404. - // TODO(bradfitz): remove this a few releases after 2026-02-16. - NodeAttrForceRegisterMagicDNSIPv4Only NodeCapability = "force-register-magicdns-ipv4-only" - - // NodeAttrCacheNetworkMaps instructs the node to persistently cache network - // maps and use them to establish peer connectivity on start, if doing so is - // supported by the client and storage is available. When this attribute is - // absent (or removed), a node that supports netmap caching will ignore and - // discard existing cached maps, and will not store any. - NodeAttrCacheNetworkMaps NodeCapability = "cache-network-maps" - - // NodeAttrDisableCacheNetworkMaps indicates that the node should not cache - // network maps (as per [NodeAttrCacheNetworkMaps]) when it normally would. - // This attribute exists to allow the policy document to override the default. - // When set, it takes precedence over [NodeAttrCacheNetworkMaps]. - NodeAttrDisableCacheNetworkMaps NodeCapability = "disable-cache-network-maps" - - // NodeAttrDisableLinuxCGNATDropRule tells Linux clients to not insert a - // blanket firewall DROP rule for inbound traffic from the CGNAT IP range - // that does not originate from the Tailscale network interface. - // This enables access to off-tailnet endpoints within that IP range. - NodeAttrDisableLinuxCGNATDropRule NodeCapability = "disable-linux-cgnat-drop-rule" - - // NodeAttrEmitRuntimeMetrics enables emission of [runtime/metrics] as - // [tailscale.com/util/clientmetric]'s. - NodeAttrEmitRuntimeMetrics NodeCapability = "emit-runtime-metrics" - - // NodeAttrDisableUDPGRO disables UDP GRO (UDP_GRO socket option on Linux) - // on the magicsock UDP socket. It exists so control can mitigate kernel - // regressions that cause throughput or correctness issues with UDP GRO on - // specific OS/kernel versions, without requiring a client release. See - // https://github.com/tailscale/tailscale/issues/19777 for example. - // Currently only consulted on Linux; may apply to other platforms as they - // gain UDP GRO support. - NodeAttrDisableUDPGRO NodeCapability = "disable-udp-gro" - - // NodeAttrDisableUDPGSO disables UDP GSO (UDP_SEGMENT socket option on - // Linux) on the magicsock UDP socket. It exists so control can mitigate - // kernel regressions that cause throughput or correctness issues with UDP - // GSO on specific OS/kernel versions, without requiring a client release. - // See https://github.com/tailscale/tailscale/issues/19777 for example. - // Currently only consulted on Linux; may apply to other platforms as they - // gain UDP GSO support. - NodeAttrDisableUDPGSO NodeCapability = "disable-udp-gso" - - // NodeAttrDisableTUNUDPGRO disables UDP GRO on the Tailscale TUN device. - // It exists so control can mitigate kernel regressions that cause - // throughput or correctness issues with TUN UDP GRO on specific OS/kernel - // versions, without requiring a client release. See - // https://github.com/tailscale/tailscale/issues/13041 for example. - // Currently only consulted on Linux; may apply to other platforms as they - // gain TUN UDP GRO support. - NodeAttrDisableTUNUDPGRO NodeCapability = "disable-tun-udp-gro" - - // NodeAttrDisableTUNTCPGRO disables TCP GRO on the Tailscale TUN device. - // It exists so control can mitigate kernel regressions that cause - // throughput or correctness issues with TUN TCP GRO on specific OS/kernel - // versions, without requiring a client release. See - // https://github.com/tailscale/tailscale/issues/13041 for example. - // Currently only consulted on Linux; may apply to other platforms as they - // gain TUN TCP GRO support. - NodeAttrDisableTUNTCPGRO NodeCapability = "disable-tun-tcp-gro" - - // NodeAttrNeverGSOEqualTail enables a sentinel-tail workaround in the - // underlay UDP packet TX path on Linux. Applies to magicsock and peer relay - // UDP sockets. The workaround avoids emitting UDP GSO batches whose - // fragments are all equal in length, at a small payload and packet overhead - // cost. It exists so control can mitigate kernel regressions that mangle - // UDP headers or checksums for equal-length GSO batches, without requiring - // a client release. See https://github.com/tailscale/tailscale/issues/19777. - NodeAttrNeverGSOEqualTail NodeCapability = "never-gso-equal-tail" -) - -const ( - // NodeAttrPrefixServices is the prefix for per-service [NodeCapMap] - // entries describing Services visible (accessible) to this node. - // Each value under such a key is of type [ServiceDetails]. - // The suffix after the prefix is an opaque server-chosen identifier; - // consumers must use [ServiceDetails.Name] as the canonical service name - // rather than parsing it from the map key. - NodeAttrPrefixServices NodeCapabilityPrefix = "services/" + CapabilityFileSharing = nodecap.FileSharing + CapabilityAdmin = nodecap.Admin + CapabilityOwner = nodecap.Owner + CapabilitySSH = nodecap.SSH + CapabilitySSHRuleIn = nodecap.SSHRuleIn + CapabilityDataPlaneAuditLogs = nodecap.DataPlaneAuditLogs + CapabilityDebug = nodecap.Debug + CapabilityHTTPS = nodecap.HTTPS + CapabilityMacUIV2 = nodecap.MacUIV2 + CapabilityServicesInDesktopClients = nodecap.ServicesInDesktopClients + CapabilityBindToInterfaceByRoute = nodecap.BindToInterfaceByRoute + CapabilityDebugDisableAlternateDefaultRouteInterface = nodecap.DebugDisableAlternateDefaultRouteInterface + CapabilityDebugDisableBindConnToInterface = nodecap.DebugDisableBindConnToInterface + CapabilityDebugDisableBindConnToInterfaceAppleExt = nodecap.DebugDisableBindConnToInterfaceAppleExt + CapabilityTailnetLock = nodecap.TailnetLock + CapabilityWarnFunnelNoInvite = nodecap.WarnFunnelNoInvite + CapabilityWarnFunnelNoHTTPS = nodecap.WarnFunnelNoHTTPS + CapabilityDebugTSDNSResolution = nodecap.DebugTSDNSResolution + CapabilityFunnelPorts = nodecap.FunnelPorts + // Deprecated: Do not add any further values here, use [nodecap] instead. + + NodeAttrDisableAndroidBindToActiveNetwork = nodecap.DisableAndroidBindToActiveNetwork + NodeAttrOnlyTCP443 = nodecap.OnlyTCP443 + NodeAttrFunnel = nodecap.Funnel + NodeAttrSSHAggregator = nodecap.SSHAggregator + NodeAttrDebugForceBackgroundSTUN = nodecap.DebugForceBackgroundSTUN + NodeAttrDebugDisableWGTrim = nodecap.DebugDisableWGTrim + NodeAttrDisableSubnetsIfPAC = nodecap.DisableSubnetsIfPAC + NodeAttrDisableUPnP = nodecap.DisableUPnP + NodeAttrDisableDeltaUpdates = nodecap.DisableDeltaUpdates + NodeAttrRandomizeClientPort = nodecap.RandomizeClientPort + NodeAttrSilentDisco = nodecap.SilentDisco + NodeAttrOneCGNATEnable = nodecap.OneCGNATEnable + NodeAttrOneCGNATDisable = nodecap.OneCGNATDisable + NodeAttrPeerMTUEnable = nodecap.PeerMTUEnable + NodeAttrDNSForwarderDisableTCPRetries = nodecap.DNSForwarderDisableTCPRetries + NodeAttrLinuxMustUseIPTables = nodecap.LinuxMustUseIPTables + NodeAttrLinuxMustUseNfTables = nodecap.LinuxMustUseNfTables + NodeAttrProbeUDPLifetime = nodecap.ProbeUDPLifetime + NodeAttrsTaildriveShare = nodecap.TaildriveShare + NodeAttrsTaildriveAccess = nodecap.TaildriveAccess + NodeAttrSuggestExitNode = nodecap.SuggestExitNode + NodeAttrDisableWebClient = nodecap.DisableWebClient + NodeAttrLogExitFlows = nodecap.LogExitFlows + NodeAttrAutoExitNode = nodecap.AutoExitNode + NodeAttrStoreAppCRoutes = nodecap.StoreAppCRoutes + NodeAttrSuggestExitNodeUI = nodecap.SuggestExitNodeUI + NodeAttrUserDialUseRoutes = nodecap.UserDialUseRoutes + NodeAttrSSHBehaviorV1 = nodecap.SSHBehaviorV1 + NodeAttrSSHBehaviorV2 = nodecap.SSHBehaviorV2 + NodeAttrDisableSplitDNSWhenNoCustomResolvers = nodecap.DisableSplitDNSWhenNoCustomResolvers + NodeAttrScopeQuad100OnMacOS = nodecap.ScopeQuad100OnMacOS + NodeAttrDisableLocalDNSOverrideViaNRPT = nodecap.DisableLocalDNSOverrideViaNRPT + NodeAttrDisableMagicSockCryptoRouting = nodecap.DisableMagicSockCryptoRouting + NodeAttrDisableCaptivePortalDetection = nodecap.DisableCaptivePortalDetection + NodeAttrDisableSkipStatusQueue = nodecap.DisableSkipStatusQueue + NodeAttrSSHEnvironmentVariables = nodecap.SSHEnvironmentVariables + NodeAttrServiceHost = nodecap.ServiceHost + NodeAttrMaxKeyDuration = nodecap.MaxKeyDuration + NodeAttrNativeIPV4 = nodecap.NativeIPV4 + NodeAttrDisableRelayServer = nodecap.DisableRelayServer + NodeAttrDisableRelayClient = nodecap.DisableRelayClient + NodeAttrMagicDNSPeerAAAA = nodecap.MagicDNSPeerAAAA + NodeAttrDNSSubdomainResolve = nodecap.DNSSubdomainResolve + NodeAttrTrafficSteering = nodecap.TrafficSteering + NodeAttrTailnetDisplayName = nodecap.TailnetDisplayName + NodeAttrClientSideReachability = nodecap.ClientSideReachability + NodeAttrClientSideReachabilityRouteCheck = nodecap.ClientSideReachabilityRouteCheck + NodeAttrDefaultAutoUpdate = nodecap.DefaultAutoUpdate + NodeAttrDisableHostsFileUpdates = nodecap.DisableHostsFileUpdates + NodeAttrForceRegisterMagicDNSIPv4Only = nodecap.ForceRegisterMagicDNSIPv4Only + NodeAttrCacheNetworkMaps = nodecap.CacheNetworkMaps + NodeAttrDisableCacheNetworkMaps = nodecap.DisableCacheNetworkMaps + NodeAttrDisableLinuxCGNATDropRule = nodecap.DisableLinuxCGNATDropRule + NodeAttrEmitRuntimeMetrics = nodecap.EmitRuntimeMetrics + NodeAttrDisableUDPGRO = nodecap.DisableUDPGRO + NodeAttrDisableUDPGSO = nodecap.DisableUDPGSO + NodeAttrDisableTUNUDPGRO = nodecap.DisableTUNUDPGRO + NodeAttrDisableTUNTCPGRO = nodecap.DisableTUNTCPGRO + NodeAttrNeverGSOEqualTail = nodecap.NeverGSOEqualTail + // Deprecated: Do not add any further values here, use [nodecap] instead. + + NodeAttrPrefixServices = nodecap.ServicesPrefix + // Deprecated: Do not add any further values here, use [nodecap] instead. ) // SetDNSRequest is a request to add a DNS record. diff --git a/tailcfg/tailcfg_clone.go b/tailcfg/tailcfg_clone.go index e1d5551cd..e8162ce64 100644 --- a/tailcfg/tailcfg_clone.go +++ b/tailcfg/tailcfg_clone.go @@ -10,6 +10,7 @@ "net/netip" "time" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/dnstype" "tailscale.com/types/key" "tailscale.com/types/opt" @@ -59,7 +60,7 @@ func (src *Node) Clone() *Node { } dst.Capabilities = append(src.Capabilities[:0:0], src.Capabilities...) if dst.CapMap != nil { - dst.CapMap = map[NodeCapability][]RawMessage{} + dst.CapMap = map[nodecap.Cap][]RawMessage{} for k := range src.CapMap { dst.CapMap[k] = append([]RawMessage{}, src.CapMap[k]...) } @@ -108,7 +109,7 @@ func (src *Node) Clone() *Node { LastSeen *time.Time Online *bool MachineAuthorized bool - Capabilities []NodeCapability + Capabilities []nodecap.Cap CapMap NodeCapMap UnsignedPeerAPIOnly bool ComputedName string diff --git a/tailcfg/tailcfg_test.go b/tailcfg/tailcfg_test.go index 5658f3111..b25bcc77f 100644 --- a/tailcfg/tailcfg_test.go +++ b/tailcfg/tailcfg_test.go @@ -17,6 +17,7 @@ "tailscale.com/ipn/ipnstate" . "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/tstest/deptest" "tailscale.com/types/key" "tailscale.com/types/opt" @@ -1081,23 +1082,23 @@ type testRule struct { } tests := []struct { name string - capType PeerCapability + capType peercap.Cap val testRule }{ { name: "empty", val: testRule{}, - capType: PeerCapability("foo"), + capType: peercap.Cap("foo"), }, { name: "some-values", val: testRule{Ports: []int{80, 443}, Name: "foo"}, - capType: PeerCapability("foo"), + capType: peercap.Cap("foo"), }, { name: "all-values", val: testRule{Ports: []int{80, 443}, Name: "foo", ToggleOn: true, Groups: inner{Groups: []string{"foo", "bar"}}, Addrs: []netip.AddrPort{testip}}, - capType: PeerCapability("foo"), + capType: peercap.Cap("foo"), }, } for _, tc := range tests { diff --git a/tailcfg/tailcfg_view.go b/tailcfg/tailcfg_view.go index 008c6fcfa..f50483bb1 100644 --- a/tailcfg/tailcfg_view.go +++ b/tailcfg/tailcfg_view.go @@ -13,6 +13,7 @@ jsonv2 "github.com/go-json-experiment/json" "github.com/go-json-experiment/json/jsontext" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/dnstype" "tailscale.com/types/key" "tailscale.com/types/opt" @@ -277,7 +278,7 @@ func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized } // "https://tailscale.com/cap/file-sharing" // // Deprecated: use CapMap instead. See https://github.com/tailscale/tailscale/issues/11508 -func (v NodeView) Capabilities() views.Slice[NodeCapability] { return views.SliceOf(v.ж.Capabilities) } +func (v NodeView) Capabilities() views.Slice[nodecap.Cap] { return views.SliceOf(v.ж.Capabilities) } // CapMap is a map of capabilities to their optional argument/data values. // @@ -288,7 +289,7 @@ func (v NodeView) Capabilities() views.Slice[NodeCapability] { return views.Slic // represented by the Capabilities field, but can now be represented by // CapMap with an empty value. // -// See NodeCapability for more information on keys. +// See [nodecap.Cap] for more information on keys. // // Metadata about nodes can be transmitted in 3 ways: // 1. MapResponse.Node.CapMap describes attributes that affect behavior for @@ -299,7 +300,7 @@ func (v NodeView) Capabilities() views.Slice[NodeCapability] { return views.Slic // 3. MapResponse.Peers[].CapMap describes attributes regarding a peer node, // such as which features the peer supports or if that peer is preferred // for a particular task vs other peers that could also be chosen. -func (v NodeView) CapMap() views.MapSlice[NodeCapability, RawMessage] { +func (v NodeView) CapMap() views.MapSlice[nodecap.Cap, RawMessage] { return views.MapSliceOf(v.ж.CapMap) } @@ -402,7 +403,7 @@ func (v NodeView) Equal(v2 NodeView) bool { return v.ж.Equal(v2.ж) } LastSeen *time.Time Online *bool MachineAuthorized bool - Capabilities []NodeCapability + Capabilities []nodecap.Cap CapMap NodeCapMap UnsignedPeerAPIOnly bool ComputedName string diff --git a/tsnet/depaware.txt b/tsnet/depaware.txt index 302f5e0d8..efd3f4117 100644 --- a/tsnet/depaware.txt +++ b/tsnet/depaware.txt @@ -226,6 +226,8 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware) 💣 tailscale.com/safesocket from tailscale.com/client/local+ tailscale.com/syncs from tailscale.com/control/controlhttp+ tailscale.com/tailcfg from tailscale.com/client/local+ + tailscale.com/tailcfg/nodecap from tailscale.com/tailcfg+ + tailscale.com/tailcfg/peercap from tailscale.com/tailcfg+ tailscale.com/tempfork/acme from tailscale.com/feature/acme tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock tailscale.com/tempfork/httprec from tailscale.com/feature/c2n diff --git a/tsnet/tailnetlock_test.go b/tsnet/tailnetlock_test.go index 397578bfd..67f87d7fd 100644 --- a/tsnet/tailnetlock_test.go +++ b/tsnet/tailnetlock_test.go @@ -39,6 +39,7 @@ "time" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tka" "tailscale.com/tstest" "tailscale.com/tstest/integration/testcontrol" @@ -73,7 +74,7 @@ func setupTailnetLockedServer(t *testing.T, ctx context.Context, extraTrustedKey // Hand out the tailnet-lock capability so the server can call // TailnetLockInit. control.DefaultNodeCapabilities = &tailcfg.NodeCapMap{ - tailcfg.CapabilityTailnetLock: nil, + nodecap.TailnetLock: nil, } s, _, s1Key = startServer(t, ctx, controlURL, "s1") diff --git a/tsnet/tsnet.go b/tsnet/tsnet.go index 14a35cbf3..3693326f8 100644 --- a/tsnet/tsnet.go +++ b/tsnet/tsnet.go @@ -186,6 +186,7 @@ "tailscale.com/net/socks5" "tailscale.com/net/tsdial" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/tsd" "tailscale.com/types/bools" "tailscale.com/types/logger" @@ -1650,12 +1651,12 @@ func (ServiceModeHTTP) network() string { return "tcp" } func (m ServiceModeHTTP) port() uint16 { return m.Port } -func (m ServiceModeHTTP) capsMap() map[string][]tailcfg.PeerCapability { - capsMap := map[string][]tailcfg.PeerCapability{} +func (m ServiceModeHTTP) capsMap() map[string][]peercap.Cap { + capsMap := map[string][]peercap.Cap{} for path, capNames := range m.AcceptAppCaps { - caps := make([]tailcfg.PeerCapability, 0, len(capNames)) + caps := make([]peercap.Cap, 0, len(capNames)) for _, c := range capNames { - caps = append(caps, tailcfg.PeerCapability(c)) + caps = append(caps, peercap.Cap(c)) } capsMap[path] = caps } diff --git a/tsnet/tsnet_test.go b/tsnet/tsnet_test.go index 85640d5bc..1d8c01d91 100644 --- a/tsnet/tsnet_test.go +++ b/tsnet/tsnet_test.go @@ -53,6 +53,7 @@ "tailscale.com/net/netns" "tailscale.com/net/packet" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstest" "tailscale.com/tstest/deptest" "tailscale.com/tstest/integration" @@ -1011,17 +1012,17 @@ func setUpServiceState(t *testing.T, name, ip string, host, client *Server, // is a mapping from the Service name to the Service VIP. cm := host.lb.NetMap().SelfNode.CapMap() svcIPMap := make(tailcfg.ServiceIPMappings) - if cm.Contains(tailcfg.NodeAttrServiceHost) { - parsed := must.Get(tailcfg.UnmarshalNodeCapViewJSON[tailcfg.ServiceIPMappings](cm, tailcfg.NodeAttrServiceHost)) + if cm.Contains(nodecap.ServiceHost) { + parsed := must.Get(tailcfg.UnmarshalNodeCapViewJSON[tailcfg.ServiceIPMappings](cm, nodecap.ServiceHost)) if len(parsed) != 1 { - t.Fatalf("expected only one capability for %v, got %d", tailcfg.NodeAttrServiceHost, len(parsed)) + t.Fatalf("expected only one capability for %v, got %d", nodecap.ServiceHost, len(parsed)) } svcIPMap = parsed[0] } svcIPMap[serviceName] = []netip.Addr{netip.MustParseAddr(ip)} svcIPMapJSON := must.Get(json.Marshal(svcIPMap)) newCM := cm.AsMap() - mak.Set(&newCM, tailcfg.NodeAttrServiceHost, []tailcfg.RawMessage{tailcfg.RawMessage(svcIPMapJSON)}) + mak.Set(&newCM, nodecap.ServiceHost, []tailcfg.RawMessage{tailcfg.RawMessage(svcIPMapJSON)}) control.SetNodeCapMap(host.lb.NodeKey(), newCM) // The Service host must be allowed to advertise the Service VIP. diff --git a/tstest/integration/integration_test.go b/tstest/integration/integration_test.go index 8a894c7cf..d11b0bbed 100644 --- a/tstest/integration/integration_test.go +++ b/tstest/integration/integration_test.go @@ -45,6 +45,7 @@ "tailscale.com/net/tstun" "tailscale.com/net/udprelay/status" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstest" "tailscale.com/tstest/integration/testcontrol" "tailscale.com/types/key" @@ -1669,7 +1670,7 @@ func testAutoUpdateDefaults(t *testing.T, useCap bool) { if mr.Node.CapMap == nil { mr.Node.CapMap = make(tailcfg.NodeCapMap) } - mr.Node.CapMap[tailcfg.NodeAttrDefaultAutoUpdate] = []tailcfg.RawMessage{ + mr.Node.CapMap[nodecap.DefaultAutoUpdate] = []tailcfg.RawMessage{ tailcfg.RawMessage(fmt.Sprintf("%t", send)), } } else { @@ -2500,7 +2501,7 @@ func TestTailnetLock(t *testing.T) { env := NewTestEnv(t) env.Control.DefaultNodeCapabilities = &tailcfg.NodeCapMap{ - tailcfg.CapabilityTailnetLock: []tailcfg.RawMessage{}, + nodecap.TailnetLock: []tailcfg.RawMessage{}, } // Start two nodes which will be our signing nodes. diff --git a/tstest/integration/testcontrol/testcontrol.go b/tstest/integration/testcontrol/testcontrol.go index fda31b398..033eb00db 100644 --- a/tstest/integration/testcontrol/testcontrol.go +++ b/tstest/integration/testcontrol/testcontrol.go @@ -33,6 +33,8 @@ "tailscale.com/net/tsaddr" "tailscale.com/syncs" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/tka" "tailscale.com/tstest/tkatest" "tailscale.com/types/key" @@ -1057,10 +1059,10 @@ func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey key. capMap = *s.DefaultNodeCapabilities } else { capMap = tailcfg.NodeCapMap{ - tailcfg.CapabilityHTTPS: []tailcfg.RawMessage{}, - tailcfg.NodeAttrFunnel: []tailcfg.RawMessage{}, - tailcfg.CapabilityFileSharing: []tailcfg.RawMessage{}, - tailcfg.CapabilityFunnelPorts + "?ports=8080,443": []tailcfg.RawMessage{}, + nodecap.HTTPS: []tailcfg.RawMessage{}, + nodecap.Funnel: []tailcfg.RawMessage{}, + nodecap.FileSharing: []tailcfg.RawMessage{}, + nodecap.FunnelPorts + "?ports=8080,443": []tailcfg.RawMessage{}, } } @@ -1605,12 +1607,12 @@ func (s *Server) serveMap(w http.ResponseWriter, r *http.Request, mkey key.Machi func packetFilterWithIngress(addRelayCaps bool, allowSrcs []string) []tailcfg.FilterRule { out := slices.Clone(tailcfg.FilterAllowAll) out[0].SrcIPs = allowSrcs - caps := []tailcfg.PeerCapability{ - tailcfg.PeerCapabilityIngress, + caps := []peercap.Cap{ + peercap.Ingress, } if addRelayCaps { - caps = append(caps, tailcfg.PeerCapabilityRelay) - caps = append(caps, tailcfg.PeerCapabilityRelayTarget) + caps = append(caps, peercap.Relay) + caps = append(caps, peercap.RelayTarget) } out = append(out, tailcfg.FilterRule{ SrcIPs: []string{"*"}, @@ -1647,9 +1649,9 @@ func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse, s.mu.Unlock() node.CapMap = nodeCapMap - node.Capabilities = append(node.Capabilities, tailcfg.NodeAttrDisableUPnP) + node.Capabilities = append(node.Capabilities, nodecap.DisableUPnP) if sshPolicy != nil { - mak.Set(&node.CapMap, tailcfg.CapabilitySSH, nil) + mak.Set(&node.CapMap, nodecap.SSH, nil) } t := time.Date(2020, 8, 3, 0, 0, 0, 1, time.UTC) diff --git a/tstest/largetailnet/largetailnet.go b/tstest/largetailnet/largetailnet.go index 73ec2da80..478973792 100644 --- a/tstest/largetailnet/largetailnet.go +++ b/tstest/largetailnet/largetailnet.go @@ -32,6 +32,7 @@ "go4.org/mem" "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstest/integration/testcontrol" "tailscale.com/types/key" ) @@ -179,7 +180,7 @@ func (s *Streamer) sendInitial(w testcontrol.MapStreamWriter, req *tailcfg.MapRe MachineAuthorized: true, Addresses: []netip.Prefix{selfIP4, selfIP6}, AllowedIPs: []netip.Prefix{selfIP4, selfIP6}, - CapMap: map[tailcfg.NodeCapability][]tailcfg.RawMessage{}, + CapMap: map[nodecap.Cap][]tailcfg.RawMessage{}, } initial := &tailcfg.MapResponse{ diff --git a/tstest/natlab/vmtest/connectivity_test.go b/tstest/natlab/vmtest/connectivity_test.go index 8cbbfdc29..8085655cf 100644 --- a/tstest/natlab/vmtest/connectivity_test.go +++ b/tstest/natlab/vmtest/connectivity_test.go @@ -8,6 +8,7 @@ "testing" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstest/natlab/vmtest" "tailscale.com/tstest/natlab/vnet" ) @@ -241,7 +242,7 @@ func TestNonTailscaleCGNATEndpoint(t *testing.T) { vmtest.OS(vmtest.Gokrazy)) n1 := env.AddNode("node-1", cgnatNW, - tailcfg.NodeCapMap{tailcfg.NodeAttrDisableLinuxCGNATDropRule: nil}, + tailcfg.NodeCapMap{nodecap.DisableLinuxCGNATDropRule: nil}, vmtest.OS(vmtest.Gokrazy)) env.Start() diff --git a/tstest/natlab/vmtest/vmtest_test.go b/tstest/natlab/vmtest/vmtest_test.go index 576982eef..1c43d2334 100644 --- a/tstest/natlab/vmtest/vmtest_test.go +++ b/tstest/natlab/vmtest/vmtest_test.go @@ -17,6 +17,7 @@ "tailscale.com/ipn" "tailscale.com/net/udprelay/status" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tstest" "tailscale.com/tstest/integration/testcontrol" "tailscale.com/tstest/natlab/vmtest" @@ -1116,10 +1117,10 @@ func TestCachedNetmapAfterRestart(t *testing.T) { a := env.AddNode("a", aNet, vmtest.OS(vmtest.Gokrazy), - tailcfg.NodeCapMap{tailcfg.NodeAttrCacheNetworkMaps: nil}) + tailcfg.NodeCapMap{nodecap.CacheNetworkMaps: nil}) b := env.AddNode("b", bNet, vmtest.OS(vmtest.Gokrazy), - tailcfg.NodeCapMap{tailcfg.NodeAttrCacheNetworkMaps: nil}) + tailcfg.NodeCapMap{nodecap.CacheNetworkMaps: nil}) connectStep := env.AddStep("Establish initial TSMP tunnel") cutControlStep := env.AddStep("Cut control server access") @@ -1206,10 +1207,10 @@ func TestDirectConnectionWithCachedNetmapOnOneNode(t *testing.T) { // Node "a" is the offline peer, node "b" is the online peer. a := env.AddNode("a", aNet, vmtest.OS(vmtest.Gokrazy), - tailcfg.NodeCapMap{tailcfg.NodeAttrCacheNetworkMaps: nil}) + tailcfg.NodeCapMap{nodecap.CacheNetworkMaps: nil}) b := env.AddNode("b", bNet, vmtest.OS(vmtest.Gokrazy), - tailcfg.NodeCapMap{tailcfg.NodeAttrCacheNetworkMaps: nil}) + tailcfg.NodeCapMap{nodecap.CacheNetworkMaps: nil}) pStr := "Ping a → b" if testPingFrom == "online" { @@ -1295,10 +1296,10 @@ func TestDirectConnectionWithCachedNetmapOnTwoNodes(t *testing.T) { a := env.AddNode("a", aNet, vmtest.OS(vmtest.Gokrazy), - tailcfg.NodeCapMap{tailcfg.NodeAttrCacheNetworkMaps: nil}) + tailcfg.NodeCapMap{nodecap.CacheNetworkMaps: nil}) b := env.AddNode("b", bNet, vmtest.OS(vmtest.Gokrazy), - tailcfg.NodeCapMap{tailcfg.NodeAttrCacheNetworkMaps: nil}) + tailcfg.NodeCapMap{nodecap.CacheNetworkMaps: nil}) checkInitialMetrics := env.AddStep("Check initial client metrics") cutControlStep := env.AddStep("Cut control server access") diff --git a/types/netmap/netmap.go b/types/netmap/netmap.go index fbf415be0..604a5d363 100644 --- a/types/netmap/netmap.go +++ b/types/netmap/netmap.go @@ -15,6 +15,7 @@ "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/tka" "tailscale.com/types/key" "tailscale.com/types/views" @@ -30,7 +31,7 @@ type NetworkMap struct { Cached bool // whether this NetworkMap was loaded from disk cache (as opposed to live from network) SelfNode tailcfg.NodeView - AllCaps set.Set[tailcfg.NodeCapability] // set version of SelfNode.Capabilities + SelfNode.CapMap + AllCaps set.Set[nodecap.Cap] // set version of SelfNode.Capabilities + SelfNode.CapMap NodeKey key.NodePublic MachineKey key.MachinePublic @@ -112,7 +113,7 @@ func (nm *NetworkMap) GetVIPServiceIPMap() tailcfg.ServiceIPMappings { return nil } - ipMaps, err := tailcfg.UnmarshalNodeCapViewJSON[tailcfg.ServiceIPMappings](nm.SelfNode.CapMap(), tailcfg.NodeAttrServiceHost) + ipMaps, err := tailcfg.UnmarshalNodeCapViewJSON[tailcfg.ServiceIPMappings](nm.SelfNode.CapMap(), nodecap.ServiceHost) if len(ipMaps) != 1 || err != nil { return nil } @@ -160,7 +161,7 @@ func (nm *NetworkMap) Services() map[tailcfg.ServiceName]tailcfg.ServiceDetails } result := make(map[tailcfg.ServiceName]tailcfg.ServiceDetails) for cap := range nm.SelfNode.CapMap().All() { - if !strings.HasPrefix(string(cap), string(tailcfg.NodeAttrPrefixServices)) { + if !strings.HasPrefix(string(cap), string(nodecap.ServicesPrefix)) { continue } svcs, err := tailcfg.UnmarshalNodeCapViewJSON[tailcfg.ServiceDetails](nm.SelfNode.CapMap(), cap) @@ -207,7 +208,7 @@ func (nm *NetworkMap) GetMachineStatus() tailcfg.MachineStatus { } // HasCap reports whether nm is non-nil and nm.AllCaps contains c. -func (nm *NetworkMap) HasCap(c tailcfg.NodeCapability) bool { +func (nm *NetworkMap) HasCap(c nodecap.Cap) bool { return nm != nil && nm.AllCaps.Contains(c) } @@ -304,7 +305,7 @@ func (nm *NetworkMap) TailnetDisplayName() string { return "" } - tailnetDisplayNames, err := tailcfg.UnmarshalNodeCapViewJSON[string](nm.SelfNode.CapMap(), tailcfg.NodeAttrTailnetDisplayName) + tailnetDisplayNames, err := tailcfg.UnmarshalNodeCapViewJSON[string](nm.SelfNode.CapMap(), nodecap.TailnetDisplayName) if err != nil || len(tailnetDisplayNames) == 0 { return "" } diff --git a/util/deephash/deephash_test.go b/util/deephash/deephash_test.go index a82203d50..d3eaaa6ca 100644 --- a/util/deephash/deephash_test.go +++ b/util/deephash/deephash_test.go @@ -23,6 +23,7 @@ "go4.org/mem" "go4.org/netipx" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/key" "tailscale.com/util/deephash/testtype" "tailscale.com/util/hashx" @@ -645,7 +646,7 @@ type I struct{ IPProto: []int{1, 2, 3, 4}, CapGrant: []tailcfg.CapGrant{{ Dsts: []netip.Prefix{netip.MustParsePrefix("1.2.3.4/32")}, - Caps: []tailcfg.PeerCapability{"foo"}, + Caps: []peercap.Cap{"foo"}, }}, }, { diff --git a/wgengine/filter/filter_test.go b/wgengine/filter/filter_test.go index 56e35b88c..8ba319fa0 100644 --- a/wgengine/filter/filter_test.go +++ b/wgengine/filter/filter_test.go @@ -23,6 +23,8 @@ "tailscale.com/net/packet" "tailscale.com/net/tsaddr" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/tstest" "tailscale.com/tstime/rate" "tailscale.com/types/ipproto" @@ -47,12 +49,12 @@ // or tailcfg.NodeCapability values. Other values panic. func m(srcs []netip.Prefix, dsts []NetPortRange, opts ...any) Match { var protos []ipproto.Proto - var caps []tailcfg.NodeCapability + var caps []nodecap.Cap for _, o := range opts { switch o := o.(type) { case ipproto.Proto: protos = append(protos, o) - case tailcfg.NodeCapability: + case nodecap.Cap: caps = append(caps, o) default: panic(fmt.Sprintf("unknown option type %T", o)) @@ -83,7 +85,7 @@ func newFilter(logf logger.Logf) *Filter { m(nets("::/0"), netports("::/0:443")), m(nets("0.0.0.0/0"), netports("0.0.0.0/0:*"), testAllowedProto), m(nets("::/0"), netports("::/0:*"), testAllowedProto), - m(nil, netports("1.2.3.4:22"), tailcfg.NodeCapability("cap-hit-1234-ssh")), + m(nil, netports("1.2.3.4:22"), nodecap.Cap("cap-hit-1234-ssh")), } // Expects traffic to 100.122.98.50, 1.2.3.4, 5.6.7.8, @@ -106,7 +108,7 @@ func TestFilter(t *testing.T) { ipWithCap := netip.MustParseAddr("10.0.0.1") ipWithoutCap := netip.MustParseAddr("10.0.0.2") - filt.srcIPHasCap = func(ip netip.Addr, cap tailcfg.NodeCapability) bool { + filt.srcIPHasCap = func(ip netip.Addr, cap nodecap.Cap) bool { return cap == "cap-hit-1234-ssh" && ip == ipWithCap } @@ -313,7 +315,7 @@ func TestParseIPSet(t *testing.T) { capTests := []struct { in string - want tailcfg.NodeCapability + want nodecap.Cap }{ {"cap:foo", "foo"}, {"cap:people-in-8.8.8.0/24", "people-in-8.8.8.0/24"}, // test precedence of "/" search @@ -990,7 +992,7 @@ func TestPeerCaps(t *testing.T) { Dsts: []netip.Prefix{ netip.MustParsePrefix("0.0.0.0/0"), }, - Caps: []tailcfg.PeerCapability{"is_ipv4"}, + Caps: []peercap.Cap{"is_ipv4"}, }}, }, { @@ -999,7 +1001,7 @@ func TestPeerCaps(t *testing.T) { Dsts: []netip.Prefix{ netip.MustParsePrefix("::/0"), }, - Caps: []tailcfg.PeerCapability{"is_ipv6"}, + Caps: []peercap.Cap{"is_ipv6"}, }}, }, { @@ -1008,7 +1010,7 @@ func TestPeerCaps(t *testing.T) { Dsts: []netip.Prefix{ netip.MustParsePrefix("100.200.0.0/16"), }, - Caps: []tailcfg.PeerCapability{"some_super_admin"}, + Caps: []peercap.Cap{"some_super_admin"}, }}, }, }) @@ -1019,37 +1021,37 @@ func TestPeerCaps(t *testing.T) { tests := []struct { name string src, dst string // IP - want []tailcfg.PeerCapability + want []peercap.Cap }{ { name: "v4", src: "1.2.3.4", dst: "2.4.5.5", - want: []tailcfg.PeerCapability{"is_ipv4"}, + want: []peercap.Cap{"is_ipv4"}, }, { name: "v6", src: "1::1", dst: "2::2", - want: []tailcfg.PeerCapability{"is_ipv6"}, + want: []peercap.Cap{"is_ipv6"}, }, { name: "admin", src: "100.199.1.2", dst: "100.200.3.4", - want: []tailcfg.PeerCapability{"is_ipv4", "some_super_admin"}, + want: []peercap.Cap{"is_ipv4", "some_super_admin"}, }, { name: "not_admin_bad_src", src: "100.198.1.2", // 198, not 199 dst: "100.200.3.4", - want: []tailcfg.PeerCapability{"is_ipv4"}, + want: []peercap.Cap{"is_ipv4"}, }, { name: "not_admin_bad_dst", src: "100.199.1.2", dst: "100.201.3.4", // 201, not 200 - want: []tailcfg.PeerCapability{"is_ipv4"}, + want: []peercap.Cap{"is_ipv4"}, }, } for _, tt := range tests { diff --git a/wgengine/filter/filtertype/filtertype.go b/wgengine/filter/filtertype/filtertype.go index aab5fe8ee..18a4020b1 100644 --- a/wgengine/filter/filtertype/filtertype.go +++ b/wgengine/filter/filtertype/filtertype.go @@ -10,6 +10,8 @@ "strings" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/ipproto" "tailscale.com/types/views" ) @@ -56,7 +58,7 @@ type CapMatch struct { // Cap is the capability that's granted if the destination IP addresses // matches Dst. - Cap tailcfg.PeerCapability + Cap peercap.Cap // Values are the raw JSON values of the capability. // See tailcfg.PeerCapability and tailcfg.PeerCapMap for details. @@ -84,7 +86,7 @@ type Match struct { // has one of these capabilities, it's also permitted. The peers are only // looked up by their self address (Node.Addresses) and not by subnet routes // they advertise. - SrcCaps []tailcfg.NodeCapability + SrcCaps []nodecap.Cap Dsts []NetPortRange // optional, if source matches Caps []CapMatch // optional, if source match diff --git a/wgengine/filter/filtertype/filtertype_clone.go b/wgengine/filter/filtertype/filtertype_clone.go index 094063a5d..34975e2d5 100644 --- a/wgengine/filter/filtertype/filtertype_clone.go +++ b/wgengine/filter/filtertype/filtertype_clone.go @@ -9,6 +9,8 @@ "net/netip" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/ipproto" "tailscale.com/types/views" ) @@ -39,7 +41,7 @@ func (src *Match) Clone() *Match { IPProto views.Slice[ipproto.Proto] Srcs []netip.Prefix SrcsContains func(netip.Addr) bool - SrcCaps []tailcfg.NodeCapability + SrcCaps []nodecap.Cap Dsts []NetPortRange Caps []CapMatch }{}) @@ -59,6 +61,6 @@ func (src *CapMatch) Clone() *CapMatch { // A compilation failure here means this code must be regenerated, with the command at the top of this file. var _CapMatchCloneNeedsRegeneration = CapMatch(struct { Dst netip.Prefix - Cap tailcfg.PeerCapability + Cap peercap.Cap Values []tailcfg.RawMessage }{}) diff --git a/wgengine/filter/match.go b/wgengine/filter/match.go index 6c157d124..33ce3814b 100644 --- a/wgengine/filter/match.go +++ b/wgengine/filter/match.go @@ -7,7 +7,7 @@ "net/netip" "tailscale.com/net/packet" - "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" "tailscale.com/types/views" "tailscale.com/wgengine/filter/filtertype" ) @@ -57,7 +57,7 @@ func srcMatches(m *filtertype.Match, srcAddr netip.Addr, hasCap CapTestFunc) boo // has a given capability. // // It is used in the fast path of evaluating filter rules so should be fast. -type CapTestFunc = func(srcIP netip.Addr, cap tailcfg.NodeCapability) bool +type CapTestFunc = func(srcIP netip.Addr, cap nodecap.Cap) bool func (ms matches) matchIPsOnly(q *packet.Parsed, hasCap CapTestFunc) bool { srcAddr := q.Src.Addr() diff --git a/wgengine/filter/tailcfg.go b/wgengine/filter/tailcfg.go index 3360b169d..06f289c95 100644 --- a/wgengine/filter/tailcfg.go +++ b/wgengine/filter/tailcfg.go @@ -12,6 +12,8 @@ "tailscale.com/net/ipset" "tailscale.com/net/netaddr" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/types/ipproto" "tailscale.com/types/views" ) @@ -104,7 +106,7 @@ func MatchesFromFilterRules(pf []tailcfg.FilterRule) ([]Match, error) { for cap, val := range cm.CapMap { m.Caps = append(m.Caps, CapMatch{ Dst: dstNet, - Cap: tailcfg.PeerCapability(cap), + Cap: peercap.Cap(cap), Values: val, }) } @@ -133,7 +135,7 @@ func MatchesFromFilterRules(pf []tailcfg.FilterRule) ([]Match, error) { // around, and ultimately use a new version of IPSet.ContainsFunc like // Contains16Func that works in [16]byte address, so we can match // at runtime without allocating? -func parseIPSet(arg string) (prefixes []netip.Prefix, peerCap tailcfg.NodeCapability, err error) { +func parseIPSet(arg string) (prefixes []netip.Prefix, peerCap nodecap.Cap, err error) { if arg == "*" { // User explicitly requested wildcard. return []netip.Prefix{ @@ -142,7 +144,7 @@ func parseIPSet(arg string) (prefixes []netip.Prefix, peerCap tailcfg.NodeCapabi }, "", nil } if cap, ok := strings.CutPrefix(arg, "cap:"); ok { - return nil, tailcfg.NodeCapability(cap), nil + return nil, nodecap.Cap(cap), nil } if strings.Contains(arg, "/") { pfx, err := netip.ParsePrefix(arg) diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index c8f8a6280..8b0ee245b 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -51,6 +51,8 @@ "tailscale.com/net/tstun" "tailscale.com/syncs" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/tsconst" "tailscale.com/tstime" "tailscale.com/tstime/mono" @@ -2481,7 +2483,7 @@ func (c *Conn) handleDiscoMessage(msg []byte, src epAddr, shouldBeRelayHandshake // unexpected return } - if !nodeHasCap(c.filt, peer, c.self, tailcfg.PeerCapabilityRelay) { + if !nodeHasCap(c.filt, peer, c.self, peercap.Relay) { return } // [Conn.mu] must not be held while publishing, or [Conn.onUDPRelayAllocResp] @@ -2934,7 +2936,7 @@ func (c *Conn) updateRelayServersSet(filt *filter.Filter, self tailcfg.NodeView, // compiled [tailcfg.CurrentCapabilityVersion]) forward. continue } - if !nodeHasCap(filt, maybeCandidate, self, tailcfg.PeerCapabilityRelayTarget) { + if !nodeHasCap(filt, maybeCandidate, self, peercap.RelayTarget) { continue } relayServers.Add(candidatePeerRelay{ @@ -2949,7 +2951,7 @@ func (c *Conn) updateRelayServersSet(filt *filter.Filter, self tailcfg.NodeView, } // nodeHasCap returns true if src has cap on dst, otherwise it returns false. -func nodeHasCap(filt *filter.Filter, src, dst tailcfg.NodeView, cap tailcfg.PeerCapability) bool { +func nodeHasCap(filt *filter.Filter, src, dst tailcfg.NodeView, cap peercap.Cap) bool { if filt == nil || !src.Valid() || !dst.Valid() { @@ -3025,8 +3027,8 @@ func (c *Conn) setNetworkMapInternal(self tailcfg.NodeView, peers []tailcfg.Node peersChanged, selfWasValid := c.updateNodes(self, peers) relayClientEnabled := self.Valid() && - !self.HasCap(tailcfg.NodeAttrDisableRelayClient) && - !self.HasCap(tailcfg.NodeAttrOnlyTCP443) + !self.HasCap(nodecap.DisableRelayClient) && + !self.HasCap(nodecap.OnlyTCP443) udpOffloadKnobsChanged := false var curGRO, curGSO bool @@ -3391,7 +3393,7 @@ func (c *Conn) relayCandidateLocked(p tailcfg.NodeView) (ok bool, cp candidatePe if !capVerIsRelayCapable(p.Cap()) { return false, candidatePeerRelay{} } - if !nodeHasCap(c.filt, p, c.self, tailcfg.PeerCapabilityRelayTarget) { + if !nodeHasCap(c.filt, p, c.self, peercap.RelayTarget) { return false, candidatePeerRelay{} } return true, candidatePeerRelay{ diff --git a/wgengine/magicsock/magicsock_test.go b/wgengine/magicsock/magicsock_test.go index 34cf2fb8c..654df9352 100644 --- a/wgengine/magicsock/magicsock_test.go +++ b/wgengine/magicsock/magicsock_test.go @@ -60,6 +60,8 @@ "tailscale.com/net/tsaddr" "tailscale.com/net/tstun" "tailscale.com/tailcfg" + "tailscale.com/tailcfg/nodecap" + "tailscale.com/tailcfg/peercap" "tailscale.com/tstest" "tailscale.com/tstest/natlab" "tailscale.com/tstime/mono" @@ -3829,7 +3831,7 @@ func Test_nodeHasCap(t *testing.T) { filt *filter.Filter src tailcfg.NodeView dst tailcfg.NodeView - cap tailcfg.PeerCapability + cap peercap.Cap want bool }{ { @@ -3840,14 +3842,14 @@ func Test_nodeHasCap(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: netip.MustParsePrefix("1.1.1.1/32"), - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, }, }, nil, nil, nil, nil, nil), src: nodeCOnlyIPv4.View(), dst: nodeAOnlyIPv4.View(), - cap: tailcfg.PeerCapabilityRelayTarget, + cap: peercap.RelayTarget, want: true, }, { @@ -3858,14 +3860,14 @@ func Test_nodeHasCap(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: netip.MustParsePrefix("::1/128"), - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, }, }, nil, nil, nil, nil, nil), src: nodeDOnlyIPv6.View(), dst: nodeBOnlyIPv6.View(), - cap: tailcfg.PeerCapabilityRelayTarget, + cap: peercap.RelayTarget, want: true, }, { @@ -3876,14 +3878,14 @@ func Test_nodeHasCap(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: netip.MustParsePrefix("::3/128"), - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, }, }, nil, nil, nil, nil, nil), src: nodeDOnlyIPv6.View(), dst: nodeBOnlyIPv6.View(), - cap: tailcfg.PeerCapabilityRelayTarget, + cap: peercap.RelayTarget, want: false, }, { @@ -3894,14 +3896,14 @@ func Test_nodeHasCap(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: netip.MustParsePrefix("::1/128"), - Cap: tailcfg.PeerCapabilityIngress, + Cap: peercap.Ingress, }, }, }, }, nil, nil, nil, nil, nil), src: nodeDOnlyIPv6.View(), dst: nodeBOnlyIPv6.View(), - cap: tailcfg.PeerCapabilityRelayTarget, + cap: peercap.RelayTarget, want: false, }, { @@ -3912,14 +3914,14 @@ func Test_nodeHasCap(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: netip.MustParsePrefix("1.1.1.1/32"), - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, }, }, nil, nil, nil, nil, nil), src: tailcfg.NodeView{}, dst: nodeAOnlyIPv4.View(), - cap: tailcfg.PeerCapabilityRelayTarget, + cap: peercap.RelayTarget, want: false, }, { @@ -3930,14 +3932,14 @@ func Test_nodeHasCap(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: netip.MustParsePrefix("1.1.1.1/32"), - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, }, }, nil, nil, nil, nil, nil), src: nodeCOnlyIPv4.View(), dst: tailcfg.NodeView{}, - cap: tailcfg.PeerCapabilityRelayTarget, + cap: peercap.RelayTarget, want: false, }, { @@ -3948,14 +3950,14 @@ func Test_nodeHasCap(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: netip.MustParsePrefix("1.1.1.1/32"), - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, }, }, nil, nil, nil, nil, nil), src: nodeCUnsigned.View(), dst: nodeAOnlyIPv4.View(), - cap: tailcfg.PeerCapabilityRelayTarget, + cap: peercap.RelayTarget, want: false, }, } @@ -4004,11 +4006,11 @@ func TestConn_SetNetworkMap_updateRelayServersSet(t *testing.T) { selfNodeNodeAttrDisableRelayClient := selfNode.Clone() selfNodeNodeAttrDisableRelayClient.CapMap = make(tailcfg.NodeCapMap) - selfNodeNodeAttrDisableRelayClient.CapMap[tailcfg.NodeAttrDisableRelayClient] = nil + selfNodeNodeAttrDisableRelayClient.CapMap[nodecap.DisableRelayClient] = nil selfNodeNodeAttrOnlyTCP443 := selfNode.Clone() selfNodeNodeAttrOnlyTCP443.CapMap = make(tailcfg.NodeCapMap) - selfNodeNodeAttrOnlyTCP443.CapMap[tailcfg.NodeAttrOnlyTCP443] = nil + selfNodeNodeAttrOnlyTCP443.CapMap[nodecap.OnlyTCP443] = nil tests := []struct { name string @@ -4026,7 +4028,7 @@ func TestConn_SetNetworkMap_updateRelayServersSet(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: selfNode.Addresses[0], - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, }, @@ -4050,7 +4052,7 @@ func TestConn_SetNetworkMap_updateRelayServersSet(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: selfNodeNodeAttrDisableRelayClient.Addresses[0], - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, }, @@ -4068,7 +4070,7 @@ func TestConn_SetNetworkMap_updateRelayServersSet(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: selfNodeNodeAttrOnlyTCP443.Addresses[0], - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, }, @@ -4086,7 +4088,7 @@ func TestConn_SetNetworkMap_updateRelayServersSet(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: selfNode.Addresses[0], - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, }, @@ -4110,7 +4112,7 @@ func TestConn_SetNetworkMap_updateRelayServersSet(t *testing.T) { Caps: []filtertype.CapMatch{ { Dst: selfNode.Addresses[0], - Cap: tailcfg.PeerCapabilityRelayTarget, + Cap: peercap.RelayTarget, }, }, },