Add serviceclientprefs, an optional feature that stores and loads the desktop clients' saved service launch preferences, one file per login profile. - Add GET|POST /localapi/v0/prefs/service-clients to load and save the current profile's service client prefs. - Add local client GetServiceClientPrefs and SetServiceClientPref that call the new local api endpoint. - Store the prefs with the ipn/store FileStore at TailscaleVarRoot()/profile-data/<profileID>/service-client-prefs/<hex-encoded-key>, so DeleteProfile cleans them up for free. Fall back to an in-memory store when there's no var root. - Register the feature and its local api route from build tagged files so the whole thing drops out under ts_omit_serviceclientprefs. - Add the serviceclient package holding Pref and Prefs (saved client, username, database name, and last used time), so the local api client and desktop apps can import the types without the feature machinery. Change-Id: I340a99c1b332d181fb1556fbf3e8003bb3b95a08 Updates: https://github.com/tailscale/tailscale/issues/20429 Signed-off-by: Rollie Ma <rollie@tailscale.com>
358 lines
9.5 KiB
Go
358 lines
9.5 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package main
|
|
|
|
import (
|
|
"maps"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"tailscale.com/feature/featuretags"
|
|
"tailscale.com/tstest/deptest"
|
|
)
|
|
|
|
func TestOmitServiceClientPrefs(t *testing.T) {
|
|
const msg = "unexpected with ts_omit_serviceclientprefs"
|
|
deptest.DepChecker{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Tags: "ts_omit_serviceclientprefs,ts_include_cli",
|
|
BadDeps: map[string]string{
|
|
"tailscale.com/feature/serviceclientprefs": msg,
|
|
},
|
|
}.Check(t)
|
|
}
|
|
|
|
func TestOmitSSH(t *testing.T) {
|
|
const msg = "unexpected with ts_omit_ssh"
|
|
deptest.DepChecker{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Tags: "ts_omit_ssh,ts_include_cli",
|
|
BadDeps: map[string]string{
|
|
"golang.org/x/crypto/ssh": msg,
|
|
"tailscale.com/ssh/tailssh": msg,
|
|
"tailscale.com/sessionrecording": msg,
|
|
"github.com/anmitsu/go-shlex": msg,
|
|
"github.com/creack/pty": msg,
|
|
"github.com/kr/fs": msg,
|
|
"github.com/pkg/sftp": msg,
|
|
"github.com/u-root/u-root/pkg/termios": msg,
|
|
"tempfork/gliderlabs/ssh": msg,
|
|
},
|
|
}.Check(t)
|
|
}
|
|
|
|
func TestOmitSyslog(t *testing.T) {
|
|
const msg = "unexpected syslog usage with ts_omit_syslog"
|
|
deptest.DepChecker{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
// Tailscale SSH's incubator also uses log/syslog, so omit
|
|
// SSH too to lock down the standard library package.
|
|
Tags: "ts_omit_syslog,ts_omit_ssh,ts_include_cli",
|
|
BadDeps: map[string]string{
|
|
"log/syslog": msg,
|
|
"tailscale.com/feature/syslog": msg,
|
|
},
|
|
}.Check(t)
|
|
}
|
|
|
|
func TestOmitSyspolicy(t *testing.T) {
|
|
const msg = "unexpected syspolicy usage with ts_omit_syspolicy"
|
|
deptest.DepChecker{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Tags: "ts_omit_syspolicy,ts_include_cli",
|
|
BadDeps: map[string]string{
|
|
"tailscale.com/util/syspolicy": msg,
|
|
"tailscale.com/util/syspolicy/setting": msg,
|
|
"tailscale.com/util/syspolicy/rsop": msg,
|
|
},
|
|
}.Check(t)
|
|
}
|
|
|
|
func TestOmitLocalClient(t *testing.T) {
|
|
deptest.DepChecker{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Tags: "ts_omit_webclient,ts_omit_relayserver,ts_omit_oauthkey,ts_omit_acme",
|
|
BadDeps: map[string]string{
|
|
"tailscale.com/client/local": "unexpected",
|
|
},
|
|
}.Check(t)
|
|
}
|
|
|
|
// Test that we can build a binary without reflect.MethodByName.
|
|
// See https://github.com/tailscale/tailscale/issues/17063
|
|
func TestOmitReflectThings(t *testing.T) {
|
|
deptest.DepChecker{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Tags: "ts_include_cli,ts_omit_systray,ts_omit_debugeventbus,ts_omit_webclient",
|
|
BadDeps: map[string]string{
|
|
"text/template": "unexpected text/template usage",
|
|
"html/template": "unexpected text/template usage",
|
|
},
|
|
OnDep: func(dep string) {
|
|
if strings.Contains(dep, "systray") {
|
|
t.Errorf("unexpected systray dep %q", dep)
|
|
}
|
|
},
|
|
}.Check(t)
|
|
}
|
|
|
|
func TestOmitDrive(t *testing.T) {
|
|
deptest.DepChecker{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Tags: "ts_omit_drive,ts_include_cli",
|
|
OnDep: func(dep string) {
|
|
if strings.Contains(dep, "driveimpl") {
|
|
t.Errorf("unexpected dep with ts_omit_drive: %q", dep)
|
|
}
|
|
if strings.Contains(dep, "webdav") {
|
|
t.Errorf("unexpected dep with ts_omit_drive: %q", dep)
|
|
}
|
|
},
|
|
}.Check(t)
|
|
}
|
|
|
|
func TestOmitPortmapper(t *testing.T) {
|
|
deptest.DepChecker{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Tags: "ts_omit_portmapper,ts_include_cli,ts_omit_debugportmapper",
|
|
OnDep: func(dep string) {
|
|
if dep == "tailscale.com/net/portmapper" {
|
|
t.Errorf("unexpected dep with ts_omit_portmapper: %q", dep)
|
|
return
|
|
}
|
|
if strings.Contains(dep, "goupnp") || strings.Contains(dep, "/soap") ||
|
|
strings.Contains(dep, "internetgateway2") {
|
|
t.Errorf("unexpected dep with ts_omit_portmapper: %q", dep)
|
|
}
|
|
},
|
|
}.Check(t)
|
|
}
|
|
|
|
func TestOmitACME(t *testing.T) {
|
|
deptest.DepChecker{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Tags: "ts_omit_acme,ts_include_cli",
|
|
OnDep: func(dep string) {
|
|
if strings.Contains(dep, "/acme") {
|
|
t.Errorf("unexpected dep with ts_omit_acme: %q", dep)
|
|
}
|
|
},
|
|
}.Check(t)
|
|
}
|
|
|
|
func TestOmitCaptivePortal(t *testing.T) {
|
|
deptest.DepChecker{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Tags: "ts_omit_captiveportal,ts_include_cli",
|
|
OnDep: func(dep string) {
|
|
if strings.Contains(dep, "captive") {
|
|
t.Errorf("unexpected dep with ts_omit_captiveportal: %q", dep)
|
|
}
|
|
},
|
|
}.Check(t)
|
|
}
|
|
|
|
func TestOmitBird(t *testing.T) {
|
|
deptest.DepChecker{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Tags: "ts_omit_bird,ts_include_cli",
|
|
OnDep: func(dep string) {
|
|
switch dep {
|
|
case "tailscale.com/chirp", "tailscale.com/feature/bird":
|
|
t.Errorf("unexpected dep with ts_omit_bird: %q", dep)
|
|
}
|
|
},
|
|
}.Check(t)
|
|
}
|
|
|
|
func TestOmitAuth(t *testing.T) {
|
|
deptest.DepChecker{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Tags: "ts_omit_oauthkey,ts_omit_identityfederation,ts_include_cli",
|
|
OnDep: func(dep string) {
|
|
if strings.HasPrefix(dep, "golang.org/x/oauth2") {
|
|
t.Errorf("unexpected oauth2 dep: %q", dep)
|
|
}
|
|
},
|
|
}.Check(t)
|
|
}
|
|
|
|
func TestOmitOutboundProxy(t *testing.T) {
|
|
deptest.DepChecker{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Tags: "ts_omit_outboundproxy,ts_include_cli",
|
|
OnDep: func(dep string) {
|
|
if strings.Contains(dep, "socks5") || strings.Contains(dep, "proxymux") {
|
|
t.Errorf("unexpected dep with ts_omit_outboundproxy: %q", dep)
|
|
}
|
|
},
|
|
}.Check(t)
|
|
}
|
|
|
|
func TestOmitDBus(t *testing.T) {
|
|
deptest.DepChecker{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Tags: "ts_omit_networkmanager,ts_omit_dbus,ts_omit_resolved,ts_omit_systray,ts_omit_ssh,ts_include_cli",
|
|
OnDep: func(dep string) {
|
|
if strings.Contains(dep, "dbus") {
|
|
t.Errorf("unexpected DBus dep: %q", dep)
|
|
}
|
|
},
|
|
}.Check(t)
|
|
}
|
|
|
|
func TestNetstack(t *testing.T) {
|
|
deptest.DepChecker{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Tags: "ts_omit_gro,ts_omit_netstack,ts_omit_outboundproxy,ts_omit_serve,ts_omit_ssh,ts_omit_webclient,ts_omit_tap",
|
|
OnDep: func(dep string) {
|
|
if strings.Contains(dep, "gvisor") {
|
|
t.Errorf("unexpected gvisor dep: %q", dep)
|
|
}
|
|
},
|
|
}.Check(t)
|
|
}
|
|
|
|
func TestOmitPortlist(t *testing.T) {
|
|
deptest.DepChecker{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Tags: "ts_omit_portlist,ts_include_cli",
|
|
OnDep: func(dep string) {
|
|
if strings.Contains(dep, "portlist") {
|
|
t.Errorf("unexpected dep: %q", dep)
|
|
}
|
|
},
|
|
}.Check(t)
|
|
}
|
|
|
|
func TestOmitRouteCheck(t *testing.T) {
|
|
deptest.DepChecker{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Tags: "ts_omit_routecheck,ts_include_cli",
|
|
OnDep: func(dep string) {
|
|
if strings.Contains(dep, "routecheck") && !strings.HasSuffix(dep, "/peernode") {
|
|
t.Errorf("unexpected dep: %q", dep)
|
|
}
|
|
},
|
|
}.Check(t)
|
|
}
|
|
|
|
func TestOmitGRO(t *testing.T) {
|
|
deptest.DepChecker{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Tags: "ts_omit_gro,ts_include_cli",
|
|
BadDeps: map[string]string{
|
|
"gvisor.dev/gvisor/pkg/tcpip/stack/gro": "unexpected dep with ts_omit_gro",
|
|
},
|
|
}.Check(t)
|
|
}
|
|
|
|
func TestOmitUseProxy(t *testing.T) {
|
|
deptest.DepChecker{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Tags: "ts_omit_useproxy,ts_include_cli",
|
|
OnDep: func(dep string) {
|
|
if strings.Contains(dep, "tshttproxy") {
|
|
t.Errorf("unexpected dep: %q", dep)
|
|
}
|
|
},
|
|
}.Check(t)
|
|
}
|
|
|
|
func minTags() string {
|
|
var tags []string
|
|
for _, f := range slices.Sorted(maps.Keys(featuretags.Features)) {
|
|
if f.IsOmittable() {
|
|
tags = append(tags, f.OmitTag())
|
|
}
|
|
}
|
|
return strings.Join(tags, ",")
|
|
}
|
|
|
|
func TestMinTailscaledNoCLI(t *testing.T) {
|
|
badSubstrs := []string{
|
|
"cbor",
|
|
"regexp",
|
|
"golang.org/x/net/proxy",
|
|
"internal/socks",
|
|
"github.com/tailscale/peercred",
|
|
"tailscale.com/types/netlogtype",
|
|
"deephash",
|
|
"util/hashx",
|
|
}
|
|
deptest.DepChecker{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Tags: minTags(),
|
|
OnDep: func(dep string) {
|
|
for _, bad := range badSubstrs {
|
|
if strings.Contains(dep, bad) {
|
|
t.Errorf("unexpected dep: %q", dep)
|
|
}
|
|
}
|
|
},
|
|
}.Check(t)
|
|
}
|
|
|
|
func TestMinTailscaledWithCLI(t *testing.T) {
|
|
badSubstrs := []string{
|
|
"cbor",
|
|
"hujson",
|
|
"multierr", // https://github.com/tailscale/tailscale/pull/17379
|
|
"tailscale.com/metrics",
|
|
"tailscale.com/tsweb/varz",
|
|
"dirwalk",
|
|
"deephash",
|
|
"util/hashx",
|
|
}
|
|
deptest.DepChecker{
|
|
GOOS: "linux",
|
|
GOARCH: "amd64",
|
|
Tags: minTags() + ",ts_include_cli",
|
|
OnDep: func(dep string) {
|
|
for _, bad := range badSubstrs {
|
|
if strings.Contains(dep, bad) {
|
|
t.Errorf("unexpected dep: %q", dep)
|
|
}
|
|
}
|
|
},
|
|
BadDeps: map[string]string{
|
|
"golang.org/x/net/http2": "unexpected x/net/http2 dep; tailscale/tailscale#17305",
|
|
"expvar": "unexpected expvar dep",
|
|
"runtime/pprof": "unexpected runtime/pprof dep",
|
|
"net/http/pprof": "unexpected net/http/pprof dep",
|
|
"github.com/mdlayher/genetlink": "unexpected genetlink dep",
|
|
"tailscale.com/clientupdate": "unexpected clientupdate dep",
|
|
"filippo.io/edwards25519": "unexpected edwards25519 dep",
|
|
"github.com/hdevalence/ed25519consensus": "unexpected ed25519consensus dep",
|
|
"tailscale.com/clientupdate/distsign": "unexpected distsign dep",
|
|
"archive/tar": "unexpected archive/tar dep",
|
|
"tailscale.com/feature/conn25": "unexpected conn25 dep",
|
|
"regexp": "unexpected regexp dep; bloats binary",
|
|
"github.com/toqueteos/webbrowser": "unexpected webbrowser dep with ts_omit_webbrowser",
|
|
"github.com/mattn/go-colorable": "unexpected go-colorable dep with ts_omit_colorable",
|
|
},
|
|
}.Check(t)
|
|
}
|