Compare commits
117 Commits
andrew/deb
...
jonathan/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5be738b118 | ||
|
|
01847e0123 | ||
|
|
42cfbf427c | ||
|
|
bcb55fdeb6 | ||
|
|
c2a4719e9e | ||
|
|
36d0ac6f8e | ||
|
|
0a5bd63d32 | ||
|
|
1ec0273473 | ||
|
|
f227083539 | ||
|
|
7e357e1636 | ||
|
|
0380cbc90d | ||
|
|
32120932a5 | ||
|
|
776a05223b | ||
|
|
1ea100e2e5 | ||
|
|
2d2b62c400 | ||
|
|
909a292a8d | ||
|
|
0acb61fbf8 | ||
|
|
dd77111462 | ||
|
|
08a9551a73 | ||
|
|
f1d10c12ac | ||
|
|
5ad0dad15e | ||
|
|
d0d33f257f | ||
|
|
8e4a29433f | ||
|
|
87ee559b6f | ||
|
|
9a64c06a20 | ||
|
|
4214e5f71b | ||
|
|
538c2e8f7c | ||
|
|
3c9be07214 | ||
|
|
72f0f53ed0 | ||
|
|
9351eec3e1 | ||
|
|
c9179bc261 | ||
|
|
6db1219185 | ||
|
|
4f4f317174 | ||
|
|
964282d34f | ||
|
|
1384c24e41 | ||
|
|
47b3476eb7 | ||
|
|
c56e0c4934 | ||
|
|
adb7a86559 | ||
|
|
8d1249550a | ||
|
|
6831a29f8b | ||
|
|
e5f67f90a2 | ||
|
|
59848fe14b | ||
|
|
87f00d76c4 | ||
|
|
76c30e014d | ||
|
|
8feb4ff5d2 | ||
|
|
359ef61263 | ||
|
|
89947606b2 | ||
|
|
b094e8c925 | ||
|
|
e3dec086e6 | ||
|
|
7f83f9fc83 | ||
|
|
6877d44965 | ||
|
|
1f51bb6891 | ||
|
|
60266be298 | ||
|
|
c6d42b1093 | ||
|
|
7ef2f72135 | ||
|
|
8aa5c3534d | ||
|
|
7b3e30f391 | ||
|
|
79b2d425cf | ||
|
|
fc1ae97e10 | ||
|
|
486a423716 | ||
|
|
7209c4f91e | ||
|
|
d86d1e7601 | ||
|
|
e070af7414 | ||
|
|
5708fc0639 | ||
|
|
25e32cc3ae | ||
|
|
21abb7f402 | ||
|
|
ac638f32c0 | ||
|
|
b5dbf155b1 | ||
|
|
8f7f9ac17e | ||
|
|
7901925ad3 | ||
|
|
8130656780 | ||
|
|
6f4a1dc6bf | ||
|
|
e968b0ecd7 | ||
|
|
e5ef35857f | ||
|
|
21509db121 | ||
|
|
727c0d6cfd | ||
|
|
32bc596062 | ||
|
|
9380e2dfc6 | ||
|
|
e1011f1387 | ||
|
|
85b9a6c601 | ||
|
|
d7bdd8e2a7 | ||
|
|
3c4c9dc1d2 | ||
|
|
80df8ffb85 | ||
|
|
471731771c | ||
|
|
78fa698fe6 | ||
|
|
482890b9ed | ||
|
|
af97e7a793 | ||
|
|
e67069550b | ||
|
|
f62e678df8 | ||
|
|
c28f5767bf | ||
|
|
5ef178fdca | ||
|
|
f3d2fd22ef | ||
|
|
aadb8d9d21 | ||
|
|
e26f76a1c4 | ||
|
|
caa3d7594f | ||
|
|
ce8969d82b | ||
|
|
7e0dd61e61 | ||
|
|
258b5042fe | ||
|
|
c3c18027c6 | ||
|
|
41f2195899 | ||
|
|
1a963342c7 | ||
|
|
80decd83c1 | ||
|
|
ed843e643f | ||
|
|
fd6ba43b97 | ||
|
|
46980c9664 | ||
|
|
817badf9ca | ||
|
|
2cf764e998 | ||
|
|
406293682c | ||
|
|
35872e86d2 | ||
|
|
b62cfc430a | ||
|
|
e9505e5432 | ||
|
|
e42c4396cf | ||
|
|
15fc6cd966 | ||
|
|
1fe0983f2d | ||
|
|
46f3feae96 | ||
|
|
4fa6cbec27 | ||
|
|
ee3bd4dbda |
5
Makefile
5
Makefile
@@ -115,10 +115,7 @@ sshintegrationtest: ## Run the SSH integration tests in various Docker container
|
||||
echo "Testing on ubuntu:focal" && docker build --build-arg="BASE=ubuntu:focal" -t ssh-ubuntu-focal ssh/tailssh/testcontainers && \
|
||||
echo "Testing on ubuntu:jammy" && docker build --build-arg="BASE=ubuntu:jammy" -t ssh-ubuntu-jammy ssh/tailssh/testcontainers && \
|
||||
echo "Testing on ubuntu:mantic" && docker build --build-arg="BASE=ubuntu:mantic" -t ssh-ubuntu-mantic ssh/tailssh/testcontainers && \
|
||||
echo "Testing on ubuntu:noble" && docker build --build-arg="BASE=ubuntu:noble" -t ssh-ubuntu-noble ssh/tailssh/testcontainers && \
|
||||
echo "Testing on fedora:38" && docker build --build-arg="BASE=dokken/fedora-38" -t ssh-fedora-38 ssh/tailssh/testcontainers && \
|
||||
echo "Testing on fedora:39" && docker build --build-arg="BASE=dokken/fedora-39" -t ssh-fedora-39 ssh/tailssh/testcontainers && \
|
||||
echo "Testing on fedora:40" && docker build --build-arg="BASE=dokken/fedora-40" -t ssh-fedora-40 ssh/tailssh/testcontainers
|
||||
echo "Testing on ubuntu:noble" && docker build --build-arg="BASE=ubuntu:noble" -t ssh-ubuntu-noble ssh/tailssh/testcontainers
|
||||
|
||||
help: ## Show this help
|
||||
@echo "\nSpecify a command. The choices are:\n"
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.65.0
|
||||
1.67.0
|
||||
|
||||
@@ -778,6 +778,17 @@ func (lc *LocalClient) SetDNS(ctx context.Context, name, value string) error {
|
||||
//
|
||||
// The ctx is only used for the duration of the call, not the lifetime of the net.Conn.
|
||||
func (lc *LocalClient) DialTCP(ctx context.Context, host string, port uint16) (net.Conn, error) {
|
||||
return lc.UserDial(ctx, "tcp", host, port)
|
||||
}
|
||||
|
||||
// UserDial connects to the host's port via Tailscale for the given network.
|
||||
//
|
||||
// The host may be a base DNS name (resolved from the netmap inside tailscaled),
|
||||
// a FQDN, or an IP address.
|
||||
//
|
||||
// The ctx is only used for the duration of the call, not the lifetime of the
|
||||
// net.Conn.
|
||||
func (lc *LocalClient) UserDial(ctx context.Context, network, host string, port uint16) (net.Conn, error) {
|
||||
connCh := make(chan net.Conn, 1)
|
||||
trace := httptrace.ClientTrace{
|
||||
GotConn: func(info httptrace.GotConnInfo) {
|
||||
@@ -790,10 +801,11 @@ func (lc *LocalClient) DialTCP(ctx context.Context, host string, port uint16) (n
|
||||
return nil, err
|
||||
}
|
||||
req.Header = http.Header{
|
||||
"Upgrade": []string{"ts-dial"},
|
||||
"Connection": []string{"upgrade"},
|
||||
"Dial-Host": []string{host},
|
||||
"Dial-Port": []string{fmt.Sprint(port)},
|
||||
"Upgrade": []string{"ts-dial"},
|
||||
"Connection": []string{"upgrade"},
|
||||
"Dial-Host": []string{host},
|
||||
"Dial-Port": []string{fmt.Sprint(port)},
|
||||
"Dial-Network": []string{network},
|
||||
}
|
||||
res, err := lc.DoLocalRequest(req)
|
||||
if err != nil {
|
||||
|
||||
@@ -35,6 +35,7 @@ func TestDeps(t *testing.T) {
|
||||
BadDeps: map[string]string{
|
||||
// Make sure we don't again accidentally bring in a dependency on
|
||||
// drive or its transitive dependencies
|
||||
"testing": "do not use testing package in production code",
|
||||
"tailscale.com/drive/driveimpl": "https://github.com/tailscale/tailscale/pull/10631",
|
||||
"github.com/studio-b12/gowebdav": "https://github.com/tailscale/tailscale/pull/10631",
|
||||
},
|
||||
|
||||
@@ -1150,7 +1150,15 @@ func (s *Server) tailscaleUp(ctx context.Context, st *ipnstate.Status, opt tails
|
||||
if !isRunning {
|
||||
ipnOptions := ipn.Options{AuthKey: opt.AuthKey}
|
||||
if opt.ControlURL != "" {
|
||||
ipnOptions.UpdatePrefs = &ipn.Prefs{ControlURL: opt.ControlURL}
|
||||
_, err := s.lc.EditPrefs(ctx, &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
ControlURL: opt.ControlURL,
|
||||
},
|
||||
ControlURLSet: true,
|
||||
})
|
||||
if err != nil {
|
||||
s.logf("edit prefs: %v", err)
|
||||
}
|
||||
}
|
||||
if err := s.lc.Start(ctx, ipnOptions); err != nil {
|
||||
s.logf("start: %v", err)
|
||||
|
||||
@@ -653,6 +653,9 @@ func (up *Updater) updateAlpineLike() (err error) {
|
||||
return fmt.Errorf(`failed to parse latest version from "apk info tailscale": %w`, err)
|
||||
}
|
||||
if !up.confirm(ver) {
|
||||
if err := checkOutdatedAlpineRepo(up.Logf, ver, up.Track); err != nil {
|
||||
up.Logf("failed to check whether Alpine release is outdated: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -690,6 +693,37 @@ func parseAlpinePackageVersion(out []byte) (string, error) {
|
||||
return "", errors.New("tailscale version not found in output")
|
||||
}
|
||||
|
||||
var apkRepoVersionRE = regexp.MustCompile(`v[0-9]+\.[0-9]+`)
|
||||
|
||||
func checkOutdatedAlpineRepo(logf logger.Logf, apkVer, track string) error {
|
||||
latest, err := LatestTailscaleVersion(track)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if latest == apkVer {
|
||||
// Actually on latest release.
|
||||
return nil
|
||||
}
|
||||
f, err := os.Open("/etc/apk/repositories")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
// Read the first repo line. Typically, there are multiple repos that all
|
||||
// contain the same version in the path, like:
|
||||
// https://dl-cdn.alpinelinux.org/alpine/v3.20/main
|
||||
// https://dl-cdn.alpinelinux.org/alpine/v3.20/community
|
||||
s := bufio.NewScanner(f)
|
||||
if !s.Scan() {
|
||||
return s.Err()
|
||||
}
|
||||
alpineVer := apkRepoVersionRE.FindString(s.Text())
|
||||
if alpineVer != "" {
|
||||
logf("The latest Tailscale release for Linux is %q, but your apk repository only provides %q.\nYour Alpine version is %q, you may need to upgrade the system to get the latest Tailscale version: https://wiki.alpinelinux.org/wiki/Upgrading_Alpine", latest, apkVer, alpineVer)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (up *Updater) updateMacSys() error {
|
||||
return errors.New("NOTREACHED: On MacSys builds, `tailscale update` is handled in Swift to launch the GUI updater")
|
||||
}
|
||||
|
||||
@@ -138,9 +138,9 @@ func initKubeClient(root string) {
|
||||
if err != nil {
|
||||
log.Fatalf("Error creating kube client: %v", err)
|
||||
}
|
||||
if root != "/" {
|
||||
// If we are running in a test, we need to set the URL to the
|
||||
// httptest server.
|
||||
if (root != "/") || os.Getenv("TS_KUBERNETES_READ_API_SERVER_ADDRESS_FROM_ENV") == "true" {
|
||||
// Derive the API server address from the environment variables
|
||||
// Used to set http server in tests, or optionally enabled by flag
|
||||
kc.SetURL(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,8 +52,10 @@
|
||||
// ${TS_CERT_DOMAIN}, it will be replaced with the value of the available FQDN.
|
||||
// It cannot be used in conjunction with TS_DEST_IP. The file is watched for changes,
|
||||
// and will be re-applied when it changes.
|
||||
// - EXPERIMENTAL_TS_CONFIGFILE_PATH: if specified, a path to tailscaled
|
||||
// config. If this is set, TS_HOSTNAME, TS_EXTRA_ARGS, TS_AUTHKEY,
|
||||
// - TS_EXPERIMENTAL_VERSIONED_CONFIG_DIR: if specified, a path to a
|
||||
// directory that containers tailscaled config in file. The config file needs to be
|
||||
// named cap-<current-tailscaled-cap>.hujson. If this is set, TS_HOSTNAME,
|
||||
// TS_EXTRA_ARGS, TS_AUTHKEY,
|
||||
// TS_ROUTES, TS_ACCEPT_DNS env vars must not be set. If this is set,
|
||||
// containerboot only runs `tailscaled --config <path-to-this-configfile>`
|
||||
// and not `tailscale up` or `tailscale set`.
|
||||
@@ -92,6 +94,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
@@ -107,6 +110,7 @@ import (
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/conffile"
|
||||
kubeutils "tailscale.com/k8s-operator"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/ptr"
|
||||
@@ -145,7 +149,7 @@ func main() {
|
||||
Socket: defaultEnv("TS_SOCKET", "/tmp/tailscaled.sock"),
|
||||
AuthOnce: defaultBool("TS_AUTH_ONCE", false),
|
||||
Root: defaultEnv("TS_TEST_ONLY_ROOT", "/"),
|
||||
TailscaledConfigFilePath: defaultEnv("EXPERIMENTAL_TS_CONFIGFILE_PATH", ""),
|
||||
TailscaledConfigFilePath: tailscaledConfigFilePath(),
|
||||
AllowProxyingClusterTrafficViaIngress: defaultBool("EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS", false),
|
||||
PodIP: defaultEnv("POD_IP", ""),
|
||||
}
|
||||
@@ -957,16 +961,23 @@ func installIngressForwardingRule(ctx context.Context, dstStr string, tsIPs []ne
|
||||
return err
|
||||
}
|
||||
var local netip.Addr
|
||||
proxyHasIPv4Address := false
|
||||
for _, pfx := range tsIPs {
|
||||
if !pfx.IsSingleIP() {
|
||||
continue
|
||||
}
|
||||
if pfx.Addr().Is4() {
|
||||
proxyHasIPv4Address = true
|
||||
}
|
||||
if pfx.Addr().Is4() != dst.Is4() {
|
||||
continue
|
||||
}
|
||||
local = pfx.Addr()
|
||||
break
|
||||
}
|
||||
if proxyHasIPv4Address && dst.Is6() {
|
||||
log.Printf("Warning: proxy backend ClusterIP is an IPv6 address and the proxy has a IPv4 tailnet address. You might need to disable IPv4 address allocation for the proxy for forwarding to work. See https://github.com/tailscale/tailscale/issues/12156")
|
||||
}
|
||||
if !local.IsValid() {
|
||||
return fmt.Errorf("no tailscale IP matching family of %s found in %v", dstStr, tsIPs)
|
||||
}
|
||||
@@ -1097,6 +1108,13 @@ type settings struct {
|
||||
|
||||
func (s *settings) validate() error {
|
||||
if s.TailscaledConfigFilePath != "" {
|
||||
dir, file := path.Split(s.TailscaledConfigFilePath)
|
||||
if _, err := os.Stat(dir); err != nil {
|
||||
return fmt.Errorf("error validating whether directory with tailscaled config file %s exists: %w", dir, err)
|
||||
}
|
||||
if _, err := os.Stat(s.TailscaledConfigFilePath); err != nil {
|
||||
return fmt.Errorf("error validating whether tailscaled config directory %q contains tailscaled config for current capability version %q: %w. If this is a Tailscale Kubernetes operator proxy, please ensure that the version of the operator is not older than the version of the proxy", dir, file, err)
|
||||
}
|
||||
if _, err := conffile.Load(s.TailscaledConfigFilePath); err != nil {
|
||||
return fmt.Errorf("error validating tailscaled configfile contents: %w", err)
|
||||
}
|
||||
@@ -1120,7 +1138,7 @@ func (s *settings) validate() error {
|
||||
return errors.New("Both TS_TAILNET_TARGET_IP and TS_TAILNET_FQDN cannot be set")
|
||||
}
|
||||
if s.TailscaledConfigFilePath != "" && (s.AcceptDNS != nil || s.AuthKey != "" || s.Routes != nil || s.ExtraArgs != "" || s.Hostname != "") {
|
||||
return errors.New("EXPERIMENTAL_TS_CONFIGFILE_PATH cannot be set in combination with TS_HOSTNAME, TS_EXTRA_ARGS, TS_AUTHKEY, TS_ROUTES, TS_ACCEPT_DNS.")
|
||||
return errors.New("TS_EXPERIMENTAL_VERSIONED_CONFIG_DIR cannot be set in combination with TS_HOSTNAME, TS_EXTRA_ARGS, TS_AUTHKEY, TS_ROUTES, TS_ACCEPT_DNS.")
|
||||
}
|
||||
if s.AllowProxyingClusterTrafficViaIngress && s.UserspaceMode {
|
||||
return errors.New("EXPERIMENTAL_ALLOW_PROXYING_CLUSTER_TRAFFIC_VIA_INGRESS is not supported in userspace mode")
|
||||
@@ -1252,3 +1270,42 @@ func isTwoStepConfigAlwaysAuth(cfg *settings) bool {
|
||||
func isOneStepConfig(cfg *settings) bool {
|
||||
return cfg.TailscaledConfigFilePath != ""
|
||||
}
|
||||
|
||||
// tailscaledConfigFilePath returns the path to the tailscaled config file that
|
||||
// should be used for the current capability version. It is determined by the
|
||||
// TS_EXPERIMENTAL_VERSIONED_CONFIG_DIR environment variable and looks for a
|
||||
// file named cap-<capability_version>.hujson in the directory. It searches for
|
||||
// the highest capability version that is less than or equal to the current
|
||||
// capability version.
|
||||
func tailscaledConfigFilePath() string {
|
||||
dir := os.Getenv("TS_EXPERIMENTAL_VERSIONED_CONFIG_DIR")
|
||||
if dir == "" {
|
||||
return ""
|
||||
}
|
||||
fe, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
log.Fatalf("error reading tailscaled config directory %q: %v", dir, err)
|
||||
}
|
||||
maxCompatVer := tailcfg.CapabilityVersion(-1)
|
||||
for _, e := range fe {
|
||||
// We don't check if type if file as in most cases this will
|
||||
// come from a mounted kube Secret, where the directory contents
|
||||
// will be various symlinks.
|
||||
if e.Type().IsDir() {
|
||||
continue
|
||||
}
|
||||
cv, err := kubeutils.CapVerFromFileName(e.Name())
|
||||
if err != nil {
|
||||
log.Printf("skipping file %q in tailscaled config directory %q: %v", e.Name(), dir, err)
|
||||
continue
|
||||
}
|
||||
if cv > maxCompatVer && cv <= tailcfg.CurrentCapabilityVersion {
|
||||
maxCompatVer = cv
|
||||
}
|
||||
}
|
||||
if maxCompatVer == -1 {
|
||||
log.Fatalf("no tailscaled config file found in %q for current capability version %q", dir, tailcfg.CurrentCapabilityVersion)
|
||||
}
|
||||
log.Printf("Using tailscaled config file %q for capability version %q", maxCompatVer, tailcfg.CurrentCapabilityVersion)
|
||||
return path.Join(dir, kubeutils.TailscaledConfigFileNameForCap(maxCompatVer))
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
"dev/net",
|
||||
"proc/sys/net/ipv4",
|
||||
"proc/sys/net/ipv6/conf/all",
|
||||
"etc",
|
||||
"etc/tailscaled",
|
||||
}
|
||||
for _, path := range dirs {
|
||||
if err := os.MkdirAll(filepath.Join(d, path), 0700); err != nil {
|
||||
@@ -80,7 +80,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
"dev/net/tun": []byte(""),
|
||||
"proc/sys/net/ipv4/ip_forward": []byte("0"),
|
||||
"proc/sys/net/ipv6/conf/all/forwarding": []byte("0"),
|
||||
"etc/tailscaled": tailscaledConfBytes,
|
||||
"etc/tailscaled/cap-95.hujson": tailscaledConfBytes,
|
||||
}
|
||||
resetFiles := func() {
|
||||
for path, content := range files {
|
||||
@@ -638,14 +638,14 @@ func TestContainerBoot(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "experimental tailscaled configfile",
|
||||
Name: "experimental tailscaled config path",
|
||||
Env: map[string]string{
|
||||
"EXPERIMENTAL_TS_CONFIGFILE_PATH": filepath.Join(d, "etc/tailscaled"),
|
||||
"TS_EXPERIMENTAL_VERSIONED_CONFIG_DIR": filepath.Join(d, "etc/tailscaled/"),
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --config=/etc/tailscaled",
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking --config=/etc/tailscaled/cap-95.hujson",
|
||||
},
|
||||
}, {
|
||||
Notify: runningNotify,
|
||||
|
||||
@@ -5,35 +5,45 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"expvar"
|
||||
"log"
|
||||
"math/rand/v2"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/slicesx"
|
||||
)
|
||||
|
||||
const refreshTimeout = time.Minute
|
||||
|
||||
type dnsEntryMap map[string][]net.IP
|
||||
type dnsEntryMap struct {
|
||||
IPs map[string][]net.IP
|
||||
Percent map[string]float64 // "foo.com" => 0.5 for 50%
|
||||
}
|
||||
|
||||
var (
|
||||
dnsCache syncs.AtomicValue[dnsEntryMap]
|
||||
dnsCache atomic.Pointer[dnsEntryMap]
|
||||
dnsCacheBytes syncs.AtomicValue[[]byte] // of JSON
|
||||
unpublishedDNSCache syncs.AtomicValue[dnsEntryMap]
|
||||
unpublishedDNSCache atomic.Pointer[dnsEntryMap]
|
||||
bootstrapLookupMap syncs.Map[string, bool]
|
||||
)
|
||||
|
||||
var (
|
||||
bootstrapDNSRequests = expvar.NewInt("counter_bootstrap_dns_requests")
|
||||
publishedDNSHits = expvar.NewInt("counter_bootstrap_dns_published_hits")
|
||||
publishedDNSMisses = expvar.NewInt("counter_bootstrap_dns_published_misses")
|
||||
unpublishedDNSHits = expvar.NewInt("counter_bootstrap_dns_unpublished_hits")
|
||||
unpublishedDNSMisses = expvar.NewInt("counter_bootstrap_dns_unpublished_misses")
|
||||
bootstrapDNSRequests = expvar.NewInt("counter_bootstrap_dns_requests")
|
||||
publishedDNSHits = expvar.NewInt("counter_bootstrap_dns_published_hits")
|
||||
publishedDNSMisses = expvar.NewInt("counter_bootstrap_dns_published_misses")
|
||||
unpublishedDNSHits = expvar.NewInt("counter_bootstrap_dns_unpublished_hits")
|
||||
unpublishedDNSMisses = expvar.NewInt("counter_bootstrap_dns_unpublished_misses")
|
||||
unpublishedDNSPercentMisses = expvar.NewInt("counter_bootstrap_dns_unpublished_percent_misses")
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -59,15 +69,13 @@ func refreshBootstrapDNS() {
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), refreshTimeout)
|
||||
defer cancel()
|
||||
dnsEntries := resolveList(ctx, strings.Split(*bootstrapDNS, ","))
|
||||
dnsEntries := resolveList(ctx, *bootstrapDNS)
|
||||
// Randomize the order of the IPs for each name to avoid the client biasing
|
||||
// to IPv6
|
||||
for k := range dnsEntries {
|
||||
ips := dnsEntries[k]
|
||||
slicesx.Shuffle(ips)
|
||||
dnsEntries[k] = ips
|
||||
for _, vv := range dnsEntries.IPs {
|
||||
slicesx.Shuffle(vv)
|
||||
}
|
||||
j, err := json.MarshalIndent(dnsEntries, "", "\t")
|
||||
j, err := json.MarshalIndent(dnsEntries.IPs, "", "\t")
|
||||
if err != nil {
|
||||
// leave the old values in place
|
||||
return
|
||||
@@ -81,27 +89,50 @@ func refreshUnpublishedDNS() {
|
||||
if *unpublishedDNS == "" {
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), refreshTimeout)
|
||||
defer cancel()
|
||||
|
||||
dnsEntries := resolveList(ctx, strings.Split(*unpublishedDNS, ","))
|
||||
dnsEntries := resolveList(ctx, *unpublishedDNS)
|
||||
unpublishedDNSCache.Store(dnsEntries)
|
||||
}
|
||||
|
||||
func resolveList(ctx context.Context, names []string) dnsEntryMap {
|
||||
dnsEntries := make(dnsEntryMap)
|
||||
// resolveList takes a comma-separated list of DNS names to resolve.
|
||||
//
|
||||
// If an entry contains a slash, it's two DNS names: the first is the one to
|
||||
// resolve and the second is that of a TXT recording containing the rollout
|
||||
// percentage in range "0".."100". If the TXT record doesn't exist or is
|
||||
// malformed, the percentage is 0. If the TXT record is not provided (there's no
|
||||
// slash), then the percentage is 100.
|
||||
func resolveList(ctx context.Context, list string) *dnsEntryMap {
|
||||
ents := strings.Split(list, ",")
|
||||
|
||||
ret := &dnsEntryMap{}
|
||||
|
||||
var r net.Resolver
|
||||
for _, name := range names {
|
||||
for _, ent := range ents {
|
||||
name, txtName, _ := strings.Cut(ent, "/")
|
||||
addrs, err := r.LookupIP(ctx, "ip", name)
|
||||
if err != nil {
|
||||
log.Printf("bootstrap DNS lookup %q: %v", name, err)
|
||||
continue
|
||||
}
|
||||
dnsEntries[name] = addrs
|
||||
mak.Set(&ret.IPs, name, addrs)
|
||||
|
||||
if txtName == "" {
|
||||
mak.Set(&ret.Percent, name, 1.0)
|
||||
continue
|
||||
}
|
||||
vals, err := r.LookupTXT(ctx, txtName)
|
||||
if err != nil {
|
||||
log.Printf("bootstrap DNS lookup %q: %v", txtName, err)
|
||||
continue
|
||||
}
|
||||
for _, v := range vals {
|
||||
if v, err := strconv.Atoi(v); err == nil && v >= 0 && v <= 100 {
|
||||
mak.Set(&ret.Percent, name, float64(v)/100)
|
||||
}
|
||||
}
|
||||
}
|
||||
return dnsEntries
|
||||
return ret
|
||||
}
|
||||
|
||||
func handleBootstrapDNS(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -115,22 +146,36 @@ func handleBootstrapDNS(w http.ResponseWriter, r *http.Request) {
|
||||
// Try answering a query from our hidden map first
|
||||
if q := r.URL.Query().Get("q"); q != "" {
|
||||
bootstrapLookupMap.Store(q, true)
|
||||
if ips, ok := unpublishedDNSCache.Load()[q]; ok && len(ips) > 0 {
|
||||
if bootstrapLookupMap.Len() > 500 { // defensive
|
||||
bootstrapLookupMap.Clear()
|
||||
}
|
||||
if m := unpublishedDNSCache.Load(); m != nil && len(m.IPs[q]) > 0 {
|
||||
unpublishedDNSHits.Add(1)
|
||||
|
||||
// Only return the specific query, not everything.
|
||||
m := dnsEntryMap{q: ips}
|
||||
j, err := json.MarshalIndent(m, "", "\t")
|
||||
if err == nil {
|
||||
w.Write(j)
|
||||
return
|
||||
percent := m.Percent[q]
|
||||
if remoteAddrMatchesPercent(r.RemoteAddr, percent) {
|
||||
// Only return the specific query, not everything.
|
||||
m := map[string][]net.IP{q: m.IPs[q]}
|
||||
j, err := json.MarshalIndent(m, "", "\t")
|
||||
if err == nil {
|
||||
w.Write(j)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
unpublishedDNSPercentMisses.Add(1)
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a "q" query for a name in the published cache
|
||||
// list, then track whether that's a hit/miss.
|
||||
if m, ok := dnsCache.Load()[q]; ok {
|
||||
if len(m) > 0 {
|
||||
m := dnsCache.Load()
|
||||
var inPub bool
|
||||
var ips []net.IP
|
||||
if m != nil {
|
||||
ips, inPub = m.IPs[q]
|
||||
}
|
||||
if inPub {
|
||||
if len(ips) > 0 {
|
||||
publishedDNSHits.Add(1)
|
||||
} else {
|
||||
publishedDNSMisses.Add(1)
|
||||
@@ -146,3 +191,29 @@ func handleBootstrapDNS(w http.ResponseWriter, r *http.Request) {
|
||||
j := dnsCacheBytes.Load()
|
||||
w.Write(j)
|
||||
}
|
||||
|
||||
// percent is [0.0, 1.0].
|
||||
func remoteAddrMatchesPercent(remoteAddr string, percent float64) bool {
|
||||
if percent == 0 {
|
||||
return false
|
||||
}
|
||||
if percent == 1 {
|
||||
return true
|
||||
}
|
||||
reqIPStr, _, err := net.SplitHostPort(remoteAddr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
reqIP, err := netip.ParseAddr(reqIPStr)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if reqIP.IsLoopback() {
|
||||
// For local testing.
|
||||
return rand.Float64() < 0.5
|
||||
}
|
||||
reqIP16 := reqIP.As16()
|
||||
rndSrc := rand.NewPCG(binary.LittleEndian.Uint64(reqIP16[:8]), binary.LittleEndian.Uint64(reqIP16[8:]))
|
||||
rnd := rand.New(rndSrc)
|
||||
return percent > rnd.Float64()
|
||||
}
|
||||
|
||||
@@ -4,15 +4,19 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/tstest/nettest"
|
||||
)
|
||||
|
||||
func BenchmarkHandleBootstrapDNS(b *testing.B) {
|
||||
@@ -37,7 +41,7 @@ func (b *bitbucketResponseWriter) Write(p []byte) (int, error) { return len(p),
|
||||
|
||||
func (b *bitbucketResponseWriter) WriteHeader(statusCode int) {}
|
||||
|
||||
func getBootstrapDNS(t *testing.T, q string) dnsEntryMap {
|
||||
func getBootstrapDNS(t *testing.T, q string) map[string][]net.IP {
|
||||
t.Helper()
|
||||
req, _ := http.NewRequest("GET", "https://localhost/bootstrap-dns?q="+url.QueryEscape(q), nil)
|
||||
w := httptest.NewRecorder()
|
||||
@@ -47,14 +51,17 @@ func getBootstrapDNS(t *testing.T, q string) dnsEntryMap {
|
||||
if res.StatusCode != 200 {
|
||||
t.Fatalf("got status=%d; want %d", res.StatusCode, 200)
|
||||
}
|
||||
var ips dnsEntryMap
|
||||
if err := json.NewDecoder(res.Body).Decode(&ips); err != nil {
|
||||
t.Fatalf("error decoding response body: %v", err)
|
||||
var m map[string][]net.IP
|
||||
var buf bytes.Buffer
|
||||
if err := json.NewDecoder(io.TeeReader(res.Body, &buf)).Decode(&m); err != nil {
|
||||
t.Fatalf("error decoding response body %q: %v", buf.Bytes(), err)
|
||||
}
|
||||
return ips
|
||||
return m
|
||||
}
|
||||
|
||||
func TestUnpublishedDNS(t *testing.T) {
|
||||
nettest.SkipIfNoNetwork(t)
|
||||
|
||||
const published = "login.tailscale.com"
|
||||
const unpublished = "log.tailscale.io"
|
||||
|
||||
@@ -104,15 +111,21 @@ func resetMetrics() {
|
||||
// Verify that we don't count an empty list in the unpublishedDNSCache as a
|
||||
// cache hit in our metrics.
|
||||
func TestUnpublishedDNSEmptyList(t *testing.T) {
|
||||
pub := dnsEntryMap{
|
||||
"tailscale.com": {net.IPv4(10, 10, 10, 10)},
|
||||
pub := &dnsEntryMap{
|
||||
IPs: map[string][]net.IP{"tailscale.com": {net.IPv4(10, 10, 10, 10)}},
|
||||
}
|
||||
dnsCache.Store(pub)
|
||||
dnsCacheBytes.Store([]byte(`{"tailscale.com":["10.10.10.10"]}`))
|
||||
|
||||
unpublishedDNSCache.Store(dnsEntryMap{
|
||||
"log.tailscale.io": {},
|
||||
"controlplane.tailscale.com": {net.IPv4(1, 2, 3, 4)},
|
||||
unpublishedDNSCache.Store(&dnsEntryMap{
|
||||
IPs: map[string][]net.IP{
|
||||
"log.tailscale.io": {},
|
||||
"controlplane.tailscale.com": {net.IPv4(1, 2, 3, 4)},
|
||||
},
|
||||
Percent: map[string]float64{
|
||||
"log.tailscale.io": 1.0,
|
||||
"controlplane.tailscale.com": 1.0,
|
||||
},
|
||||
})
|
||||
|
||||
t.Run("CacheMiss", func(t *testing.T) {
|
||||
@@ -122,8 +135,8 @@ func TestUnpublishedDNSEmptyList(t *testing.T) {
|
||||
ips := getBootstrapDNS(t, q)
|
||||
|
||||
// Expected our public map to be returned on a cache miss
|
||||
if !reflect.DeepEqual(ips, pub) {
|
||||
t.Errorf("got ips=%+v; want %+v", ips, pub)
|
||||
if !reflect.DeepEqual(ips, pub.IPs) {
|
||||
t.Errorf("got ips=%+v; want %+v", ips, pub.IPs)
|
||||
}
|
||||
if v := unpublishedDNSHits.Value(); v != 0 {
|
||||
t.Errorf("got hits=%d; want 0", v)
|
||||
@@ -138,7 +151,7 @@ func TestUnpublishedDNSEmptyList(t *testing.T) {
|
||||
t.Run("CacheHit", func(t *testing.T) {
|
||||
resetMetrics()
|
||||
ips := getBootstrapDNS(t, "controlplane.tailscale.com")
|
||||
want := dnsEntryMap{"controlplane.tailscale.com": {net.IPv4(1, 2, 3, 4)}}
|
||||
want := map[string][]net.IP{"controlplane.tailscale.com": {net.IPv4(1, 2, 3, 4)}}
|
||||
if !reflect.DeepEqual(ips, want) {
|
||||
t.Errorf("got ips=%+v; want %+v", ips, want)
|
||||
}
|
||||
@@ -163,3 +176,54 @@ func TestLookupMetric(t *testing.T) {
|
||||
t.Errorf("bootstrapLookupMap.Len() want=5, got %v", bootstrapLookupMap.Len())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoteAddrMatchesPercent(t *testing.T) {
|
||||
tests := []struct {
|
||||
remoteAddr string
|
||||
percent float64
|
||||
want bool
|
||||
}{
|
||||
// 0% and 100%.
|
||||
{"10.0.0.1:1234", 0.0, false},
|
||||
{"10.0.0.1:1234", 1.0, true},
|
||||
|
||||
// Invalid IP.
|
||||
{"", 1.0, true},
|
||||
{"", 0.0, false},
|
||||
{"", 0.5, false},
|
||||
|
||||
// Small manual sample at 50%. The func uses a deterministic PRNG seed.
|
||||
{"1.2.3.4:567", 0.5, true},
|
||||
{"1.2.3.5:567", 0.5, true},
|
||||
{"1.2.3.6:567", 0.5, false},
|
||||
{"1.2.3.7:567", 0.5, true},
|
||||
{"1.2.3.8:567", 0.5, false},
|
||||
{"1.2.3.9:567", 0.5, true},
|
||||
{"1.2.3.10:567", 0.5, true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := remoteAddrMatchesPercent(tt.remoteAddr, tt.percent)
|
||||
if got != tt.want {
|
||||
t.Errorf("remoteAddrMatchesPercent(%q, %v) = %v; want %v", tt.remoteAddr, tt.percent, got, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
var match, all int
|
||||
const wantPercent = 0.5
|
||||
for a := range 256 {
|
||||
for b := range 256 {
|
||||
all++
|
||||
if remoteAddrMatchesPercent(
|
||||
netip.AddrPortFrom(netip.AddrFrom4([4]byte{1, 2, byte(a), byte(b)}), 12345).String(),
|
||||
wantPercent) {
|
||||
match++
|
||||
}
|
||||
}
|
||||
}
|
||||
gotPercent := float64(match) / float64(all)
|
||||
const tolerance = 0.005
|
||||
t.Logf("got percent %v (goal %v)", gotPercent, wantPercent)
|
||||
if gotPercent < wantPercent-tolerance || gotPercent > wantPercent+tolerance {
|
||||
t.Errorf("got %v; want %v ± %v", gotPercent, wantPercent, tolerance)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
encoding/pem from crypto/tls+
|
||||
errors from bufio+
|
||||
expvar from github.com/prometheus/client_golang/prometheus+
|
||||
flag from tailscale.com/cmd/derper+
|
||||
flag from tailscale.com/cmd/derper
|
||||
fmt from compress/flate+
|
||||
go/token from google.golang.org/protobuf/internal/strs
|
||||
hash from crypto+
|
||||
@@ -253,7 +253,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
math/rand from github.com/mdlayher/netlink+
|
||||
math/rand/v2 from tailscale.com/util/fastuuid
|
||||
math/rand/v2 from tailscale.com/util/fastuuid+
|
||||
mime from github.com/prometheus/common/expfmt+
|
||||
mime/multipart from net/http
|
||||
mime/quotedprintable from mime/multipart
|
||||
@@ -277,7 +277,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
runtime/debug from github.com/prometheus/client_golang/prometheus+
|
||||
runtime/metrics from github.com/prometheus/client_golang/prometheus+
|
||||
runtime/pprof from net/http/pprof
|
||||
runtime/trace from net/http/pprof+
|
||||
runtime/trace from net/http/pprof
|
||||
slices from tailscale.com/ipn/ipnstate+
|
||||
sort from compress/flate+
|
||||
strconv from compress/flate+
|
||||
@@ -285,7 +285,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
sync from compress/flate+
|
||||
sync/atomic from context+
|
||||
syscall from crypto/rand+
|
||||
testing from tailscale.com/util/syspolicy
|
||||
text/tabwriter from runtime/pprof
|
||||
time from compress/gzip+
|
||||
unicode from bytes+
|
||||
|
||||
@@ -55,7 +55,7 @@ var (
|
||||
meshPSKFile = flag.String("mesh-psk-file", defaultMeshPSKFile(), "if non-empty, path to file containing the mesh pre-shared key file. It should contain some hex string; whitespace is trimmed.")
|
||||
meshWith = flag.String("mesh-with", "", "optional comma-separated list of hostnames to mesh with; the server's own hostname can be in the list")
|
||||
bootstrapDNS = flag.String("bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns")
|
||||
unpublishedDNS = flag.String("unpublished-bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns and not publish in the list")
|
||||
unpublishedDNS = flag.String("unpublished-bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns and not publish in the list. If an entry contains a slash, the second part names a DNS record to poll for its TXT record with a `0` to `100` value for rollout percentage.")
|
||||
verifyClients = flag.Bool("verify-clients", false, "verify clients to this DERP server through a local tailscaled instance.")
|
||||
verifyClientURL = flag.String("verify-client-url", "", "if non-empty, an admission controller URL for permitting client connections; see tailcfg.DERPAdmitClientRequest")
|
||||
verifyFailOpen = flag.Bool("verify-client-url-fail-open", true, "whether we fail open if --verify-client-url is unreachable")
|
||||
@@ -191,7 +191,12 @@ func main() {
|
||||
http.Error(w, "derp server disabled", http.StatusNotFound)
|
||||
}))
|
||||
}
|
||||
mux.HandleFunc("/derp/probe", probeHandler)
|
||||
|
||||
// These two endpoints are the same. Different versions of the clients
|
||||
// have assumes different paths over time so we support both.
|
||||
mux.HandleFunc("/derp/probe", derphttp.ProbeHandler)
|
||||
mux.HandleFunc("/derp/latency-check", derphttp.ProbeHandler)
|
||||
|
||||
go refreshBootstrapDNSLoop()
|
||||
mux.HandleFunc("/bootstrap-dns", tsweb.BrowserHeaderHandlerFunc(handleBootstrapDNS))
|
||||
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -370,17 +375,6 @@ func isChallengeChar(c rune) bool {
|
||||
c == '.' || c == '-' || c == '_'
|
||||
}
|
||||
|
||||
// probeHandler is the endpoint that js/wasm clients hit to measure
|
||||
// DERP latency, since they can't do UDP STUN queries.
|
||||
func probeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "HEAD", "GET":
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
default:
|
||||
http.Error(w, "bogus probe method", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
var validProdHostname = regexp.MustCompile(`^derp([^.]*)\.tailscale\.com\.?$`)
|
||||
|
||||
func prodAutocertHostPolicy(_ context.Context, host string) error {
|
||||
|
||||
@@ -99,6 +99,7 @@ func TestNoContent(t *testing.T) {
|
||||
func TestDeps(t *testing.T) {
|
||||
deptest.DepChecker{
|
||||
BadDeps: map[string]string{
|
||||
"testing": "do not use testing package in production code",
|
||||
"gvisor.dev/gvisor/pkg/buffer": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/cpuid": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/tcpip": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
|
||||
@@ -51,6 +51,10 @@ operatorConfig:
|
||||
# proxies created by the operator.
|
||||
# https://tailscale.com/kb/1236/kubernetes-operator/#cluster-ingress
|
||||
# https://tailscale.com/kb/1236/kubernetes-operator/#cluster-egress
|
||||
# Note that this section contains only a few global configuration options and
|
||||
# will not be updated with more configuration options in the future.
|
||||
# If you need more configuration options, take a look at ProxyClass:
|
||||
# https://tailscale.com/kb/1236/kubernetes-operator#cluster-resource-customization-using-proxyclass-custom-resource
|
||||
proxyConfig:
|
||||
image:
|
||||
repo: tailscale/tailscale
|
||||
|
||||
@@ -38,10 +38,10 @@ func main() {
|
||||
}
|
||||
repoRoot := "../../"
|
||||
switch os.Args[1] {
|
||||
case "helmcrd": // insert CRD to Helm templates behind a installCRDs=true conditional check
|
||||
log.Print("Adding Connector CRD to Helm templates")
|
||||
case "helmcrd": // insert CRDs to Helm templates behind a installCRDs=true conditional check
|
||||
log.Print("Adding CRDs to Helm templates")
|
||||
if err := generate("./"); err != nil {
|
||||
log.Fatalf("error adding Connector CRD to Helm templates: %v", err)
|
||||
log.Fatalf("error adding CRDs to Helm templates: %v", err)
|
||||
}
|
||||
return
|
||||
case "staticmanifests": // generate static manifests from Helm templates (including the CRD)
|
||||
|
||||
@@ -41,6 +41,12 @@ const (
|
||||
|
||||
messageNameserverCreationFailed = "Failed creating nameserver resources: %v"
|
||||
messageMultipleDNSConfigsPresent = "Multiple DNSConfig resources found in cluster. Please ensure no more than one is present."
|
||||
|
||||
defaultNameserverImageRepo = "tailscale/k8s-nameserver"
|
||||
// TODO (irbekrm): once we start publishing nameserver images for stable
|
||||
// track, replace 'unstable' here with the version of this operator
|
||||
// instance.
|
||||
defaultNameserverImageTag = "unstable"
|
||||
)
|
||||
|
||||
// NameserverReconciler knows how to create nameserver resources in cluster in
|
||||
@@ -163,11 +169,13 @@ func (a *NameserverReconciler) maybeProvision(ctx context.Context, tsDNSCfg *tsa
|
||||
ownerRefs: []metav1.OwnerReference{*metav1.NewControllerRef(tsDNSCfg, tsapi.SchemeGroupVersion.WithKind("DNSConfig"))},
|
||||
namespace: a.tsNamespace,
|
||||
labels: labels,
|
||||
imageRepo: defaultNameserverImageRepo,
|
||||
imageTag: defaultNameserverImageTag,
|
||||
}
|
||||
if tsDNSCfg.Spec.Nameserver.Image.Repo != "" {
|
||||
if tsDNSCfg.Spec.Nameserver.Image != nil && tsDNSCfg.Spec.Nameserver.Image.Repo != "" {
|
||||
dCfg.imageRepo = tsDNSCfg.Spec.Nameserver.Image.Repo
|
||||
}
|
||||
if tsDNSCfg.Spec.Nameserver.Image.Tag != "" {
|
||||
if tsDNSCfg.Spec.Nameserver.Image != nil && tsDNSCfg.Spec.Nameserver.Image.Tag != "" {
|
||||
dCfg.imageTag = tsDNSCfg.Spec.Nameserver.Image.Tag
|
||||
}
|
||||
for _, deployable := range []deployable{saDeployable, deployDeployable, svcDeployable, cmDeployable} {
|
||||
|
||||
@@ -115,4 +115,13 @@ func TestNameserverReconciler(t *testing.T) {
|
||||
Data: map[string]string{"records.json": string(bs)},
|
||||
}
|
||||
expectEqual(t, fc, wantCm, nil)
|
||||
|
||||
// Verify that if dnsconfig.spec.nameserver.image.{repo,tag} are unset,
|
||||
// the nameserver image defaults to tailscale/k8s-nameserver:unstable.
|
||||
mustUpdate(t, fc, "", "test", func(dnsCfg *tsapi.DNSConfig) {
|
||||
dnsCfg.Spec.Nameserver.Image = nil
|
||||
})
|
||||
expectReconciled(t, nr, "", "test")
|
||||
wantsDeploy.Spec.Template.Spec.Containers[0].Image = "tailscale/k8s-nameserver:unstable"
|
||||
expectEqual(t, fc, wantsDeploy, nil)
|
||||
}
|
||||
|
||||
@@ -45,12 +45,12 @@ import (
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
// Generate static manifests for deploying Tailscale operator on Kubernetes from the operator's Helm chart.
|
||||
//go:generate go run tailscale.com/cmd/k8s-operator/generate staticmanifests
|
||||
|
||||
// Generate Connector and ProxyClass CustomResourceDefinition yamls from their Go types.
|
||||
//go:generate go run sigs.k8s.io/controller-tools/cmd/controller-gen crd schemapatch:manifests=./deploy/crds output:dir=./deploy/crds paths=../../k8s-operator/apis/...
|
||||
|
||||
// Generate static manifests for deploying Tailscale operator on Kubernetes from the operator's Helm chart.
|
||||
//go:generate go run tailscale.com/cmd/k8s-operator/generate staticmanifests
|
||||
|
||||
// Generate CRD docs from the yamls
|
||||
//go:generate go run fybrik.io/crdoc --resources=./deploy/crds --output=../../k8s-operator/api.md
|
||||
|
||||
@@ -60,12 +60,13 @@ func main() {
|
||||
tailscale.I_Acknowledge_This_API_Is_Unstable = true
|
||||
|
||||
var (
|
||||
tsNamespace = defaultEnv("OPERATOR_NAMESPACE", "")
|
||||
tslogging = defaultEnv("OPERATOR_LOGGING", "info")
|
||||
image = defaultEnv("PROXY_IMAGE", "tailscale/tailscale:latest")
|
||||
priorityClassName = defaultEnv("PROXY_PRIORITY_CLASS_NAME", "")
|
||||
tags = defaultEnv("PROXY_TAGS", "tag:k8s")
|
||||
tsFirewallMode = defaultEnv("PROXY_FIREWALL_MODE", "")
|
||||
tsNamespace = defaultEnv("OPERATOR_NAMESPACE", "")
|
||||
tslogging = defaultEnv("OPERATOR_LOGGING", "info")
|
||||
image = defaultEnv("PROXY_IMAGE", "tailscale/tailscale:latest")
|
||||
priorityClassName = defaultEnv("PROXY_PRIORITY_CLASS_NAME", "")
|
||||
tags = defaultEnv("PROXY_TAGS", "tag:k8s")
|
||||
tsFirewallMode = defaultEnv("PROXY_FIREWALL_MODE", "")
|
||||
isDefaultLoadBalancer = defaultBool("OPERATOR_DEFAULT_LOAD_BALANCER", false)
|
||||
)
|
||||
|
||||
var opts []kzap.Opts
|
||||
@@ -94,9 +95,19 @@ func main() {
|
||||
defer s.Close()
|
||||
restConfig := config.GetConfigOrDie()
|
||||
maybeLaunchAPIServerProxy(zlog, restConfig, s, mode)
|
||||
// TODO (irbekrm): gather the reconciler options into an opts struct
|
||||
// rather than passing a million of them in one by one.
|
||||
runReconcilers(zlog, s, tsNamespace, restConfig, tsClient, image, priorityClassName, tags, tsFirewallMode)
|
||||
rOpts := reconcilerOpts{
|
||||
log: zlog,
|
||||
tsServer: s,
|
||||
tsClient: tsClient,
|
||||
tailscaleNamespace: tsNamespace,
|
||||
restConfig: restConfig,
|
||||
proxyImage: image,
|
||||
proxyPriorityClassName: priorityClassName,
|
||||
proxyActAsDefaultLoadBalancer: isDefaultLoadBalancer,
|
||||
proxyTags: tags,
|
||||
proxyFirewallMode: tsFirewallMode,
|
||||
}
|
||||
runReconcilers(rOpts)
|
||||
}
|
||||
|
||||
// initTSNet initializes the tsnet.Server and logs in to Tailscale. It uses the
|
||||
@@ -204,11 +215,8 @@ waitOnline:
|
||||
|
||||
// runReconcilers starts the controller-runtime manager and registers the
|
||||
// ServiceReconciler. It blocks forever.
|
||||
func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string, restConfig *rest.Config, tsClient *tailscale.Client, image, priorityClassName, tags, tsFirewallMode string) {
|
||||
var (
|
||||
isDefaultLoadBalancer = defaultBool("OPERATOR_DEFAULT_LOAD_BALANCER", false)
|
||||
)
|
||||
startlog := zlog.Named("startReconcilers")
|
||||
func runReconcilers(opts reconcilerOpts) {
|
||||
startlog := opts.log.Named("startReconcilers")
|
||||
// For secrets and statefulsets, we only get permission to touch the objects
|
||||
// in the controller's own namespace. This cannot be expressed by
|
||||
// .Watches(...) below, instead you have to add a per-type field selector to
|
||||
@@ -216,7 +224,7 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
||||
// implicitly filter what parts of the world the builder code gets to see at
|
||||
// all.
|
||||
nsFilter := cache.ByObject{
|
||||
Field: client.InNamespace(tsNamespace).AsSelector(),
|
||||
Field: client.InNamespace(opts.tailscaleNamespace).AsSelector(),
|
||||
}
|
||||
mgrOpts := manager.Options{
|
||||
// TODO (irbekrm): stricter filtering what we watch/cache/call
|
||||
@@ -234,7 +242,7 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
||||
},
|
||||
Scheme: tsapi.GlobalScheme,
|
||||
}
|
||||
mgr, err := manager.New(restConfig, mgrOpts)
|
||||
mgr, err := manager.New(opts.restConfig, mgrOpts)
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not create manager: %v", err)
|
||||
}
|
||||
@@ -248,13 +256,13 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
||||
eventRecorder := mgr.GetEventRecorderFor("tailscale-operator")
|
||||
ssr := &tailscaleSTSReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
tsnetServer: s,
|
||||
tsClient: tsClient,
|
||||
defaultTags: strings.Split(tags, ","),
|
||||
operatorNamespace: tsNamespace,
|
||||
proxyImage: image,
|
||||
proxyPriorityClassName: priorityClassName,
|
||||
tsFirewallMode: tsFirewallMode,
|
||||
tsnetServer: opts.tsServer,
|
||||
tsClient: opts.tsClient,
|
||||
defaultTags: strings.Split(opts.proxyTags, ","),
|
||||
operatorNamespace: opts.tailscaleNamespace,
|
||||
proxyImage: opts.proxyImage,
|
||||
proxyPriorityClassName: opts.proxyPriorityClassName,
|
||||
tsFirewallMode: opts.proxyFirewallMode,
|
||||
}
|
||||
err = builder.
|
||||
ControllerManagedBy(mgr).
|
||||
@@ -266,10 +274,10 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
||||
Complete(&ServiceReconciler{
|
||||
ssr: ssr,
|
||||
Client: mgr.GetClient(),
|
||||
logger: zlog.Named("service-reconciler"),
|
||||
isDefaultLoadBalancer: isDefaultLoadBalancer,
|
||||
logger: opts.log.Named("service-reconciler"),
|
||||
isDefaultLoadBalancer: opts.proxyActAsDefaultLoadBalancer,
|
||||
recorder: eventRecorder,
|
||||
tsNamespace: tsNamespace,
|
||||
tsNamespace: opts.tailscaleNamespace,
|
||||
})
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not create service reconciler: %v", err)
|
||||
@@ -291,7 +299,7 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
||||
ssr: ssr,
|
||||
recorder: eventRecorder,
|
||||
Client: mgr.GetClient(),
|
||||
logger: zlog.Named("ingress-reconciler"),
|
||||
logger: opts.log.Named("ingress-reconciler"),
|
||||
})
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not create ingress reconciler: %v", err)
|
||||
@@ -310,14 +318,14 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
||||
ssr: ssr,
|
||||
recorder: eventRecorder,
|
||||
Client: mgr.GetClient(),
|
||||
logger: zlog.Named("connector-reconciler"),
|
||||
logger: opts.log.Named("connector-reconciler"),
|
||||
clock: tstime.DefaultClock{},
|
||||
})
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not create connector reconciler: %v", err)
|
||||
}
|
||||
// TODO (irbekrm): switch to metadata-only watches for resources whose
|
||||
// spec we don't need to inspect to reduce memory consumption
|
||||
// spec we don't need to inspect to reduce memory consumption.
|
||||
// https://github.com/kubernetes-sigs/controller-runtime/issues/1159
|
||||
nameserverFilter := handler.EnqueueRequestsFromMapFunc(managedResourceHandlerForType("nameserver"))
|
||||
err = builder.ControllerManagedBy(mgr).
|
||||
@@ -328,11 +336,10 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
||||
Watches(&corev1.ServiceAccount{}, nameserverFilter).
|
||||
Complete(&NameserverReconciler{
|
||||
recorder: eventRecorder,
|
||||
tsNamespace: tsNamespace,
|
||||
|
||||
Client: mgr.GetClient(),
|
||||
logger: zlog.Named("nameserver-reconciler"),
|
||||
clock: tstime.DefaultClock{},
|
||||
tsNamespace: opts.tailscaleNamespace,
|
||||
Client: mgr.GetClient(),
|
||||
logger: opts.log.Named("nameserver-reconciler"),
|
||||
clock: tstime.DefaultClock{},
|
||||
})
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not create nameserver reconciler: %v", err)
|
||||
@@ -342,7 +349,7 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
||||
Complete(&ProxyClassReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
recorder: eventRecorder,
|
||||
logger: zlog.Named("proxyclass-reconciler"),
|
||||
logger: opts.log.Named("proxyclass-reconciler"),
|
||||
clock: tstime.DefaultClock{},
|
||||
})
|
||||
if err != nil {
|
||||
@@ -355,12 +362,12 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
||||
dnsRREpsOpts := handler.EnqueueRequestsFromMapFunc(dnsRecordsReconcilerEndpointSliceHandler)
|
||||
// On DNSConfig changes, reconcile all headless Services for
|
||||
// ingress/egress proxies in operator namespace.
|
||||
dnsRRDNSConfigOpts := handler.EnqueueRequestsFromMapFunc(enqueueAllIngressEgressProxySvcsInNS(tsNamespace, mgr.GetClient(), logger))
|
||||
dnsRRDNSConfigOpts := handler.EnqueueRequestsFromMapFunc(enqueueAllIngressEgressProxySvcsInNS(opts.tailscaleNamespace, mgr.GetClient(), logger))
|
||||
// On Service events, if it is an ingress/egress proxy headless Service, reconcile it.
|
||||
dnsRRServiceOpts := handler.EnqueueRequestsFromMapFunc(dnsRecordsReconcilerServiceHandler)
|
||||
// On Ingress events, if it is a tailscale Ingress or if tailscale is the default ingress controller, reconcile the proxy
|
||||
// headless Service.
|
||||
dnsRRIngressOpts := handler.EnqueueRequestsFromMapFunc(dnsRecordsReconcilerIngressHandler(tsNamespace, isDefaultLoadBalancer, mgr.GetClient(), logger))
|
||||
dnsRRIngressOpts := handler.EnqueueRequestsFromMapFunc(dnsRecordsReconcilerIngressHandler(opts.tailscaleNamespace, opts.proxyActAsDefaultLoadBalancer, mgr.GetClient(), logger))
|
||||
err = builder.ControllerManagedBy(mgr).
|
||||
Named("dns-records-reconciler").
|
||||
Watches(&corev1.Service{}, dnsRRServiceOpts).
|
||||
@@ -369,9 +376,9 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
||||
Watches(&tsapi.DNSConfig{}, dnsRRDNSConfigOpts).
|
||||
Complete(&dnsRecordsReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
tsNamespace: tsNamespace,
|
||||
logger: zlog.Named("dns-records-reconciler"),
|
||||
isDefaultLoadBalancer: isDefaultLoadBalancer,
|
||||
tsNamespace: opts.tailscaleNamespace,
|
||||
logger: opts.log.Named("dns-records-reconciler"),
|
||||
isDefaultLoadBalancer: opts.proxyActAsDefaultLoadBalancer,
|
||||
})
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not create DNS records reconciler: %v", err)
|
||||
@@ -382,6 +389,42 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
|
||||
}
|
||||
}
|
||||
|
||||
type reconcilerOpts struct {
|
||||
log *zap.SugaredLogger
|
||||
tsServer *tsnet.Server
|
||||
tsClient *tailscale.Client
|
||||
tailscaleNamespace string // namespace in which operator resources will be deployed
|
||||
restConfig *rest.Config // config for connecting to the kube API server
|
||||
proxyImage string // <proxy-image-repo>:<proxy-image-tag>
|
||||
// proxyPriorityClassName isPriorityClass to be set for proxy Pods. This
|
||||
// is a legacy mechanism for cluster resource configuration options -
|
||||
// going forward use ProxyClass.
|
||||
// https://kubernetes.io/docs/concepts/scheduling-eviction/pod-priority-preemption/#priorityclass
|
||||
proxyPriorityClassName string
|
||||
// proxyTags are ACL tags to tag proxy auth keys. Multiple tags should
|
||||
// be provided as a string with comma-separated tag values. Proxy tags
|
||||
// default to tag:k8s.
|
||||
// https://tailscale.com/kb/1085/auth-keys
|
||||
proxyTags string
|
||||
// proxyActAsDefaultLoadBalancer determines whether this operator
|
||||
// instance should act as the default ingress controller when looking at
|
||||
// Ingress resources with unset ingress.spec.ingressClassName.
|
||||
// TODO (irbekrm): this setting does not respect the default
|
||||
// IngressClass.
|
||||
// https://kubernetes.io/docs/concepts/services-networking/ingress/#default-ingress-class
|
||||
// We should fix that and preferably integrate with that mechanism as
|
||||
// well - perhaps make the operator itself create the default
|
||||
// IngressClass if this is set to true.
|
||||
proxyActAsDefaultLoadBalancer bool
|
||||
// proxyFirewallMode determines whether non-userspace proxies should use
|
||||
// iptables or nftables for firewall configuration. Accepted values are
|
||||
// iptables, nftables and auto. If set to auto, proxy will automatically
|
||||
// determine which mode is supported for a given host (prefer nftables).
|
||||
// Auto is usually the best choice, unless you want to explicitly set
|
||||
// specific mode for debugging purposes.
|
||||
proxyFirewallMode string
|
||||
}
|
||||
|
||||
// enqueueAllIngressEgressProxySvcsinNS returns a reconcile request for each
|
||||
// ingress/egress proxy headless Service found in the provided namespace.
|
||||
func enqueueAllIngressEgressProxySvcsInNS(ns string, cl client.Client, logger *zap.SugaredLogger) handler.MapFunc {
|
||||
|
||||
@@ -1182,7 +1182,7 @@ func TestTailscaledConfigfileHash(t *testing.T) {
|
||||
parentType: "svc",
|
||||
hostname: "default-test",
|
||||
clusterTargetIP: "10.20.30.40",
|
||||
confFileHash: "705e5ffd0bd5326237efdf542c850a65a54101284d5daa30775420fcc64d89c1",
|
||||
confFileHash: "e09bededa0379920141cbd0b0dbdf9b8b66545877f9e8397423f5ce3e1ba439e",
|
||||
}
|
||||
expectEqual(t, fc, expectedSTS(t, fc, o), nil)
|
||||
|
||||
@@ -1192,7 +1192,7 @@ func TestTailscaledConfigfileHash(t *testing.T) {
|
||||
mak.Set(&svc.Annotations, AnnotationHostname, "another-test")
|
||||
})
|
||||
o.hostname = "another-test"
|
||||
o.confFileHash = "1a087f887825d2b75d3673c7c2b0131f8ec1f0b1cb761d33e236dd28350dfe23"
|
||||
o.confFileHash = "5d754cf55463135ee34aa9821f2fd8483b53eb0570c3740c84a086304f427684"
|
||||
expectReconciled(t, sr, "default", "test")
|
||||
expectEqual(t, fc, expectedSTS(t, fc, o), nil)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"sigs.k8s.io/yaml"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
kubeutils "tailscale.com/k8s-operator"
|
||||
tsoperator "tailscale.com/k8s-operator"
|
||||
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
|
||||
"tailscale.com/net/netutil"
|
||||
@@ -92,10 +93,6 @@ const (
|
||||
podAnnotationLastSetTailnetTargetFQDN = "tailscale.com/operator-last-set-ts-tailnet-target-fqdn"
|
||||
// podAnnotationLastSetConfigFileHash is sha256 hash of the current tailscaled configuration contents.
|
||||
podAnnotationLastSetConfigFileHash = "tailscale.com/operator-last-set-config-file-hash"
|
||||
|
||||
// tailscaledConfigKey is the name of the key in proxy Secret Data that
|
||||
// holds the tailscaled config contents.
|
||||
tailscaledConfigKey = "tailscaled"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -174,11 +171,11 @@ func (a *tailscaleSTSReconciler) Provision(ctx context.Context, logger *zap.Suga
|
||||
return nil, fmt.Errorf("failed to reconcile headless service: %w", err)
|
||||
}
|
||||
|
||||
secretName, tsConfigHash, err := a.createOrGetSecret(ctx, logger, sts, hsvc)
|
||||
secretName, tsConfigHash, configs, err := a.createOrGetSecret(ctx, logger, sts, hsvc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create or get API key secret: %w", err)
|
||||
}
|
||||
_, err = a.reconcileSTS(ctx, logger, sts, hsvc, secretName, tsConfigHash)
|
||||
_, err = a.reconcileSTS(ctx, logger, sts, hsvc, secretName, tsConfigHash, configs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to reconcile statefulset: %w", err)
|
||||
}
|
||||
@@ -291,7 +288,7 @@ func (a *tailscaleSTSReconciler) reconcileHeadlessService(ctx context.Context, l
|
||||
return createOrUpdate(ctx, a.Client, a.operatorNamespace, hsvc, func(svc *corev1.Service) { svc.Spec = hsvc.Spec })
|
||||
}
|
||||
|
||||
func (a *tailscaleSTSReconciler) createOrGetSecret(ctx context.Context, logger *zap.SugaredLogger, stsC *tailscaleSTSConfig, hsvc *corev1.Service) (string, string, error) {
|
||||
func (a *tailscaleSTSReconciler) createOrGetSecret(ctx context.Context, logger *zap.SugaredLogger, stsC *tailscaleSTSConfig, hsvc *corev1.Service) (secretName, hash string, configs tailscaleConfigs, _ error) {
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
// Hardcode a -0 suffix so that in future, if we support
|
||||
@@ -307,25 +304,23 @@ func (a *tailscaleSTSReconciler) createOrGetSecret(ctx context.Context, logger *
|
||||
logger.Debugf("secret %s/%s already exists", secret.GetNamespace(), secret.GetName())
|
||||
orig = secret.DeepCopy()
|
||||
} else if !apierrors.IsNotFound(err) {
|
||||
return "", "", err
|
||||
return "", "", nil, err
|
||||
}
|
||||
|
||||
var (
|
||||
authKey, hash string
|
||||
)
|
||||
var authKey string
|
||||
if orig == nil {
|
||||
// Initially it contains only tailscaled config, but when the
|
||||
// proxy starts, it will also store there the state, certs and
|
||||
// ACME account key.
|
||||
sts, err := getSingleObject[appsv1.StatefulSet](ctx, a.Client, a.operatorNamespace, stsC.ChildResourceLabels)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return "", "", nil, err
|
||||
}
|
||||
if sts != nil {
|
||||
// StatefulSet exists, so we have already created the secret.
|
||||
// If the secret is missing, they should delete the StatefulSet.
|
||||
logger.Errorf("Tailscale proxy secret doesn't exist, but the corresponding StatefulSet %s/%s already does. Something is wrong, please delete the StatefulSet.", sts.GetNamespace(), sts.GetName())
|
||||
return "", "", nil
|
||||
return "", "", nil, nil
|
||||
}
|
||||
// Create API Key secret which is going to be used by the statefulset
|
||||
// to authenticate with Tailscale.
|
||||
@@ -336,45 +331,58 @@ func (a *tailscaleSTSReconciler) createOrGetSecret(ctx context.Context, logger *
|
||||
}
|
||||
authKey, err = a.newAuthKey(ctx, tags)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return "", "", nil, err
|
||||
}
|
||||
}
|
||||
confFileBytes, h, err := tailscaledConfig(stsC, authKey, orig)
|
||||
configs, err := tailscaledConfig(stsC, authKey, orig)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("error creating tailscaled config: %w", err)
|
||||
return "", "", nil, fmt.Errorf("error creating tailscaled config: %w", err)
|
||||
}
|
||||
hash, err = tailscaledConfigHash(configs)
|
||||
if err != nil {
|
||||
return "", "", nil, fmt.Errorf("error calculating hash of tailscaled configs: %w", err)
|
||||
}
|
||||
|
||||
latest := tailcfg.CapabilityVersion(-1)
|
||||
var latestConfig ipn.ConfigVAlpha
|
||||
for key, val := range configs {
|
||||
fn := kubeutils.TailscaledConfigFileNameForCap(key)
|
||||
b, err := json.Marshal(val)
|
||||
if err != nil {
|
||||
return "", "", nil, fmt.Errorf("error marshalling tailscaled config: %w", err)
|
||||
}
|
||||
mak.Set(&secret.StringData, fn, string(b))
|
||||
if key > latest {
|
||||
latest = key
|
||||
latestConfig = val
|
||||
}
|
||||
}
|
||||
hash = h
|
||||
mak.Set(&secret.StringData, tailscaledConfigKey, string(confFileBytes))
|
||||
|
||||
if stsC.ServeConfig != nil {
|
||||
j, err := json.Marshal(stsC.ServeConfig)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return "", "", nil, err
|
||||
}
|
||||
mak.Set(&secret.StringData, "serve-config", string(j))
|
||||
}
|
||||
|
||||
if orig != nil {
|
||||
logger.Debugf("patching the existing proxy Secret with tailscaled config %s", sanitizeConfigBytes(secret.Data[tailscaledConfigKey]))
|
||||
logger.Debugf("patching the existing proxy Secret with tailscaled config %s", sanitizeConfigBytes(latestConfig))
|
||||
if err := a.Patch(ctx, secret, client.MergeFrom(orig)); err != nil {
|
||||
return "", "", err
|
||||
return "", "", nil, err
|
||||
}
|
||||
} else {
|
||||
logger.Debugf("creating a new Secret for the proxy with tailscaled config %s", sanitizeConfigBytes([]byte(secret.StringData[tailscaledConfigKey])))
|
||||
logger.Debugf("creating a new Secret for the proxy with tailscaled config %s", sanitizeConfigBytes(latestConfig))
|
||||
if err := a.Create(ctx, secret); err != nil {
|
||||
return "", "", err
|
||||
return "", "", nil, err
|
||||
}
|
||||
}
|
||||
return secret.Name, hash, nil
|
||||
return secret.Name, hash, configs, nil
|
||||
}
|
||||
|
||||
// sanitizeConfigBytes returns ipn.ConfigVAlpha in string form with redacted
|
||||
// auth key.
|
||||
func sanitizeConfigBytes(bs []byte) string {
|
||||
c := &ipn.ConfigVAlpha{}
|
||||
if err := json.Unmarshal(bs, c); err != nil {
|
||||
return "invalid config"
|
||||
}
|
||||
func sanitizeConfigBytes(c ipn.ConfigVAlpha) string {
|
||||
if c.AuthKey != nil {
|
||||
c.AuthKey = ptr.To("**redacted**")
|
||||
}
|
||||
@@ -437,7 +445,7 @@ var proxyYaml []byte
|
||||
//go:embed deploy/manifests/userspace-proxy.yaml
|
||||
var userspaceProxyYaml []byte
|
||||
|
||||
func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.SugaredLogger, sts *tailscaleSTSConfig, headlessSvc *corev1.Service, proxySecret, tsConfigHash string) (*appsv1.StatefulSet, error) {
|
||||
func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.SugaredLogger, sts *tailscaleSTSConfig, headlessSvc *corev1.Service, proxySecret, tsConfigHash string, configs map[tailcfg.CapabilityVersion]ipn.ConfigVAlpha) (*appsv1.StatefulSet, error) {
|
||||
ss := new(appsv1.StatefulSet)
|
||||
if sts.ServeConfig != nil && sts.ForwardClusterTrafficViaL7IngressProxy != true { // If forwarding cluster traffic via is required we need non-userspace + NET_ADMIN + forwarding
|
||||
if err := yaml.Unmarshal(userspaceProxyYaml, &ss); err != nil {
|
||||
@@ -493,9 +501,15 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
|
||||
Value: proxySecret,
|
||||
},
|
||||
corev1.EnvVar{
|
||||
// Old tailscaled config key is still used for backwards compatibility.
|
||||
Name: "EXPERIMENTAL_TS_CONFIGFILE_PATH",
|
||||
Value: "/etc/tsconfig/tailscaled",
|
||||
},
|
||||
corev1.EnvVar{
|
||||
// New style is in the form of cap-<capability-version>.hujson.
|
||||
Name: "TS_EXPERIMENTAL_VERSIONED_CONFIG_DIR",
|
||||
Value: "/etc/tsconfig",
|
||||
},
|
||||
)
|
||||
if sts.ForwardClusterTrafficViaL7IngressProxy {
|
||||
container.Env = append(container.Env, corev1.EnvVar{
|
||||
@@ -505,18 +519,16 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
|
||||
}
|
||||
// Configure containeboot to run tailscaled with a configfile read from the state Secret.
|
||||
mak.Set(&ss.Spec.Template.Annotations, podAnnotationLastSetConfigFileHash, tsConfigHash)
|
||||
pod.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, corev1.Volume{
|
||||
|
||||
configVolume := corev1.Volume{
|
||||
Name: "tailscaledconfig",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{
|
||||
SecretName: proxySecret,
|
||||
Items: []corev1.KeyToPath{{
|
||||
Key: tailscaledConfigKey,
|
||||
Path: tailscaledConfigKey,
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
pod.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, configVolume)
|
||||
container.VolumeMounts = append(container.VolumeMounts, corev1.VolumeMount{
|
||||
Name: "tailscaledconfig",
|
||||
ReadOnly: true,
|
||||
@@ -571,10 +583,7 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{
|
||||
SecretName: proxySecret,
|
||||
Items: []corev1.KeyToPath{{
|
||||
Key: "serve-config",
|
||||
Path: "serve-config",
|
||||
}},
|
||||
Items: []corev1.KeyToPath{{Key: "serve-config", Path: "serve-config"}},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -716,42 +725,82 @@ func enableMetrics(ss *appsv1.StatefulSet, pc *tsapi.ProxyClass) {
|
||||
}
|
||||
}
|
||||
|
||||
func readAuthKey(secret *corev1.Secret, key string) (*string, error) {
|
||||
origConf := &ipn.ConfigVAlpha{}
|
||||
if err := json.Unmarshal([]byte(secret.Data[key]), origConf); err != nil {
|
||||
return nil, fmt.Errorf("error unmarshaling previous tailscaled config in %q: %w", key, err)
|
||||
}
|
||||
return origConf.AuthKey, nil
|
||||
}
|
||||
|
||||
// tailscaledConfig takes a proxy config, a newly generated auth key if
|
||||
// generated and a Secret with the previous proxy state and auth key and
|
||||
// produces returns tailscaled configuration and a hash of that configuration.
|
||||
func tailscaledConfig(stsC *tailscaleSTSConfig, newAuthkey string, oldSecret *corev1.Secret) ([]byte, string, error) {
|
||||
conf := ipn.ConfigVAlpha{
|
||||
Version: "alpha0",
|
||||
AcceptDNS: "false",
|
||||
AcceptRoutes: "false", // AcceptRoutes defaults to true
|
||||
Locked: "false",
|
||||
Hostname: &stsC.Hostname,
|
||||
// returns tailscaled configuration and a hash of that configuration.
|
||||
//
|
||||
// As of 2024-05-09 it also returns legacy tailscaled config without the
|
||||
// later added NoStatefulFilter field to support proxies older than cap95.
|
||||
// TODO (irbekrm): remove the legacy config once we no longer need to support
|
||||
// versions older than cap94,
|
||||
// https://tailscale.com/kb/1236/kubernetes-operator#operator-and-proxies
|
||||
func tailscaledConfig(stsC *tailscaleSTSConfig, newAuthkey string, oldSecret *corev1.Secret) (tailscaleConfigs, error) {
|
||||
conf := &ipn.ConfigVAlpha{
|
||||
Version: "alpha0",
|
||||
AcceptDNS: "false",
|
||||
AcceptRoutes: "false", // AcceptRoutes defaults to true
|
||||
Locked: "false",
|
||||
Hostname: &stsC.Hostname,
|
||||
NoStatefulFiltering: "false",
|
||||
}
|
||||
|
||||
// For egress proxies only, we need to ensure that stateful filtering is
|
||||
// not in place so that traffic from cluster can be forwarded via
|
||||
// Tailscale IPs.
|
||||
if stsC.TailnetTargetFQDN != "" || stsC.TailnetTargetIP != "" {
|
||||
conf.NoStatefulFiltering = "true"
|
||||
}
|
||||
if stsC.Connector != nil {
|
||||
routes, err := netutil.CalcAdvertiseRoutes(stsC.Connector.routes, stsC.Connector.isExitNode)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("error calculating routes: %w", err)
|
||||
return nil, fmt.Errorf("error calculating routes: %w", err)
|
||||
}
|
||||
conf.AdvertiseRoutes = routes
|
||||
}
|
||||
if newAuthkey != "" {
|
||||
conf.AuthKey = &newAuthkey
|
||||
} else if oldSecret != nil && len(oldSecret.Data[tailscaledConfigKey]) > 0 { // write to StringData, read from Data as StringData is write-only
|
||||
origConf := &ipn.ConfigVAlpha{}
|
||||
if err := json.Unmarshal([]byte(oldSecret.Data[tailscaledConfigKey]), origConf); err != nil {
|
||||
return nil, "", fmt.Errorf("error unmarshaling previous tailscaled config: %w", err)
|
||||
} else if oldSecret != nil {
|
||||
var err error
|
||||
latest := tailcfg.CapabilityVersion(-1)
|
||||
latestStr := ""
|
||||
for k, data := range oldSecret.Data {
|
||||
// write to StringData, read from Data as StringData is write-only
|
||||
if len(data) == 0 {
|
||||
continue
|
||||
}
|
||||
v, err := kubeutils.CapVerFromFileName(k)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if v > latest {
|
||||
latestStr = k
|
||||
latest = v
|
||||
}
|
||||
}
|
||||
// Allow for configs that don't contain an auth key. Perhaps
|
||||
// users have some mechanisms to delete them. Auth key is
|
||||
// normally not needed after the initial login.
|
||||
if latestStr != "" {
|
||||
conf.AuthKey, err = readAuthKey(oldSecret, latestStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
conf.AuthKey = origConf.AuthKey
|
||||
}
|
||||
confFileBytes, err := json.Marshal(conf)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("error marshaling tailscaled config : %w", err)
|
||||
}
|
||||
hash, err := hashBytes(confFileBytes)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("error calculating config hash: %w", err)
|
||||
}
|
||||
return confFileBytes, hash, nil
|
||||
capVerConfigs := make(map[tailcfg.CapabilityVersion]ipn.ConfigVAlpha)
|
||||
capVerConfigs[95] = *conf
|
||||
// legacy config should not contain NoStatefulFiltering field.
|
||||
conf.NoStatefulFiltering.Clear()
|
||||
capVerConfigs[94] = *conf
|
||||
return capVerConfigs, nil
|
||||
}
|
||||
|
||||
// ptrObject is a type constraint for pointer types that implement
|
||||
@@ -761,7 +810,9 @@ type ptrObject[T any] interface {
|
||||
*T
|
||||
}
|
||||
|
||||
// hashBytes produces a hash for the provided bytes that is the same across
|
||||
type tailscaleConfigs map[tailcfg.CapabilityVersion]ipn.ConfigVAlpha
|
||||
|
||||
// hashBytes produces a hash for the provided tailscaled config that is the same across
|
||||
// different invocations of this code. We do not use the
|
||||
// tailscale.com/deephash.Hash here because that produces a different hash for
|
||||
// the same value in different tailscale builds. The hash we are producing here
|
||||
@@ -770,10 +821,13 @@ type ptrObject[T any] interface {
|
||||
// thing that changed is operator version (the hash is also exposed to users via
|
||||
// an annotation and might be confusing if it changes without the config having
|
||||
// changed).
|
||||
func hashBytes(b []byte) (string, error) {
|
||||
h := sha256.New()
|
||||
_, err := h.Write(b)
|
||||
func tailscaledConfigHash(c tailscaleConfigs) (string, error) {
|
||||
b, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error marshalling tailscaled configs: %w", err)
|
||||
}
|
||||
h := sha256.New()
|
||||
if _, err = h.Write(b); err != nil {
|
||||
return "", fmt.Errorf("error calculating hash: %w", err)
|
||||
}
|
||||
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
||||
|
||||
@@ -161,7 +161,7 @@ func (a *ServiceReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
|
||||
}
|
||||
if violations := validateService(svc); len(violations) > 0 {
|
||||
msg := fmt.Sprintf("unable to provision proxy resources: invalid Service: %s", strings.Join(violations, ", "))
|
||||
a.recorder.Event(svc, corev1.EventTypeWarning, "INVALIDSERVCICE", msg)
|
||||
a.recorder.Event(svc, corev1.EventTypeWarning, "INVALIDSERVICE", msg)
|
||||
a.logger.Error(msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ func expectedSTS(t *testing.T, cl client.Client, opts configOpts) *appsv1.Statef
|
||||
{Name: "POD_IP", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{APIVersion: "", FieldPath: "status.podIP"}, ResourceFieldRef: nil, ConfigMapKeyRef: nil, SecretKeyRef: nil}},
|
||||
{Name: "TS_KUBE_SECRET", Value: opts.secretName},
|
||||
{Name: "EXPERIMENTAL_TS_CONFIGFILE_PATH", Value: "/etc/tsconfig/tailscaled"},
|
||||
{Name: "TS_EXPERIMENTAL_VERSIONED_CONFIG_DIR", Value: "/etc/tsconfig"},
|
||||
},
|
||||
SecurityContext: &corev1.SecurityContext{
|
||||
Capabilities: &corev1.Capabilities{
|
||||
@@ -89,12 +90,6 @@ func expectedSTS(t *testing.T, cl client.Client, opts configOpts) *appsv1.Statef
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{
|
||||
SecretName: opts.secretName,
|
||||
Items: []corev1.KeyToPath{
|
||||
{
|
||||
Key: "tailscaled",
|
||||
Path: "tailscaled",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -144,9 +139,7 @@ func expectedSTS(t *testing.T, cl client.Client, opts configOpts) *appsv1.Statef
|
||||
Name: "TS_SERVE_CONFIG",
|
||||
Value: "/etc/tailscaled/serve-config",
|
||||
})
|
||||
volumes = append(volumes, corev1.Volume{
|
||||
Name: "serve-config", VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: opts.secretName, Items: []corev1.KeyToPath{{Path: "serve-config", Key: "serve-config"}}}},
|
||||
})
|
||||
volumes = append(volumes, corev1.Volume{Name: "serve-config", VolumeSource: corev1.VolumeSource{Secret: &corev1.SecretVolumeSource{SecretName: opts.secretName, Items: []corev1.KeyToPath{{Key: "serve-config", Path: "serve-config"}}}}})
|
||||
tsContainer.VolumeMounts = append(tsContainer.VolumeMounts, corev1.VolumeMount{Name: "serve-config", ReadOnly: true, MountPath: "/etc/tailscaled"})
|
||||
}
|
||||
ss := &appsv1.StatefulSet{
|
||||
@@ -229,6 +222,7 @@ func expectedSTSUserspace(t *testing.T, cl client.Client, opts configOpts) *apps
|
||||
{Name: "POD_IP", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{APIVersion: "", FieldPath: "status.podIP"}, ResourceFieldRef: nil, ConfigMapKeyRef: nil, SecretKeyRef: nil}},
|
||||
{Name: "TS_KUBE_SECRET", Value: opts.secretName},
|
||||
{Name: "EXPERIMENTAL_TS_CONFIGFILE_PATH", Value: "/etc/tsconfig/tailscaled"},
|
||||
{Name: "TS_EXPERIMENTAL_VERSIONED_CONFIG_DIR", Value: "/etc/tsconfig"},
|
||||
{Name: "TS_SERVE_CONFIG", Value: "/etc/tailscaled/serve-config"},
|
||||
},
|
||||
ImagePullPolicy: "Always",
|
||||
@@ -243,20 +237,12 @@ func expectedSTSUserspace(t *testing.T, cl client.Client, opts configOpts) *apps
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{
|
||||
SecretName: opts.secretName,
|
||||
Items: []corev1.KeyToPath{
|
||||
{
|
||||
Key: "tailscaled",
|
||||
Path: "tailscaled",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{Name: "serve-config",
|
||||
VolumeSource: corev1.VolumeSource{
|
||||
Secret: &corev1.SecretVolumeSource{SecretName: opts.secretName,
|
||||
Items: []corev1.KeyToPath{{Key: "serve-config", Path: "serve-config"}}}},
|
||||
},
|
||||
Secret: &corev1.SecretVolumeSource{SecretName: opts.secretName, Items: []corev1.KeyToPath{{Key: "serve-config", Path: "serve-config"}}}}},
|
||||
}
|
||||
ss := &appsv1.StatefulSet{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
@@ -388,7 +374,17 @@ func expectedSecret(t *testing.T, opts configOpts) *corev1.Secret {
|
||||
if err != nil {
|
||||
t.Fatalf("error marshalling tailscaled config")
|
||||
}
|
||||
if opts.tailnetTargetFQDN != "" || opts.tailnetTargetIP != "" {
|
||||
conf.NoStatefulFiltering = "true"
|
||||
} else {
|
||||
conf.NoStatefulFiltering = "false"
|
||||
}
|
||||
bn, err := json.Marshal(conf)
|
||||
if err != nil {
|
||||
t.Fatalf("error marshalling tailscaled config")
|
||||
}
|
||||
mak.Set(&s.StringData, "tailscaled", string(b))
|
||||
mak.Set(&s.StringData, "cap-95.hujson", string(bn))
|
||||
labels := map[string]string{
|
||||
"tailscale.com/managed": "true",
|
||||
"tailscale.com/parent-resource": "test",
|
||||
@@ -463,7 +459,7 @@ func mustUpdateStatus[T any, O ptrObject[T]](t *testing.T, client client.Client,
|
||||
// they are not present in the passed object and use the modify func to remove
|
||||
// them from the cluster object. If no such modifications are needed, you can
|
||||
// pass nil in place of the modify function.
|
||||
func expectEqual[T any, O ptrObject[T]](t *testing.T, client client.Client, want O, modify func(O)) {
|
||||
func expectEqual[T any, O ptrObject[T]](t *testing.T, client client.Client, want O, modifier func(O)) {
|
||||
t.Helper()
|
||||
got := O(new(T))
|
||||
if err := client.Get(context.Background(), types.NamespacedName{
|
||||
@@ -477,8 +473,8 @@ func expectEqual[T any, O ptrObject[T]](t *testing.T, client client.Client, want
|
||||
// so just remove it from both got and want.
|
||||
got.SetResourceVersion("")
|
||||
want.SetResourceVersion("")
|
||||
if modify != nil {
|
||||
modify(got)
|
||||
if modifier != nil {
|
||||
modifier(got)
|
||||
}
|
||||
if diff := cmp.Diff(got, want); diff != "" {
|
||||
t.Fatalf("unexpected object (-got +want):\n%s", diff)
|
||||
|
||||
@@ -28,7 +28,6 @@ import (
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -58,8 +57,6 @@ func main() {
|
||||
ts := &tsnet.Server{
|
||||
Dir: *tailscaleDir,
|
||||
Hostname: *hostname,
|
||||
// Make the stdout logs a clean audit log of connections.
|
||||
Logf: logger.Discard,
|
||||
}
|
||||
|
||||
if os.Getenv("TS_AUTHKEY") == "" {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/tstest/integration"
|
||||
"tailscale.com/tstest/integration/testcontrol"
|
||||
"tailscale.com/tstest/nettest"
|
||||
"tailscale.com/types/appctype"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/types/key"
|
||||
@@ -98,8 +100,8 @@ func startNode(t *testing.T, ctx context.Context, controlURL, hostname string) (
|
||||
Store: new(mem.Store),
|
||||
Ephemeral: true,
|
||||
}
|
||||
if !*verboseNodes {
|
||||
s.Logf = logger.Discard
|
||||
if *verboseNodes {
|
||||
s.Logf = log.Printf
|
||||
}
|
||||
t.Cleanup(func() { s.Close() })
|
||||
|
||||
@@ -111,6 +113,7 @@ func startNode(t *testing.T, ctx context.Context, controlURL, hostname string) (
|
||||
}
|
||||
|
||||
func TestSNIProxyWithNetmapConfig(t *testing.T) {
|
||||
nettest.SkipIfNoNetwork(t)
|
||||
c, controlURL := startControl(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
@@ -189,6 +192,7 @@ func TestSNIProxyWithNetmapConfig(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSNIProxyWithFlagConfig(t *testing.T) {
|
||||
nettest.SkipIfNoNetwork(t)
|
||||
_, controlURL := startControl(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -20,7 +20,7 @@ func main() {
|
||||
}
|
||||
host := os.Args[1]
|
||||
|
||||
uaddr, err := net.ResolveUDPAddr("udp", host+":3478")
|
||||
uaddr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(host, "3478"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/version/distro"
|
||||
@@ -176,9 +177,10 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "bare_up_means_up",
|
||||
flags: []string{},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
Hostname: "foo",
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
Hostname: "foo",
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
@@ -186,12 +188,12 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "losing_hostname",
|
||||
flags: []string{"--accept-dns"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
Hostname: "foo",
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AllowSingleHosts: true,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
Hostname: "foo",
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
want: accidentalUpPrefix + " --accept-dns --hostname=foo",
|
||||
},
|
||||
@@ -199,11 +201,11 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "hostname_changing_explicitly",
|
||||
flags: []string{"--hostname=bar"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AllowSingleHosts: true,
|
||||
Hostname: "foo",
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
Hostname: "foo",
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
@@ -211,11 +213,11 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "hostname_changing_empty_explicitly",
|
||||
flags: []string{"--hostname="},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AllowSingleHosts: true,
|
||||
Hostname: "foo",
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
Hostname: "foo",
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
@@ -231,11 +233,11 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "implicit_operator_change",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
OperatorUser: "alice",
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
OperatorUser: "alice",
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
curUser: "eve",
|
||||
want: accidentalUpPrefix + " --hostname=foo --operator=alice",
|
||||
@@ -244,11 +246,11 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "implicit_operator_matches_shell_user",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
OperatorUser: "alice",
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
OperatorUser: "alice",
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
curUser: "alice",
|
||||
want: "",
|
||||
@@ -257,15 +259,15 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "error_advertised_routes_exit_node_removed",
|
||||
flags: []string{"--advertise-routes=10.0.42.0/24"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.42.0/24"),
|
||||
netip.MustParsePrefix("0.0.0.0/0"),
|
||||
netip.MustParsePrefix("::/0"),
|
||||
},
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
want: accidentalUpPrefix + " --advertise-routes=10.0.42.0/24 --advertise-exit-node",
|
||||
},
|
||||
@@ -273,15 +275,15 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "advertised_routes_exit_node_removed_explicit",
|
||||
flags: []string{"--advertise-routes=10.0.42.0/24", "--advertise-exit-node=false"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.42.0/24"),
|
||||
netip.MustParsePrefix("0.0.0.0/0"),
|
||||
netip.MustParsePrefix("::/0"),
|
||||
},
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
@@ -289,15 +291,15 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "advertised_routes_includes_the_0_routes", // but no --advertise-exit-node
|
||||
flags: []string{"--advertise-routes=11.1.43.0/24,0.0.0.0/0,::/0"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.42.0/24"),
|
||||
netip.MustParsePrefix("0.0.0.0/0"),
|
||||
netip.MustParsePrefix("::/0"),
|
||||
},
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
@@ -305,10 +307,10 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "advertise_exit_node", // Issue 1859
|
||||
flags: []string{"--advertise-exit-node"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
@@ -316,14 +318,14 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "advertise_exit_node_over_existing_routes",
|
||||
flags: []string{"--advertise-exit-node"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
|
||||
AdvertiseRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("1.2.0.0/16"),
|
||||
},
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
want: accidentalUpPrefix + " --advertise-exit-node --advertise-routes=1.2.0.0/16",
|
||||
},
|
||||
@@ -331,15 +333,15 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "advertise_exit_node_over_existing_routes_and_exit_node",
|
||||
flags: []string{"--advertise-exit-node"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/0"),
|
||||
netip.MustParsePrefix("::/0"),
|
||||
netip.MustParsePrefix("1.2.0.0/16"),
|
||||
},
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
want: accidentalUpPrefix + " --advertise-exit-node --advertise-routes=1.2.0.0/16",
|
||||
},
|
||||
@@ -347,12 +349,12 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "exit_node_clearing", // Issue 1777
|
||||
flags: []string{"--exit-node="},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
|
||||
ExitNodeID: "fooID",
|
||||
ExitNodeID: "fooID",
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
@@ -360,59 +362,59 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "remove_all_implicit",
|
||||
flags: []string{"--force-reauth"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
RouteAll: true,
|
||||
AllowSingleHosts: false,
|
||||
ExitNodeIP: netip.MustParseAddr("100.64.5.6"),
|
||||
CorpDNS: false,
|
||||
ShieldsUp: true,
|
||||
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
||||
Hostname: "myhostname",
|
||||
ForceDaemon: true,
|
||||
WantRunning: true,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
RouteAll: true,
|
||||
ExitNodeIP: netip.MustParseAddr("100.64.5.6"),
|
||||
CorpDNS: false,
|
||||
ShieldsUp: true,
|
||||
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
||||
Hostname: "myhostname",
|
||||
ForceDaemon: true,
|
||||
AdvertiseRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/16"),
|
||||
netip.MustParsePrefix("0.0.0.0/0"),
|
||||
netip.MustParsePrefix("::/0"),
|
||||
},
|
||||
NetfilterMode: preftype.NetfilterNoDivert,
|
||||
OperatorUser: "alice",
|
||||
NetfilterMode: preftype.NetfilterNoDivert,
|
||||
OperatorUser: "alice",
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
curUser: "eve",
|
||||
want: accidentalUpPrefix + " --force-reauth --accept-dns=false --accept-routes --advertise-exit-node --advertise-routes=10.0.0.0/16 --advertise-tags=tag:foo,tag:bar --exit-node=100.64.5.6 --host-routes=false --hostname=myhostname --netfilter-mode=nodivert --operator=alice --shields-up",
|
||||
want: accidentalUpPrefix + " --force-reauth --accept-dns=false --accept-routes --advertise-exit-node --advertise-routes=10.0.0.0/16 --advertise-tags=tag:foo,tag:bar --exit-node=100.64.5.6 --hostname=myhostname --netfilter-mode=nodivert --operator=alice --shields-up",
|
||||
},
|
||||
{
|
||||
name: "remove_all_implicit_except_hostname",
|
||||
flags: []string{"--hostname=newhostname"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
RouteAll: true,
|
||||
AllowSingleHosts: false,
|
||||
ExitNodeIP: netip.MustParseAddr("100.64.5.6"),
|
||||
CorpDNS: false,
|
||||
ShieldsUp: true,
|
||||
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
||||
Hostname: "myhostname",
|
||||
ForceDaemon: true,
|
||||
WantRunning: true,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
RouteAll: true,
|
||||
ExitNodeIP: netip.MustParseAddr("100.64.5.6"),
|
||||
CorpDNS: false,
|
||||
ShieldsUp: true,
|
||||
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
||||
Hostname: "myhostname",
|
||||
ForceDaemon: true,
|
||||
AdvertiseRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("10.0.0.0/16"),
|
||||
},
|
||||
NetfilterMode: preftype.NetfilterNoDivert,
|
||||
OperatorUser: "alice",
|
||||
NetfilterMode: preftype.NetfilterNoDivert,
|
||||
OperatorUser: "alice",
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
curUser: "eve",
|
||||
want: accidentalUpPrefix + " --hostname=newhostname --accept-dns=false --accept-routes --advertise-routes=10.0.0.0/16 --advertise-tags=tag:foo,tag:bar --exit-node=100.64.5.6 --host-routes=false --netfilter-mode=nodivert --operator=alice --shields-up",
|
||||
want: accidentalUpPrefix + " --hostname=newhostname --accept-dns=false --accept-routes --advertise-routes=10.0.0.0/16 --advertise-tags=tag:foo,tag:bar --exit-node=100.64.5.6 --netfilter-mode=nodivert --operator=alice --shields-up",
|
||||
},
|
||||
{
|
||||
name: "loggedout_is_implicit",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
LoggedOut: true,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
LoggedOut: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
want: "", // not an error. LoggedOut is implicit.
|
||||
},
|
||||
@@ -422,10 +424,9 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "make_windows_exit_node",
|
||||
flags: []string{"--advertise-exit-node"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
RouteAll: true,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
RouteAll: true,
|
||||
|
||||
// And assume this no-op accidental pre-1.8 value:
|
||||
NoSNAT: true,
|
||||
@@ -437,8 +438,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "ignore_netfilter_change_non_linux",
|
||||
flags: []string{"--accept-dns"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
|
||||
NetfilterMode: preftype.NetfilterNoDivert, // we never had this bug, but pretend it got set non-zero on Windows somehow
|
||||
},
|
||||
@@ -449,15 +449,15 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "operator_losing_routes_step1", // https://twitter.com/EXPbits/status/1390418145047887877
|
||||
flags: []string{"--operator=expbits"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/0"),
|
||||
netip.MustParsePrefix("::/0"),
|
||||
netip.MustParsePrefix("1.2.0.0/16"),
|
||||
},
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
want: accidentalUpPrefix + " --operator=expbits --advertise-exit-node --advertise-routes=1.2.0.0/16",
|
||||
},
|
||||
@@ -465,15 +465,15 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "operator_losing_routes_step2", // https://twitter.com/EXPbits/status/1390418145047887877
|
||||
flags: []string{"--operator=expbits", "--advertise-routes=1.2.0.0/16"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/0"),
|
||||
netip.MustParsePrefix("::/0"),
|
||||
netip.MustParsePrefix("1.2.0.0/16"),
|
||||
},
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
want: accidentalUpPrefix + " --advertise-routes=1.2.0.0/16 --operator=expbits --advertise-exit-node",
|
||||
},
|
||||
@@ -481,13 +481,13 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "errors_preserve_explicit_flags",
|
||||
flags: []string{"--reset", "--force-reauth=false", "--authkey=secretrand"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AllowSingleHosts: true,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
|
||||
Hostname: "foo",
|
||||
Hostname: "foo",
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
want: accidentalUpPrefix + " --auth-key=secretrand --force-reauth=false --reset --hostname=foo",
|
||||
},
|
||||
@@ -495,12 +495,12 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "error_exit_node_omit_with_ip_pref",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
|
||||
ExitNodeIP: netip.MustParseAddr("100.64.5.4"),
|
||||
ExitNodeIP: netip.MustParseAddr("100.64.5.4"),
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.4",
|
||||
},
|
||||
@@ -509,12 +509,12 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
flags: []string{"--hostname=foo"},
|
||||
curExitNodeIP: netip.MustParseAddr("100.64.5.7"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
|
||||
ExitNodeID: "some_stable_id",
|
||||
ExitNodeID: "some_stable_id",
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.7",
|
||||
},
|
||||
@@ -523,13 +523,13 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
flags: []string{"--hostname=foo"},
|
||||
curExitNodeIP: netip.MustParseAddr("100.2.3.4"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
|
||||
ExitNodeAllowLANAccess: true,
|
||||
ExitNodeID: "some_stable_id",
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
want: accidentalUpPrefix + " --hostname=foo --exit-node-allow-lan-access --exit-node=100.2.3.4",
|
||||
},
|
||||
@@ -537,10 +537,10 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "ignore_login_server_synonym",
|
||||
flags: []string{"--login-server=https://controlplane.tailscale.com"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
want: "", // not an error
|
||||
},
|
||||
@@ -548,10 +548,10 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "ignore_login_server_synonym_on_other_change",
|
||||
flags: []string{"--netfilter-mode=off"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: false,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: false,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
want: accidentalUpPrefix + " --netfilter-mode=off --accept-dns=false",
|
||||
},
|
||||
@@ -561,11 +561,11 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "synology_permit_omit_accept_routes",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
RouteAll: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
RouteAll: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
goos: "linux",
|
||||
distro: distro.Synology,
|
||||
@@ -577,11 +577,11 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "not_synology_dont_permit_omit_accept_routes",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
RouteAll: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
RouteAll: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
goos: "linux",
|
||||
distro: "", // not Synology
|
||||
@@ -591,11 +591,11 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "profile_name_ignored_in_up",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ProfileName: "foo",
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ProfileName: "foo",
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
goos: "linux",
|
||||
want: "",
|
||||
@@ -655,12 +655,12 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
goos: "linux",
|
||||
args: upArgsFromOSArgs("linux"),
|
||||
want: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
NoSNAT: false,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
NoSNAT: false,
|
||||
NoStatefulFiltering: "true",
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
CorpDNS: true,
|
||||
AutoUpdate: ipn.AutoUpdatePrefs{
|
||||
Check: true,
|
||||
},
|
||||
@@ -671,12 +671,13 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
goos: "windows",
|
||||
args: upArgsFromOSArgs("windows"),
|
||||
want: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
RouteAll: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
CorpDNS: true,
|
||||
RouteAll: true,
|
||||
NoSNAT: false,
|
||||
NoStatefulFiltering: "true",
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AutoUpdate: ipn.AutoUpdatePrefs{
|
||||
Check: true,
|
||||
},
|
||||
@@ -686,15 +687,15 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
name: "advertise_default_route",
|
||||
args: upArgsFromOSArgs("linux", "--advertise-exit-node"),
|
||||
want: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
CorpDNS: true,
|
||||
AdvertiseRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/0"),
|
||||
netip.MustParsePrefix("::/0"),
|
||||
},
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
NoStatefulFiltering: "true",
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AutoUpdate: ipn.AutoUpdatePrefs{
|
||||
Check: true,
|
||||
},
|
||||
@@ -781,9 +782,10 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
},
|
||||
wantWarn: "netfilter=nodivert; add iptables calls to ts-* chains manually.",
|
||||
want: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
NetfilterMode: preftype.NetfilterNoDivert,
|
||||
NoSNAT: true,
|
||||
WantRunning: true,
|
||||
NetfilterMode: preftype.NetfilterNoDivert,
|
||||
NoSNAT: true,
|
||||
NoStatefulFiltering: "true",
|
||||
AutoUpdate: ipn.AutoUpdatePrefs{
|
||||
Check: true,
|
||||
},
|
||||
@@ -797,9 +799,10 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
},
|
||||
wantWarn: "netfilter=off; configure iptables yourself.",
|
||||
want: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
NetfilterMode: preftype.NetfilterOff,
|
||||
NoSNAT: true,
|
||||
WantRunning: true,
|
||||
NetfilterMode: preftype.NetfilterOff,
|
||||
NoSNAT: true,
|
||||
NoStatefulFiltering: "true",
|
||||
AutoUpdate: ipn.AutoUpdatePrefs{
|
||||
Check: true,
|
||||
},
|
||||
@@ -813,8 +816,9 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
netfilterMode: "off",
|
||||
},
|
||||
want: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
NoSNAT: true,
|
||||
WantRunning: true,
|
||||
NoSNAT: true,
|
||||
NoStatefulFiltering: "true",
|
||||
AdvertiseRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("fd7a:115c:a1e0:b1a::bb:10.0.0.0/112"),
|
||||
},
|
||||
@@ -831,8 +835,9 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
netfilterMode: "off",
|
||||
},
|
||||
want: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
NoSNAT: true,
|
||||
WantRunning: true,
|
||||
NoSNAT: true,
|
||||
NoStatefulFiltering: "true",
|
||||
AdvertiseRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("fd7a:115c:a1e0:b1a::aabb:10.0.0.0/112"),
|
||||
},
|
||||
@@ -914,6 +919,9 @@ func TestPrefFlagMapping(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
switch prefName {
|
||||
case "AllowSingleHosts":
|
||||
// Fake pref for downgrade compat. See #12058.
|
||||
continue
|
||||
case "WantRunning", "Persist", "LoggedOut":
|
||||
// All explicitly handled (ignored) by checkForAccidentalSettingReverts.
|
||||
continue
|
||||
@@ -1021,7 +1029,6 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
AdvertiseRoutesSet: true,
|
||||
AdvertiseTagsSet: true,
|
||||
AllowSingleHostsSet: true,
|
||||
AppConnectorSet: true,
|
||||
ControlURLSet: true,
|
||||
CorpDNSSet: true,
|
||||
@@ -1031,6 +1038,7 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
HostnameSet: true,
|
||||
NetfilterModeSet: true,
|
||||
NoSNATSet: true,
|
||||
NoStatefulFilteringSet: true,
|
||||
OperatorUserSet: true,
|
||||
RouteAllSet: true,
|
||||
RunSSHSet: true,
|
||||
@@ -1053,11 +1061,11 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
name: "change_login_server",
|
||||
flags: []string{"--login-server=https://localhost:1000"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
wantSimpleUp: true,
|
||||
@@ -1068,11 +1076,11 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
name: "change_tags",
|
||||
flags: []string{"--advertise-tags=tag:foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
},
|
||||
@@ -1081,11 +1089,11 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
name: "explicit_empty_operator",
|
||||
flags: []string{"--operator="},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
OperatorUser: "somebody",
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
OperatorUser: "somebody",
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
env: upCheckEnv{user: "somebody", backendState: "Running"},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
@@ -1102,11 +1110,11 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
name: "enable_ssh",
|
||||
flags: []string{"--ssh"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
RunSSHSet: true,
|
||||
@@ -1123,12 +1131,12 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
name: "disable_ssh",
|
||||
flags: []string{"--ssh=false"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
RunSSH: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
||||
CorpDNS: true,
|
||||
RunSSH: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
RunSSHSet: true,
|
||||
@@ -1148,12 +1156,12 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
flags: []string{"--ssh=false"},
|
||||
sshOverTailscale: true,
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
RunSSH: true,
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
RunSSH: true,
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
RunSSHSet: true,
|
||||
@@ -1172,11 +1180,11 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
flags: []string{"--ssh=true"},
|
||||
sshOverTailscale: true,
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
RunSSHSet: true,
|
||||
@@ -1195,11 +1203,11 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
flags: []string{"--ssh=true", "--accept-risk=lose-ssh"},
|
||||
sshOverTailscale: true,
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
RunSSHSet: true,
|
||||
@@ -1217,12 +1225,12 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
flags: []string{"--ssh=false", "--accept-risk=lose-ssh"},
|
||||
sshOverTailscale: true,
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
RunSSH: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{UserProfile: tailcfg.UserProfile{LoginName: "crawshaw.github"}},
|
||||
CorpDNS: true,
|
||||
RunSSH: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
RunSSHSet: true,
|
||||
@@ -1240,10 +1248,10 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
flags: []string{"--force-reauth"},
|
||||
sshOverTailscale: true,
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
wantErrSubtr: "aborted, no changes made",
|
||||
@@ -1253,10 +1261,10 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
flags: []string{"--force-reauth", "--accept-risk=lose-ssh"},
|
||||
sshOverTailscale: true,
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
wantJustEditMP: nil,
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
@@ -1265,10 +1273,10 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
name: "advertise_connector",
|
||||
flags: []string{"--advertise-connector"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
AppConnectorSet: true,
|
||||
@@ -1285,13 +1293,13 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
name: "no_advertise_connector",
|
||||
flags: []string{"--advertise-connector=false"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AppConnector: ipn.AppConnectorPrefs{
|
||||
Advertise: true,
|
||||
},
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
AppConnectorSet: true,
|
||||
|
||||
@@ -97,7 +97,7 @@ func Inject(root *ffcli.Command, hide func(*ffcli.Command), usageFunc func(*ffcl
|
||||
&ffcli.Command{
|
||||
Name: "completion",
|
||||
ShortUsage: root.Name + " completion <shell> [--flags] [--descs]",
|
||||
ShortHelp: "Shell tab-completion scripts.",
|
||||
ShortHelp: "Shell tab-completion scripts",
|
||||
LongHelp: fmt.Sprintf(cobra.UsageTemplate, root.Name),
|
||||
|
||||
// Print help if run without args.
|
||||
|
||||
@@ -127,13 +127,13 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
|
||||
|
||||
printf("\nReport:\n")
|
||||
printf("\t* UDP: %v\n", report.UDP)
|
||||
if report.GlobalV4 != "" {
|
||||
printf("\t* IPv4: yes, %v\n", report.GlobalV4)
|
||||
if report.GlobalV4.IsValid() {
|
||||
printf("\t* IPv4: yes, %s\n", report.GlobalV4)
|
||||
} else {
|
||||
printf("\t* IPv4: (no addr found)\n")
|
||||
}
|
||||
if report.GlobalV6 != "" {
|
||||
printf("\t* IPv6: yes, %v\n", report.GlobalV6)
|
||||
if report.GlobalV6.IsValid() {
|
||||
printf("\t* IPv6: yes, %s\n", report.GlobalV6)
|
||||
} else if report.IPv6 {
|
||||
printf("\t* IPv6: (no addr found)\n")
|
||||
} else if report.OSHasIPv6 {
|
||||
@@ -142,7 +142,6 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
|
||||
printf("\t* IPv6: no, unavailable in OS\n")
|
||||
}
|
||||
printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP)
|
||||
printf("\t* HairPinning: %v\n", report.HairPinning)
|
||||
printf("\t* PortMapping: %v\n", portMapping(report))
|
||||
if report.CaptivePortal != "" {
|
||||
printf("\t* CaptivePortal: %v\n", report.CaptivePortal)
|
||||
|
||||
@@ -222,7 +222,8 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
|
||||
|
||||
if st.Enabled && st.NodeKey != nil && !st.PublicKey.IsZero() {
|
||||
if st.NodeKeySigned {
|
||||
fmt.Println("This node is accessible under tailnet lock.")
|
||||
fmt.Println("This node is accessible under tailnet lock. Node signature:")
|
||||
fmt.Println(st.NodeKeySignature.String())
|
||||
} else {
|
||||
fmt.Println("This node is LOCKED OUT by tailnet-lock, and action is required to establish connectivity.")
|
||||
fmt.Printf("Run the following command on a node with a trusted key:\n\ttailscale lock sign %v %s\n", st.NodeKey, st.PublicKey.CLIString())
|
||||
|
||||
@@ -58,6 +58,9 @@ type setArgsT struct {
|
||||
updateCheck bool
|
||||
updateApply bool
|
||||
postureChecking bool
|
||||
snat bool
|
||||
statefulFiltering bool
|
||||
netfilterMode string
|
||||
}
|
||||
|
||||
func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
|
||||
@@ -98,6 +101,10 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
|
||||
setf.StringVar(&setArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
|
||||
}
|
||||
switch goos {
|
||||
case "linux":
|
||||
setf.BoolVar(&setArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
|
||||
setf.BoolVar(&setArgs.statefulFiltering, "stateful-filtering", false, "apply stateful filtering to forwarded packets (subnet routers, exit nodes, etc.)")
|
||||
setf.StringVar(&setArgs.netfilterMode, "netfilter-mode", defaultNetfilterMode(), "netfilter mode (one of on, nodivert, off)")
|
||||
case "windows":
|
||||
setf.BoolVar(&setArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
|
||||
}
|
||||
@@ -121,6 +128,9 @@ func runSet(ctx context.Context, args []string) (retErr error) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Note that even though we set the values here regardless of whether the
|
||||
// user passed the flag, the value is only used if the user passed the flag.
|
||||
// See updateMaskedPrefsFromUpOrSetFlag.
|
||||
maskedPrefs := &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
ProfileName: setArgs.profileName,
|
||||
@@ -132,6 +142,7 @@ func runSet(ctx context.Context, args []string) (retErr error) {
|
||||
RunWebClient: setArgs.runWebClient,
|
||||
Hostname: setArgs.hostname,
|
||||
OperatorUser: setArgs.opUser,
|
||||
NoSNAT: !setArgs.snat,
|
||||
ForceDaemon: setArgs.forceDaemon,
|
||||
AutoUpdate: ipn.AutoUpdatePrefs{
|
||||
Check: setArgs.updateCheck,
|
||||
@@ -140,10 +151,22 @@ func runSet(ctx context.Context, args []string) (retErr error) {
|
||||
AppConnector: ipn.AppConnectorPrefs{
|
||||
Advertise: setArgs.advertiseConnector,
|
||||
},
|
||||
PostureChecking: setArgs.postureChecking,
|
||||
PostureChecking: setArgs.postureChecking,
|
||||
NoStatefulFiltering: opt.NewBool(!setArgs.statefulFiltering),
|
||||
},
|
||||
}
|
||||
|
||||
if effectiveGOOS() == "linux" {
|
||||
nfMode, warning, err := netfilterModeFromFlag(setArgs.netfilterMode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if warning != "" {
|
||||
warnf(warning)
|
||||
}
|
||||
maskedPrefs.Prefs.NetfilterMode = nfMode
|
||||
}
|
||||
|
||||
if setArgs.exitNodeIP != "" {
|
||||
if err := maskedPrefs.Prefs.SetExitNodeIP(setArgs.exitNodeIP, st); err != nil {
|
||||
var e ipn.ExitNodeLocalIPError
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -105,7 +104,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT, cmd string) *flag.FlagSet {
|
||||
upf.StringVar(&upArgs.server, "login-server", ipn.DefaultControlURL, "base URL of control server")
|
||||
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", acceptRouteDefault(goos), "accept routes advertised by other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
|
||||
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, hidden+"install host routes to other Tailscale nodes")
|
||||
upf.Var(notFalseVar{}, "host-routes", hidden+"install host routes to other Tailscale nodes (must be true as of Tailscale 1.67+)")
|
||||
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP or base name) for internet traffic, or empty string to not use an exit node")
|
||||
upf.BoolVar(&upArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
|
||||
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
||||
@@ -122,6 +121,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT, cmd string) *flag.FlagSet {
|
||||
switch goos {
|
||||
case "linux":
|
||||
upf.BoolVar(&upArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
|
||||
upf.BoolVar(&upArgs.statefulFiltering, "stateful-filtering", false, "apply stateful filtering to forwarded packets (subnet routers, exit nodes, etc.)")
|
||||
upf.StringVar(&upArgs.netfilterMode, "netfilter-mode", defaultNetfilterMode(), "netfilter mode (one of on, nodivert, off)")
|
||||
case "windows":
|
||||
upf.BoolVar(&upArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
|
||||
@@ -143,6 +143,18 @@ func newUpFlagSet(goos string, upArgs *upArgsT, cmd string) *flag.FlagSet {
|
||||
return upf
|
||||
}
|
||||
|
||||
// notFalseVar is is a flag.Value that can only be "true", if set.
|
||||
type notFalseVar struct{}
|
||||
|
||||
func (notFalseVar) IsBoolFlag() bool { return true }
|
||||
func (notFalseVar) Set(v string) error {
|
||||
if v != "true" {
|
||||
return fmt.Errorf("unsupported value; only 'true' is allowed")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (notFalseVar) String() string { return "true" }
|
||||
|
||||
func defaultNetfilterMode() string {
|
||||
if distro.Get() == distro.Synology {
|
||||
return "off"
|
||||
@@ -156,7 +168,6 @@ type upArgsT struct {
|
||||
server string
|
||||
acceptRoutes bool
|
||||
acceptDNS bool
|
||||
singleRoutes bool
|
||||
exitNodeIP string
|
||||
exitNodeAllowLANAccess bool
|
||||
shieldsUp bool
|
||||
@@ -169,6 +180,7 @@ type upArgsT struct {
|
||||
advertiseTags string
|
||||
advertiseConnector bool
|
||||
snat bool
|
||||
statefulFiltering bool
|
||||
netfilterMode string
|
||||
authKeyOrFile string // "secret" or "file:/path/to/secret"
|
||||
hostname string
|
||||
@@ -277,7 +289,6 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
|
||||
prefs.ExitNodeAllowLANAccess = upArgs.exitNodeAllowLANAccess
|
||||
prefs.CorpDNS = upArgs.acceptDNS
|
||||
prefs.AllowSingleHosts = upArgs.singleRoutes
|
||||
prefs.ShieldsUp = upArgs.shieldsUp
|
||||
prefs.RunSSH = upArgs.runSSH
|
||||
prefs.RunWebClient = upArgs.runWebClient
|
||||
@@ -292,24 +303,44 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
if goos == "linux" {
|
||||
prefs.NoSNAT = !upArgs.snat
|
||||
|
||||
switch upArgs.netfilterMode {
|
||||
case "on":
|
||||
prefs.NetfilterMode = preftype.NetfilterOn
|
||||
case "nodivert":
|
||||
prefs.NetfilterMode = preftype.NetfilterNoDivert
|
||||
warnf("netfilter=nodivert; add iptables calls to ts-* chains manually.")
|
||||
case "off":
|
||||
prefs.NetfilterMode = preftype.NetfilterOff
|
||||
if defaultNetfilterMode() != "off" {
|
||||
warnf("netfilter=off; configure iptables yourself.")
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid value --netfilter-mode=%q", upArgs.netfilterMode)
|
||||
// Backfills for NoStatefulFiltering occur when loading a profile; just set it explicitly here.
|
||||
prefs.NoStatefulFiltering.Set(!upArgs.statefulFiltering)
|
||||
v, warning, err := netfilterModeFromFlag(upArgs.netfilterMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prefs.NetfilterMode = v
|
||||
if warning != "" {
|
||||
warnf(warning)
|
||||
}
|
||||
}
|
||||
return prefs, nil
|
||||
}
|
||||
|
||||
// netfilterModeFromFlag returns the preftype.NetfilterMode for the provided
|
||||
// flag value. It returns a warning if there is something the user should know
|
||||
// about the value.
|
||||
func netfilterModeFromFlag(v string) (_ preftype.NetfilterMode, warning string, _ error) {
|
||||
switch v {
|
||||
case "on", "nodivert", "off":
|
||||
default:
|
||||
return preftype.NetfilterOn, "", fmt.Errorf("invalid value --netfilter-mode=%q", v)
|
||||
}
|
||||
m, err := preftype.ParseNetfilterMode(v)
|
||||
if err != nil {
|
||||
return preftype.NetfilterOn, "", err
|
||||
}
|
||||
switch m {
|
||||
case preftype.NetfilterNoDivert:
|
||||
warning = "netfilter=nodivert; add iptables calls to ts-* chains manually."
|
||||
case preftype.NetfilterOff:
|
||||
if defaultNetfilterMode() != "off" {
|
||||
warning = "netfilter=off; configure iptables yourself."
|
||||
}
|
||||
}
|
||||
return m, warning, nil
|
||||
}
|
||||
|
||||
// updatePrefs returns how to edit preferences based on the
|
||||
// flag-provided 'prefs' and the currently active 'curPrefs'.
|
||||
//
|
||||
@@ -406,6 +437,11 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
|
||||
// printAuthURL reports whether we should print out the
|
||||
// provided auth URL from an IPN notify.
|
||||
printAuthURL := func(url string) bool {
|
||||
if url == "" {
|
||||
// Probably unnecessary but we used to have a bug where tailscaled
|
||||
// could send an empty URL over the IPN bus. ~Harmless to keep.
|
||||
return false
|
||||
}
|
||||
if upArgs.authKeyOrFile != "" {
|
||||
// Issue 1755: when using an authkey, don't
|
||||
// show an authURL that might still be pending
|
||||
@@ -477,11 +513,6 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
|
||||
|
||||
watchCtx, cancelWatch := context.WithCancel(ctx)
|
||||
defer cancelWatch()
|
||||
watcher, err := localClient.WatchIPNBus(watchCtx, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
go func() {
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
@@ -494,29 +525,62 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
|
||||
}()
|
||||
|
||||
running := make(chan bool, 1) // gets value once in state ipn.Running
|
||||
pumpErr := make(chan error, 1)
|
||||
watchErr := make(chan error, 1)
|
||||
|
||||
// localAPIMu should be held while doing mutable LocalAPI calls
|
||||
// to the backend. In particular, it prevents StartLoginInteractive from
|
||||
// being called from the watcher goroutine while the Start call from
|
||||
// the other goroutine is in progress.
|
||||
// See https://github.com/tailscale/tailscale/issues/7036#issuecomment-2053771466
|
||||
// TODO(bradfitz): simplify this once #11649 is cleaned up and Start is
|
||||
// hopefully removed.
|
||||
var localAPIMu sync.Mutex
|
||||
// Special case: bare "tailscale up" means to just start
|
||||
// running, if there's ever been a login.
|
||||
if simpleUp {
|
||||
_, err := localClient.EditPrefs(ctx, &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
WantRunning: true,
|
||||
},
|
||||
WantRunningSet: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := localClient.CheckPrefs(ctx, prefs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
startLoginInteractive := sync.OnceFunc(func() {
|
||||
localAPIMu.Lock()
|
||||
defer localAPIMu.Unlock()
|
||||
localClient.StartLoginInteractive(ctx)
|
||||
})
|
||||
authKey, err := upArgs.getAuthKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authKey, err = resolveAuthKey(ctx, authKey, upArgs.advertiseTags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = localClient.Start(ctx, ipn.Options{
|
||||
AuthKey: authKey,
|
||||
UpdatePrefs: prefs,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if upArgs.forceReauth || !st.HaveNodeKey {
|
||||
err := localClient.StartLoginInteractive(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
watcher, err := localClient.WatchIPNBus(watchCtx, ipn.NotifyInitialState)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer watcher.Close()
|
||||
|
||||
go func() {
|
||||
var printed bool // whether we've yet printed anything to stdout or stderr
|
||||
var lastURLPrinted string
|
||||
|
||||
for {
|
||||
n, err := watcher.Next()
|
||||
if err != nil {
|
||||
pumpErr <- err
|
||||
watchErr <- err
|
||||
return
|
||||
}
|
||||
if n.ErrMessage != nil {
|
||||
@@ -525,8 +589,6 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
|
||||
}
|
||||
if s := n.State; s != nil {
|
||||
switch *s {
|
||||
case ipn.NeedsLogin:
|
||||
startLoginInteractive()
|
||||
case ipn.NeedsMachineAuth:
|
||||
printed = true
|
||||
if env.upArgs.json {
|
||||
@@ -549,12 +611,17 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
|
||||
cancelWatch()
|
||||
}
|
||||
}
|
||||
if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
|
||||
if url := n.BrowseToURL; url != nil {
|
||||
authURL := *url
|
||||
if !printAuthURL(authURL) || authURL == lastURLPrinted {
|
||||
continue
|
||||
}
|
||||
printed = true
|
||||
lastURLPrinted = authURL
|
||||
if upArgs.json {
|
||||
js := &upOutputJSON{AuthURL: *url, BackendState: st.BackendState}
|
||||
js := &upOutputJSON{AuthURL: authURL, BackendState: st.BackendState}
|
||||
|
||||
q, err := qrcode.New(*url, qrcode.Medium)
|
||||
q, err := qrcode.New(authURL, qrcode.Medium)
|
||||
if err == nil {
|
||||
png, err := q.PNG(128)
|
||||
if err == nil {
|
||||
@@ -569,9 +636,9 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
|
||||
outln(string(data))
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
|
||||
fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", authURL)
|
||||
if upArgs.qr {
|
||||
q, err := qrcode.New(*url, qrcode.Medium)
|
||||
q, err := qrcode.New(authURL, qrcode.Medium)
|
||||
if err != nil {
|
||||
log.Printf("QR code error: %v", err)
|
||||
} else {
|
||||
@@ -583,47 +650,6 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
|
||||
}
|
||||
}()
|
||||
|
||||
// Special case: bare "tailscale up" means to just start
|
||||
// running, if there's ever been a login.
|
||||
if simpleUp {
|
||||
localAPIMu.Lock()
|
||||
_, err := localClient.EditPrefs(ctx, &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
WantRunning: true,
|
||||
},
|
||||
WantRunningSet: true,
|
||||
})
|
||||
localAPIMu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := localClient.CheckPrefs(ctx, prefs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
authKey, err := upArgs.getAuthKey()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
authKey, err = resolveAuthKey(ctx, authKey, upArgs.advertiseTags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
localAPIMu.Lock()
|
||||
err = localClient.Start(ctx, ipn.Options{
|
||||
AuthKey: authKey,
|
||||
UpdatePrefs: prefs,
|
||||
})
|
||||
localAPIMu.Unlock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if upArgs.forceReauth {
|
||||
startLoginInteractive()
|
||||
}
|
||||
}
|
||||
|
||||
// This whole 'up' mechanism is too complicated and results in
|
||||
// hairy stuff like this select. We're ultimately waiting for
|
||||
// 'running' to be done, but even in the case where
|
||||
@@ -647,7 +673,7 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
|
||||
default:
|
||||
}
|
||||
return watchCtx.Err()
|
||||
case err := <-pumpErr:
|
||||
case err := <-watchErr:
|
||||
select {
|
||||
case <-running:
|
||||
return nil
|
||||
@@ -724,12 +750,12 @@ func init() {
|
||||
addPrefFlagMapping("accept-dns", "CorpDNS")
|
||||
addPrefFlagMapping("accept-routes", "RouteAll")
|
||||
addPrefFlagMapping("advertise-tags", "AdvertiseTags")
|
||||
addPrefFlagMapping("host-routes", "AllowSingleHosts")
|
||||
addPrefFlagMapping("hostname", "Hostname")
|
||||
addPrefFlagMapping("login-server", "ControlURL")
|
||||
addPrefFlagMapping("netfilter-mode", "NetfilterMode")
|
||||
addPrefFlagMapping("shields-up", "ShieldsUp")
|
||||
addPrefFlagMapping("snat-subnet-routes", "NoSNAT")
|
||||
addPrefFlagMapping("stateful-filtering", "NoStatefulFiltering")
|
||||
addPrefFlagMapping("exit-node-allow-lan-access", "ExitNodeAllowLANAccess")
|
||||
addPrefFlagMapping("unattended", "ForceDaemon")
|
||||
addPrefFlagMapping("operator", "OperatorUser")
|
||||
@@ -762,7 +788,7 @@ func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||
// correspond to an ipn.Pref.
|
||||
func preflessFlag(flagName string) bool {
|
||||
switch flagName {
|
||||
case "auth-key", "force-reauth", "reset", "qr", "json", "timeout", "accept-risk":
|
||||
case "auth-key", "force-reauth", "reset", "qr", "json", "timeout", "accept-risk", "host-routes":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -859,11 +885,26 @@ func checkForAccidentalSettingReverts(newPrefs, curPrefs *ipn.Prefs, env upCheck
|
||||
// Issue 6811. Ignore on Synology.
|
||||
continue
|
||||
}
|
||||
if flagName == "stateful-filtering" && valCur == true && valNew == false && env.goos == "linux" {
|
||||
// See https://github.com/tailscale/tailscale/issues/12307
|
||||
// Stateful filtering was on by default in tailscale 1.66.0-1.66.3, then off in 1.66.4.
|
||||
// This broke Tailscale installations in containerized
|
||||
// environments that use the default containerboot
|
||||
// configuration that configures tailscale using
|
||||
// 'tailscale up' command, which requires that all
|
||||
// previously set flags are explicitly provided on
|
||||
// subsequent restarts.
|
||||
continue
|
||||
}
|
||||
missing = append(missing, fmtFlagValueArg(flagName, valCur))
|
||||
}
|
||||
if len(missing) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Some previously provided flags are missing. This run of 'tailscale
|
||||
// up' will error out.
|
||||
|
||||
sort.Strings(missing)
|
||||
|
||||
// Compute the stringification of the explicitly provided args in flagSet
|
||||
@@ -914,7 +955,7 @@ func applyImplicitPrefs(prefs, oldPrefs *ipn.Prefs, env upCheckEnv) {
|
||||
|
||||
func flagAppliesToOS(flag, goos string) bool {
|
||||
switch flag {
|
||||
case "netfilter-mode", "snat-subnet-routes":
|
||||
case "netfilter-mode", "snat-subnet-routes", "stateful-filtering":
|
||||
return goos == "linux"
|
||||
case "unattended":
|
||||
return goos == "windows"
|
||||
@@ -958,8 +999,6 @@ func prefsToFlags(env upCheckEnv, prefs *ipn.Prefs) (flagVal map[string]any) {
|
||||
set(prefs.ControlURL)
|
||||
case "accept-routes":
|
||||
set(prefs.RouteAll)
|
||||
case "host-routes":
|
||||
set(prefs.AllowSingleHosts)
|
||||
case "accept-dns":
|
||||
set(prefs.CorpDNS)
|
||||
case "shields-up":
|
||||
@@ -989,6 +1028,16 @@ func prefsToFlags(env upCheckEnv, prefs *ipn.Prefs) (flagVal map[string]any) {
|
||||
set(prefs.AppConnector.Advertise)
|
||||
case "snat-subnet-routes":
|
||||
set(!prefs.NoSNAT)
|
||||
case "stateful-filtering":
|
||||
// We only set the stateful-filtering flag to false if
|
||||
// the pref (negated!) is explicitly set to true; unset
|
||||
// or false is treated as enabled.
|
||||
val, ok := prefs.NoStatefulFiltering.Get()
|
||||
if ok && val {
|
||||
set(false)
|
||||
} else {
|
||||
set(true)
|
||||
}
|
||||
case "netfilter-mode":
|
||||
set(prefs.NetfilterMode.String())
|
||||
case "unattended":
|
||||
|
||||
@@ -299,7 +299,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
regexp from github.com/coreos/go-iptables/iptables+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from nhooyr.io/websocket/internal/xsync+
|
||||
runtime/trace from testing
|
||||
slices from tailscale.com/client/web+
|
||||
sort from archive/tar+
|
||||
strconv from archive/tar+
|
||||
@@ -307,7 +306,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
sync from archive/tar+
|
||||
sync/atomic from context+
|
||||
syscall from archive/tar+
|
||||
testing from tailscale.com/util/syspolicy
|
||||
text/tabwriter from github.com/peterbourgon/ff/v3/ffcli+
|
||||
text/template from html/template
|
||||
text/template/parse from html/template+
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
func TestDeps(t *testing.T) {
|
||||
deptest.DepChecker{
|
||||
BadDeps: map[string]string{
|
||||
"testing": "do not use testing package in production code",
|
||||
"gvisor.dev/gvisor/pkg/buffer": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/cpuid": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/tcpip": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
|
||||
@@ -89,7 +89,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
LW 💣 github.com/digitalocean/go-smbios/smbios from tailscale.com/posture
|
||||
💣 github.com/djherbis/times from tailscale.com/drive/driveimpl
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/gaissmai/bart from tailscale.com/net/tstun
|
||||
github.com/gaissmai/bart from tailscale.com/net/tstun+
|
||||
github.com/go-json-experiment/json/internal from github.com/go-json-experiment/json/internal/jsonflags+
|
||||
github.com/go-json-experiment/json/internal/jsonflags from github.com/go-json-experiment/json/internal/jsonopts+
|
||||
github.com/go-json-experiment/json/internal/jsonopts from github.com/go-json-experiment/json/jsontext
|
||||
@@ -144,6 +144,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/pierrec/lz4/v4/internal/xxh32 from github.com/pierrec/lz4/v4/internal/lz4stream
|
||||
LD github.com/pkg/sftp from tailscale.com/ssh/tailssh
|
||||
LD github.com/pkg/sftp/internal/encoding/ssh/filexfer from github.com/pkg/sftp
|
||||
D github.com/prometheus-community/pro-bing from tailscale.com/wgengine/netstack
|
||||
L 💣 github.com/safchain/ethtool from tailscale.com/net/netkernelconf+
|
||||
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
|
||||
W 💣 github.com/tailscale/go-winio from tailscale.com/safesocket
|
||||
@@ -319,6 +320,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/clientupdate/distsign+
|
||||
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
|
||||
tailscale.com/omit from tailscale.com/ipn/conffile
|
||||
tailscale.com/paths from tailscale.com/client/tailscale+
|
||||
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/posture from tailscale.com/ipn/ipnlocal
|
||||
@@ -439,7 +441,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/net/http2 from golang.org/x/net/http2/h2c+
|
||||
golang.org/x/net/http2/h2c from tailscale.com/ipn/ipnlocal
|
||||
golang.org/x/net/http2/hpack from golang.org/x/net/http2+
|
||||
golang.org/x/net/icmp from tailscale.com/net/ping
|
||||
golang.org/x/net/icmp from tailscale.com/net/ping+
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/ipv4 from github.com/miekg/dns+
|
||||
golang.org/x/net/ipv6 from github.com/miekg/dns+
|
||||
@@ -552,7 +554,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from github.com/aws/aws-sdk-go-v2/internal/sync/singleflight+
|
||||
runtime/pprof from net/http/pprof+
|
||||
runtime/trace from net/http/pprof+
|
||||
runtime/trace from net/http/pprof
|
||||
slices from tailscale.com/appc+
|
||||
sort from archive/tar+
|
||||
strconv from archive/tar+
|
||||
@@ -560,7 +562,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
sync from archive/tar+
|
||||
sync/atomic from context+
|
||||
syscall from archive/tar+
|
||||
testing from tailscale.com/util/syspolicy
|
||||
text/tabwriter from runtime/pprof
|
||||
text/template from html/template
|
||||
text/template/parse from html/template+
|
||||
|
||||
@@ -35,6 +35,7 @@ import (
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/drive/driveimpl"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/conffile"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
@@ -117,7 +118,7 @@ var args struct {
|
||||
tunname string
|
||||
|
||||
cleanUp bool
|
||||
confFile string
|
||||
confFile string // empty, file path, or "vm:user-data"
|
||||
debug string
|
||||
port uint16
|
||||
statepath string
|
||||
@@ -168,7 +169,7 @@ func main() {
|
||||
flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket")
|
||||
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
|
||||
flag.BoolVar(&args.disableLogs, "no-logs-no-support", false, "disable log uploads; this also disables any technical support")
|
||||
flag.StringVar(&args.confFile, "config", "", "path to config file")
|
||||
flag.StringVar(&args.confFile, "config", "", "path to config file, or 'vm:user-data' to use the VM's user-data (EC2)")
|
||||
|
||||
if len(os.Args) > 0 && filepath.Base(os.Args[0]) == "tailscale" && beCLI != nil {
|
||||
beCLI()
|
||||
@@ -480,6 +481,15 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID,
|
||||
lb, err := getLocalBackend(ctx, logf, logID, sys)
|
||||
if err == nil {
|
||||
logf("got LocalBackend in %v", time.Since(t0).Round(time.Millisecond))
|
||||
if lb.Prefs().Valid() {
|
||||
if err := lb.Start(ipn.Options{}); err != nil {
|
||||
logf("LocalBackend.Start: %v", err)
|
||||
lb.Shutdown()
|
||||
lbErr.Store(err)
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
srv.SetLocalBackend(lb)
|
||||
close(wgEngineCreated)
|
||||
return
|
||||
@@ -538,14 +548,25 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
|
||||
return ok
|
||||
}
|
||||
dialer.NetstackDialTCP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) {
|
||||
// Note: don't just return ns.DialContextTCP or we'll
|
||||
// return an interface containing a nil pointer.
|
||||
// Note: don't just return ns.DialContextTCP or we'll return
|
||||
// *gonet.TCPConn(nil) instead of a nil interface which trips up
|
||||
// callers.
|
||||
tcpConn, err := ns.DialContextTCP(ctx, dst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tcpConn, nil
|
||||
}
|
||||
dialer.NetstackDialUDP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) {
|
||||
// Note: don't just return ns.DialContextUDP or we'll return
|
||||
// *gonet.UDPConn(nil) instead of a nil interface which trips up
|
||||
// callers.
|
||||
udpConn, err := ns.DialContextUDP(ctx, dst)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return udpConn, nil
|
||||
}
|
||||
}
|
||||
if socksListener != nil || httpProxyListener != nil {
|
||||
var addrs []string
|
||||
|
||||
@@ -20,6 +20,7 @@ func TestDeps(t *testing.T) {
|
||||
GOOS: "darwin",
|
||||
GOARCH: "arm64",
|
||||
BadDeps: map[string]string{
|
||||
"testing": "do not use testing package in production code",
|
||||
"gvisor.dev/gvisor/pkg/hostarch": "will crash on non-4K page sizes; see https://github.com/tailscale/tailscale/issues/8658",
|
||||
},
|
||||
}.Check(t)
|
||||
@@ -28,6 +29,7 @@ func TestDeps(t *testing.T) {
|
||||
GOOS: "linux",
|
||||
GOARCH: "arm64",
|
||||
BadDeps: map[string]string{
|
||||
"testing": "do not use testing package in production code",
|
||||
"gvisor.dev/gvisor/pkg/hostarch": "will crash on non-4K page sizes; see https://github.com/tailscale/tailscale/issues/8658",
|
||||
},
|
||||
}.Check(t)
|
||||
|
||||
@@ -298,11 +298,10 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
|
||||
go func() {
|
||||
err := i.lb.Start(ipn.Options{
|
||||
UpdatePrefs: &ipn.Prefs{
|
||||
ControlURL: i.controlURL,
|
||||
RouteAll: false,
|
||||
AllowSingleHosts: true,
|
||||
WantRunning: true,
|
||||
Hostname: i.hostname,
|
||||
ControlURL: i.controlURL,
|
||||
RouteAll: false,
|
||||
WantRunning: true,
|
||||
Hostname: i.hostname,
|
||||
},
|
||||
AuthKey: i.authKey,
|
||||
})
|
||||
|
||||
@@ -40,7 +40,6 @@ import (
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/lazy"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/must"
|
||||
@@ -95,8 +94,8 @@ func main() {
|
||||
ts := &tsnet.Server{
|
||||
Hostname: "idp",
|
||||
}
|
||||
if !*flagVerbose {
|
||||
ts.Logf = logger.Discard
|
||||
if *flagVerbose {
|
||||
ts.Logf = log.Printf
|
||||
}
|
||||
st, err = ts.Up(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -26,9 +26,8 @@ import (
|
||||
|
||||
type LoginGoal struct {
|
||||
_ structs.Incomparable
|
||||
token *tailcfg.Oauth2Token // oauth token to use when logging in
|
||||
flags LoginFlags // flags to use when logging in
|
||||
url string // auth url that needs to be visited
|
||||
flags LoginFlags // flags to use when logging in
|
||||
url string // auth url that needs to be visited
|
||||
}
|
||||
|
||||
var _ Client = (*Auto)(nil)
|
||||
@@ -338,7 +337,7 @@ func (c *Auto) authRoutine() {
|
||||
url, err = c.direct.WaitLoginURL(ctx, goal.url)
|
||||
f = "WaitLoginURL"
|
||||
} else {
|
||||
url, err = c.direct.TryLogin(ctx, goal.token, goal.flags)
|
||||
url, err = c.direct.TryLogin(ctx, goal.flags)
|
||||
f = "TryLogin"
|
||||
}
|
||||
if err != nil {
|
||||
@@ -348,9 +347,14 @@ func (c *Auto) authRoutine() {
|
||||
continue
|
||||
}
|
||||
if url != "" {
|
||||
// goal.url ought to be empty here.
|
||||
// However, not all control servers get this right,
|
||||
// and logging about it here just generates noise.
|
||||
// goal.url ought to be empty here. However, not all control servers
|
||||
// get this right, and logging about it here just generates noise.
|
||||
//
|
||||
// TODO(bradfitz): I don't follow that comment. Our own testcontrol
|
||||
// used by tstest/integration hits this path, in fact.
|
||||
if c.direct.panicOnUse {
|
||||
panic("tainted client")
|
||||
}
|
||||
c.mu.Lock()
|
||||
c.urlToVisit = url
|
||||
c.loginGoal = &LoginGoal{
|
||||
@@ -607,17 +611,19 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Auto) Login(t *tailcfg.Oauth2Token, flags LoginFlags) {
|
||||
c.logf("client.Login(%v, %v)", t != nil, flags)
|
||||
func (c *Auto) Login(flags LoginFlags) {
|
||||
c.logf("client.Login(%v)", flags)
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.closed {
|
||||
return
|
||||
}
|
||||
if c.direct != nil && c.direct.panicOnUse {
|
||||
panic("tainted client")
|
||||
}
|
||||
c.wantLoggedIn = true
|
||||
c.loginGoal = &LoginGoal{
|
||||
token: t,
|
||||
flags: flags,
|
||||
}
|
||||
c.cancelMapCtxLocked()
|
||||
@@ -632,6 +638,9 @@ func (c *Auto) Logout(ctx context.Context) error {
|
||||
c.wantLoggedIn = false
|
||||
c.loginGoal = nil
|
||||
closed := c.closed
|
||||
if c.direct != nil && c.direct.panicOnUse {
|
||||
panic("tainted client")
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
if closed {
|
||||
@@ -668,37 +677,35 @@ func (c *Auto) UpdateEndpoints(endpoints []tailcfg.Endpoint) {
|
||||
}
|
||||
|
||||
func (c *Auto) Shutdown() {
|
||||
c.logf("client.Shutdown()")
|
||||
|
||||
c.mu.Lock()
|
||||
closed := c.closed
|
||||
direct := c.direct
|
||||
if !closed {
|
||||
c.closed = true
|
||||
c.observerQueue.Shutdown()
|
||||
c.cancelAuthCtxLocked()
|
||||
c.cancelMapCtxLocked()
|
||||
for _, w := range c.unpauseWaiters {
|
||||
w <- false
|
||||
}
|
||||
c.unpauseWaiters = nil
|
||||
if c.closed {
|
||||
c.mu.Unlock()
|
||||
return
|
||||
}
|
||||
c.logf("client.Shutdown ...")
|
||||
|
||||
direct := c.direct
|
||||
c.closed = true
|
||||
c.observerQueue.Shutdown()
|
||||
c.cancelAuthCtxLocked()
|
||||
c.cancelMapCtxLocked()
|
||||
for _, w := range c.unpauseWaiters {
|
||||
w <- false
|
||||
}
|
||||
c.unpauseWaiters = nil
|
||||
c.mu.Unlock()
|
||||
|
||||
c.logf("client.Shutdown")
|
||||
if !closed {
|
||||
c.unregisterHealthWatch()
|
||||
<-c.authDone
|
||||
<-c.mapDone
|
||||
<-c.updateDone
|
||||
if direct != nil {
|
||||
direct.Close()
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
c.observerQueue.Wait(ctx)
|
||||
c.logf("Client.Shutdown done.")
|
||||
c.unregisterHealthWatch()
|
||||
<-c.authDone
|
||||
<-c.mapDone
|
||||
<-c.updateDone
|
||||
if direct != nil {
|
||||
direct.Close()
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
c.observerQueue.Wait(ctx)
|
||||
c.logf("Client.Shutdown done.")
|
||||
}
|
||||
|
||||
// NodePublicKey returns the node public key currently in use. This is
|
||||
|
||||
@@ -43,8 +43,9 @@ type Client interface {
|
||||
// Login begins an interactive or non-interactive login process.
|
||||
// Client will eventually call the Status callback with either a
|
||||
// LoginFinished flag (on success) or an auth URL (if further
|
||||
// interaction is needed).
|
||||
Login(*tailcfg.Oauth2Token, LoginFlags)
|
||||
// interaction is needed). It merely sets the process in motion,
|
||||
// and doesn't wait for it to complete.
|
||||
Login(LoginFlags)
|
||||
// Logout starts a synchronous logout process. It doesn't return
|
||||
// until the logout operation has been completed.
|
||||
Logout(context.Context) error
|
||||
|
||||
@@ -80,6 +80,7 @@ type Direct struct {
|
||||
onClientVersion func(*tailcfg.ClientVersion) // or nil
|
||||
onControlTime func(time.Time) // or nil
|
||||
onTailnetDefaultAutoUpdate func(bool) // or nil
|
||||
panicOnUse bool // if true, panic if client is used (for testing)
|
||||
|
||||
dialPlan ControlDialPlanner // can be nil
|
||||
|
||||
@@ -310,6 +311,9 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
}
|
||||
c.serverNoiseKey = key.NewMachine().Public() // prevent early error before hitting test client
|
||||
}
|
||||
if strings.Contains(opts.ServerURL, "controlplane.tailscale.com") && envknob.Bool("TS_PANIC_IF_HIT_MAIN_CONTROL") {
|
||||
c.panicOnUse = true
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@@ -397,9 +401,12 @@ func (c *Direct) TryLogout(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Direct) TryLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags LoginFlags) (url string, err error) {
|
||||
c.logf("[v1] direct.TryLogin(token=%v, flags=%v)", t != nil, flags)
|
||||
return c.doLoginOrRegen(ctx, loginOpt{Token: t, Flags: flags})
|
||||
func (c *Direct) TryLogin(ctx context.Context, flags LoginFlags) (url string, err error) {
|
||||
if strings.Contains(c.serverURL, "controlplane.tailscale.com") && envknob.Bool("TS_PANIC_IF_HIT_MAIN_CONTROL") {
|
||||
panic(fmt.Sprintf("[unexpected] controlclient: TryLogin called on %s; tainted=%v", c.serverURL, c.panicOnUse))
|
||||
}
|
||||
c.logf("[v1] direct.TryLogin(flags=%v)", flags)
|
||||
return c.doLoginOrRegen(ctx, loginOpt{Flags: flags})
|
||||
}
|
||||
|
||||
// WaitLoginURL sits in a long poll waiting for the user to authenticate at url.
|
||||
@@ -434,7 +441,6 @@ func (c *Direct) SetExpirySooner(ctx context.Context, expiry time.Time) error {
|
||||
}
|
||||
|
||||
type loginOpt struct {
|
||||
Token *tailcfg.Oauth2Token
|
||||
Flags LoginFlags
|
||||
Regen bool // generate a new nodekey, can be overridden in doLogin
|
||||
URL string
|
||||
@@ -459,6 +465,9 @@ func (c *Direct) hostInfoLocked() *tailcfg.Hostinfo {
|
||||
}
|
||||
|
||||
func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, newURL string, nks tkatype.MarshaledSignature, err error) {
|
||||
if c.panicOnUse {
|
||||
panic("tainted client")
|
||||
}
|
||||
c.mu.Lock()
|
||||
persist := c.persist.AsStruct()
|
||||
tryingNewKey := c.tryingNewKey
|
||||
@@ -549,7 +558,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
|
||||
var nodeKeySignature tkatype.MarshaledSignature
|
||||
if !oldNodeKey.IsZero() && opt.OldNodeKeySignature != nil {
|
||||
if nodeKeySignature, err = resignNKS(persist.NetworkLockKey, tryingNewKey.Public(), opt.OldNodeKeySignature); err != nil {
|
||||
if nodeKeySignature, err = tka.ResignNKS(persist.NetworkLockKey, tryingNewKey.Public(), opt.OldNodeKeySignature); err != nil {
|
||||
c.logf("Failed re-signing node-key signature: %v", err)
|
||||
}
|
||||
} else if isWrapped {
|
||||
@@ -600,10 +609,9 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
c.logf("RegisterReq: onode=%v node=%v fup=%v nks=%v",
|
||||
request.OldNodeKey.ShortString(),
|
||||
request.NodeKey.ShortString(), opt.URL != "", len(nodeKeySignature) > 0)
|
||||
if opt.Token != nil || authKey != "" {
|
||||
if authKey != "" {
|
||||
request.Auth = &tailcfg.RegisterResponseAuth{
|
||||
Oauth2Token: opt.Token,
|
||||
AuthKey: authKey,
|
||||
AuthKey: authKey,
|
||||
}
|
||||
}
|
||||
err = signRegisterRequest(&request, c.serverURL, c.serverLegacyKey, machinePrivKey.Public())
|
||||
@@ -721,45 +729,6 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
return false, resp.AuthURL, nil, nil
|
||||
}
|
||||
|
||||
// resignNKS re-signs a node-key signature for a new node-key.
|
||||
//
|
||||
// This only matters on network-locked tailnets, because node-key signatures are
|
||||
// how other nodes know that a node-key is authentic. When the node-key is
|
||||
// rotated then the existing signature becomes invalid, so this function is
|
||||
// responsible for generating a new wrapping signature to certify the new node-key.
|
||||
//
|
||||
// The signature itself is a SigRotation signature, which embeds the old signature
|
||||
// and certifies the new node-key as a replacement for the old by signing the new
|
||||
// signature with RotationPubkey (which is the node's own network-lock key).
|
||||
func resignNKS(priv key.NLPrivate, nodeKey key.NodePublic, oldNKS tkatype.MarshaledSignature) (tkatype.MarshaledSignature, error) {
|
||||
var oldSig tka.NodeKeySignature
|
||||
if err := oldSig.Unserialize(oldNKS); err != nil {
|
||||
return nil, fmt.Errorf("decoding NKS: %w", err)
|
||||
}
|
||||
|
||||
nk, err := nodeKey.MarshalBinary()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshalling node-key: %w", err)
|
||||
}
|
||||
|
||||
if bytes.Equal(nk, oldSig.Pubkey) {
|
||||
// The old signature is valid for the node-key we are using, so just
|
||||
// use it verbatim.
|
||||
return oldNKS, nil
|
||||
}
|
||||
|
||||
newSig := tka.NodeKeySignature{
|
||||
SigKind: tka.SigRotation,
|
||||
Pubkey: nk,
|
||||
Nested: &oldSig,
|
||||
}
|
||||
if newSig.Signature, err = priv.SignNKS(newSig.SigHash()); err != nil {
|
||||
return nil, fmt.Errorf("signing NKS: %w", err)
|
||||
}
|
||||
|
||||
return newSig.Serialize(), nil
|
||||
}
|
||||
|
||||
// newEndpoints acquires c.mu and sets the local port and endpoints and reports
|
||||
// whether they've changed.
|
||||
//
|
||||
@@ -832,6 +801,9 @@ const watchdogTimeout = 120 * time.Second
|
||||
//
|
||||
// If nu is nil, OmitPeers will be set to true.
|
||||
func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu NetmapUpdater) error {
|
||||
if c.panicOnUse {
|
||||
panic("tainted client")
|
||||
}
|
||||
if isStreaming && nu == nil {
|
||||
panic("cb must be non-nil if isStreaming is true")
|
||||
}
|
||||
@@ -1528,6 +1500,9 @@ func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) (err er
|
||||
}
|
||||
|
||||
func (c *Direct) DoNoiseRequest(req *http.Request) (*http.Response, error) {
|
||||
if c.panicOnUse {
|
||||
panic("tainted client")
|
||||
}
|
||||
nc, err := c.getNoiseClient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1624,6 +1599,9 @@ func (c *Direct) ReportHealthChange(sys health.Subsystem, sysErr error) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if c.panicOnUse {
|
||||
panic("tainted client")
|
||||
}
|
||||
req := &tailcfg.HealthChangeRequest{
|
||||
Subsys: string(sys),
|
||||
NodeKey: nodeKey,
|
||||
|
||||
@@ -736,6 +736,10 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
|
||||
if was.IsWireGuardOnly() != n.IsWireGuardOnly {
|
||||
return nil, false
|
||||
}
|
||||
case "IsJailed":
|
||||
if was.IsJailed() != n.IsJailed {
|
||||
return nil, false
|
||||
}
|
||||
case "Expired":
|
||||
if was.Expired() != n.Expired {
|
||||
return nil, false
|
||||
|
||||
@@ -76,6 +76,11 @@ type Knobs struct {
|
||||
// AppCStoreRoutes is whether the node should store RouteInfo to StateStore
|
||||
// if it's an app connector.
|
||||
AppCStoreRoutes atomic.Bool
|
||||
|
||||
// UserDialUseRoutes is whether tsdial.Dialer.UserDial should use routes to determine
|
||||
// how to dial the destination address. When true, it also makes the DNS forwarder
|
||||
// use UserDial instead of SystemDial when dialing resolvers.
|
||||
UserDialUseRoutes atomic.Bool
|
||||
}
|
||||
|
||||
// UpdateFromNodeAttributes updates k (if non-nil) based on the provided self
|
||||
@@ -101,6 +106,7 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
|
||||
seamlessKeyRenewal = has(tailcfg.NodeAttrSeamlessKeyRenewal)
|
||||
probeUDPLifetime = has(tailcfg.NodeAttrProbeUDPLifetime)
|
||||
appCStoreRoutes = has(tailcfg.NodeAttrStoreAppCRoutes)
|
||||
userDialUseRoutes = has(tailcfg.NodeAttrUserDialUseRoutes)
|
||||
)
|
||||
|
||||
if has(tailcfg.NodeAttrOneCGNATEnable) {
|
||||
@@ -124,6 +130,7 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
|
||||
k.SeamlessKeyRenewal.Store(seamlessKeyRenewal)
|
||||
k.ProbeUDPLifetime.Store(probeUDPLifetime)
|
||||
k.AppCStoreRoutes.Store(appCStoreRoutes)
|
||||
k.UserDialUseRoutes.Store(userDialUseRoutes)
|
||||
}
|
||||
|
||||
// AsDebugJSON returns k as something that can be marshalled with json.Marshal
|
||||
@@ -148,5 +155,6 @@ func (k *Knobs) AsDebugJSON() map[string]any {
|
||||
"SeamlessKeyRenewal": k.SeamlessKeyRenewal.Load(),
|
||||
"ProbeUDPLifetime": k.ProbeUDPLifetime.Load(),
|
||||
"AppCStoreRoutes": k.AppCStoreRoutes.Load(),
|
||||
"UserDialUseRoutes": k.UserDialUseRoutes.Load(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,20 +329,36 @@ func NewServer(privateKey key.NodePrivate, logf logger.Logf) *Server {
|
||||
s.initMetacert()
|
||||
s.packetsRecvDisco = s.packetsRecvByKind.Get("disco")
|
||||
s.packetsRecvOther = s.packetsRecvByKind.Get("other")
|
||||
s.packetsDroppedReasonCounters = []*expvar.Int{
|
||||
s.packetsDroppedReason.Get("unknown_dest"),
|
||||
s.packetsDroppedReason.Get("unknown_dest_on_fwd"),
|
||||
s.packetsDroppedReason.Get("gone_disconnected"),
|
||||
s.packetsDroppedReason.Get("gone_not_here"),
|
||||
s.packetsDroppedReason.Get("queue_head"),
|
||||
s.packetsDroppedReason.Get("queue_tail"),
|
||||
s.packetsDroppedReason.Get("write_error"),
|
||||
}
|
||||
|
||||
s.packetsDroppedReasonCounters = s.genPacketsDroppedReasonCounters()
|
||||
|
||||
s.packetsDroppedTypeDisco = s.packetsDroppedType.Get("disco")
|
||||
s.packetsDroppedTypeOther = s.packetsDroppedType.Get("other")
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Server) genPacketsDroppedReasonCounters() []*expvar.Int {
|
||||
getMetric := s.packetsDroppedReason.Get
|
||||
ret := []*expvar.Int{
|
||||
dropReasonUnknownDest: getMetric("unknown_dest"),
|
||||
dropReasonUnknownDestOnFwd: getMetric("unknown_dest_on_fwd"),
|
||||
dropReasonGoneDisconnected: getMetric("gone_disconnected"),
|
||||
dropReasonQueueHead: getMetric("queue_head"),
|
||||
dropReasonQueueTail: getMetric("queue_tail"),
|
||||
dropReasonWriteError: getMetric("write_error"),
|
||||
dropReasonDupClient: getMetric("dup_client"),
|
||||
}
|
||||
if len(ret) != int(numDropReasons) {
|
||||
panic("dropReason metrics out of sync")
|
||||
}
|
||||
for i := range numDropReasons {
|
||||
if ret[i] == nil {
|
||||
panic("dropReason metrics out of sync")
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// SetMesh sets the pre-shared key that regional DERP servers used to mesh
|
||||
// amongst themselves.
|
||||
//
|
||||
@@ -776,7 +792,6 @@ func (c *sclient) run(ctx context.Context) error {
|
||||
var grp errgroup.Group
|
||||
sendCtx, cancelSender := context.WithCancel(ctx)
|
||||
grp.Go(func() error { return c.sendLoop(sendCtx) })
|
||||
grp.Go(func() error { return c.statsLoop(sendCtx) })
|
||||
defer func() {
|
||||
cancelSender()
|
||||
if err := grp.Wait(); err != nil && !c.s.isClosed() {
|
||||
@@ -788,6 +803,8 @@ func (c *sclient) run(ctx context.Context) error {
|
||||
}
|
||||
}()
|
||||
|
||||
c.startStatsLoop(sendCtx)
|
||||
|
||||
for {
|
||||
ft, fl, err := readFrameHeader(c.br)
|
||||
c.debugLogf("read frame type %d len %d err %v", ft, fl, err)
|
||||
@@ -1046,6 +1063,7 @@ const (
|
||||
dropReasonQueueTail // destination queue is full, dropped packet at queue tail
|
||||
dropReasonWriteError // OS write() failed
|
||||
dropReasonDupClient // the public key is connected 2+ times (active/active, fighting)
|
||||
numDropReasons // unused; keep last
|
||||
)
|
||||
|
||||
func (s *Server) recordDrop(packetBytes []byte, srcKey, dstKey key.NodePublic, reason dropReason) {
|
||||
|
||||
@@ -7,6 +7,7 @@ package derp
|
||||
|
||||
import "context"
|
||||
|
||||
func (c *sclient) statsLoop(ctx context.Context) error {
|
||||
return nil
|
||||
func (c *sclient) startStatsLoop(ctx context.Context) {
|
||||
// Nothing to do
|
||||
return
|
||||
}
|
||||
|
||||
@@ -12,40 +12,43 @@ import (
|
||||
"tailscale.com/net/tcpinfo"
|
||||
)
|
||||
|
||||
func (c *sclient) statsLoop(ctx context.Context) error {
|
||||
func (c *sclient) startStatsLoop(ctx context.Context) {
|
||||
// Get the RTT initially to verify it's supported.
|
||||
conn := c.tcpConn()
|
||||
if conn == nil {
|
||||
c.s.tcpRtt.Add("non-tcp", 1)
|
||||
return nil
|
||||
return
|
||||
}
|
||||
if _, err := tcpinfo.RTT(conn); err != nil {
|
||||
c.logf("error fetching initial RTT: %v", err)
|
||||
c.s.tcpRtt.Add("error", 1)
|
||||
return nil
|
||||
return
|
||||
}
|
||||
|
||||
const statsInterval = 10 * time.Second
|
||||
|
||||
ticker, tickerChannel := c.s.clock.NewTicker(statsInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
statsLoop:
|
||||
for {
|
||||
select {
|
||||
case <-tickerChannel:
|
||||
rtt, err := tcpinfo.RTT(conn)
|
||||
if err != nil {
|
||||
continue statsLoop
|
||||
}
|
||||
|
||||
// TODO(andrew): more metrics?
|
||||
c.s.tcpRtt.Add(durationToLabel(rtt), 1)
|
||||
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
// Don't launch a goroutine; use a timer instead.
|
||||
var gatherStats func()
|
||||
gatherStats = func() {
|
||||
// Do nothing if the context is finished.
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Reschedule ourselves when this stats gathering is finished.
|
||||
defer c.s.clock.AfterFunc(statsInterval, gatherStats)
|
||||
|
||||
// Gather TCP RTT information.
|
||||
rtt, err := tcpinfo.RTT(conn)
|
||||
if err == nil {
|
||||
c.s.tcpRtt.Add(durationToLabel(rtt), 1)
|
||||
}
|
||||
|
||||
// TODO(andrew): more metrics?
|
||||
}
|
||||
|
||||
// Kick off the initial timer.
|
||||
c.s.clock.AfterFunc(statsInterval, gatherStats)
|
||||
}
|
||||
|
||||
// tcpConn attempts to get the underlying *net.TCPConn from this client's
|
||||
|
||||
@@ -136,8 +136,15 @@ func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, netMon *netmo
|
||||
|
||||
// NewNetcheckClient returns a Client that's only able to have its DialRegionTLS method called.
|
||||
// It's used by the netcheck package.
|
||||
func NewNetcheckClient(logf logger.Logf) *Client {
|
||||
return &Client{logf: logf, clock: tstime.StdClock{}}
|
||||
func NewNetcheckClient(logf logger.Logf, netMon *netmon.Monitor) *Client {
|
||||
if netMon == nil {
|
||||
panic("nil netMon")
|
||||
}
|
||||
return &Client{
|
||||
logf: logf,
|
||||
clock: tstime.StdClock{},
|
||||
netMon: netMon,
|
||||
}
|
||||
}
|
||||
|
||||
// NewClient returns a new DERP-over-HTTP client. It connects lazily.
|
||||
|
||||
@@ -20,6 +20,16 @@ const fastStartHeader = "Derp-Fast-Start"
|
||||
|
||||
func Handler(s *derp.Server) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// These are installed both here and in cmd/derper. The check here
|
||||
// catches both cmd/derper run with DERP disabled (STUN only mode) as
|
||||
// well as DERP being run in tests with derphttp.Handler directly,
|
||||
// as netcheck still assumes this replies.
|
||||
switch r.URL.Path {
|
||||
case "/derp/probe", "/derp/latency-check":
|
||||
ProbeHandler(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
up := strings.ToLower(r.Header.Get("Upgrade"))
|
||||
if up != "websocket" && up != "derp" {
|
||||
if up != "" {
|
||||
@@ -58,3 +68,14 @@ func Handler(s *derp.Server) http.Handler {
|
||||
s.Accept(r.Context(), netConn, conn, netConn.RemoteAddr().String())
|
||||
})
|
||||
}
|
||||
|
||||
// ProbeHandler is the endpoint that clients without UDP access (including js/wasm) hit to measure
|
||||
// DERP latency, as a replacement for UDP STUN queries.
|
||||
func ProbeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "HEAD", "GET":
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
default:
|
||||
http.Error(w, "bogus probe method", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -464,3 +465,24 @@ func TestLocalAddrNoMutex(t *testing.T) {
|
||||
t.Errorf("got error %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbe(t *testing.T) {
|
||||
h := Handler(nil)
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
want int
|
||||
}{
|
||||
{"/derp/probe", 200},
|
||||
{"/derp/latency-check", 200},
|
||||
{"/derp/sdf", http.StatusUpgradeRequired},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
rec := httptest.NewRecorder()
|
||||
h.ServeHTTP(rec, httptest.NewRequest("GET", tt.path, nil))
|
||||
if got := rec.Result().StatusCode; got != tt.want {
|
||||
t.Errorf("for path %q got HTTP status %v; want %v", tt.path, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,11 +18,12 @@ func _() {
|
||||
_ = x[dropReasonQueueTail-4]
|
||||
_ = x[dropReasonWriteError-5]
|
||||
_ = x[dropReasonDupClient-6]
|
||||
_ = x[numDropReasons-7]
|
||||
}
|
||||
|
||||
const _dropReason_name = "UnknownDestUnknownDestOnFwdGoneDisconnectedQueueHeadQueueTailWriteErrorDupClient"
|
||||
const _dropReason_name = "UnknownDestUnknownDestOnFwdGoneDisconnectedQueueHeadQueueTailWriteErrorDupClientnumDropReasons"
|
||||
|
||||
var _dropReason_index = [...]uint8{0, 11, 27, 43, 52, 61, 71, 80}
|
||||
var _dropReason_index = [...]uint8{0, 11, 27, 43, 52, 61, 71, 80, 94}
|
||||
|
||||
func (i dropReason) String() string {
|
||||
if i < 0 || i >= dropReason(len(_dropReason_index)-1) {
|
||||
|
||||
@@ -29,5 +29,6 @@ spec:
|
||||
- name: TS_ROUTES
|
||||
value: "{{TS_ROUTES}}"
|
||||
securityContext:
|
||||
runAsUser: 1000
|
||||
runAsGroup: 1000
|
||||
capabilities:
|
||||
add:
|
||||
- NET_ADMIN
|
||||
|
||||
@@ -81,24 +81,39 @@ type Handler struct {
|
||||
staticRoot string
|
||||
}
|
||||
|
||||
var cacheInvalidatingMethods = map[string]bool{
|
||||
"PUT": true,
|
||||
"POST": true,
|
||||
"COPY": true,
|
||||
"MKCOL": true,
|
||||
"MOVE": true,
|
||||
"PROPPATCH": true,
|
||||
"DELETE": true,
|
||||
}
|
||||
|
||||
// ServeHTTP implements http.Handler.
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "PROPFIND" {
|
||||
h.handlePROPFIND(w, r)
|
||||
pathComponents := shared.CleanAndSplit(r.URL.Path)
|
||||
mpl := h.maxPathLength(r)
|
||||
|
||||
switch r.Method {
|
||||
case "PROPFIND":
|
||||
h.handlePROPFIND(w, r, pathComponents, mpl)
|
||||
return
|
||||
case "LOCK":
|
||||
h.handleLOCK(w, r, pathComponents, mpl)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method != "GET" {
|
||||
// If the user is performing a modification (e.g. PUT, MKDIR, etc),
|
||||
_, shouldInvalidate := cacheInvalidatingMethods[r.Method]
|
||||
if shouldInvalidate {
|
||||
// If the user is performing a modification (e.g. PUT, MKDIR, etc.),
|
||||
// we need to invalidate the StatCache to make sure we're not knowingly
|
||||
// showing stale stats.
|
||||
// TODO(oxtoacart): maybe be more selective about invalidating cache
|
||||
// TODO(oxtoacart): maybe only invalidate specific paths
|
||||
h.StatCache.invalidate()
|
||||
}
|
||||
|
||||
mpl := h.maxPathLength(r)
|
||||
pathComponents := shared.CleanAndSplit(r.URL.Path)
|
||||
|
||||
if len(pathComponents) >= mpl {
|
||||
h.delegate(mpl, pathComponents[mpl-1:], w, r)
|
||||
return
|
||||
@@ -130,6 +145,8 @@ func (h *Handler) handle(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// delegate sends the request to the Child WebDAV server.
|
||||
func (h *Handler) delegate(mpl int, pathComponents []string, w http.ResponseWriter, r *http.Request) {
|
||||
rewriteIfHeader(r, pathComponents, mpl)
|
||||
|
||||
dest := r.Header.Get("Destination")
|
||||
if dest != "" {
|
||||
// Rewrite destination header
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package compositedav
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
)
|
||||
|
||||
var (
|
||||
hrefRegex = regexp.MustCompile(`(?s)<D:href>/?([^<]*)/?</D:href>`)
|
||||
)
|
||||
|
||||
func (h *Handler) handlePROPFIND(w http.ResponseWriter, r *http.Request) {
|
||||
pathComponents := shared.CleanAndSplit(r.URL.Path)
|
||||
mpl := h.maxPathLength(r)
|
||||
if !shared.IsRoot(r.URL.Path) && len(pathComponents)+getDepth(r) > mpl {
|
||||
// Delegate to a Child.
|
||||
depth := getDepth(r)
|
||||
|
||||
cached := h.StatCache.get(r.URL.Path, depth)
|
||||
if cached != nil {
|
||||
w.Header().Del("Content-Length")
|
||||
w.WriteHeader(http.StatusMultiStatus)
|
||||
w.Write(cached)
|
||||
return
|
||||
}
|
||||
|
||||
// Use a buffering ResponseWriter so that we can manipulate the result.
|
||||
// The only thing we use from the original ResponseWriter is Header().
|
||||
bw := &bufferingResponseWriter{ResponseWriter: w}
|
||||
|
||||
mpl := h.maxPathLength(r)
|
||||
h.delegate(mpl, pathComponents[mpl-1:], bw, r)
|
||||
|
||||
// Fixup paths to add the requested path as a prefix.
|
||||
pathPrefix := shared.Join(pathComponents[0:mpl]...)
|
||||
b := hrefRegex.ReplaceAll(bw.buf.Bytes(), []byte(fmt.Sprintf("<D:href>%s/$1</D:href>", pathPrefix)))
|
||||
|
||||
if h.StatCache != nil && bw.status == http.StatusMultiStatus && b != nil {
|
||||
h.StatCache.set(r.URL.Path, depth, b)
|
||||
}
|
||||
|
||||
w.Header().Del("Content-Length")
|
||||
w.WriteHeader(bw.status)
|
||||
w.Write(b)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
h.handle(w, r)
|
||||
}
|
||||
|
||||
func getDepth(r *http.Request) int {
|
||||
switch r.Header.Get("Depth") {
|
||||
case "0":
|
||||
return 0
|
||||
case "1":
|
||||
return 1
|
||||
case "infinity":
|
||||
return math.MaxInt
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type bufferingResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (bw *bufferingResponseWriter) WriteHeader(statusCode int) {
|
||||
bw.status = statusCode
|
||||
}
|
||||
|
||||
func (bw *bufferingResponseWriter) Write(p []byte) (int, error) {
|
||||
return bw.buf.Write(p)
|
||||
}
|
||||
122
drive/driveimpl/compositedav/rewriting.go
Normal file
122
drive/driveimpl/compositedav/rewriting.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package compositedav
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
)
|
||||
|
||||
var (
|
||||
responseHrefRegex = regexp.MustCompile(`(?s)(<D:(response|lockroot)>)<D:href>/?([^<]*)/?</D:href>`)
|
||||
ifHrefRegex = regexp.MustCompile(`^<(https?://[^/]+)?([^>]+)>`)
|
||||
)
|
||||
|
||||
func (h *Handler) handlePROPFIND(w http.ResponseWriter, r *http.Request, pathComponents []string, mpl int) {
|
||||
if shouldDelegateToChild(r, pathComponents, mpl) {
|
||||
// Delegate to a Child.
|
||||
depth := getDepth(r)
|
||||
|
||||
status, result := h.StatCache.getOr(r.URL.Path, depth, func() (int, []byte) {
|
||||
return h.delegateRewriting(w, r, pathComponents, mpl)
|
||||
})
|
||||
|
||||
respondRewritten(w, status, result)
|
||||
return
|
||||
}
|
||||
|
||||
h.handle(w, r)
|
||||
}
|
||||
|
||||
func (h *Handler) handleLOCK(w http.ResponseWriter, r *http.Request, pathComponents []string, mpl int) {
|
||||
if shouldDelegateToChild(r, pathComponents, mpl) {
|
||||
// Delegate to a Child.
|
||||
status, result := h.delegateRewriting(w, r, pathComponents, mpl)
|
||||
respondRewritten(w, status, result)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "locking of top level directories is not allowed", http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
// shouldDelegateToChild decides whether a request should be delegated to a
|
||||
// child filesystem, as opposed to being handled by this filesystem. It checks
|
||||
// the depth of the requested path, and if it's deeper than the portion of the
|
||||
// tree that's handled by the parent, returns true.
|
||||
func shouldDelegateToChild(r *http.Request, pathComponents []string, mpl int) bool {
|
||||
return !shared.IsRoot(r.URL.Path) && len(pathComponents)+getDepth(r) > mpl
|
||||
}
|
||||
|
||||
func (h *Handler) delegateRewriting(w http.ResponseWriter, r *http.Request, pathComponents []string, mpl int) (int, []byte) {
|
||||
// Use a buffering ResponseWriter so that we can manipulate the result.
|
||||
// The only thing we use from the original ResponseWriter is Header().
|
||||
bw := &bufferingResponseWriter{ResponseWriter: w}
|
||||
|
||||
h.delegate(mpl, pathComponents[mpl-1:], bw, r)
|
||||
|
||||
// Fixup paths to add the requested path as a prefix, escaped for inclusion in XML.
|
||||
pp := shared.EscapeForXML(shared.Join(pathComponents[0:mpl]...))
|
||||
b := responseHrefRegex.ReplaceAll(bw.buf.Bytes(), []byte(fmt.Sprintf("$1<D:href>%s/$3</D:href>", pp)))
|
||||
return bw.status, b
|
||||
}
|
||||
|
||||
func respondRewritten(w http.ResponseWriter, status int, result []byte) {
|
||||
w.Header().Del("Content-Length")
|
||||
w.WriteHeader(status)
|
||||
if result != nil {
|
||||
w.Write(result)
|
||||
}
|
||||
}
|
||||
|
||||
func getDepth(r *http.Request) int {
|
||||
switch r.Header.Get("Depth") {
|
||||
case "0":
|
||||
return 0
|
||||
case "1":
|
||||
return 1
|
||||
case "infinity":
|
||||
return math.MaxInt16 // a really large number, but not infinity (avoids wrapping when we do arithmetic with this)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type bufferingResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
func (bw *bufferingResponseWriter) WriteHeader(statusCode int) {
|
||||
bw.status = statusCode
|
||||
}
|
||||
|
||||
func (bw *bufferingResponseWriter) Write(p []byte) (int, error) {
|
||||
return bw.buf.Write(p)
|
||||
}
|
||||
|
||||
// rewriteIfHeader rewrites URLs in the If header by removing the host and the
|
||||
// portion of the path that corresponds to this composite filesystem. This way,
|
||||
// when we delegate requests to child filesystems, the If header will reference
|
||||
// a path that makes sense on those filesystems.
|
||||
//
|
||||
// See http://www.webdav.org/specs/rfc4918.html#HEADER_If
|
||||
func rewriteIfHeader(r *http.Request, pathComponents []string, mpl int) {
|
||||
ih := r.Header.Get("If")
|
||||
if ih == "" {
|
||||
return
|
||||
}
|
||||
matches := ifHrefRegex.FindStringSubmatch(ih)
|
||||
if len(matches) == 3 {
|
||||
pp := shared.JoinEscaped(pathComponents[0:mpl]...)
|
||||
p := strings.Replace(shared.JoinEscaped(pathComponents...), pp, "", 1)
|
||||
nih := ifHrefRegex.ReplaceAllString(ih, fmt.Sprintf("<%s>", p))
|
||||
r.Header.Set("If", nih)
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,19 @@
|
||||
package compositedav
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"log"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jellydator/ttlcache/v3"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
)
|
||||
|
||||
var (
|
||||
notFound = newCacheEntry(http.StatusNotFound, nil)
|
||||
)
|
||||
|
||||
// StatCache provides a cache for directory listings and file metadata.
|
||||
@@ -17,22 +26,97 @@ import (
|
||||
// This is similar to the DirectoryCacheLifetime setting of Windows' built-in
|
||||
// SMB client, see
|
||||
// https://learn.microsoft.com/en-us/previous-versions/windows/it-pro/windows-7/ff686200(v=ws.10)
|
||||
//
|
||||
// StatCache is built specifically to cache the results of PROPFIND requests,
|
||||
// which come back as MultiStatus XML responses. Typical clients will issue two
|
||||
// kinds of PROPFIND:
|
||||
//
|
||||
// The first kind of PROPFIND is a directory listing performed to depth 1. At
|
||||
// this depth, the resulting XML will contain stats for the requested folder as
|
||||
// well as for all children of that folder.
|
||||
//
|
||||
// The second kind of PROPFIND is a file listing performed to depth 0. At this
|
||||
// depth, the resulting XML will contain stats only for the requested file.
|
||||
//
|
||||
// In order to avoid round-trips, when a PROPFIND at depth 0 is attempted, and
|
||||
// the requested file is not in the cache, StatCache will check to see if the
|
||||
// parent folder of that file is cached. If so, StatCache infers the correct
|
||||
// MultiStatus for the file according to the following logic:
|
||||
//
|
||||
// 1. If the parent folder is NotFound (404), treat the file itself as NotFound
|
||||
// 2. If the parent folder's XML doesn't contain the file, treat it as
|
||||
// NotFound.
|
||||
// 3. If the parent folder's XML contains the file, build a MultiStatus for the
|
||||
// file based on the parent's XML.
|
||||
//
|
||||
// To avoid inconsistencies from the perspective of the client, any operations
|
||||
// that modify the filesystem (e.g. PUT, MKDIR, etc.) should call invalidate()
|
||||
// to invalidate the cache.
|
||||
type StatCache struct {
|
||||
TTL time.Duration
|
||||
|
||||
// mu guards the below values.
|
||||
mu sync.Mutex
|
||||
cachesByDepthAndPath map[int]*ttlcache.Cache[string, []byte]
|
||||
cachesByDepthAndPath map[int]*ttlcache.Cache[string, *cacheEntry]
|
||||
}
|
||||
|
||||
func (c *StatCache) get(name string, depth int) []byte {
|
||||
// getOr checks the cache for the named value at the given depth. If a cached
|
||||
// value was found, it returns http.StatusMultiStatus along with the cached
|
||||
// value. Otherwise, it executes the given function and returns the resulting
|
||||
// status and value. If the function returned http.StatusMultiStatus, getOr
|
||||
// caches the resulting value at the given name and depth before returning.
|
||||
func (c *StatCache) getOr(name string, depth int, or func() (int, []byte)) (int, []byte) {
|
||||
ce := c.get(name, depth)
|
||||
if ce == nil {
|
||||
// Not cached, fetch value.
|
||||
status, raw := or()
|
||||
ce = newCacheEntry(status, raw)
|
||||
if status == http.StatusMultiStatus || status == http.StatusNotFound {
|
||||
// Got a legit status, cache value
|
||||
c.set(name, depth, ce)
|
||||
}
|
||||
}
|
||||
return ce.Status, ce.Raw
|
||||
}
|
||||
|
||||
// get retrieves the entry for the named file at the given depth. If no entry
|
||||
// is found, and depth == 0, get will check to see if the parent path of name
|
||||
// is present in the cache at depth 1. If so, it will infer that the child does
|
||||
// not exist and return notFound (404).
|
||||
func (c *StatCache) get(name string, depth int) *cacheEntry {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
name = shared.Normalize(name)
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
ce := c.tryGetLocked(name, depth)
|
||||
if ce != nil {
|
||||
// Cache hit.
|
||||
return ce
|
||||
}
|
||||
|
||||
if depth > 0 {
|
||||
// Cache miss.
|
||||
return nil
|
||||
}
|
||||
|
||||
// At depth 0, if child's parent is in the cache, and the child isn't
|
||||
// cached, we can infer that the child is notFound.
|
||||
p := c.tryGetLocked(shared.Parent(name), 1)
|
||||
if p != nil {
|
||||
return notFound
|
||||
}
|
||||
|
||||
// No parent in cache, cache miss.
|
||||
return nil
|
||||
}
|
||||
|
||||
// tryGetLocked requires that c.mu be held.
|
||||
func (c *StatCache) tryGetLocked(name string, depth int) *cacheEntry {
|
||||
if c.cachesByDepthAndPath == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -47,28 +131,80 @@ func (c *StatCache) get(name string, depth int) []byte {
|
||||
return item.Value()
|
||||
}
|
||||
|
||||
func (c *StatCache) set(name string, depth int, value []byte) {
|
||||
// set stores the given cacheEntry in the cache at the given name and depth. If
|
||||
// the depth is 1, set also populates depth 0 entries in the cache for the bare
|
||||
// name. If status is StatusMultiStatus, set will parse the PROPFIND result and
|
||||
// store depth 0 entries for all children. If parsing the result fails, nothing
|
||||
// is cached.
|
||||
func (c *StatCache) set(name string, depth int, ce *cacheEntry) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
|
||||
name = shared.Normalize(name)
|
||||
|
||||
var self *cacheEntry
|
||||
var children map[string]*cacheEntry
|
||||
if depth == 1 {
|
||||
switch ce.Status {
|
||||
case http.StatusNotFound:
|
||||
// Record notFound as the self entry.
|
||||
self = ce
|
||||
case http.StatusMultiStatus:
|
||||
// Parse the raw MultiStatus and extract specific responses
|
||||
// corresponding to the self entry (e.g. the directory, but at depth 0)
|
||||
// and children (e.g. files within the directory) so that subsequent
|
||||
// requests for these can be satisfied from the cache.
|
||||
var ms multiStatus
|
||||
err := xml.Unmarshal(ce.Raw, &ms)
|
||||
if err != nil {
|
||||
// unparseable MultiStatus response, don't cache
|
||||
log.Printf("statcache.set error: %s", err)
|
||||
return
|
||||
}
|
||||
children = make(map[string]*cacheEntry, len(ms.Responses)-1)
|
||||
for i := 0; i < len(ms.Responses); i++ {
|
||||
response := ms.Responses[i]
|
||||
name := shared.Normalize(response.Href)
|
||||
raw := marshalMultiStatus(response)
|
||||
entry := newCacheEntry(ce.Status, raw)
|
||||
if i == 0 {
|
||||
self = entry
|
||||
} else {
|
||||
children[name] = entry
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.setLocked(name, depth, ce)
|
||||
if self != nil {
|
||||
c.setLocked(name, 0, self)
|
||||
}
|
||||
for childName, child := range children {
|
||||
c.setLocked(childName, 0, child)
|
||||
}
|
||||
}
|
||||
|
||||
// setLocked requires that c.mu be held.
|
||||
func (c *StatCache) setLocked(name string, depth int, ce *cacheEntry) {
|
||||
if c.cachesByDepthAndPath == nil {
|
||||
c.cachesByDepthAndPath = make(map[int]*ttlcache.Cache[string, []byte])
|
||||
c.cachesByDepthAndPath = make(map[int]*ttlcache.Cache[string, *cacheEntry])
|
||||
}
|
||||
cache := c.cachesByDepthAndPath[depth]
|
||||
if cache == nil {
|
||||
cache = ttlcache.New(
|
||||
ttlcache.WithTTL[string, []byte](c.TTL),
|
||||
ttlcache.WithTTL[string, *cacheEntry](c.TTL),
|
||||
)
|
||||
go cache.Start()
|
||||
c.cachesByDepthAndPath[depth] = cache
|
||||
}
|
||||
cache.Set(name, value, ttlcache.DefaultTTL)
|
||||
cache.Set(name, ce, ttlcache.DefaultTTL)
|
||||
}
|
||||
|
||||
// invalidate invalidates the entire cache.
|
||||
func (c *StatCache) invalidate() {
|
||||
if c == nil {
|
||||
return
|
||||
@@ -90,3 +226,54 @@ func (c *StatCache) stop() {
|
||||
cache.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
type cacheEntry struct {
|
||||
Status int
|
||||
Raw []byte
|
||||
}
|
||||
|
||||
func newCacheEntry(status int, raw []byte) *cacheEntry {
|
||||
return &cacheEntry{Status: status, Raw: raw}
|
||||
}
|
||||
|
||||
type propStat struct {
|
||||
InnerXML []byte `xml:",innerxml"`
|
||||
}
|
||||
|
||||
type response struct {
|
||||
XMLName xml.Name `xml:"response"`
|
||||
Href string `xml:"href"`
|
||||
PropStats []*propStat `xml:"propstat"`
|
||||
}
|
||||
|
||||
type multiStatus struct {
|
||||
XMLName xml.Name `xml:"multistatus"`
|
||||
Responses []*response `xml:"response"`
|
||||
}
|
||||
|
||||
// marshalMultiStatus performs custom marshalling of a MultiStatus to preserve
|
||||
// the original formatting, namespacing, etc. Doing this with Go's XML encoder
|
||||
// is somewhere between difficult and impossible, which is why we use this more
|
||||
// manual approach.
|
||||
func marshalMultiStatus(response *response) []byte {
|
||||
// TODO(percy): maybe pool these buffers
|
||||
var buf bytes.Buffer
|
||||
buf.WriteString(multistatusTemplateStart)
|
||||
buf.WriteString(response.Href)
|
||||
buf.WriteString(hrefEnd)
|
||||
for _, propStat := range response.PropStats {
|
||||
buf.WriteString(propstatStart)
|
||||
buf.Write(propStat.InnerXML)
|
||||
buf.WriteString(propstatEnd)
|
||||
}
|
||||
buf.WriteString(multistatusTemplateEnd)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
const (
|
||||
multistatusTemplateStart = `<?xml version="1.0" encoding="UTF-8"?><D:multistatus xmlns:D="DAV:"><D:response><D:href>`
|
||||
hrefEnd = `</D:href>`
|
||||
propstatStart = `<D:propstat>`
|
||||
propstatEnd = `</D:propstat>`
|
||||
multistatusTemplateEnd = `</D:response></D:multistatus>`
|
||||
)
|
||||
|
||||
@@ -4,17 +4,65 @@
|
||||
package compositedav
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
var (
|
||||
val = []byte("1")
|
||||
file = "file"
|
||||
)
|
||||
var parentPath = "/parent"
|
||||
|
||||
var childPath = "/parent/child.txt"
|
||||
|
||||
var parentResponse = `<D:response>
|
||||
<D:href>/parent/</D:href>
|
||||
<D:propstat>
|
||||
<D:prop>
|
||||
<D:getlastmodified>Mon, 29 Apr 2024 19:52:23 GMT</D:getlastmodified>
|
||||
<D:creationdate>Fri, 19 Apr 2024 04:13:34 GMT</D:creationdate>
|
||||
<D:resourcetype>
|
||||
<D:collection xmlns:D="DAV:" />
|
||||
</D:resourcetype>
|
||||
</D:prop>
|
||||
<D:status>HTTP/1.1 200 OK</D:status>
|
||||
</D:propstat>
|
||||
</D:response>`
|
||||
|
||||
var childResponse = `
|
||||
<D:response>
|
||||
<D:href>/parent/child.txt</D:href>
|
||||
<D:propstat>
|
||||
<D:prop>
|
||||
<D:getlastmodified>Mon, 29 Apr 2024 19:52:23 GMT</D:getlastmodified>
|
||||
<D:creationdate>Fri, 19 Apr 2024 04:13:34 GMT</D:creationdate>
|
||||
<D:resourcetype>
|
||||
<D:collection xmlns:D="DAV:" />
|
||||
</D:resourcetype>
|
||||
</D:prop>
|
||||
<D:status>HTTP/1.1 200 OK</D:status>
|
||||
</D:propstat>
|
||||
</D:response>`
|
||||
|
||||
var fullParent = []byte(
|
||||
strings.ReplaceAll(
|
||||
fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?><D:multistatus xmlns:D="DAV:">%s%s</D:multistatus>`, parentResponse, childResponse),
|
||||
"\n", ""))
|
||||
|
||||
var partialParent = []byte(
|
||||
strings.ReplaceAll(
|
||||
fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?><D:multistatus xmlns:D="DAV:">%s</D:multistatus>`, parentResponse),
|
||||
"\n", ""))
|
||||
|
||||
var fullChild = []byte(
|
||||
strings.ReplaceAll(
|
||||
fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?><D:multistatus xmlns:D="DAV:">%s</D:multistatus>`, childResponse),
|
||||
"\n", ""))
|
||||
|
||||
func TestStatCacheNoTimeout(t *testing.T) {
|
||||
// Make sure we don't leak goroutines
|
||||
@@ -24,22 +72,23 @@ func TestStatCacheNoTimeout(t *testing.T) {
|
||||
defer c.stop()
|
||||
|
||||
// check get before set
|
||||
fetched := c.get(file, 1)
|
||||
fetched := c.get(childPath, 0)
|
||||
if fetched != nil {
|
||||
t.Errorf("got %q, want nil", fetched)
|
||||
t.Errorf("got %v, want nil", fetched)
|
||||
}
|
||||
|
||||
// set new stat
|
||||
c.set(file, 1, val)
|
||||
fetched = c.get(file, 1)
|
||||
if !bytes.Equal(fetched, val) {
|
||||
t.Errorf("got %q, want %q", fetched, val)
|
||||
ce := newCacheEntry(http.StatusMultiStatus, fullChild)
|
||||
c.set(childPath, 0, ce)
|
||||
fetched = c.get(childPath, 0)
|
||||
if diff := cmp.Diff(fetched, ce); diff != "" {
|
||||
t.Errorf("should have gotten cached value; (-got+want):%v", diff)
|
||||
}
|
||||
|
||||
// fetch stat again, should still be cached
|
||||
fetched = c.get(file, 1)
|
||||
if !bytes.Equal(fetched, val) {
|
||||
t.Errorf("got %q, want %q", fetched, val)
|
||||
fetched = c.get(childPath, 0)
|
||||
if diff := cmp.Diff(fetched, ce); diff != "" {
|
||||
t.Errorf("should still have gotten cached value; (-got+want):%v", diff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,25 +100,114 @@ func TestStatCacheTimeout(t *testing.T) {
|
||||
defer c.stop()
|
||||
|
||||
// set new stat
|
||||
c.set(file, 1, val)
|
||||
fetched := c.get(file, 1)
|
||||
if !bytes.Equal(fetched, val) {
|
||||
t.Errorf("got %q, want %q", fetched, val)
|
||||
ce := newCacheEntry(http.StatusMultiStatus, fullChild)
|
||||
c.set(childPath, 0, ce)
|
||||
fetched := c.get(childPath, 0)
|
||||
if diff := cmp.Diff(fetched, ce); diff != "" {
|
||||
t.Errorf("should have gotten cached value; (-got+want):%v", diff)
|
||||
}
|
||||
|
||||
// wait for cache to expire and refetch stat, should be empty now
|
||||
time.Sleep(c.TTL * 2)
|
||||
|
||||
fetched = c.get(file, 1)
|
||||
fetched = c.get(childPath, 0)
|
||||
if fetched != nil {
|
||||
t.Errorf("invalidate should have cleared cached value")
|
||||
t.Errorf("cached value should have expired")
|
||||
}
|
||||
|
||||
c.set(file, 1, val)
|
||||
c.set(childPath, 0, ce)
|
||||
// invalidate the cache and make sure nothing is returned
|
||||
c.invalidate()
|
||||
fetched = c.get(file, 1)
|
||||
fetched = c.get(childPath, 0)
|
||||
if fetched != nil {
|
||||
t.Errorf("invalidate should have cleared cached value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParentChildRelationship(t *testing.T) {
|
||||
// Make sure we don't leak goroutines
|
||||
tstest.ResourceCheck(t)
|
||||
|
||||
c := &StatCache{TTL: 24 * time.Hour} // don't expire
|
||||
defer c.stop()
|
||||
|
||||
missingParentPath := "/missingparent"
|
||||
unparseableParentPath := "/unparseable"
|
||||
|
||||
c.set(parentPath, 1, newCacheEntry(http.StatusMultiStatus, fullParent))
|
||||
c.set(missingParentPath, 1, newCacheEntry(http.StatusNotFound, nil))
|
||||
c.set(unparseableParentPath, 1, newCacheEntry(http.StatusMultiStatus, []byte("<this will not parse")))
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
depth int
|
||||
want *cacheEntry
|
||||
}{
|
||||
{
|
||||
path: parentPath,
|
||||
depth: 1,
|
||||
want: newCacheEntry(http.StatusMultiStatus, fullParent),
|
||||
},
|
||||
{
|
||||
path: parentPath,
|
||||
depth: 0,
|
||||
want: newCacheEntry(http.StatusMultiStatus, partialParent),
|
||||
},
|
||||
{
|
||||
path: childPath,
|
||||
depth: 0,
|
||||
want: newCacheEntry(http.StatusMultiStatus, fullChild),
|
||||
},
|
||||
{
|
||||
path: path.Join(parentPath, "nonexistent.txt"),
|
||||
depth: 0,
|
||||
want: notFound,
|
||||
},
|
||||
{
|
||||
path: missingParentPath,
|
||||
depth: 1,
|
||||
want: notFound,
|
||||
},
|
||||
{
|
||||
path: missingParentPath,
|
||||
depth: 0,
|
||||
want: notFound,
|
||||
},
|
||||
{
|
||||
path: path.Join(missingParentPath, "filename.txt"),
|
||||
depth: 0,
|
||||
want: notFound,
|
||||
},
|
||||
{
|
||||
path: unparseableParentPath,
|
||||
depth: 1,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
path: unparseableParentPath,
|
||||
depth: 0,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
path: path.Join(unparseableParentPath, "filename.txt"),
|
||||
depth: 0,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
path: "/unknown",
|
||||
depth: 1,
|
||||
want: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%d%s", test.depth, test.path), func(t *testing.T) {
|
||||
got := c.get(test.path, test.depth)
|
||||
if diff := cmp.Diff(got, test.want); diff != "" {
|
||||
t.Errorf("unexpected cached value; (-got+want):%v", diff)
|
||||
log.Printf("want\n%s", test.want.Raw)
|
||||
log.Printf("got\n%s", got.Raw)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -30,14 +32,29 @@ import (
|
||||
const (
|
||||
domain = `test$%domain.com`
|
||||
|
||||
remote1 = `rem ote$%1`
|
||||
remote2 = `_rem ote$%2`
|
||||
share11 = `sha re$%11`
|
||||
share12 = `_sha re$%12`
|
||||
file111 = `fi le$%111.txt`
|
||||
remote1 = `rem ote$%<>1`
|
||||
remote2 = `_rem ote$%<>2`
|
||||
share11 = `sha re$%<>11`
|
||||
share12 = `_sha re$%<>12`
|
||||
file112 = `file112.txt`
|
||||
)
|
||||
|
||||
var (
|
||||
file111 = `fi le$%<>111.txt`
|
||||
)
|
||||
|
||||
func init() {
|
||||
if runtime.GOOS == "windows" {
|
||||
// file with less than and greater than doesn't work on Windows
|
||||
file111 = `fi le$%111.txt`
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
lockRootRegex = regexp.MustCompile(`<D:lockroot><D:href>/?([^<]*)/?</D:href>`)
|
||||
lockTokenRegex = regexp.MustCompile(`<D:locktoken><D:href>([0-9]+)/?</D:href>`)
|
||||
)
|
||||
|
||||
func init() {
|
||||
// set AllowShareAs() to false so that we don't try to use sub-processes
|
||||
// for access files on disk.
|
||||
@@ -145,6 +162,206 @@ func TestSecretTokenAuth(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLOCK(t *testing.T) {
|
||||
s := newSystem(t)
|
||||
|
||||
s.addRemote(remote1)
|
||||
s.addShare(remote1, share11, drive.PermissionReadWrite)
|
||||
s.writeFile("writing file to read/write remote should succeed", remote1, share11, file111, "hello world", true)
|
||||
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{DisableKeepAlives: true},
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("http://%s/%s/%s/%s/%s",
|
||||
s.local.l.Addr(),
|
||||
url.PathEscape(domain),
|
||||
url.PathEscape(remote1),
|
||||
url.PathEscape(share11),
|
||||
url.PathEscape(file111))
|
||||
|
||||
// First acquire a lock with a short timeout
|
||||
req, err := http.NewRequest("LOCK", u, strings.NewReader(lockBody))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Depth", "infinity")
|
||||
req.Header.Set("Timeout", "Second-1")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("expected LOCK to succeed, but got status %d", resp.StatusCode)
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
submatches := lockRootRegex.FindStringSubmatch(string(body))
|
||||
if len(submatches) != 2 {
|
||||
t.Fatal("failed to find lockroot")
|
||||
}
|
||||
want := shared.EscapeForXML(pathTo(remote1, share11, file111))
|
||||
got := submatches[1]
|
||||
if got != want {
|
||||
t.Fatalf("want lockroot %q, got %q", want, got)
|
||||
}
|
||||
|
||||
submatches = lockTokenRegex.FindStringSubmatch(string(body))
|
||||
if len(submatches) != 2 {
|
||||
t.Fatal("failed to find locktoken")
|
||||
}
|
||||
lockToken := submatches[1]
|
||||
ifHeader := fmt.Sprintf("<%s> (<%s>)", u, lockToken)
|
||||
|
||||
// Then refresh the lock with a longer timeout
|
||||
req, err = http.NewRequest("LOCK", u, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Depth", "infinity")
|
||||
req.Header.Set("Timeout", "Second-600")
|
||||
req.Header.Set("If", ifHeader)
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("expected LOCK refresh to succeed, but got status %d", resp.StatusCode)
|
||||
}
|
||||
body, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
submatches = lockRootRegex.FindStringSubmatch(string(body))
|
||||
if len(submatches) != 2 {
|
||||
t.Fatal("failed to find lockroot after refresh")
|
||||
}
|
||||
want = shared.EscapeForXML(pathTo(remote1, share11, file111))
|
||||
got = submatches[1]
|
||||
if got != want {
|
||||
t.Fatalf("want lockroot after refresh %q, got %q", want, got)
|
||||
}
|
||||
|
||||
submatches = lockTokenRegex.FindStringSubmatch(string(body))
|
||||
if len(submatches) != 2 {
|
||||
t.Fatal("failed to find locktoken after refresh")
|
||||
}
|
||||
if submatches[1] != lockToken {
|
||||
t.Fatalf("on refresh, lock token changed from %q to %q", lockToken, submatches[1])
|
||||
}
|
||||
|
||||
// Then wait past the original timeout, then try to delete without the lock
|
||||
// (should fail)
|
||||
time.Sleep(1 * time.Second)
|
||||
req, err = http.NewRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 423 {
|
||||
t.Fatalf("deleting without lock token should fail with 423, but got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Then delete with the lock (should succeed)
|
||||
req, err = http.NewRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
req.Header.Set("If", ifHeader)
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 204 {
|
||||
t.Fatalf("deleting with lock token should have succeeded with 204, but got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUNLOCK(t *testing.T) {
|
||||
s := newSystem(t)
|
||||
|
||||
s.addRemote(remote1)
|
||||
s.addShare(remote1, share11, drive.PermissionReadWrite)
|
||||
s.writeFile("writing file to read/write remote should succeed", remote1, share11, file111, "hello world", true)
|
||||
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{DisableKeepAlives: true},
|
||||
}
|
||||
|
||||
u := fmt.Sprintf("http://%s/%s/%s/%s/%s",
|
||||
s.local.l.Addr(),
|
||||
url.PathEscape(domain),
|
||||
url.PathEscape(remote1),
|
||||
url.PathEscape(share11),
|
||||
url.PathEscape(file111))
|
||||
|
||||
// Acquire a lock
|
||||
req, err := http.NewRequest("LOCK", u, strings.NewReader(lockBody))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Depth", "infinity")
|
||||
req.Header.Set("Timeout", "Second-600")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
t.Fatalf("expected LOCK to succeed, but got status %d", resp.StatusCode)
|
||||
}
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
submatches := lockTokenRegex.FindStringSubmatch(string(body))
|
||||
if len(submatches) != 2 {
|
||||
t.Fatal("failed to find locktoken")
|
||||
}
|
||||
lockToken := submatches[1]
|
||||
|
||||
// Release the lock
|
||||
req, err = http.NewRequest("UNLOCK", u, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req.Header.Set("Lock-Token", fmt.Sprintf("<%s>", lockToken))
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 204 {
|
||||
t.Fatalf("expected UNLOCK to succeed with a 204, but got status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Then delete without the lock (should succeed)
|
||||
req, err = http.NewRequest("DELETE", u, nil)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 204 {
|
||||
t.Fatalf("deleting without lock should have succeeded with 204, but got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
type local struct {
|
||||
l net.Listener
|
||||
fs *FileSystemForLocal
|
||||
@@ -184,7 +401,7 @@ func newSystem(t *testing.T) *system {
|
||||
// Make sure we don't leak goroutines
|
||||
tstest.ResourceCheck(t)
|
||||
|
||||
fs := NewFileSystemForLocal(log.Printf)
|
||||
fs := newFileSystemForLocal(log.Printf, nil)
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to Listen: %s", err)
|
||||
@@ -486,3 +703,9 @@ func (a *noopAuthenticator) Clone() gowebdav.Authenticator {
|
||||
func (a *noopAuthenticator) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const lockBody = `<?xml version="1.0" encoding="utf-8" ?>
|
||||
<D:lockinfo xmlns:D='DAV:'>
|
||||
<D:lockscope><D:exclusive/></D:lockscope>
|
||||
<D:locktype><D:write/></D:locktype>
|
||||
</D:lockinfo>`
|
||||
|
||||
@@ -151,6 +151,9 @@ func (s *FileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
// WebDAV's locking code compares the lock resources with the request's
|
||||
// host header, set this to empty to avoid mismatches.
|
||||
r.Host = ""
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,10 @@ const (
|
||||
// NewFileSystemForLocal starts serving a filesystem for local clients.
|
||||
// Inbound connections must be handed to HandleConn.
|
||||
func NewFileSystemForLocal(logf logger.Logf) *FileSystemForLocal {
|
||||
return newFileSystemForLocal(logf, &compositedav.StatCache{TTL: statCacheTTL})
|
||||
}
|
||||
|
||||
func newFileSystemForLocal(logf logger.Logf, statCache *compositedav.StatCache) *FileSystemForLocal {
|
||||
if logf == nil {
|
||||
logf = log.Printf
|
||||
}
|
||||
@@ -34,7 +38,7 @@ func NewFileSystemForLocal(logf logger.Logf) *FileSystemForLocal {
|
||||
logf: logf,
|
||||
h: &compositedav.Handler{
|
||||
Logf: logf,
|
||||
StatCache: &compositedav.StatCache{TTL: statCacheTTL},
|
||||
StatCache: statCache,
|
||||
},
|
||||
listener: newConnListener(),
|
||||
}
|
||||
|
||||
@@ -26,6 +26,17 @@ func CleanAndSplit(p string) []string {
|
||||
return strings.Split(strings.Trim(path.Clean(p), sepStringAndDot), sepString)
|
||||
}
|
||||
|
||||
// Normalize normalizes the given path (e.g. dropping trailing slashes).
|
||||
func Normalize(p string) string {
|
||||
return Join(CleanAndSplit(p)...)
|
||||
}
|
||||
|
||||
// Parent extracts the parent of the given path.
|
||||
func Parent(p string) string {
|
||||
parts := CleanAndSplit(p)
|
||||
return Join(parts[:len(parts)-1]...)
|
||||
}
|
||||
|
||||
// Join behaves like path.Join() but also includes a leading slash.
|
||||
func Join(parts ...string) string {
|
||||
fullParts := make([]string, 0, len(parts))
|
||||
|
||||
16
drive/driveimpl/shared/xml.go
Normal file
16
drive/driveimpl/shared/xml.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package shared
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
)
|
||||
|
||||
// EscapeForXML escapes the given string for use in XML text.
|
||||
func EscapeForXML(s string) string {
|
||||
result := bytes.NewBuffer(nil)
|
||||
xml.Escape(result, []byte(s))
|
||||
return result.String()
|
||||
}
|
||||
4
go.mod
4
go.mod
@@ -14,6 +14,7 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.64
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.33.0
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.44.7
|
||||
github.com/bramvdbogaerde/go-scp v1.4.0
|
||||
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||
github.com/creack/pty v1.1.21
|
||||
@@ -37,7 +38,7 @@ require (
|
||||
github.com/google/go-cmp v0.6.0
|
||||
github.com/google/go-containerregistry v0.18.0
|
||||
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806
|
||||
github.com/google/uuid v1.5.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/goreleaser/nfpm/v2 v2.33.1
|
||||
github.com/hdevalence/ed25519consensus v0.2.0
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
@@ -60,6 +61,7 @@ require (
|
||||
github.com/peterbourgon/ff/v3 v3.4.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/sftp v1.13.6
|
||||
github.com/prometheus-community/pro-bing v0.4.0
|
||||
github.com/prometheus/client_golang v1.18.0
|
||||
github.com/prometheus/common v0.46.0
|
||||
github.com/safchain/ethtool v0.3.0
|
||||
|
||||
8
go.sum
8
go.sum
@@ -177,6 +177,8 @@ github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ
|
||||
github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k=
|
||||
github.com/bombsimon/wsl/v3 v3.4.0 h1:RkSxjT3tmlptwfgEgTgU+KYKLI35p/tviNXNXiL2aNU=
|
||||
github.com/bombsimon/wsl/v3 v3.4.0/go.mod h1:KkIB+TXkqy6MvK9BDZVbZxKNYsE1/oLRJbIFtf14qqo=
|
||||
github.com/bramvdbogaerde/go-scp v1.4.0 h1:jKMwpwCbcX1KyvDbm/PDJuXcMuNVlLGi0Q0reuzjyKY=
|
||||
github.com/bramvdbogaerde/go-scp v1.4.0/go.mod h1:on2aH5AxaFb2G0N5Vsdy6B0Ml7k9HuHSwfo1y0QzAbQ=
|
||||
github.com/breml/bidichk v0.2.4 h1:i3yedFWWQ7YzjdZJHnPo9d/xURinSq3OM+gyM43K4/8=
|
||||
github.com/breml/bidichk v0.2.4/go.mod h1:7Zk0kRFt1LIZxtQdl9W9JwGAcLTTkOs+tN7wuEYGJ3s=
|
||||
github.com/breml/errchkjson v0.3.1 h1:hlIeXuspTyt8Y/UmP5qy1JocGNR00KQHgfaNtRAjoxQ=
|
||||
@@ -468,8 +470,8 @@ github.com/google/rpmpack v0.5.0 h1:L16KZ3QvkFGpYhmp23iQip+mx1X39foEsqszjMNBm8A=
|
||||
github.com/google/rpmpack v0.5.0/go.mod h1:uqVAUVQLq8UY2hCDfmJ/+rtO3aw7qyhc90rCVEabEfI=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||
@@ -731,6 +733,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/polyfloyd/go-errorlint v1.4.1 h1:r8ru5FhXSn34YU1GJDOuoJv2LdsQkPmK325EOpPMJlM=
|
||||
github.com/polyfloyd/go-errorlint v1.4.1/go.mod h1:k6fU/+fQe38ednoZS51T7gSIGQW1y94d6TkSr35OzH8=
|
||||
github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4=
|
||||
github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
|
||||
@@ -1 +1 @@
|
||||
48d71857bf5352daaa10b61dd3e9b1c0dd51e27a
|
||||
467a489ae3c080d80f4cfdd05f2aa08cb44c9d6a
|
||||
|
||||
@@ -32,7 +32,8 @@ type ConfigVAlpha struct {
|
||||
AdvertiseRoutes []netip.Prefix `json:",omitempty"`
|
||||
DisableSNAT opt.Bool `json:",omitempty"`
|
||||
|
||||
NetfilterMode *string `json:",omitempty"` // "on", "off", "nodivert"
|
||||
NetfilterMode *string `json:",omitempty"` // "on", "off", "nodivert"
|
||||
NoStatefulFiltering opt.Bool `json:",omitempty"`
|
||||
|
||||
PostureChecking opt.Bool `json:",omitempty"`
|
||||
RunSSHServer opt.Bool `json:",omitempty"` // Tailscale SSH
|
||||
@@ -50,6 +51,7 @@ func (c *ConfigVAlpha) ToPrefs() (MaskedPrefs, error) {
|
||||
if c == nil {
|
||||
return mp, nil
|
||||
}
|
||||
|
||||
mp.WantRunning = !c.Enabled.EqualBool(false)
|
||||
mp.WantRunningSet = mp.WantRunning || c.Enabled != ""
|
||||
if c.ServerURL != nil {
|
||||
@@ -98,6 +100,11 @@ func (c *ConfigVAlpha) ToPrefs() (MaskedPrefs, error) {
|
||||
mp.NoSNAT = c.DisableSNAT.EqualBool(true)
|
||||
mp.NoSNAT = true
|
||||
}
|
||||
if c.NoStatefulFiltering != "" {
|
||||
mp.NoStatefulFiltering = c.NoStatefulFiltering
|
||||
mp.NoStatefulFilteringSet = true
|
||||
}
|
||||
|
||||
if c.NetfilterMode != nil {
|
||||
m, err := preftype.ParseNetfilterMode(*c.NetfilterMode)
|
||||
if err != nil {
|
||||
|
||||
59
ipn/conffile/cloudconf.go
Normal file
59
ipn/conffile/cloudconf.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package conffile
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/omit"
|
||||
)
|
||||
|
||||
func getEC2MetadataToken() (string, error) {
|
||||
if omit.AWS {
|
||||
return "", omit.Err
|
||||
}
|
||||
req, _ := http.NewRequest("PUT", "http://169.254.169.254/latest/api/token", nil)
|
||||
req.Header.Add("X-aws-ec2-metadata-token-ttl-seconds", "300")
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get metadata token: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
return "", fmt.Errorf("failed to get metadata token: %v", res.Status)
|
||||
}
|
||||
all, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read metadata token: %w", err)
|
||||
}
|
||||
return strings.TrimSpace(string(all)), nil
|
||||
}
|
||||
|
||||
func readVMUserData() ([]byte, error) {
|
||||
// TODO(bradfitz): support GCP, Azure, Proxmox/cloud-init
|
||||
// (NoCloud/ConfigDrive ISO), etc.
|
||||
|
||||
if omit.AWS {
|
||||
return nil, omit.Err
|
||||
}
|
||||
token, tokErr := getEC2MetadataToken()
|
||||
req, _ := http.NewRequest("GET", "http://169.254.169.254/latest/user-data", nil)
|
||||
req.Header.Add("X-aws-ec2-metadata-token", token)
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
if tokErr != nil {
|
||||
return nil, fmt.Errorf("failed to get VM user data: %v; also failed to get metadata token: %v", res.Status, tokErr)
|
||||
}
|
||||
return nil, errors.New(res.Status)
|
||||
}
|
||||
return io.ReadAll(res.Body)
|
||||
}
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
|
||||
// Config describes a config file.
|
||||
type Config struct {
|
||||
Path string // disk path of HuJSON
|
||||
Path string // disk path of HuJSON, or VMUserDataPath
|
||||
Raw []byte // raw bytes from disk, in HuJSON form
|
||||
Std []byte // standardized JSON form
|
||||
Version string // "alpha0" for now
|
||||
@@ -35,13 +35,22 @@ func (c *Config) WantRunning() bool {
|
||||
return c != nil && !c.Parsed.Enabled.EqualBool(false)
|
||||
}
|
||||
|
||||
// VMUserDataPath is a sentinel value for Load to use to get the data
|
||||
// from the VM's metadata service's user-data field.
|
||||
const VMUserDataPath = "vm:user-data"
|
||||
|
||||
// Load reads and parses the config file at the provided path on disk.
|
||||
func Load(path string) (*Config, error) {
|
||||
var c Config
|
||||
c.Path = path
|
||||
|
||||
var err error
|
||||
c.Raw, err = os.ReadFile(path)
|
||||
|
||||
switch path {
|
||||
case VMUserDataPath:
|
||||
c.Raw, err = readVMUserData()
|
||||
default:
|
||||
c.Raw, err = os.ReadFile(path)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
)
|
||||
@@ -39,7 +40,6 @@ func (src *Prefs) Clone() *Prefs {
|
||||
var _PrefsCloneNeedsRegeneration = Prefs(struct {
|
||||
ControlURL string
|
||||
RouteAll bool
|
||||
AllowSingleHosts bool
|
||||
ExitNodeID tailcfg.StableNodeID
|
||||
ExitNodeIP netip.Addr
|
||||
InternalExitNodePrior tailcfg.StableNodeID
|
||||
@@ -57,6 +57,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
|
||||
Egg bool
|
||||
AdvertiseRoutes []netip.Prefix
|
||||
NoSNAT bool
|
||||
NoStatefulFiltering opt.Bool
|
||||
NetfilterMode preftype.NetfilterMode
|
||||
OperatorUser string
|
||||
ProfileName string
|
||||
@@ -65,6 +66,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
|
||||
PostureChecking bool
|
||||
NetfilterKind string
|
||||
DriveShares []*drive.Share
|
||||
AllowSingleHosts marshalAsTrueInJSON
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
func TestDeps(t *testing.T) {
|
||||
deptest.DepChecker{
|
||||
BadDeps: map[string]string{
|
||||
"testing": "do not use testing package in production code",
|
||||
"gvisor.dev/gvisor/pkg/buffer": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/cpuid": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
"gvisor.dev/gvisor/pkg/tcpip": "https://github.com/tailscale/tailscale/issues/9756",
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/types/views"
|
||||
@@ -66,7 +67,6 @@ func (v *PrefsView) UnmarshalJSON(b []byte) error {
|
||||
|
||||
func (v PrefsView) ControlURL() string { return v.ж.ControlURL }
|
||||
func (v PrefsView) RouteAll() bool { return v.ж.RouteAll }
|
||||
func (v PrefsView) AllowSingleHosts() bool { return v.ж.AllowSingleHosts }
|
||||
func (v PrefsView) ExitNodeID() tailcfg.StableNodeID { return v.ж.ExitNodeID }
|
||||
func (v PrefsView) ExitNodeIP() netip.Addr { return v.ж.ExitNodeIP }
|
||||
func (v PrefsView) InternalExitNodePrior() tailcfg.StableNodeID { return v.ж.InternalExitNodePrior }
|
||||
@@ -86,6 +86,7 @@ func (v PrefsView) AdvertiseRoutes() views.Slice[netip.Prefix] {
|
||||
return views.SliceOf(v.ж.AdvertiseRoutes)
|
||||
}
|
||||
func (v PrefsView) NoSNAT() bool { return v.ж.NoSNAT }
|
||||
func (v PrefsView) NoStatefulFiltering() opt.Bool { return v.ж.NoStatefulFiltering }
|
||||
func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.NetfilterMode }
|
||||
func (v PrefsView) OperatorUser() string { return v.ж.OperatorUser }
|
||||
func (v PrefsView) ProfileName() string { return v.ж.ProfileName }
|
||||
@@ -96,13 +97,13 @@ func (v PrefsView) NetfilterKind() string { return v.ж.Netfilte
|
||||
func (v PrefsView) DriveShares() views.SliceView[*drive.Share, drive.ShareView] {
|
||||
return views.SliceOfViews[*drive.Share, drive.ShareView](v.ж.DriveShares)
|
||||
}
|
||||
func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() }
|
||||
func (v PrefsView) AllowSingleHosts() marshalAsTrueInJSON { return v.ж.AllowSingleHosts }
|
||||
func (v PrefsView) Persist() persist.PersistView { return v.ж.Persist.View() }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _PrefsViewNeedsRegeneration = Prefs(struct {
|
||||
ControlURL string
|
||||
RouteAll bool
|
||||
AllowSingleHosts bool
|
||||
ExitNodeID tailcfg.StableNodeID
|
||||
ExitNodeIP netip.Addr
|
||||
InternalExitNodePrior tailcfg.StableNodeID
|
||||
@@ -120,6 +121,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
|
||||
Egg bool
|
||||
AdvertiseRoutes []netip.Prefix
|
||||
NoSNAT bool
|
||||
NoStatefulFiltering opt.Bool
|
||||
NetfilterMode preftype.NetfilterMode
|
||||
OperatorUser string
|
||||
ProfileName string
|
||||
@@ -128,6 +130,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
|
||||
PostureChecking bool
|
||||
NetfilterKind string
|
||||
DriveShares []*drive.Share
|
||||
AllowSingleHosts marshalAsTrueInJSON
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
|
||||
@@ -478,17 +478,44 @@ func findCmdTailscale() (string, error) {
|
||||
}
|
||||
|
||||
func tailscaleUpdateCmd(cmdTS string) *exec.Cmd {
|
||||
defaultCmd := exec.Command(cmdTS, "update", "--yes")
|
||||
if runtime.GOOS != "linux" {
|
||||
return exec.Command(cmdTS, "update", "--yes")
|
||||
return defaultCmd
|
||||
}
|
||||
if _, err := exec.LookPath("systemd-run"); err != nil {
|
||||
return exec.Command(cmdTS, "update", "--yes")
|
||||
return defaultCmd
|
||||
}
|
||||
|
||||
// When systemd-run is available, use it to run the update command. This
|
||||
// creates a new temporary unit separate from the tailscaled unit. When
|
||||
// tailscaled is restarted during the update, systemd won't kill this
|
||||
// temporary update unit, which could cause unexpected breakage.
|
||||
return exec.Command("systemd-run", "--wait", "--pipe", "--collect", cmdTS, "update", "--yes")
|
||||
//
|
||||
// We want to use the --wait flag for systemd-run, to block the update
|
||||
// command until completion and collect output. But this flag was added in
|
||||
// systemd 232, so we need to check the version first.
|
||||
//
|
||||
// The output will look like:
|
||||
//
|
||||
// systemd 255 (255.7-1-arch)
|
||||
// +PAM +AUDIT ... other feature flags ...
|
||||
systemdVerOut, err := exec.Command("systemd-run", "--version").Output()
|
||||
if err != nil {
|
||||
return defaultCmd
|
||||
}
|
||||
parts := strings.Fields(string(systemdVerOut))
|
||||
if len(parts) < 2 || parts[0] != "systemd" {
|
||||
return defaultCmd
|
||||
}
|
||||
systemdVer, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return defaultCmd
|
||||
}
|
||||
if systemdVer < 232 {
|
||||
return exec.Command("systemd-run", "--pipe", "--collect", cmdTS, "update", "--yes")
|
||||
} else {
|
||||
return exec.Command("systemd-run", "--wait", "--pipe", "--collect", cmdTS, "update", "--yes")
|
||||
}
|
||||
}
|
||||
|
||||
func regularFileExists(path string) bool {
|
||||
|
||||
@@ -312,7 +312,7 @@ func (b *LocalBackend) updateDrivePeersLocked(nm *netmap.NetworkMap) {
|
||||
driveRemotes = b.driveRemotesFromPeers(nm)
|
||||
}
|
||||
|
||||
fs.SetRemotes(b.netMap.Domain, driveRemotes, &driveTransport{b: b})
|
||||
fs.SetRemotes(b.netMap.Domain, driveRemotes, b.newDriveTransport())
|
||||
}
|
||||
|
||||
func (b *LocalBackend) driveRemotesFromPeers(nm *netmap.NetworkMap) []*drive.Remote {
|
||||
|
||||
@@ -151,6 +151,12 @@ type watchSession struct {
|
||||
sessionID string
|
||||
}
|
||||
|
||||
// lastSuggestedExitNode stores the last suggested exit node ID and name in local backend.
|
||||
type lastSuggestedExitNode struct {
|
||||
id tailcfg.StableNodeID
|
||||
name string
|
||||
}
|
||||
|
||||
// LocalBackend is the glue between the major pieces of the Tailscale
|
||||
// network software: the cloud control plane (via controlclient), the
|
||||
// network data plane (via wgengine), and the user-facing UIs and CLIs
|
||||
@@ -224,7 +230,8 @@ type LocalBackend struct {
|
||||
ccGen clientGen // function for producing controlclient; lazily populated
|
||||
sshServer SSHServer // or nil, initialized lazily.
|
||||
appConnector *appc.AppConnector // or nil, initialized when configured.
|
||||
notify func(ipn.Notify)
|
||||
// notifyCancel cancels notifications to the current SetNotifyCallback.
|
||||
notifyCancel context.CancelFunc
|
||||
cc controlclient.Client
|
||||
ccAuto *controlclient.Auto // if cc is of type *controlclient.Auto
|
||||
machinePrivKey key.MachinePrivate
|
||||
@@ -252,10 +259,8 @@ type LocalBackend struct {
|
||||
endpoints []tailcfg.Endpoint
|
||||
blocked bool
|
||||
keyExpired bool
|
||||
authURL string // cleared on Notify
|
||||
authURLSticky string // not cleared on Notify
|
||||
authURL string // non-empty if not Running
|
||||
authURLTime time.Time // when the authURL was received from the control server
|
||||
interact bool
|
||||
egg bool
|
||||
prevIfState *netmon.State
|
||||
peerAPIServer *peerAPIServer // or nil
|
||||
@@ -324,6 +329,10 @@ type LocalBackend struct {
|
||||
|
||||
// outgoingFiles keeps track of Taildrop outgoing files keyed to their OutgoingFile.ID
|
||||
outgoingFiles map[string]*ipn.OutgoingFile
|
||||
|
||||
// lastSuggestedExitNode stores the last suggested exit node ID and name.
|
||||
// lastSuggestedExitNode updates whenever the suggestion changes.
|
||||
lastSuggestedExitNode lastSuggestedExitNode
|
||||
}
|
||||
|
||||
// HealthTracker returns the health tracker for the backend.
|
||||
@@ -358,17 +367,16 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
||||
if dialer.NetMon() == nil {
|
||||
return nil, errors.New("dialer to NewLocalBackend must have a NetMon")
|
||||
}
|
||||
_ = sys.MagicSock.Get() // or panic
|
||||
mConn := sys.MagicSock.Get()
|
||||
|
||||
goos := envknob.GOOS()
|
||||
if loginFlags&controlclient.LocalBackendStartKeyOSNeutral != 0 {
|
||||
goos = ""
|
||||
}
|
||||
pm, err := newProfileManagerWithGOOS(store, logf, goos)
|
||||
pm, err := newProfileManagerWithGOOS(store, logf, sys.HealthTracker(), goos)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pm.health = sys.HealthTracker()
|
||||
if sds, ok := store.(ipn.StateStoreDialerSetter); ok {
|
||||
sds.SetDialer(dialer.SystemDial)
|
||||
}
|
||||
@@ -393,7 +401,6 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
||||
osshare.SetFileSharingEnabled(false, logf)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
portpoll := new(portlist.Poller)
|
||||
clock := tstime.StdClock{}
|
||||
|
||||
b := &LocalBackend{
|
||||
@@ -411,7 +418,7 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
||||
pm: pm,
|
||||
backendLogID: logID,
|
||||
state: ipn.NoState,
|
||||
portpoll: portpoll,
|
||||
portpoll: new(portlist.Poller),
|
||||
em: newExpiryManager(logf),
|
||||
gotPortPollRes: make(chan struct{}),
|
||||
loginFlags: loginFlags,
|
||||
@@ -419,6 +426,7 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
||||
selfUpdateProgress: make([]ipnstate.UpdateProgress, 0),
|
||||
lastSelfUpdateState: ipnstate.UpdateFinished,
|
||||
}
|
||||
mConn.SetNetInfoCallback(b.setNetInfo)
|
||||
|
||||
netMon := sys.NetMon.Get()
|
||||
b.sockstatLogger, err = sockstatlog.NewLogger(logpolicy.LogsDir(logf), logf, logID, netMon, sys.HealthTracker())
|
||||
@@ -431,7 +439,9 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo
|
||||
}
|
||||
|
||||
// Default filter blocks everything and logs nothing, until Start() is called.
|
||||
b.setFilter(filter.NewAllowNone(logf, &netipx.IPSet{}))
|
||||
noneFilter := filter.NewAllowNone(logf, &netipx.IPSet{})
|
||||
b.setFilter(noneFilter)
|
||||
b.e.SetJailedFilter(noneFilter)
|
||||
|
||||
b.setTCPPortsIntercepted(nil)
|
||||
|
||||
@@ -605,6 +615,8 @@ func (b *LocalBackend) ReloadConfig() (ok bool, err error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var assumeNetworkUpdateForTest = envknob.RegisterBool("TS_ASSUME_NETWORK_UP_FOR_TEST")
|
||||
|
||||
// pauseOrResumeControlClientLocked pauses b.cc if there is no network available
|
||||
// or if the LocalBackend is in Stopped state with a valid NetMap. In all other
|
||||
// cases, it unpauses it. It is a no-op if b.cc is nil.
|
||||
@@ -615,7 +627,7 @@ func (b *LocalBackend) pauseOrResumeControlClientLocked() {
|
||||
return
|
||||
}
|
||||
networkUp := b.prevIfState.AnyInterfaceUp()
|
||||
b.cc.SetPaused((b.state == ipn.Stopped && b.netMap != nil) || (!networkUp && !testenv.InTest()))
|
||||
b.cc.SetPaused((b.state == ipn.Stopped && b.netMap != nil) || (!networkUp && !testenv.InTest() && !assumeNetworkUpdateForTest()))
|
||||
}
|
||||
|
||||
// linkChange is our network monitor callback, called whenever the network changes.
|
||||
@@ -697,6 +709,9 @@ func (b *LocalBackend) Shutdown() {
|
||||
b.debugSink.Close()
|
||||
b.debugSink = nil
|
||||
}
|
||||
if b.notifyCancel != nil {
|
||||
b.notifyCancel()
|
||||
}
|
||||
b.mu.Unlock()
|
||||
b.webClientShutdown()
|
||||
|
||||
@@ -768,11 +783,12 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
s.Version = version.Long()
|
||||
s.TUN = !b.sys.IsNetstack()
|
||||
s.BackendState = b.state.String()
|
||||
s.AuthURL = b.authURLSticky
|
||||
s.AuthURL = b.authURL
|
||||
if prefs := b.pm.CurrentPrefs(); prefs.Valid() && prefs.AutoUpdate().Check {
|
||||
s.ClientVersion = b.lastClientVersion
|
||||
}
|
||||
s.Health = b.health.AppendWarnings(s.Health)
|
||||
s.HaveNodeKey = b.hasNodeKeyLocked()
|
||||
|
||||
// TODO(bradfitz): move this health check into a health.Warnable
|
||||
// and remove from here.
|
||||
@@ -1121,7 +1137,6 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
|
||||
prefsChanged := false
|
||||
prefs := b.pm.CurrentPrefs().AsStruct()
|
||||
netMap := b.netMap
|
||||
interact := b.interact
|
||||
|
||||
if prefs.ControlURL == "" {
|
||||
// Once we get a message from the control plane, set
|
||||
@@ -1140,7 +1155,6 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
|
||||
}
|
||||
if st.URL != "" {
|
||||
b.authURL = st.URL
|
||||
b.authURLSticky = st.URL
|
||||
b.authURLTime = b.clock.Now()
|
||||
}
|
||||
if (wasBlocked || b.seamlessRenewalEnabled()) && st.LoginFinished() {
|
||||
@@ -1258,9 +1272,7 @@ func (b *LocalBackend) SetControlClientStatus(c controlclient.Client, st control
|
||||
}
|
||||
if st.URL != "" {
|
||||
b.logf("Received auth URL: %.20v...", st.URL)
|
||||
if interact {
|
||||
b.popBrowserAuthNow()
|
||||
}
|
||||
b.popBrowserAuthNow()
|
||||
}
|
||||
b.stateMachine()
|
||||
// This is currently (2020-07-28) necessary; conditionally disabling it is fragile!
|
||||
@@ -1544,10 +1556,26 @@ func endpointsEqual(x, y []tailcfg.Endpoint) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// SetNotifyCallback sets the function to call when the backend has something to
|
||||
// notify the frontend about. Only one callback can be set at a time, so calling
|
||||
// this function will replace the previous callback.
|
||||
func (b *LocalBackend) SetNotifyCallback(notify func(ipn.Notify)) {
|
||||
ctx, cancel := context.WithCancel(b.ctx)
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.notify = notify
|
||||
prevCancel := b.notifyCancel
|
||||
b.notifyCancel = cancel
|
||||
b.mu.Unlock()
|
||||
if prevCancel != nil {
|
||||
prevCancel()
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go b.WatchNotifications(ctx, 0, wg.Done, func(n *ipn.Notify) bool {
|
||||
notify(*n)
|
||||
return true
|
||||
})
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// SetHTTPTestClient sets an alternate HTTP client to use with
|
||||
@@ -1579,6 +1607,14 @@ func (b *LocalBackend) NodeViewByIDForTest(id tailcfg.NodeID) (_ tailcfg.NodeVie
|
||||
return n, ok
|
||||
}
|
||||
|
||||
// DisablePortMapperForTest disables the portmapper for tests.
|
||||
// It must be called before Start.
|
||||
func (b *LocalBackend) DisablePortMapperForTest() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.portpoll = nil
|
||||
}
|
||||
|
||||
// PeersForTest returns all the current peers, sorted by Node.ID,
|
||||
// for integration tests in another repo.
|
||||
func (b *LocalBackend) PeersForTest() []tailcfg.NodeView {
|
||||
@@ -1591,9 +1627,7 @@ func (b *LocalBackend) PeersForTest() []tailcfg.NodeView {
|
||||
return ret
|
||||
}
|
||||
|
||||
func (b *LocalBackend) getNewControlClientFunc() clientGen {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
func (b *LocalBackend) getNewControlClientFuncLocked() clientGen {
|
||||
if b.ccGen == nil {
|
||||
// Initialize it rather than just returning the
|
||||
// default to make any future call to
|
||||
@@ -1618,11 +1652,17 @@ func (b *LocalBackend) getNewControlClientFunc() clientGen {
|
||||
func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
b.logf("Start")
|
||||
|
||||
b.mu.Lock()
|
||||
var clientToShutdown controlclient.Client
|
||||
defer func() {
|
||||
if clientToShutdown != nil {
|
||||
clientToShutdown.Shutdown()
|
||||
}
|
||||
}()
|
||||
unlock := b.lockAndGetUnlock()
|
||||
defer unlock()
|
||||
|
||||
if opts.UpdatePrefs != nil {
|
||||
if err := b.checkPrefsLocked(opts.UpdatePrefs); err != nil {
|
||||
b.mu.Unlock()
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1654,7 +1694,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
// into sync with the minimal changes. But that's not how it
|
||||
// is right now, which is a sign that the code is still too
|
||||
// complicated.
|
||||
prevCC := b.resetControlClientLocked()
|
||||
clientToShutdown = b.resetControlClientLocked()
|
||||
httpTestClient := b.httpTestClient
|
||||
|
||||
if b.hostinfo != nil {
|
||||
@@ -1683,7 +1723,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
wantRunning := prefs.WantRunning()
|
||||
if wantRunning {
|
||||
if err := b.initMachineKeyLocked(); err != nil {
|
||||
b.mu.Unlock()
|
||||
return fmt.Errorf("initMachineKeyLocked: %w", err)
|
||||
}
|
||||
}
|
||||
@@ -1702,7 +1741,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
persistv = new(persist.Persist)
|
||||
}
|
||||
b.updateFilterLocked(nil, ipn.PrefsView{})
|
||||
b.mu.Unlock()
|
||||
|
||||
if b.portpoll != nil {
|
||||
b.portpollOnce.Do(func() {
|
||||
@@ -1734,15 +1772,11 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
debugFlags = append([]string{"netstack"}, debugFlags...)
|
||||
}
|
||||
|
||||
if prevCC != nil {
|
||||
prevCC.Shutdown()
|
||||
}
|
||||
|
||||
// TODO(apenwarr): The only way to change the ServerURL is to
|
||||
// re-run b.Start, because this is the only place we create a
|
||||
// new controlclient. EditPrefs allows you to overwrite ServerURL,
|
||||
// but it won't take effect until the next Start.
|
||||
cc, err := b.getNewControlClientFunc()(controlclient.Options{
|
||||
cc, err := b.getNewControlClientFuncLocked()(controlclient.Options{
|
||||
GetMachinePrivateKey: b.createGetMachinePrivateKeyFunc(),
|
||||
Logf: logger.WithPrefix(b.logf, "control: "),
|
||||
Persist: *persistv,
|
||||
@@ -1772,14 +1806,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
return err
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
// Even though we reset b.cc above, we might have raced with
|
||||
// another Start() call. If so, shut down the previous one again
|
||||
// as we do not know if it was created with the same options.
|
||||
prevCC = b.resetControlClientLocked()
|
||||
if prevCC != nil {
|
||||
defer prevCC.Shutdown() // must be called after b.mu is unlocked
|
||||
}
|
||||
b.setControlClientLocked(cc)
|
||||
endpoints := b.endpoints
|
||||
|
||||
@@ -1790,33 +1816,36 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
if b.tka != nil {
|
||||
head, err := b.tka.authority.Head().MarshalText()
|
||||
if err != nil {
|
||||
b.mu.Unlock()
|
||||
return fmt.Errorf("marshalling tka head: %w", err)
|
||||
}
|
||||
tkaHead = string(head)
|
||||
}
|
||||
confWantRunning := b.conf != nil && wantRunning
|
||||
b.mu.Unlock()
|
||||
|
||||
if endpoints != nil {
|
||||
cc.UpdateEndpoints(endpoints)
|
||||
}
|
||||
cc.SetTKAHead(tkaHead)
|
||||
|
||||
b.MagicConn().SetNetInfoCallback(b.setNetInfo)
|
||||
|
||||
blid := b.backendLogID.String()
|
||||
b.logf("Backend: logs: be:%v fe:%v", blid, opts.FrontendLogID)
|
||||
b.send(ipn.Notify{BackendLogID: &blid})
|
||||
b.send(ipn.Notify{Prefs: &prefs})
|
||||
b.sendLocked(ipn.Notify{
|
||||
BackendLogID: &blid,
|
||||
Prefs: &prefs,
|
||||
})
|
||||
|
||||
if !loggedOut && (b.hasNodeKey() || confWantRunning) {
|
||||
// Even if !WantRunning, we should verify our key, if there
|
||||
// is one. If you want tailscaled to be completely idle,
|
||||
// use logout instead.
|
||||
cc.Login(nil, controlclient.LoginDefault)
|
||||
if !loggedOut && (b.hasNodeKeyLocked() || confWantRunning) {
|
||||
// If we know that we're either logged in or meant to be
|
||||
// running, tell the controlclient that it should also assume
|
||||
// that we need to be logged in.
|
||||
//
|
||||
// Without this, the state machine transitions to "NeedsLogin" implying
|
||||
// that user interaction is required, which is not the case and can
|
||||
// regress tsnet.Server restarts.
|
||||
cc.Login(controlclient.LoginDefault)
|
||||
}
|
||||
b.stateMachine()
|
||||
b.stateMachineLockedOnEntry(unlock)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1924,7 +1953,9 @@ func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs ipn.P
|
||||
|
||||
if !haveNetmap {
|
||||
b.logf("[v1] netmap packet filter: (not ready yet)")
|
||||
b.setFilter(filter.NewAllowNone(b.logf, logNets))
|
||||
noneFilter := filter.NewAllowNone(b.logf, logNets)
|
||||
b.setFilter(noneFilter)
|
||||
b.e.SetJailedFilter(noneFilter)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1936,6 +1967,9 @@ func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs ipn.P
|
||||
b.logf("[v1] netmap packet filter: %v filters", len(packetFilter))
|
||||
b.setFilter(filter.New(packetFilter, localNets, logNets, oldFilter, b.logf))
|
||||
}
|
||||
// The filter for a jailed node is the exact same as a ShieldsUp filter.
|
||||
oldJailedFilter := b.e.GetJailedFilter()
|
||||
b.e.SetJailedFilter(filter.NewShieldsUpFilter(localNets, logNets, oldJailedFilter, b.logf))
|
||||
|
||||
if b.sshServer != nil {
|
||||
go b.sshServer.OnPolicyChange()
|
||||
@@ -2241,8 +2275,8 @@ func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWa
|
||||
if mask&ipn.NotifyInitialState != 0 {
|
||||
ini.SessionID = sessionID
|
||||
ini.State = ptr.To(b.state)
|
||||
if b.state == ipn.NeedsLogin {
|
||||
ini.BrowseToURL = ptr.To(b.authURLSticky)
|
||||
if b.state == ipn.NeedsLogin && b.authURL != "" {
|
||||
ini.BrowseToURL = ptr.To(b.authURL)
|
||||
}
|
||||
}
|
||||
if mask&ipn.NotifyInitialPrefs != 0 {
|
||||
@@ -2296,11 +2330,27 @@ func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWa
|
||||
// TODO(marwan-at-work): streaming background logs?
|
||||
defer b.DeleteForegroundSession(sessionID)
|
||||
|
||||
var lastURLPop string // to dup suppress URL popups
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case n, ok := <-ch:
|
||||
// URLs flow into Notify.BrowseToURL via two means:
|
||||
// 1. From MapResponse.PopBrowserURL, which already says they're dup
|
||||
// suppressed if identical, and that's done by the controlclient,
|
||||
// so this added later adds nothing.
|
||||
//
|
||||
// 2. From the controlclient auth routes, on register. This makes sure
|
||||
// we don't tell clients (mac, windows, android) to pop the same URL
|
||||
// multiple times.
|
||||
if n != nil && n.BrowseToURL != nil {
|
||||
if v := *n.BrowseToURL; v == lastURLPop {
|
||||
n.BrowseToURL = nil
|
||||
} else {
|
||||
lastURLPop = v
|
||||
}
|
||||
}
|
||||
if !ok || !fn(n) {
|
||||
return
|
||||
}
|
||||
@@ -2377,6 +2427,13 @@ func (b *LocalBackend) DebugPickNewDERP() error {
|
||||
//
|
||||
// b.mu must not be held.
|
||||
func (b *LocalBackend) send(n ipn.Notify) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.sendLocked(n)
|
||||
}
|
||||
|
||||
// sendLocked is like send, but assumes b.mu is already held.
|
||||
func (b *LocalBackend) sendLocked(n ipn.Notify) {
|
||||
if n.Prefs != nil {
|
||||
n.Prefs = ptr.To(stripKeysFromPrefs(*n.Prefs))
|
||||
}
|
||||
@@ -2384,8 +2441,6 @@ func (b *LocalBackend) send(n ipn.Notify) {
|
||||
n.Version = version.Long()
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
notifyFunc := b.notify
|
||||
apiSrv := b.peerAPIServer
|
||||
if mayDeref(apiSrv).taildrop.HasFilesWaiting() {
|
||||
n.FilesWaiting = &empty.Message{}
|
||||
@@ -2398,12 +2453,6 @@ func (b *LocalBackend) send(n ipn.Notify) {
|
||||
// Drop the notification if the channel is full.
|
||||
}
|
||||
}
|
||||
|
||||
b.mu.Unlock()
|
||||
|
||||
if notifyFunc != nil {
|
||||
notifyFunc(n)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) sendFileNotify() {
|
||||
@@ -2413,9 +2462,8 @@ func (b *LocalBackend) sendFileNotify() {
|
||||
for _, wakeWaiter := range b.fileWaiters {
|
||||
wakeWaiter()
|
||||
}
|
||||
notifyFunc := b.notify
|
||||
apiSrv := b.peerAPIServer
|
||||
if notifyFunc == nil || apiSrv == nil {
|
||||
if apiSrv == nil {
|
||||
b.mu.Unlock()
|
||||
return
|
||||
}
|
||||
@@ -2438,16 +2486,15 @@ func (b *LocalBackend) sendFileNotify() {
|
||||
func (b *LocalBackend) popBrowserAuthNow() {
|
||||
b.mu.Lock()
|
||||
url := b.authURL
|
||||
b.interact = false
|
||||
b.authURL = "" // but NOT clearing authURLSticky
|
||||
expired := b.keyExpired
|
||||
b.mu.Unlock()
|
||||
|
||||
b.logf("popBrowserAuthNow: url=%v, key-expired=%v, seamless-key-renewal=%v", url != "", b.keyExpired, b.seamlessRenewalEnabled())
|
||||
b.logf("popBrowserAuthNow: url=%v, key-expired=%v, seamless-key-renewal=%v", url != "", expired, b.seamlessRenewalEnabled())
|
||||
|
||||
// Deconfigure the local network data plane if:
|
||||
// - seamless key renewal is not enabled;
|
||||
// - key is expired (in which case tailnet connectivity is down anyway).
|
||||
if !b.seamlessRenewalEnabled() || b.keyExpired {
|
||||
if !b.seamlessRenewalEnabled() || expired {
|
||||
b.blockEngineUpdates(true)
|
||||
b.stopEngineAndWait()
|
||||
}
|
||||
@@ -2766,7 +2813,6 @@ func (b *LocalBackend) StartLoginInteractive(ctx context.Context) error {
|
||||
if b.cc == nil {
|
||||
panic("LocalBackend.assertClient: b.cc == nil")
|
||||
}
|
||||
b.interact = true
|
||||
url := b.authURL
|
||||
timeSinceAuthURLCreated := b.clock.Since(b.authURLTime)
|
||||
cc := b.cc
|
||||
@@ -2779,7 +2825,7 @@ func (b *LocalBackend) StartLoginInteractive(ctx context.Context) error {
|
||||
if url != "" && timeSinceAuthURLCreated < ((7*24*time.Hour)-(1*time.Hour)) {
|
||||
b.popBrowserAuthNow()
|
||||
} else {
|
||||
cc.Login(nil, b.loginFlags|controlclient.LoginInteractive)
|
||||
cc.Login(b.loginFlags | controlclient.LoginInteractive)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -3293,7 +3339,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(newp *ipn.Prefs, unlock unlockOnce)
|
||||
|
||||
if !oldp.WantRunning() && newp.WantRunning {
|
||||
b.logf("transitioning to running; doing Login...")
|
||||
cc.Login(nil, controlclient.LoginDefault)
|
||||
cc.Login(controlclient.LoginDefault)
|
||||
}
|
||||
|
||||
if oldp.WantRunning() != newp.WantRunning {
|
||||
@@ -3579,6 +3625,7 @@ func (b *LocalBackend) authReconfig() {
|
||||
nm := b.netMap
|
||||
hasPAC := b.prevIfState.HasPAC()
|
||||
disableSubnetsIfPAC := nm.HasCap(tailcfg.NodeAttrDisableSubnetsIfPAC)
|
||||
userDialUseRoutes := nm.HasCap(tailcfg.NodeAttrUserDialUseRoutes)
|
||||
dohURL, dohURLOK := exitNodeCanProxyDNS(nm, b.peers, prefs.ExitNodeID())
|
||||
dcfg := dnsConfigForNetmap(nm, b.peers, prefs, b.logf, version.OS())
|
||||
// If the current node is an app connector, ensure the app connector machine is started
|
||||
@@ -3602,9 +3649,6 @@ func (b *LocalBackend) authReconfig() {
|
||||
if prefs.RouteAll() {
|
||||
flags |= netmap.AllowSubnetRoutes
|
||||
}
|
||||
if prefs.AllowSingleHosts() {
|
||||
flags |= netmap.AllowSingleHosts
|
||||
}
|
||||
if hasPAC && disableSubnetsIfPAC {
|
||||
if flags&netmap.AllowSubnetRoutes != 0 {
|
||||
b.logf("authReconfig: have PAC; disabling subnet routes")
|
||||
@@ -3636,6 +3680,12 @@ func (b *LocalBackend) authReconfig() {
|
||||
}
|
||||
b.logf("[v1] authReconfig: ra=%v dns=%v 0x%02x: %v", prefs.RouteAll(), prefs.CorpDNS(), flags, err)
|
||||
|
||||
if userDialUseRoutes {
|
||||
b.dialer.SetRoutes(rcfg.Routes, rcfg.LocalRoutes)
|
||||
} else {
|
||||
b.dialer.SetRoutes(nil, nil)
|
||||
}
|
||||
|
||||
b.initPeerAPIListener()
|
||||
}
|
||||
|
||||
@@ -3687,8 +3737,8 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, peers map[tailcfg.NodeID]tailcfg.
|
||||
}
|
||||
|
||||
// selfV6Only is whether we only have IPv6 addresses ourselves.
|
||||
selfV6Only := views.SliceContainsFunc(nm.GetAddresses(), tsaddr.PrefixIs6) &&
|
||||
!views.SliceContainsFunc(nm.GetAddresses(), tsaddr.PrefixIs4)
|
||||
selfV6Only := nm.GetAddresses().ContainsFunc(tsaddr.PrefixIs6) &&
|
||||
!nm.GetAddresses().ContainsFunc(tsaddr.PrefixIs4)
|
||||
dcfg.OnlyIPv6 = selfV6Only
|
||||
|
||||
// Populate MagicDNS records. We do this unconditionally so that
|
||||
@@ -4135,13 +4185,22 @@ func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs ipn.PrefsView, oneC
|
||||
netfilterKind = prefs.NetfilterKind()
|
||||
}
|
||||
|
||||
var doStatefulFiltering bool
|
||||
if v, ok := prefs.NoStatefulFiltering().Get(); ok && !v {
|
||||
// The preferences explicitly "do stateful filtering" is turned
|
||||
// off, or to expand the double negative, to do stateful
|
||||
// filtering. Do so.
|
||||
doStatefulFiltering = true
|
||||
}
|
||||
|
||||
rs := &router.Config{
|
||||
LocalAddrs: unmapIPPrefixes(cfg.Addresses),
|
||||
SubnetRoutes: unmapIPPrefixes(prefs.AdvertiseRoutes().AsSlice()),
|
||||
SNATSubnetRoutes: !prefs.NoSNAT(),
|
||||
NetfilterMode: prefs.NetfilterMode(),
|
||||
Routes: peerRoutes(b.logf, cfg.Peers, singleRouteThreshold),
|
||||
NetfilterKind: netfilterKind,
|
||||
LocalAddrs: unmapIPPrefixes(cfg.Addresses),
|
||||
SubnetRoutes: unmapIPPrefixes(prefs.AdvertiseRoutes().AsSlice()),
|
||||
SNATSubnetRoutes: !prefs.NoSNAT(),
|
||||
StatefulFiltering: doStatefulFiltering,
|
||||
NetfilterMode: prefs.NetfilterMode(),
|
||||
Routes: peerRoutes(b.logf, cfg.Peers, singleRouteThreshold),
|
||||
NetfilterKind: netfilterKind,
|
||||
}
|
||||
|
||||
if distro.Get() == distro.Synology {
|
||||
@@ -4267,12 +4326,20 @@ func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State, unlock unlock
|
||||
oldState := b.state
|
||||
b.state = newState
|
||||
prefs := b.pm.CurrentPrefs()
|
||||
|
||||
// Some temporary (2024-05-05) debugging code to help us catch
|
||||
// https://github.com/tailscale/tailscale/issues/11962 in the act.
|
||||
if prefs.WantRunning() &&
|
||||
prefs.ControlURLOrDefault() == ipn.DefaultControlURL &&
|
||||
envknob.Bool("TS_PANIC_IF_HIT_MAIN_CONTROL") {
|
||||
panic("[unexpected] use of main control server in integration test")
|
||||
}
|
||||
|
||||
netMap := b.netMap
|
||||
activeLogin := b.activeLogin
|
||||
authURL := b.authURL
|
||||
if newState == ipn.Running {
|
||||
b.authURL = ""
|
||||
b.authURLSticky = ""
|
||||
b.authURLTime = time.Time{}
|
||||
} else if oldState == ipn.Running {
|
||||
// Transitioning away from running.
|
||||
@@ -4327,14 +4394,6 @@ func (b *LocalBackend) enterStateLockedOnEntry(newState ipn.State, unlock unlock
|
||||
}
|
||||
}
|
||||
|
||||
// hasNodeKey reports whether a non-zero node key is present in the current
|
||||
// prefs.
|
||||
func (b *LocalBackend) hasNodeKey() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.hasNodeKeyLocked()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) hasNodeKeyLocked() bool {
|
||||
// we can't use b.Prefs(), because it strips the keys, oops!
|
||||
p := b.pm.CurrentPrefs()
|
||||
@@ -4432,6 +4491,12 @@ func (b *LocalBackend) nextStateLocked() ipn.State {
|
||||
// Or maybe just call the state machine from fewer places.
|
||||
func (b *LocalBackend) stateMachine() {
|
||||
unlock := b.lockAndGetUnlock()
|
||||
b.stateMachineLockedOnEntry(unlock)
|
||||
}
|
||||
|
||||
// stateMachineLockedOnEntry is like stateMachine but requires b.mu be held to
|
||||
// call it, but it unlocks b.mu when done (via unlock, a once func).
|
||||
func (b *LocalBackend) stateMachineLockedOnEntry(unlock unlockOnce) {
|
||||
b.enterStateLockedOnEntry(b.nextStateLocked(), unlock)
|
||||
}
|
||||
|
||||
@@ -4533,6 +4598,8 @@ func (b *LocalBackend) resetControlClientLocked() controlclient.Client {
|
||||
return nil
|
||||
}
|
||||
|
||||
b.authURL = ""
|
||||
|
||||
// When we clear the control client, stop any outstanding netmap expiry
|
||||
// timer; synthesizing a new netmap while we don't have a control
|
||||
// client will break things.
|
||||
@@ -4577,7 +4644,6 @@ func (b *LocalBackend) ResetForClientDisconnect() {
|
||||
}
|
||||
b.keyExpired = false
|
||||
b.authURL = ""
|
||||
b.authURLSticky = ""
|
||||
b.authURLTime = time.Time{}
|
||||
b.activeLogin = ""
|
||||
b.resetDialPlan()
|
||||
@@ -4814,13 +4880,6 @@ func (b *LocalBackend) updatePeersFromNetmapLocked(nm *netmap.NetworkMap) {
|
||||
}
|
||||
}
|
||||
|
||||
// driveTransport is an http.RoundTripper that uses the latest value of
|
||||
// b.Dialer().PeerAPITransport() for each round trip and imposes a short
|
||||
// dial timeout to avoid hanging on connecting to offline/unreachable hosts.
|
||||
type driveTransport struct {
|
||||
b *LocalBackend
|
||||
}
|
||||
|
||||
// responseBodyWrapper wraps an io.ReadCloser and stores
|
||||
// the number of bytesRead.
|
||||
type responseBodyWrapper struct {
|
||||
@@ -4874,6 +4933,20 @@ func (rbw *responseBodyWrapper) Close() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// driveTransport is an http.RoundTripper that wraps
|
||||
// b.Dialer().PeerAPITransport() with metrics tracking.
|
||||
type driveTransport struct {
|
||||
b *LocalBackend
|
||||
tr *http.Transport
|
||||
}
|
||||
|
||||
func (b *LocalBackend) newDriveTransport() *driveTransport {
|
||||
return &driveTransport{
|
||||
b: b,
|
||||
tr: b.Dialer().PeerAPITransport(),
|
||||
}
|
||||
}
|
||||
|
||||
func (dt *driveTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
||||
// Some WebDAV clients include origin and refer headers, which peerapi does
|
||||
// not like. Remove them.
|
||||
@@ -4931,18 +5004,7 @@ func (dt *driveTransport) RoundTrip(req *http.Request) (resp *http.Response, err
|
||||
}
|
||||
}()
|
||||
|
||||
// dialTimeout is fairly aggressive to avoid hangs on contacting offline or
|
||||
// unreachable hosts.
|
||||
dialTimeout := 1 * time.Second // TODO(oxtoacart): tune this
|
||||
|
||||
tr := dt.b.Dialer().PeerAPITransport().Clone()
|
||||
dialContext := tr.DialContext
|
||||
tr.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, dialTimeout)
|
||||
defer cancel()
|
||||
return dialContext(ctxWithTimeout, network, addr)
|
||||
}
|
||||
return tr.RoundTrip(req)
|
||||
return dt.tr.RoundTrip(req)
|
||||
}
|
||||
|
||||
// roundTraffic rounds bytes. This is used to preserve user privacy within logs.
|
||||
@@ -5958,7 +6020,8 @@ func (b *LocalBackend) resetForProfileChangeLockedOnEntry(unlock unlockOnce) err
|
||||
}
|
||||
b.lastServeConfJSON = mem.B(nil)
|
||||
b.serveConfig = ipn.ServeConfigView{}
|
||||
b.enterStateLockedOnEntry(ipn.NoState, unlock) // Reset state; releases b.mu
|
||||
b.lastSuggestedExitNode = lastSuggestedExitNode{} // Reset last suggested exit node.
|
||||
b.enterStateLockedOnEntry(ipn.NoState, unlock) // Reset state; releases b.mu
|
||||
b.health.SetLocalLogConfigHealth(nil)
|
||||
return b.Start(ipn.Options{})
|
||||
}
|
||||
@@ -6335,6 +6398,7 @@ func mayDeref[T any](p *T) (v T) {
|
||||
|
||||
var ErrNoPreferredDERP = errors.New("no preferred DERP, try again later")
|
||||
var ErrCannotSuggestExitNode = errors.New("unable to suggest an exit node, try again later")
|
||||
var ErrUnableToSuggestLastExitNode = errors.New("unable to suggest last exit node")
|
||||
|
||||
// SuggestExitNode computes a suggestion based on the current netmap and last netcheck report. If
|
||||
// there are multiple equally good options, one is selected at random, so the result is not stable. To be
|
||||
@@ -6347,21 +6411,59 @@ func (b *LocalBackend) SuggestExitNode() (response apitype.ExitNodeSuggestionRes
|
||||
b.mu.Lock()
|
||||
lastReport := b.MagicConn().GetLastNetcheckReport(b.ctx)
|
||||
netMap := b.netMap
|
||||
lastSuggestedExitNode := b.lastSuggestedExitNode
|
||||
b.mu.Unlock()
|
||||
if lastReport == nil || netMap == nil {
|
||||
return response, ErrCannotSuggestExitNode
|
||||
last, err := lastSuggestedExitNode.asAPIType()
|
||||
if err != nil {
|
||||
return response, ErrCannotSuggestExitNode
|
||||
}
|
||||
return last, err
|
||||
}
|
||||
seed := time.Now().UnixNano()
|
||||
r := rand.New(rand.NewSource(seed))
|
||||
return suggestExitNode(lastReport, netMap, r)
|
||||
res, err := suggestExitNode(lastReport, netMap, r)
|
||||
if err != nil {
|
||||
last, err := lastSuggestedExitNode.asAPIType()
|
||||
if err != nil {
|
||||
return response, ErrCannotSuggestExitNode
|
||||
}
|
||||
return last, err
|
||||
}
|
||||
b.mu.Lock()
|
||||
b.lastSuggestedExitNode.id = res.ID
|
||||
b.lastSuggestedExitNode.name = res.Name
|
||||
b.mu.Unlock()
|
||||
return res, err
|
||||
}
|
||||
|
||||
// asAPIType formats a response with the last suggested exit node's ID and name.
|
||||
// Returns error if there is no id or name.
|
||||
// Used as a fallback before returning a nil response and error.
|
||||
func (n lastSuggestedExitNode) asAPIType() (res apitype.ExitNodeSuggestionResponse, _ error) {
|
||||
if n.id != "" && n.name != "" {
|
||||
res.ID = n.id
|
||||
res.Name = n.name
|
||||
return res, nil
|
||||
}
|
||||
return res, ErrUnableToSuggestLastExitNode
|
||||
}
|
||||
|
||||
func suggestExitNode(report *netcheck.Report, netMap *netmap.NetworkMap, r *rand.Rand) (res apitype.ExitNodeSuggestionResponse, err error) {
|
||||
if report.PreferredDERP == 0 {
|
||||
return res, ErrNoPreferredDERP
|
||||
}
|
||||
var allowedCandidates set.Set[string]
|
||||
if allowed, err := syspolicy.GetStringArray(syspolicy.AllowedSuggestedExitNodes, nil); err != nil {
|
||||
return res, fmt.Errorf("unable to read %s policy: %w", syspolicy.AllowedSuggestedExitNodes, err)
|
||||
} else if allowed != nil && len(allowed) > 0 {
|
||||
allowedCandidates = set.SetOf(allowed)
|
||||
}
|
||||
candidates := make([]tailcfg.NodeView, 0, len(netMap.Peers))
|
||||
for _, peer := range netMap.Peers {
|
||||
if allowedCandidates != nil && !allowedCandidates.Contains(string(peer.StableID())) {
|
||||
continue
|
||||
}
|
||||
if peer.CapMap().Has(tailcfg.NodeAttrSuggestExitNode) && tsaddr.ContainsExitRoutes(peer.AllowedIPs()) {
|
||||
candidates = append(candidates, peer)
|
||||
}
|
||||
|
||||
@@ -25,10 +25,12 @@ import (
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"tailscale.com/appc"
|
||||
"tailscale.com/appc/appctest"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/clientupdate"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/drive/driveimpl"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/net/netcheck"
|
||||
@@ -429,7 +431,7 @@ func newTestLocalBackend(t testing.TB) *LocalBackend {
|
||||
sys := new(tsd.System)
|
||||
store := new(mem.Store)
|
||||
sys.Set(store)
|
||||
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set)
|
||||
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker())
|
||||
if err != nil {
|
||||
t.Fatalf("NewFakeUserspaceEngine: %v", err)
|
||||
}
|
||||
@@ -1593,6 +1595,9 @@ type mockSyspolicyHandler struct {
|
||||
// queried by the current test. If the policy is expected but unset, then
|
||||
// use nil, otherwise use a string equal to the policy's desired value.
|
||||
stringPolicies map[syspolicy.Key]*string
|
||||
// stringArrayPolicies is the collection of policies that we expected to see
|
||||
// queries by the current test, that return policy string arrays.
|
||||
stringArrayPolicies map[syspolicy.Key][]string
|
||||
// failUnknownPolicies is set if policies other than those in stringPolicies
|
||||
// (uint64 or bool policies are not supported by mockSyspolicyHandler yet)
|
||||
// should be considered a test failure if they are queried.
|
||||
@@ -1630,6 +1635,12 @@ func (h *mockSyspolicyHandler) ReadStringArray(key string) ([]string, error) {
|
||||
if h.failUnknownPolicies {
|
||||
h.t.Errorf("ReadStringArray(%q) unexpectedly called", key)
|
||||
}
|
||||
if s, ok := h.stringArrayPolicies[syspolicy.Key(key)]; ok {
|
||||
if s == nil {
|
||||
return []string{}, syspolicy.ErrNoSuchKey
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
return nil, syspolicy.ErrNoSuchKey
|
||||
}
|
||||
|
||||
@@ -1849,7 +1860,7 @@ func TestSetExitNodeIDPolicy(t *testing.T) {
|
||||
if test.prefs == nil {
|
||||
test.prefs = ipn.NewPrefs()
|
||||
}
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, new(health.Tracker)))
|
||||
pm.prefs = test.prefs.View()
|
||||
b.netMap = test.nm
|
||||
b.pm = pm
|
||||
@@ -2131,7 +2142,7 @@ func TestApplySysPolicy(t *testing.T) {
|
||||
wantPrefs.ControlURL = ipn.DefaultControlURL
|
||||
}
|
||||
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, new(health.Tracker)))
|
||||
pm.prefs = usePrefs.View()
|
||||
|
||||
b := newTestBackend(t)
|
||||
@@ -3436,6 +3447,486 @@ func TestMinLatencyDERPregion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastSuggestedExitNodeAsAPIType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
lastSuggestedExitNode lastSuggestedExitNode
|
||||
wantRes apitype.ExitNodeSuggestionResponse
|
||||
wantLastSuggestedExitNode lastSuggestedExitNode
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
name: "last suggested exit node is populated",
|
||||
lastSuggestedExitNode: lastSuggestedExitNode{id: "test", name: "test"},
|
||||
wantRes: apitype.ExitNodeSuggestionResponse{ID: "test", Name: "test"},
|
||||
wantLastSuggestedExitNode: lastSuggestedExitNode{id: "test", name: "test"},
|
||||
},
|
||||
{
|
||||
name: "last suggested exit node is not populated",
|
||||
wantErr: ErrUnableToSuggestLastExitNode,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := tt.lastSuggestedExitNode.asAPIType()
|
||||
if got != tt.wantRes || err != tt.wantErr {
|
||||
t.Errorf("got %v error %v, want %v error %v", got, err, tt.wantRes, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalBackendSuggestExitNode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
lastSuggestedExitNode lastSuggestedExitNode
|
||||
report *netcheck.Report
|
||||
netMap netmap.NetworkMap
|
||||
allowedSuggestedExitNodes []string
|
||||
wantID tailcfg.StableNodeID
|
||||
wantName string
|
||||
wantErr error
|
||||
wantLastSuggestedExitNode lastSuggestedExitNode
|
||||
}{
|
||||
{
|
||||
name: "nil netmap, returns last suggested exit node",
|
||||
lastSuggestedExitNode: lastSuggestedExitNode{name: "test", id: "test"},
|
||||
report: &netcheck.Report{
|
||||
RegionLatency: map[int]time.Duration{
|
||||
1: 0,
|
||||
2: -1,
|
||||
3: 0,
|
||||
},
|
||||
},
|
||||
wantID: "test",
|
||||
wantName: "test",
|
||||
wantLastSuggestedExitNode: lastSuggestedExitNode{name: "test", id: "test"},
|
||||
},
|
||||
{
|
||||
name: "nil report, returns last suggested exit node",
|
||||
lastSuggestedExitNode: lastSuggestedExitNode{name: "test", id: "test"},
|
||||
netMap: netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
Addresses: []netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.1.1/32"),
|
||||
netip.MustParsePrefix("fe70::1/128"),
|
||||
},
|
||||
}).View(),
|
||||
DERPMap: &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {},
|
||||
2: {},
|
||||
3: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantID: "test",
|
||||
wantName: "test",
|
||||
wantLastSuggestedExitNode: lastSuggestedExitNode{name: "test", id: "test"},
|
||||
},
|
||||
{
|
||||
name: "found better derp node, last suggested exit node updates",
|
||||
lastSuggestedExitNode: lastSuggestedExitNode{name: "test", id: "test"},
|
||||
report: &netcheck.Report{
|
||||
RegionLatency: map[int]time.Duration{
|
||||
1: 10,
|
||||
2: 10,
|
||||
3: 5,
|
||||
},
|
||||
PreferredDERP: 1,
|
||||
},
|
||||
netMap: netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
Addresses: []netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.1.1/32"),
|
||||
netip.MustParsePrefix("fe70::1/128"),
|
||||
},
|
||||
}).View(),
|
||||
DERPMap: &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {},
|
||||
2: {},
|
||||
3: {},
|
||||
},
|
||||
},
|
||||
Peers: []tailcfg.NodeView{
|
||||
(&tailcfg.Node{
|
||||
ID: 2,
|
||||
StableID: "test",
|
||||
Name: "test",
|
||||
DERP: "127.3.3.40:1",
|
||||
AllowedIPs: []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"),
|
||||
},
|
||||
CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
|
||||
tailcfg.NodeAttrSuggestExitNode: {},
|
||||
}),
|
||||
}).View(),
|
||||
(&tailcfg.Node{
|
||||
ID: 3,
|
||||
StableID: "foo",
|
||||
Name: "foo",
|
||||
DERP: "127.3.3.40:3",
|
||||
AllowedIPs: []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"),
|
||||
},
|
||||
CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
|
||||
tailcfg.NodeAttrSuggestExitNode: {},
|
||||
}),
|
||||
}).View(),
|
||||
},
|
||||
},
|
||||
wantID: "foo",
|
||||
wantName: "foo",
|
||||
wantLastSuggestedExitNode: lastSuggestedExitNode{name: "foo", id: "foo"},
|
||||
},
|
||||
{
|
||||
name: "found better mullvad node, last suggested exit node updates",
|
||||
lastSuggestedExitNode: lastSuggestedExitNode{name: "San Jose", id: "3"},
|
||||
report: &netcheck.Report{
|
||||
RegionLatency: map[int]time.Duration{
|
||||
1: 0,
|
||||
2: 0,
|
||||
3: 0,
|
||||
},
|
||||
PreferredDERP: 1,
|
||||
},
|
||||
netMap: netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
Addresses: []netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.1.1/32"),
|
||||
netip.MustParsePrefix("fe70::1/128"),
|
||||
},
|
||||
}).View(),
|
||||
DERPMap: &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {
|
||||
Latitude: 40.73061,
|
||||
Longitude: -73.935242,
|
||||
},
|
||||
2: {},
|
||||
3: {},
|
||||
},
|
||||
},
|
||||
Peers: []tailcfg.NodeView{
|
||||
(&tailcfg.Node{
|
||||
ID: 2,
|
||||
StableID: "2",
|
||||
AllowedIPs: []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"),
|
||||
},
|
||||
Name: "Dallas",
|
||||
Hostinfo: (&tailcfg.Hostinfo{
|
||||
Location: &tailcfg.Location{
|
||||
Latitude: 32.89748,
|
||||
Longitude: -97.040443,
|
||||
Priority: 100,
|
||||
},
|
||||
}).View(),
|
||||
CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
|
||||
tailcfg.NodeAttrSuggestExitNode: {},
|
||||
}),
|
||||
}).View(),
|
||||
(&tailcfg.Node{
|
||||
ID: 3,
|
||||
StableID: "3",
|
||||
AllowedIPs: []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"),
|
||||
},
|
||||
Name: "San Jose",
|
||||
Hostinfo: (&tailcfg.Hostinfo{
|
||||
Location: &tailcfg.Location{
|
||||
Latitude: 37.3382082,
|
||||
Longitude: -121.8863286,
|
||||
Priority: 20,
|
||||
},
|
||||
}).View(),
|
||||
CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
|
||||
tailcfg.NodeAttrSuggestExitNode: {},
|
||||
}),
|
||||
}).View(),
|
||||
},
|
||||
},
|
||||
wantID: "2",
|
||||
wantName: "Dallas",
|
||||
wantLastSuggestedExitNode: lastSuggestedExitNode{name: "Dallas", id: "2"},
|
||||
},
|
||||
{
|
||||
name: "ErrNoPreferredDERP, use last suggested exit node",
|
||||
lastSuggestedExitNode: lastSuggestedExitNode{name: "test", id: "test"},
|
||||
report: &netcheck.Report{
|
||||
RegionLatency: map[int]time.Duration{
|
||||
1: 10,
|
||||
2: 10,
|
||||
3: 5,
|
||||
},
|
||||
PreferredDERP: 0,
|
||||
},
|
||||
netMap: netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
Addresses: []netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.1.1/32"),
|
||||
netip.MustParsePrefix("fe70::1/128"),
|
||||
},
|
||||
}).View(),
|
||||
DERPMap: &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {},
|
||||
2: {},
|
||||
3: {},
|
||||
},
|
||||
},
|
||||
Peers: []tailcfg.NodeView{
|
||||
(&tailcfg.Node{
|
||||
ID: 2,
|
||||
StableID: "test",
|
||||
Name: "test",
|
||||
DERP: "127.3.3.40:1",
|
||||
AllowedIPs: []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"),
|
||||
},
|
||||
CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
|
||||
tailcfg.NodeAttrSuggestExitNode: {},
|
||||
}),
|
||||
}).View(),
|
||||
(&tailcfg.Node{
|
||||
ID: 3,
|
||||
StableID: "foo",
|
||||
Name: "foo",
|
||||
DERP: "127.3.3.40:3",
|
||||
AllowedIPs: []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"),
|
||||
},
|
||||
CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
|
||||
tailcfg.NodeAttrSuggestExitNode: {},
|
||||
}),
|
||||
}).View(),
|
||||
},
|
||||
},
|
||||
wantID: "test",
|
||||
wantName: "test",
|
||||
wantLastSuggestedExitNode: lastSuggestedExitNode{name: "test", id: "test"},
|
||||
},
|
||||
{
|
||||
name: "ErrNoPreferredDERP, use last suggested exit node",
|
||||
lastSuggestedExitNode: lastSuggestedExitNode{name: "test", id: "test"},
|
||||
report: &netcheck.Report{
|
||||
RegionLatency: map[int]time.Duration{
|
||||
1: 10,
|
||||
2: 10,
|
||||
3: 5,
|
||||
},
|
||||
PreferredDERP: 0,
|
||||
},
|
||||
netMap: netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
Addresses: []netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.1.1/32"),
|
||||
netip.MustParsePrefix("fe70::1/128"),
|
||||
},
|
||||
}).View(),
|
||||
DERPMap: &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {},
|
||||
2: {},
|
||||
3: {},
|
||||
},
|
||||
},
|
||||
Peers: []tailcfg.NodeView{
|
||||
(&tailcfg.Node{
|
||||
ID: 2,
|
||||
StableID: "test",
|
||||
Name: "test",
|
||||
DERP: "127.3.3.40:1",
|
||||
AllowedIPs: []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"),
|
||||
},
|
||||
CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
|
||||
tailcfg.NodeAttrSuggestExitNode: {},
|
||||
}),
|
||||
}).View(),
|
||||
(&tailcfg.Node{
|
||||
ID: 3,
|
||||
StableID: "foo",
|
||||
Name: "foo",
|
||||
DERP: "127.3.3.40:3",
|
||||
AllowedIPs: []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"),
|
||||
},
|
||||
CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
|
||||
tailcfg.NodeAttrSuggestExitNode: {},
|
||||
}),
|
||||
}).View(),
|
||||
},
|
||||
},
|
||||
wantID: "test",
|
||||
wantName: "test",
|
||||
wantLastSuggestedExitNode: lastSuggestedExitNode{name: "test", id: "test"},
|
||||
},
|
||||
{
|
||||
name: "unable to use last suggested exit node",
|
||||
report: &netcheck.Report{
|
||||
RegionLatency: map[int]time.Duration{
|
||||
1: 10,
|
||||
2: 10,
|
||||
3: 5,
|
||||
},
|
||||
PreferredDERP: 0,
|
||||
},
|
||||
wantErr: ErrCannotSuggestExitNode,
|
||||
},
|
||||
{
|
||||
name: "only pick from allowed suggested exit nodes",
|
||||
lastSuggestedExitNode: lastSuggestedExitNode{name: "test", id: "test"},
|
||||
report: &netcheck.Report{
|
||||
RegionLatency: map[int]time.Duration{
|
||||
1: 10,
|
||||
2: 10,
|
||||
3: 5,
|
||||
},
|
||||
PreferredDERP: 1,
|
||||
},
|
||||
netMap: netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
Addresses: []netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.1.1/32"),
|
||||
netip.MustParsePrefix("fe70::1/128"),
|
||||
},
|
||||
}).View(),
|
||||
DERPMap: &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {},
|
||||
2: {},
|
||||
3: {},
|
||||
},
|
||||
},
|
||||
Peers: []tailcfg.NodeView{
|
||||
(&tailcfg.Node{
|
||||
ID: 2,
|
||||
StableID: "test",
|
||||
Name: "test",
|
||||
DERP: "127.3.3.40:1",
|
||||
AllowedIPs: []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"),
|
||||
},
|
||||
CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
|
||||
tailcfg.NodeAttrSuggestExitNode: {},
|
||||
tailcfg.NodeAttrAutoExitNode: {},
|
||||
}),
|
||||
}).View(),
|
||||
(&tailcfg.Node{
|
||||
ID: 3,
|
||||
StableID: "foo",
|
||||
Name: "foo",
|
||||
DERP: "127.3.3.40:3",
|
||||
AllowedIPs: []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"),
|
||||
},
|
||||
CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
|
||||
tailcfg.NodeAttrSuggestExitNode: {},
|
||||
tailcfg.NodeAttrAutoExitNode: {},
|
||||
}),
|
||||
}).View(),
|
||||
},
|
||||
},
|
||||
allowedSuggestedExitNodes: []string{"test"},
|
||||
wantID: "test",
|
||||
wantName: "test",
|
||||
wantLastSuggestedExitNode: lastSuggestedExitNode{name: "test", id: "test"},
|
||||
},
|
||||
{
|
||||
name: "allowed suggested exit nodes not nil but length 0",
|
||||
lastSuggestedExitNode: lastSuggestedExitNode{name: "test", id: "test"},
|
||||
report: &netcheck.Report{
|
||||
RegionLatency: map[int]time.Duration{
|
||||
1: 10,
|
||||
2: 10,
|
||||
3: 5,
|
||||
},
|
||||
PreferredDERP: 1,
|
||||
},
|
||||
netMap: netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{
|
||||
Addresses: []netip.Prefix{
|
||||
netip.MustParsePrefix("100.64.1.1/32"),
|
||||
netip.MustParsePrefix("fe70::1/128"),
|
||||
},
|
||||
}).View(),
|
||||
DERPMap: &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {},
|
||||
2: {},
|
||||
3: {},
|
||||
},
|
||||
},
|
||||
Peers: []tailcfg.NodeView{
|
||||
(&tailcfg.Node{
|
||||
ID: 2,
|
||||
StableID: "test",
|
||||
Name: "test",
|
||||
DERP: "127.3.3.40:1",
|
||||
AllowedIPs: []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"),
|
||||
},
|
||||
CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
|
||||
tailcfg.NodeAttrSuggestExitNode: {},
|
||||
tailcfg.NodeAttrAutoExitNode: {},
|
||||
}),
|
||||
}).View(),
|
||||
(&tailcfg.Node{
|
||||
ID: 3,
|
||||
StableID: "foo",
|
||||
Name: "foo",
|
||||
DERP: "127.3.3.40:3",
|
||||
AllowedIPs: []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/0"), netip.MustParsePrefix("::/0"),
|
||||
},
|
||||
CapMap: (tailcfg.NodeCapMap)(map[tailcfg.NodeCapability][]tailcfg.RawMessage{
|
||||
tailcfg.NodeAttrSuggestExitNode: {},
|
||||
tailcfg.NodeAttrAutoExitNode: {},
|
||||
}),
|
||||
}).View(),
|
||||
},
|
||||
},
|
||||
allowedSuggestedExitNodes: []string{},
|
||||
wantID: "foo",
|
||||
wantName: "foo",
|
||||
wantLastSuggestedExitNode: lastSuggestedExitNode{name: "foo", id: "foo"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
lb := newTestLocalBackend(t)
|
||||
msh := &mockSyspolicyHandler{
|
||||
t: t,
|
||||
stringArrayPolicies: map[syspolicy.Key][]string{
|
||||
syspolicy.AllowedSuggestedExitNodes: nil,
|
||||
},
|
||||
}
|
||||
if len(tt.allowedSuggestedExitNodes) != 0 {
|
||||
msh.stringArrayPolicies[syspolicy.AllowedSuggestedExitNodes] = tt.allowedSuggestedExitNodes
|
||||
}
|
||||
syspolicy.SetHandlerForTest(t, msh)
|
||||
lb.lastSuggestedExitNode = tt.lastSuggestedExitNode
|
||||
lb.netMap = &tt.netMap
|
||||
lb.sys.MagicSock.Get().SetLastNetcheckReportForTest(context.Background(), tt.report)
|
||||
got, err := lb.SuggestExitNode()
|
||||
if got.ID != tt.wantID {
|
||||
t.Errorf("ID=%v, want=%v", got.ID, tt.wantID)
|
||||
}
|
||||
if got.Name != tt.wantName {
|
||||
t.Errorf("Name=%v, want=%v", got.Name, tt.wantName)
|
||||
}
|
||||
if lb.lastSuggestedExitNode != tt.wantLastSuggestedExitNode {
|
||||
t.Errorf("lastSuggestedExitNode=%v, want=%v", lb.lastSuggestedExitNode, tt.wantLastSuggestedExitNode)
|
||||
}
|
||||
if err != tt.wantErr {
|
||||
t.Errorf("Error=%v, want=%v", err, tt.wantErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableAutoUpdates(t *testing.T) {
|
||||
lb := newTestLocalBackend(t)
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ func TestLocalLogLines(t *testing.T) {
|
||||
sys := new(tsd.System)
|
||||
store := new(mem.Store)
|
||||
sys.Set(store)
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set)
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"tailscale.com/health/healthmsg"
|
||||
@@ -27,10 +28,12 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/tkatype"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
// TODO(tom): RPC retry/backoff was broken and has been removed. Fix?
|
||||
@@ -66,6 +69,7 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
|
||||
return // TKA not enabled.
|
||||
}
|
||||
|
||||
tracker := rotationTracker{logf: b.logf}
|
||||
var toDelete map[int]bool // peer index => true
|
||||
for i, p := range nm.Peers {
|
||||
if p.UnsignedPeerAPIOnly() {
|
||||
@@ -76,21 +80,32 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
|
||||
b.logf("Network lock is dropping peer %v(%v) due to missing signature", p.ID(), p.StableID())
|
||||
mak.Set(&toDelete, i, true)
|
||||
} else {
|
||||
if err := b.tka.authority.NodeKeyAuthorized(p.Key(), p.KeySignature().AsSlice()); err != nil {
|
||||
details, err := b.tka.authority.NodeKeyAuthorizedWithDetails(p.Key(), p.KeySignature().AsSlice())
|
||||
if err != nil {
|
||||
b.logf("Network lock is dropping peer %v(%v) due to failed signature check: %v", p.ID(), p.StableID(), err)
|
||||
mak.Set(&toDelete, i, true)
|
||||
continue
|
||||
}
|
||||
if details != nil {
|
||||
// Rotation details are returned when the node key is signed by a valid SigRotation signature.
|
||||
tracker.addRotationDetails(p.Key(), details)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
obsoleteByRotation := tracker.obsoleteKeys()
|
||||
|
||||
// nm.Peers is ordered, so deletion must be order-preserving.
|
||||
if len(toDelete) > 0 {
|
||||
if len(toDelete) > 0 || len(obsoleteByRotation) > 0 {
|
||||
peers := make([]tailcfg.NodeView, 0, len(nm.Peers))
|
||||
filtered := make([]ipnstate.TKAFilteredPeer, 0, len(toDelete))
|
||||
filtered := make([]ipnstate.TKAFilteredPeer, 0, len(toDelete)+len(obsoleteByRotation))
|
||||
for i, p := range nm.Peers {
|
||||
if !toDelete[i] {
|
||||
if !toDelete[i] && !obsoleteByRotation.Contains(p.Key()) {
|
||||
peers = append(peers, p)
|
||||
} else {
|
||||
if obsoleteByRotation.Contains(p.Key()) {
|
||||
b.logf("Network lock is dropping peer %v(%v) due to key rotation", p.ID(), p.StableID())
|
||||
}
|
||||
// Record information about the node we filtered out.
|
||||
fp := ipnstate.TKAFilteredPeer{
|
||||
Name: p.Name(),
|
||||
@@ -122,6 +137,84 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
|
||||
}
|
||||
}
|
||||
|
||||
// rotationTracker determines the set of node keys that are made obsolete by key
|
||||
// rotation.
|
||||
// - for each SigRotation signature, all previous node keys referenced by the
|
||||
// nested signatures are marked as obsolete.
|
||||
// - if there are multiple SigRotation signatures tracing back to the same
|
||||
// wrapping pubkey (e.g. if a node is cloned with all its keys), we keep
|
||||
// just one of them, marking the others as obsolete.
|
||||
type rotationTracker struct {
|
||||
// obsolete is the set of node keys that are obsolete due to key rotation.
|
||||
// users of rotationTracker should use the obsoleteKeys method for complete results.
|
||||
obsolete set.Set[key.NodePublic]
|
||||
|
||||
// byWrappingKey keeps track of rotation details per wrapping pubkey.
|
||||
byWrappingKey map[string][]sigRotationDetails
|
||||
|
||||
logf logger.Logf
|
||||
}
|
||||
|
||||
// sigRotationDetails holds information about a node key signed by a SigRotation.
|
||||
type sigRotationDetails struct {
|
||||
np key.NodePublic
|
||||
numPrevKeys int
|
||||
}
|
||||
|
||||
// addRotationDetails records the rotation signature details for a node key.
|
||||
func (r *rotationTracker) addRotationDetails(np key.NodePublic, d *tka.RotationDetails) {
|
||||
r.obsolete.Make()
|
||||
r.obsolete.AddSlice(d.PrevNodeKeys)
|
||||
rd := sigRotationDetails{
|
||||
np: np,
|
||||
numPrevKeys: len(d.PrevNodeKeys),
|
||||
}
|
||||
if r.byWrappingKey == nil {
|
||||
r.byWrappingKey = make(map[string][]sigRotationDetails)
|
||||
}
|
||||
wp := string(d.WrappingPubkey)
|
||||
r.byWrappingKey[wp] = append(r.byWrappingKey[wp], rd)
|
||||
}
|
||||
|
||||
// obsoleteKeys returns the set of node keys that are obsolete due to key rotation.
|
||||
func (r *rotationTracker) obsoleteKeys() set.Set[key.NodePublic] {
|
||||
for _, v := range r.byWrappingKey {
|
||||
// If there are multiple rotation signatures with the same wrapping
|
||||
// pubkey, we need to decide which one is the "latest", and keep it.
|
||||
// The signature with the largest number of previous keys is likely to
|
||||
// be the latest, unless it has been marked as obsolete (rotated out) by
|
||||
// another signature (which might happen in the future if we start
|
||||
// compacting long rotated signature chains).
|
||||
slices.SortStableFunc(v, func(a, b sigRotationDetails) int {
|
||||
// Group all obsolete keys after non-obsolete keys.
|
||||
if ao, bo := r.obsolete.Contains(a.np), r.obsolete.Contains(b.np); ao != bo {
|
||||
if ao {
|
||||
return 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
// Sort by decreasing number of previous keys.
|
||||
return b.numPrevKeys - a.numPrevKeys
|
||||
})
|
||||
// If there are several signatures with the same number of previous
|
||||
// keys, we cannot determine which one is the latest, so all of them are
|
||||
// rejected for safety.
|
||||
if len(v) >= 2 && v[0].numPrevKeys == v[1].numPrevKeys {
|
||||
r.logf("at least two nodes (%s and %s) have equally valid rotation signatures with the same wrapping pubkey, rejecting", v[0].np, v[1].np)
|
||||
for _, rd := range v {
|
||||
r.obsolete.Add(rd.np)
|
||||
}
|
||||
} else {
|
||||
// The first key in v is the one with the longest chain of previous
|
||||
// keys, so it must be the newest one. Mark all older keys as obsolete.
|
||||
for _, rd := range v[1:] {
|
||||
r.obsolete.Add(rd.np)
|
||||
}
|
||||
}
|
||||
}
|
||||
return r.obsolete
|
||||
}
|
||||
|
||||
// tkaSyncIfNeeded examines TKA info reported from the control plane,
|
||||
// performing the steps necessary to synchronize local tka state.
|
||||
//
|
||||
@@ -423,8 +516,12 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
|
||||
copy(head[:], h[:])
|
||||
|
||||
var selfAuthorized bool
|
||||
nodeKeySignature := &tka.NodeKeySignature{}
|
||||
if b.netMap != nil {
|
||||
selfAuthorized = b.tka.authority.NodeKeyAuthorized(b.netMap.SelfNode.Key(), b.netMap.SelfNode.KeySignature().AsSlice()) == nil
|
||||
if err := nodeKeySignature.Unserialize(b.netMap.SelfNode.KeySignature().AsSlice()); err != nil {
|
||||
b.logf("failed to decode self node key signature: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
keys := b.tka.authority.Keys()
|
||||
@@ -445,14 +542,15 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
|
||||
stateID1, _ := b.tka.authority.StateIDs()
|
||||
|
||||
return &ipnstate.NetworkLockStatus{
|
||||
Enabled: true,
|
||||
Head: &head,
|
||||
PublicKey: nlPriv.Public(),
|
||||
NodeKey: nodeKey,
|
||||
NodeKeySigned: selfAuthorized,
|
||||
TrustedKeys: outKeys,
|
||||
FilteredPeers: filtered,
|
||||
StateID: stateID1,
|
||||
Enabled: true,
|
||||
Head: &head,
|
||||
PublicKey: nlPriv.Public(),
|
||||
NodeKey: nodeKey,
|
||||
NodeKeySigned: selfAuthorized,
|
||||
NodeKeySignature: nodeKeySignature,
|
||||
TrustedKeys: outKeys,
|
||||
FilteredPeers: filtered,
|
||||
StateID: stateID1,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,10 +13,14 @@ import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
go4mem "go4.org/mem"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
@@ -29,6 +33,7 @@ import (
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/tkatype"
|
||||
"tailscale.com/util/must"
|
||||
"tailscale.com/util/set"
|
||||
)
|
||||
|
||||
type observerFunc func(controlclient.Status)
|
||||
@@ -148,7 +153,7 @@ func TestTKAEnablementFlow(t *testing.T) {
|
||||
temp := t.TempDir()
|
||||
|
||||
cc := fakeControlClient(t, client)
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, new(health.Tracker)))
|
||||
must.Do(pm.SetPrefs((&ipn.Prefs{
|
||||
Persist: &persist.Persist{
|
||||
PrivateNodeKey: nodePriv,
|
||||
@@ -188,7 +193,7 @@ func TestTKADisablementFlow(t *testing.T) {
|
||||
nlPriv := key.NewNLPrivate()
|
||||
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
|
||||
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, new(health.Tracker)))
|
||||
must.Do(pm.SetPrefs((&ipn.Prefs{
|
||||
Persist: &persist.Persist{
|
||||
PrivateNodeKey: nodePriv,
|
||||
@@ -380,7 +385,7 @@ func TestTKASync(t *testing.T) {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
nodePriv := key.NewNode()
|
||||
nlPriv := key.NewNLPrivate()
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, new(health.Tracker)))
|
||||
must.Do(pm.SetPrefs((&ipn.Prefs{
|
||||
Persist: &persist.Persist{
|
||||
PrivateNodeKey: nodePriv,
|
||||
@@ -562,18 +567,32 @@ func TestTKAFilterNetmap(t *testing.T) {
|
||||
}
|
||||
n4Sig.Signature[3] = 42 // mess up the signature
|
||||
n4Sig.Signature[4] = 42 // mess up the signature
|
||||
n5GoodSig, err := signNodeKey(tailcfg.TKASignInfo{NodePublic: n5.Public()}, nlPriv)
|
||||
|
||||
n5nl := key.NewNLPrivate()
|
||||
n5InitialSig, err := signNodeKey(tailcfg.TKASignInfo{NodePublic: n5.Public(), RotationPubkey: n5nl.Public().Verifier()}, nlPriv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resign := func(nl key.NLPrivate, currentSig tkatype.MarshaledSignature) (key.NodePrivate, tkatype.MarshaledSignature) {
|
||||
nk := key.NewNode()
|
||||
sig, err := tka.ResignNKS(nl, nk.Public(), currentSig)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return nk, sig
|
||||
}
|
||||
|
||||
n5Rotated, n5RotatedSig := resign(n5nl, n5InitialSig.Serialize())
|
||||
|
||||
nm := &netmap.NetworkMap{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()},
|
||||
{ID: 2, Key: n2.Public(), KeySignature: nil}, // missing sig
|
||||
{ID: 3, Key: n3.Public(), KeySignature: n1GoodSig.Serialize()}, // someone elses sig
|
||||
{ID: 4, Key: n4.Public(), KeySignature: n4Sig.Serialize()}, // messed-up signature
|
||||
{ID: 5, Key: n5.Public(), KeySignature: n5GoodSig.Serialize()},
|
||||
{ID: 2, Key: n2.Public(), KeySignature: nil}, // missing sig
|
||||
{ID: 3, Key: n3.Public(), KeySignature: n1GoodSig.Serialize()}, // someone elses sig
|
||||
{ID: 4, Key: n4.Public(), KeySignature: n4Sig.Serialize()}, // messed-up signature
|
||||
{ID: 50, Key: n5.Public(), KeySignature: n5InitialSig.Serialize()}, // rotated
|
||||
{ID: 51, Key: n5Rotated.Public(), KeySignature: n5RotatedSig},
|
||||
}),
|
||||
}
|
||||
|
||||
@@ -585,12 +604,39 @@ func TestTKAFilterNetmap(t *testing.T) {
|
||||
|
||||
want := nodeViews([]*tailcfg.Node{
|
||||
{ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()},
|
||||
{ID: 5, Key: n5.Public(), KeySignature: n5GoodSig.Serialize()},
|
||||
{ID: 51, Key: n5Rotated.Public(), KeySignature: n5RotatedSig},
|
||||
})
|
||||
nodePubComparer := cmp.Comparer(func(x, y key.NodePublic) bool {
|
||||
return x.Raw32() == y.Raw32()
|
||||
})
|
||||
if diff := cmp.Diff(nm.Peers, want, nodePubComparer); diff != "" {
|
||||
if diff := cmp.Diff(want, nm.Peers, nodePubComparer); diff != "" {
|
||||
t.Errorf("filtered netmap differs (-want, +got):\n%s", diff)
|
||||
}
|
||||
|
||||
// Create two more node signatures using the same wrapping key as n5.
|
||||
// Since they have the same rotation chain, both will be filtered out.
|
||||
n7, n7Sig := resign(n5nl, n5RotatedSig)
|
||||
n8, n8Sig := resign(n5nl, n5RotatedSig)
|
||||
|
||||
nm = &netmap.NetworkMap{
|
||||
Peers: nodeViews([]*tailcfg.Node{
|
||||
{ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()},
|
||||
{ID: 2, Key: n2.Public(), KeySignature: nil}, // missing sig
|
||||
{ID: 3, Key: n3.Public(), KeySignature: n1GoodSig.Serialize()}, // someone elses sig
|
||||
{ID: 4, Key: n4.Public(), KeySignature: n4Sig.Serialize()}, // messed-up signature
|
||||
{ID: 50, Key: n5.Public(), KeySignature: n5InitialSig.Serialize()}, // rotated
|
||||
{ID: 51, Key: n5Rotated.Public(), KeySignature: n5RotatedSig}, // rotated
|
||||
{ID: 7, Key: n7.Public(), KeySignature: n7Sig}, // same rotation chain as n8
|
||||
{ID: 8, Key: n8.Public(), KeySignature: n8Sig}, // same rotation chain as n7
|
||||
}),
|
||||
}
|
||||
|
||||
b.tkaFilterNetmapLocked(nm)
|
||||
|
||||
want = nodeViews([]*tailcfg.Node{
|
||||
{ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()},
|
||||
})
|
||||
if diff := cmp.Diff(want, nm.Peers, nodePubComparer); diff != "" {
|
||||
t.Errorf("filtered netmap differs (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
@@ -602,7 +648,7 @@ func TestTKADisable(t *testing.T) {
|
||||
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
|
||||
nlPriv := key.NewNLPrivate()
|
||||
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, new(health.Tracker)))
|
||||
must.Do(pm.SetPrefs((&ipn.Prefs{
|
||||
Persist: &persist.Persist{
|
||||
PrivateNodeKey: nodePriv,
|
||||
@@ -693,7 +739,7 @@ func TestTKASign(t *testing.T) {
|
||||
toSign := key.NewNode()
|
||||
nlPriv := key.NewNLPrivate()
|
||||
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, new(health.Tracker)))
|
||||
must.Do(pm.SetPrefs((&ipn.Prefs{
|
||||
Persist: &persist.Persist{
|
||||
PrivateNodeKey: nodePriv,
|
||||
@@ -782,7 +828,7 @@ func TestTKAForceDisable(t *testing.T) {
|
||||
nlPriv := key.NewNLPrivate()
|
||||
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
|
||||
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, new(health.Tracker)))
|
||||
must.Do(pm.SetPrefs((&ipn.Prefs{
|
||||
Persist: &persist.Persist{
|
||||
PrivateNodeKey: nodePriv,
|
||||
@@ -877,7 +923,7 @@ func TestTKAAffectedSigs(t *testing.T) {
|
||||
// toSign := key.NewNode()
|
||||
nlPriv := key.NewNLPrivate()
|
||||
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, new(health.Tracker)))
|
||||
must.Do(pm.SetPrefs((&ipn.Prefs{
|
||||
Persist: &persist.Persist{
|
||||
PrivateNodeKey: nodePriv,
|
||||
@@ -1010,7 +1056,7 @@ func TestTKARecoverCompromisedKeyFlow(t *testing.T) {
|
||||
cosignPriv := key.NewNLPrivate()
|
||||
compromisedPriv := key.NewNLPrivate()
|
||||
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, new(health.Tracker)))
|
||||
must.Do(pm.SetPrefs((&ipn.Prefs{
|
||||
Persist: &persist.Persist{
|
||||
PrivateNodeKey: nodePriv,
|
||||
@@ -1101,7 +1147,7 @@ func TestTKARecoverCompromisedKeyFlow(t *testing.T) {
|
||||
|
||||
// Cosign using the cosigning key.
|
||||
{
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, new(health.Tracker)))
|
||||
must.Do(pm.SetPrefs((&ipn.Prefs{
|
||||
Persist: &persist.Persist{
|
||||
PrivateNodeKey: nodePriv,
|
||||
@@ -1129,3 +1175,85 @@ func TestTKARecoverCompromisedKeyFlow(t *testing.T) {
|
||||
t.Errorf("NetworkLockSubmitRecoveryAUM() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRotationTracker(t *testing.T) {
|
||||
newNK := func(idx byte) key.NodePublic {
|
||||
// single-byte public key to make it human-readable in tests.
|
||||
raw32 := [32]byte{idx}
|
||||
return key.NodePublicFromRaw32(go4mem.B(raw32[:]))
|
||||
}
|
||||
n1, n2, n3, n4, n5 := newNK(1), newNK(2), newNK(3), newNK(4), newNK(5)
|
||||
|
||||
pk1, pk2, pk3 := []byte{1}, []byte{2}, []byte{3}
|
||||
type addDetails struct {
|
||||
np key.NodePublic
|
||||
details *tka.RotationDetails
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
addDetails []addDetails
|
||||
want set.Set[key.NodePublic]
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "single_prev_key",
|
||||
addDetails: []addDetails{
|
||||
{np: n1, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n2}, WrappingPubkey: pk1}},
|
||||
},
|
||||
want: set.SetOf([]key.NodePublic{n2}),
|
||||
},
|
||||
{
|
||||
name: "several_prev_keys",
|
||||
addDetails: []addDetails{
|
||||
{np: n1, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n2}, WrappingPubkey: pk1}},
|
||||
{np: n3, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n4}, WrappingPubkey: pk2}},
|
||||
{np: n2, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n3, n4}, WrappingPubkey: pk1}},
|
||||
},
|
||||
want: set.SetOf([]key.NodePublic{n2, n3, n4}),
|
||||
},
|
||||
{
|
||||
name: "several_per_pubkey_latest_wins",
|
||||
addDetails: []addDetails{
|
||||
{np: n2, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1}, WrappingPubkey: pk3}},
|
||||
{np: n3, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2}, WrappingPubkey: pk3}},
|
||||
{np: n4, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2, n3}, WrappingPubkey: pk3}},
|
||||
{np: n5, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n4}, WrappingPubkey: pk3}},
|
||||
},
|
||||
want: set.SetOf([]key.NodePublic{n1, n2, n3, n4}),
|
||||
},
|
||||
{
|
||||
name: "several_per_pubkey_same_chain_length_all_rejected",
|
||||
addDetails: []addDetails{
|
||||
{np: n2, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1}, WrappingPubkey: pk3}},
|
||||
{np: n3, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2}, WrappingPubkey: pk3}},
|
||||
{np: n4, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2}, WrappingPubkey: pk3}},
|
||||
{np: n5, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2}, WrappingPubkey: pk3}},
|
||||
},
|
||||
want: set.SetOf([]key.NodePublic{n1, n2, n3, n4, n5}),
|
||||
},
|
||||
{
|
||||
name: "several_per_pubkey_longest_wins",
|
||||
addDetails: []addDetails{
|
||||
{np: n2, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1}, WrappingPubkey: pk3}},
|
||||
{np: n3, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2}, WrappingPubkey: pk3}},
|
||||
{np: n4, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2}, WrappingPubkey: pk3}},
|
||||
{np: n5, details: &tka.RotationDetails{PrevNodeKeys: []key.NodePublic{n1, n2, n3}, WrappingPubkey: pk3}},
|
||||
},
|
||||
want: set.SetOf([]key.NodePublic{n1, n2, n3, n4}),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := &rotationTracker{logf: t.Logf}
|
||||
for _, ad := range tt.addDetails {
|
||||
r.addRotationDetails(ad.np, ad.details)
|
||||
}
|
||||
if got := r.obsoleteKeys(); !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("rotationTracker.obsoleteKeys() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"tailscale.com/appc"
|
||||
"tailscale.com/appc/appctest"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -641,8 +642,9 @@ func TestPeerAPIReplyToDNSQueries(t *testing.T) {
|
||||
h.isSelf = false
|
||||
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
|
||||
|
||||
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0)
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
ht := new(health.Tracker)
|
||||
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht)
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
|
||||
h.ps = &peerAPIServer{
|
||||
b: &LocalBackend{
|
||||
e: eng,
|
||||
@@ -691,8 +693,9 @@ func TestPeerAPIPrettyReplyCNAME(t *testing.T) {
|
||||
var h peerAPIHandler
|
||||
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
|
||||
|
||||
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0)
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
ht := new(health.Tracker)
|
||||
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht)
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
|
||||
var a *appc.AppConnector
|
||||
if shouldStore {
|
||||
a = appc.NewAppConnector(t.Logf, &appctest.RouteCollector{}, &appc.RouteInfo{}, fakeStoreRoutes)
|
||||
@@ -763,8 +766,9 @@ func TestPeerAPIReplyToDNSQueriesAreObserved(t *testing.T) {
|
||||
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
|
||||
|
||||
rc := &appctest.RouteCollector{}
|
||||
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0)
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
ht := new(health.Tracker)
|
||||
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht)
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
|
||||
var a *appc.AppConnector
|
||||
if shouldStore {
|
||||
a = appc.NewAppConnector(t.Logf, rc, &appc.RouteInfo{}, fakeStoreRoutes)
|
||||
@@ -825,9 +829,10 @@ func TestPeerAPIReplyToDNSQueriesAreObservedWithCNAMEFlattening(t *testing.T) {
|
||||
var h peerAPIHandler
|
||||
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
|
||||
|
||||
ht := new(health.Tracker)
|
||||
rc := &appctest.RouteCollector{}
|
||||
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0)
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht)
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
|
||||
var a *appc.AppConnector
|
||||
if shouldStore {
|
||||
a = appc.NewAppConnector(t.Logf, rc, &appc.RouteInfo{}, fakeStoreRoutes)
|
||||
|
||||
@@ -353,9 +353,9 @@ func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error
|
||||
if err != nil {
|
||||
return ipn.PrefsView{}, err
|
||||
}
|
||||
savedPrefs, err := ipn.PrefsFromBytes(bs)
|
||||
if err != nil {
|
||||
return ipn.PrefsView{}, fmt.Errorf("PrefsFromBytes: %v", err)
|
||||
savedPrefs := ipn.NewPrefs()
|
||||
if err := ipn.PrefsFromBytes(bs, savedPrefs); err != nil {
|
||||
return ipn.PrefsView{}, fmt.Errorf("parsing saved prefs: %v", err)
|
||||
}
|
||||
pm.logf("using backend prefs for %q: %v", key, savedPrefs.Pretty())
|
||||
|
||||
@@ -371,12 +371,13 @@ func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error
|
||||
// https://github.com/tailscale/tailscale/pull/11814/commits/1613b18f8280c2bce786980532d012c9f0454fa2#diff-314ba0d799f70c8998940903efb541e511f352b39a9eeeae8d475c921d66c2ac
|
||||
// prefs could set AutoUpdate.Apply=true via EditPrefs or tailnet
|
||||
// auto-update defaults. After that change, such value is "invalid" and
|
||||
// cause any EditPrefs calls to fail (other than disabling autp-updates).
|
||||
// cause any EditPrefs calls to fail (other than disabling auto-updates).
|
||||
//
|
||||
// Reset AutoUpdate.Apply if we detect such invalid prefs.
|
||||
if savedPrefs.AutoUpdate.Apply.EqualBool(true) && !clientupdate.CanAutoUpdate() {
|
||||
savedPrefs.AutoUpdate.Apply.Clear()
|
||||
}
|
||||
|
||||
return savedPrefs.View(), nil
|
||||
}
|
||||
|
||||
@@ -485,7 +486,8 @@ func (pm *profileManager) CurrentPrefs() ipn.PrefsView {
|
||||
|
||||
// ReadStartupPrefsForTest reads the startup prefs from disk. It is only used for testing.
|
||||
func ReadStartupPrefsForTest(logf logger.Logf, store ipn.StateStore) (ipn.PrefsView, error) {
|
||||
pm, err := newProfileManager(store, logf)
|
||||
ht := new(health.Tracker) // in tests, don't care about the health status
|
||||
pm, err := newProfileManager(store, logf, ht)
|
||||
if err != nil {
|
||||
return ipn.PrefsView{}, err
|
||||
}
|
||||
@@ -494,8 +496,8 @@ func ReadStartupPrefsForTest(logf logger.Logf, store ipn.StateStore) (ipn.PrefsV
|
||||
|
||||
// newProfileManager creates a new ProfileManager using the provided StateStore.
|
||||
// It also loads the list of known profiles from the StateStore.
|
||||
func newProfileManager(store ipn.StateStore, logf logger.Logf) (*profileManager, error) {
|
||||
return newProfileManagerWithGOOS(store, logf, envknob.GOOS())
|
||||
func newProfileManager(store ipn.StateStore, logf logger.Logf, health *health.Tracker) (*profileManager, error) {
|
||||
return newProfileManagerWithGOOS(store, logf, health, envknob.GOOS())
|
||||
}
|
||||
|
||||
func readAutoStartKey(store ipn.StateStore, goos string) (ipn.StateKey, error) {
|
||||
@@ -528,7 +530,7 @@ func readKnownProfiles(store ipn.StateStore) (map[ipn.ProfileID]*ipn.LoginProfil
|
||||
return knownProfiles, nil
|
||||
}
|
||||
|
||||
func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, goos string) (*profileManager, error) {
|
||||
func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *health.Tracker, goos string) (*profileManager, error) {
|
||||
logf = logger.WithPrefix(logf, "pm: ")
|
||||
stateKey, err := readAutoStartKey(store, goos)
|
||||
if err != nil {
|
||||
@@ -544,6 +546,7 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, goos stri
|
||||
store: store,
|
||||
knownProfiles: knownProfiles,
|
||||
logf: logf,
|
||||
health: ht,
|
||||
}
|
||||
|
||||
if stateKey != "" {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"tailscale.com/clientupdate"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -24,7 +25,7 @@ import (
|
||||
func TestProfileCurrentUserSwitch(t *testing.T) {
|
||||
store := new(mem.Store)
|
||||
|
||||
pm, err := newProfileManagerWithGOOS(store, logger.Discard, "linux")
|
||||
pm, err := newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -61,7 +62,7 @@ func TestProfileCurrentUserSwitch(t *testing.T) {
|
||||
t.Fatalf("CurrentPrefs() = %v, want emptyPrefs", pm.CurrentPrefs().Pretty())
|
||||
}
|
||||
|
||||
pm, err = newProfileManagerWithGOOS(store, logger.Discard, "linux")
|
||||
pm, err = newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -79,7 +80,7 @@ func TestProfileCurrentUserSwitch(t *testing.T) {
|
||||
func TestProfileList(t *testing.T) {
|
||||
store := new(mem.Store)
|
||||
|
||||
pm, err := newProfileManagerWithGOOS(store, logger.Discard, "linux")
|
||||
pm, err := newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -283,7 +284,7 @@ func TestProfileDupe(t *testing.T) {
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
store := new(mem.Store)
|
||||
pm, err := newProfileManagerWithGOOS(store, logger.Discard, "linux")
|
||||
pm, err := newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -316,7 +317,7 @@ func TestProfileDupe(t *testing.T) {
|
||||
func TestProfileManagement(t *testing.T) {
|
||||
store := new(mem.Store)
|
||||
|
||||
pm, err := newProfileManagerWithGOOS(store, logger.Discard, "linux")
|
||||
pm, err := newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -414,7 +415,7 @@ func TestProfileManagement(t *testing.T) {
|
||||
t.Logf("Recreate profile manager from store")
|
||||
// Recreate the profile manager to ensure that it can load the profiles
|
||||
// from the store at startup.
|
||||
pm, err = newProfileManagerWithGOOS(store, logger.Discard, "linux")
|
||||
pm, err = newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -430,7 +431,7 @@ func TestProfileManagement(t *testing.T) {
|
||||
t.Logf("Recreate profile manager from store after deleting default profile")
|
||||
// Recreate the profile manager to ensure that it can load the profiles
|
||||
// from the store at startup.
|
||||
pm, err = newProfileManagerWithGOOS(store, logger.Discard, "linux")
|
||||
pm, err = newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -472,7 +473,7 @@ func TestProfileManagement(t *testing.T) {
|
||||
t.Fatal("SetPrefs failed to save auto-update setting")
|
||||
}
|
||||
// Re-load profiles to trigger migration for invalid auto-update value.
|
||||
pm, err = newProfileManagerWithGOOS(store, logger.Discard, "linux")
|
||||
pm, err = newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "linux")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -494,7 +495,7 @@ func TestProfileManagementWindows(t *testing.T) {
|
||||
|
||||
store := new(mem.Store)
|
||||
|
||||
pm, err := newProfileManagerWithGOOS(store, logger.Discard, "windows")
|
||||
pm, err := newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "windows")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -565,7 +566,7 @@ func TestProfileManagementWindows(t *testing.T) {
|
||||
t.Logf("Recreate profile manager from store, should reset prefs")
|
||||
// Recreate the profile manager to ensure that it can load the profiles
|
||||
// from the store at startup.
|
||||
pm, err = newProfileManagerWithGOOS(store, logger.Discard, "windows")
|
||||
pm, err = newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "windows")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -590,7 +591,7 @@ func TestProfileManagementWindows(t *testing.T) {
|
||||
}
|
||||
|
||||
// Recreate the profile manager to ensure that it starts with test profile.
|
||||
pm, err = newProfileManagerWithGOOS(store, logger.Discard, "windows")
|
||||
pm, err = newProfileManagerWithGOOS(store, logger.Discard, new(health.Tracker), "windows")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -599,3 +600,16 @@ func TestProfileManagementWindows(t *testing.T) {
|
||||
t.Fatalf("CurrentUserID = %q; want %q", pm.CurrentUserID(), uid)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDefaultPrefs tests that defaultPrefs is just NewPrefs with
|
||||
// LoggedOut=true (the Prefs we use before connecting to control). We shouldn't
|
||||
// be putting any defaulting there, and instead put all defaults in NewPrefs.
|
||||
func TestDefaultPrefs(t *testing.T) {
|
||||
p1 := ipn.NewPrefs()
|
||||
p1.LoggedOut = true
|
||||
p1.WantRunning = false
|
||||
p2 := defaultPrefs
|
||||
if !p1.View().Equals(p2) {
|
||||
t.Errorf("defaultPrefs is %s, want %s; defaultPrefs should only modify WantRunning and LoggedOut, all other defaults should be in ipn.NewPrefs.", p2.Pretty(), p1.Pretty())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ func (pm *profileManager) loadLegacyPrefs() (string, ipn.PrefsView, error) {
|
||||
}
|
||||
|
||||
prefsPath := filepath.Join(userLegacyPrefsDir, legacyPrefsFile+legacyPrefsExt)
|
||||
prefs, err := ipn.LoadPrefs(prefsPath)
|
||||
prefs, err := ipn.LoadPrefsWindows(prefsPath)
|
||||
pm.dlogf("ipn.LoadPrefs(%q) = %v, %v", prefsPath, prefs, err)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return "", ipn.PrefsView{}, errAlreadyMigrated
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -671,7 +672,10 @@ func newTestBackend(t *testing.T) *LocalBackend {
|
||||
}
|
||||
|
||||
sys := &tsd.System{}
|
||||
e, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{SetSubsystem: sys.Set})
|
||||
e, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
SetSubsystem: sys.Set,
|
||||
HealthTracker: sys.HealthTracker(),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -686,7 +690,7 @@ func newTestBackend(t *testing.T) *LocalBackend {
|
||||
dir := t.TempDir()
|
||||
b.SetVarRoot(dir)
|
||||
|
||||
pm := must.Get(newProfileManager(new(mem.Store), logf))
|
||||
pm := must.Get(newProfileManager(new(mem.Store), logf, new(health.Tracker)))
|
||||
pm.currentProfile = &ipn.LoginProfile{ID: "id0"}
|
||||
b.pm = pm
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/must"
|
||||
@@ -49,7 +50,7 @@ type fakeSSHServer struct {
|
||||
}
|
||||
|
||||
func TestGetSSHUsernames(t *testing.T) {
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, new(health.Tracker)))
|
||||
b := &LocalBackend{pm: pm, store: pm.Store()}
|
||||
b.sshServer = fakeSSHServer{}
|
||||
res, err := b.getSSHUsernames(new(tailcfg.C2NSSHUsernamesRequest))
|
||||
|
||||
@@ -97,7 +97,6 @@ type mockControl struct {
|
||||
paused atomic.Bool
|
||||
|
||||
mu sync.Mutex
|
||||
machineKey key.MachinePrivate
|
||||
persist *persist.Persist
|
||||
calls []string
|
||||
authBlocked bool
|
||||
@@ -134,12 +133,6 @@ func (cc *mockControl) populateKeys() (newKeys bool) {
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
|
||||
if cc.machineKey.IsZero() {
|
||||
cc.logf("Copying machineKey.")
|
||||
cc.machineKey, _ = cc.opts.GetMachinePrivateKey()
|
||||
newKeys = true
|
||||
}
|
||||
|
||||
if cc.persist == nil {
|
||||
cc.persist = &persist.Persist{}
|
||||
}
|
||||
@@ -205,8 +198,8 @@ func (cc *mockControl) Shutdown() {
|
||||
// Login starts a login process. Note that in this mock, we don't automatically
|
||||
// generate notifications about the progress of the login operation. You have to
|
||||
// call send() as required by the test.
|
||||
func (cc *mockControl) Login(t *tailcfg.Oauth2Token, flags controlclient.LoginFlags) {
|
||||
cc.logf("Login token=%v flags=%v", t, flags)
|
||||
func (cc *mockControl) Login(flags controlclient.LoginFlags) {
|
||||
cc.logf("Login flags=%v", flags)
|
||||
cc.called("Login")
|
||||
newKeys := cc.populateKeys()
|
||||
|
||||
@@ -272,7 +265,7 @@ func (b *LocalBackend) nonInteractiveLoginForStateTest() {
|
||||
cc := b.cc
|
||||
b.mu.Unlock()
|
||||
|
||||
cc.Login(nil, b.loginFlags|controlclient.LoginInteractive)
|
||||
cc.Login(b.loginFlags | controlclient.LoginInteractive)
|
||||
}
|
||||
|
||||
// A very precise test of the sequence of function calls generated by
|
||||
@@ -305,7 +298,7 @@ func TestStateMachine(t *testing.T) {
|
||||
sys := new(tsd.System)
|
||||
store := new(testStateStorage)
|
||||
sys.Set(store)
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set)
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker())
|
||||
if err != nil {
|
||||
t.Fatalf("NewFakeUserspaceEngine: %v", err)
|
||||
}
|
||||
@@ -316,6 +309,7 @@ func TestStateMachine(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("NewLocalBackend: %v", err)
|
||||
}
|
||||
b.DisablePortMapperForTest()
|
||||
|
||||
var cc, previousCC *mockControl
|
||||
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
|
||||
@@ -335,7 +329,7 @@ func TestStateMachine(t *testing.T) {
|
||||
(n.Prefs != nil && n.Prefs.Valid()) ||
|
||||
n.BrowseToURL != nil ||
|
||||
n.LoginFinished != nil {
|
||||
logf("%v\n\n", n)
|
||||
logf("%+v\n\n", n)
|
||||
notifies.put(n)
|
||||
} else {
|
||||
logf("(ignored) %v\n\n", n)
|
||||
@@ -412,7 +406,7 @@ func TestStateMachine(t *testing.T) {
|
||||
// the user needs to visit a login URL.
|
||||
t.Logf("\n\nLogin (url response)")
|
||||
|
||||
notifies.expect(2)
|
||||
notifies.expect(3)
|
||||
b.EditPrefs(&ipn.MaskedPrefs{
|
||||
ControlURLSet: true,
|
||||
Prefs: ipn.Prefs{
|
||||
@@ -427,12 +421,15 @@ func TestStateMachine(t *testing.T) {
|
||||
// ...but backend eats that notification, because the user
|
||||
// didn't explicitly request interactive login yet, and
|
||||
// we're already in NeedsLogin state.
|
||||
nn := notifies.drain(2)
|
||||
nn := notifies.drain(3)
|
||||
|
||||
c.Assert(nn[1].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[1].Prefs.LoggedOut(), qt.IsTrue)
|
||||
c.Assert(nn[1].Prefs.WantRunning(), qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
c.Assert(nn[2].BrowseToURL, qt.IsNotNil)
|
||||
c.Assert(url1, qt.Equals, *nn[2].BrowseToURL)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
// Now we'll try an interactive login.
|
||||
@@ -440,13 +437,10 @@ func TestStateMachine(t *testing.T) {
|
||||
// ask control to do anything. Instead backend will emit an event
|
||||
// indicating that the UI should browse to the given URL.
|
||||
t.Logf("\n\nLogin (interactive)")
|
||||
notifies.expect(1)
|
||||
notifies.expect(0)
|
||||
b.StartLoginInteractive(context.Background())
|
||||
{
|
||||
nn := notifies.drain(1)
|
||||
cc.assertCalls()
|
||||
c.Assert(nn[0].BrowseToURL, qt.IsNotNil)
|
||||
c.Assert(url1, qt.Equals, *nn[0].BrowseToURL)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
@@ -459,9 +453,8 @@ func TestStateMachine(t *testing.T) {
|
||||
notifies.expect(0)
|
||||
b.StartLoginInteractive(context.Background())
|
||||
{
|
||||
notifies.drain(0)
|
||||
// backend asks control for another login sequence
|
||||
cc.assertCalls("Login")
|
||||
cc.assertCalls()
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
@@ -830,7 +823,7 @@ func TestStateMachine(t *testing.T) {
|
||||
// The last test case is the most common one: restarting when both
|
||||
// logged in and WantRunning.
|
||||
t.Logf("\n\nStart5")
|
||||
notifies.expect(2)
|
||||
notifies.expect(1)
|
||||
c.Assert(b.Start(ipn.Options{}), qt.IsNil)
|
||||
{
|
||||
// NOTE: cc.Shutdown() is correct here, since we didn't call
|
||||
@@ -838,27 +831,27 @@ func TestStateMachine(t *testing.T) {
|
||||
previousCC.assertShutdown(false)
|
||||
cc.assertCalls("New", "Login")
|
||||
|
||||
nn := notifies.drain(2)
|
||||
nn := notifies.drain(1)
|
||||
cc.assertCalls()
|
||||
c.Assert(nn[0].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[0].Prefs.LoggedOut(), qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.WantRunning(), qt.IsTrue)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
c.Assert(b.State(), qt.Equals, ipn.NoState)
|
||||
}
|
||||
|
||||
// Control server accepts our valid key from before.
|
||||
t.Logf("\n\nLoginFinished5")
|
||||
notifies.expect(2)
|
||||
notifies.expect(1)
|
||||
cc.send(nil, "", true, &netmap.NetworkMap{
|
||||
SelfNode: (&tailcfg.Node{MachineAuthorized: true}).View(),
|
||||
})
|
||||
{
|
||||
nn := notifies.drain(2)
|
||||
nn := notifies.drain(1)
|
||||
cc.assertCalls()
|
||||
// NOTE: No LoginFinished message since no interactive
|
||||
// login was needed.
|
||||
c.Assert(nn[1].State, qt.IsNotNil)
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[1].State)
|
||||
c.Assert(nn[0].State, qt.IsNotNil)
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[0].State)
|
||||
// NOTE: No prefs change this time. WantRunning stays true.
|
||||
// We were in Starting in the first place, so that doesn't
|
||||
// change either.
|
||||
@@ -909,7 +902,7 @@ func TestEditPrefsHasNoKeys(t *testing.T) {
|
||||
logf := tstest.WhileTestRunningLogger(t)
|
||||
sys := new(tsd.System)
|
||||
sys.Set(new(mem.Store))
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set)
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker())
|
||||
if err != nil {
|
||||
t.Fatalf("NewFakeUserspaceEngine: %v", err)
|
||||
}
|
||||
|
||||
@@ -46,9 +46,6 @@ type Server struct {
|
||||
// is true, the ForceDaemon pref can override this.
|
||||
resetOnZero bool
|
||||
|
||||
startBackendOnce sync.Once
|
||||
runCalled atomic.Bool
|
||||
|
||||
// mu guards the fields that follow.
|
||||
// lock order: mu, then LocalBackend.mu
|
||||
mu sync.Mutex
|
||||
@@ -471,16 +468,15 @@ func New(logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor) *Server
|
||||
|
||||
// SetLocalBackend sets the server's LocalBackend.
|
||||
//
|
||||
// If b.Run has already been called, then lb.Start will be called.
|
||||
// Otherwise Start will be called once Run is called.
|
||||
// It should only call be called after calling lb.Start.
|
||||
func (s *Server) SetLocalBackend(lb *ipnlocal.LocalBackend) {
|
||||
if lb == nil {
|
||||
panic("nil LocalBackend")
|
||||
}
|
||||
|
||||
if !s.lb.CompareAndSwap(nil, lb) {
|
||||
panic("already set")
|
||||
}
|
||||
s.startBackendIfNeeded()
|
||||
|
||||
s.mu.Lock()
|
||||
s.backendWaiter.wakeAll()
|
||||
@@ -490,21 +486,6 @@ func (s *Server) SetLocalBackend(lb *ipnlocal.LocalBackend) {
|
||||
// https://github.com/tailscale/tailscale/issues/6522
|
||||
}
|
||||
|
||||
func (b *Server) startBackendIfNeeded() {
|
||||
if !b.runCalled.Load() {
|
||||
return
|
||||
}
|
||||
lb := b.lb.Load()
|
||||
if lb == nil {
|
||||
return
|
||||
}
|
||||
if lb.Prefs().Valid() {
|
||||
b.startBackendOnce.Do(func() {
|
||||
lb.Start(ipn.Options{})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// connIdentityContextKey is the http.Request.Context's context.Value key for either an
|
||||
// *ipnauth.ConnIdentity or an error.
|
||||
type connIdentityContextKey struct{}
|
||||
@@ -517,7 +498,6 @@ type connIdentityContextKey struct{}
|
||||
// If the Server's LocalBackend has already been set, Run starts it.
|
||||
// Otherwise, the next call to SetLocalBackend will start it.
|
||||
func (s *Server) Run(ctx context.Context, ln net.Listener) error {
|
||||
s.runCalled.Store(true)
|
||||
defer func() {
|
||||
if lb := s.lb.Load(); lb != nil {
|
||||
lb.Shutdown()
|
||||
@@ -537,7 +517,6 @@ func (s *Server) Run(ctx context.Context, ln net.Listener) error {
|
||||
ln.Close()
|
||||
}()
|
||||
|
||||
s.startBackendIfNeeded()
|
||||
systemd.Ready()
|
||||
|
||||
hs := &http.Server{
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/types/views"
|
||||
@@ -41,6 +42,9 @@ type Status struct {
|
||||
// "Starting", "Running".
|
||||
BackendState string
|
||||
|
||||
// HaveNodeKey is whether the current profile has a node key configured.
|
||||
HaveNodeKey bool `json:",omitempty"`
|
||||
|
||||
AuthURL string // current URL provided by control to authorize client
|
||||
TailscaleIPs []netip.Addr // Tailscale IP(s) assigned to this node
|
||||
Self *PeerStatus
|
||||
@@ -123,6 +127,9 @@ type NetworkLockStatus struct {
|
||||
// NodeKeySigned is true if our node is authorized by network-lock.
|
||||
NodeKeySigned bool
|
||||
|
||||
// NodeKeySignature is the current signature of this node's key.
|
||||
NodeKeySignature *tka.NodeKeySignature
|
||||
|
||||
// TrustedKeys describes the keys currently trusted to make changes
|
||||
// to network-lock.
|
||||
TrustedKeys []TKAKey
|
||||
|
||||
@@ -6,6 +6,7 @@ package localapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"cmp"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
@@ -1939,8 +1940,10 @@ func (h *Handler) serveDial(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
network := cmp.Or(r.Header.Get("Dial-Network"), "tcp")
|
||||
|
||||
addr := net.JoinHostPort(hostStr, portStr)
|
||||
outConn, err := h.b.Dialer().UserDial(r.Context(), "tcp", addr)
|
||||
outConn, err := h.b.Dialer().UserDial(r.Context(), network, addr)
|
||||
if err != nil {
|
||||
http.Error(w, "dial failure: "+err.Error(), http.StatusBadGateway)
|
||||
return
|
||||
|
||||
@@ -314,7 +314,7 @@ func newTestLocalBackend(t testing.TB) *ipnlocal.LocalBackend {
|
||||
sys := new(tsd.System)
|
||||
store := new(mem.Store)
|
||||
sys.Set(store)
|
||||
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set)
|
||||
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker())
|
||||
if err != nil {
|
||||
t.Fatalf("NewFakeUserspaceEngine: %v", err)
|
||||
}
|
||||
|
||||
90
ipn/prefs.go
90
ipn/prefs.go
@@ -75,18 +75,6 @@ type Prefs struct {
|
||||
// controlled by ExitNodeID/IP below.
|
||||
RouteAll bool
|
||||
|
||||
// AllowSingleHosts specifies whether to install routes for each
|
||||
// node IP on the tailscale network, in addition to a route for
|
||||
// the whole network.
|
||||
// This corresponds to the "tailscale up --host-routes" value,
|
||||
// which defaults to true.
|
||||
//
|
||||
// TODO(danderson): why do we have this? It dumps a lot of stuff
|
||||
// into the routing table, and a single network route _should_ be
|
||||
// all that we need. But when I turn this off in my tailscaled,
|
||||
// packets stop flowing. What's up with that?
|
||||
AllowSingleHosts bool
|
||||
|
||||
// ExitNodeID and ExitNodeIP specify the node that should be used
|
||||
// as an exit node for internet traffic. At most one of these
|
||||
// should be non-zero.
|
||||
@@ -203,6 +191,20 @@ type Prefs struct {
|
||||
// Linux-only.
|
||||
NoSNAT bool
|
||||
|
||||
// NoStatefulFiltering specifies whether to apply stateful filtering when
|
||||
// advertising routes in AdvertiseRoutes. The default is to not apply
|
||||
// stateful filtering.
|
||||
//
|
||||
// To allow inbound connections from advertised routes, both NoSNAT and
|
||||
// NoStatefulFiltering must be true.
|
||||
//
|
||||
// This is an opt.Bool because it was first added after NoSNAT, with a
|
||||
// backfill based on the value of that parameter. The backfill has been
|
||||
// removed since then, but the field remains an opt.Bool.
|
||||
//
|
||||
// Linux-only.
|
||||
NoStatefulFiltering opt.Bool `json:",omitempty"`
|
||||
|
||||
// NetfilterMode specifies how much to manage netfilter rules for
|
||||
// Tailscale, if at all.
|
||||
NetfilterMode preftype.NetfilterMode
|
||||
@@ -237,6 +239,16 @@ type Prefs struct {
|
||||
// by name.
|
||||
DriveShares []*drive.Share
|
||||
|
||||
// AllowSingleHosts was a legacy field that was always true
|
||||
// for the past 4.5 years. It controlled whether Tailscale
|
||||
// peers got /32 or /127 routes for each other.
|
||||
// As of 2024-05-17 we're starting to ignore it, but to let
|
||||
// people still downgrade Tailscale versions and not break
|
||||
// all peer-to-peer networking we still write it to disk (as JSON)
|
||||
// so it can be loaded back by old versions.
|
||||
// TODO(bradfitz): delete this in 2025 sometime. See #12058.
|
||||
AllowSingleHosts marshalAsTrueInJSON
|
||||
|
||||
// The Persist field is named 'Config' in the file for backward
|
||||
// compatibility with earlier versions.
|
||||
// TODO(apenwarr): We should move this out of here, it's not a pref.
|
||||
@@ -267,6 +279,13 @@ func (au1 AutoUpdatePrefs) Equals(au2 AutoUpdatePrefs) bool {
|
||||
ok1 == ok2
|
||||
}
|
||||
|
||||
type marshalAsTrueInJSON struct{}
|
||||
|
||||
var trueJSON = []byte("true")
|
||||
|
||||
func (marshalAsTrueInJSON) MarshalJSON() ([]byte, error) { return trueJSON, nil }
|
||||
func (*marshalAsTrueInJSON) UnmarshalJSON([]byte) error { return nil }
|
||||
|
||||
// AppConnectorPrefs are the app connector settings for the node agent.
|
||||
type AppConnectorPrefs struct {
|
||||
// Advertise specifies whether the app connector subsystem is advertising
|
||||
@@ -284,7 +303,6 @@ type MaskedPrefs struct {
|
||||
|
||||
ControlURLSet bool `json:",omitempty"`
|
||||
RouteAllSet bool `json:",omitempty"`
|
||||
AllowSingleHostsSet bool `json:",omitempty"`
|
||||
ExitNodeIDSet bool `json:",omitempty"`
|
||||
ExitNodeIPSet bool `json:",omitempty"`
|
||||
InternalExitNodePriorSet bool `json:",omitempty"` // Internal; can't be set by LocalAPI clients
|
||||
@@ -302,6 +320,7 @@ type MaskedPrefs struct {
|
||||
EggSet bool `json:",omitempty"`
|
||||
AdvertiseRoutesSet bool `json:",omitempty"`
|
||||
NoSNATSet bool `json:",omitempty"`
|
||||
NoStatefulFilteringSet bool `json:",omitempty"`
|
||||
NetfilterModeSet bool `json:",omitempty"`
|
||||
OperatorUserSet bool `json:",omitempty"`
|
||||
ProfileNameSet bool `json:",omitempty"`
|
||||
@@ -468,9 +487,6 @@ func (p *Prefs) pretty(goos string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Prefs{")
|
||||
fmt.Fprintf(&sb, "ra=%v ", p.RouteAll)
|
||||
if !p.AllowSingleHosts {
|
||||
sb.WriteString("mesh=false ")
|
||||
}
|
||||
fmt.Fprintf(&sb, "dns=%v want=%v ", p.CorpDNS, p.WantRunning)
|
||||
if p.RunSSH {
|
||||
sb.WriteString("ssh=true ")
|
||||
@@ -501,6 +517,13 @@ func (p *Prefs) pretty(goos string) string {
|
||||
if len(p.AdvertiseRoutes) > 0 || p.NoSNAT {
|
||||
fmt.Fprintf(&sb, "snat=%v ", !p.NoSNAT)
|
||||
}
|
||||
if len(p.AdvertiseRoutes) > 0 || p.NoStatefulFiltering.EqualBool(true) {
|
||||
// Only print if we're advertising any routes, or the user has
|
||||
// turned off stateful filtering (NoStatefulFiltering=true ⇒
|
||||
// StatefulFiltering=false).
|
||||
bb, _ := p.NoStatefulFiltering.Get()
|
||||
fmt.Fprintf(&sb, "statefulFiltering=%v ", !bb)
|
||||
}
|
||||
if len(p.AdvertiseTags) > 0 {
|
||||
fmt.Fprintf(&sb, "tags=%s ", strings.Join(p.AdvertiseTags, ","))
|
||||
}
|
||||
@@ -556,7 +579,6 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
||||
|
||||
return p.ControlURL == p2.ControlURL &&
|
||||
p.RouteAll == p2.RouteAll &&
|
||||
p.AllowSingleHosts == p2.AllowSingleHosts &&
|
||||
p.ExitNodeID == p2.ExitNodeID &&
|
||||
p.ExitNodeIP == p2.ExitNodeIP &&
|
||||
p.InternalExitNodePrior == p2.InternalExitNodePrior &&
|
||||
@@ -569,6 +591,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
||||
p.NotepadURLs == p2.NotepadURLs &&
|
||||
p.ShieldsUp == p2.ShieldsUp &&
|
||||
p.NoSNAT == p2.NoSNAT &&
|
||||
p.NoStatefulFiltering == p2.NoStatefulFiltering &&
|
||||
p.NetfilterMode == p2.NetfilterMode &&
|
||||
p.OperatorUser == p2.OperatorUser &&
|
||||
p.Hostname == p2.Hostname &&
|
||||
@@ -638,11 +661,11 @@ func NewPrefs() *Prefs {
|
||||
// later anyway.
|
||||
ControlURL: "",
|
||||
|
||||
RouteAll: true,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
WantRunning: false,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
RouteAll: true,
|
||||
CorpDNS: true,
|
||||
WantRunning: false,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
NoStatefulFiltering: opt.NewBool(true),
|
||||
AutoUpdate: AutoUpdatePrefs{
|
||||
Check: true,
|
||||
Apply: opt.Bool("unset"),
|
||||
@@ -851,24 +874,21 @@ func (p *Prefs) ShouldWebClientBeRunning() bool {
|
||||
return p.WantRunning && p.RunWebClient
|
||||
}
|
||||
|
||||
// PrefsFromBytes deserializes Prefs from a JSON blob.
|
||||
func PrefsFromBytes(b []byte) (*Prefs, error) {
|
||||
p := NewPrefs()
|
||||
// PrefsFromBytes deserializes Prefs from a JSON blob b into base. Values in
|
||||
// base are preserved, unless they are populated in the JSON blob.
|
||||
func PrefsFromBytes(b []byte, base *Prefs) error {
|
||||
if len(b) == 0 {
|
||||
return p, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(b, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
return json.Unmarshal(b, base)
|
||||
}
|
||||
|
||||
var jsonEscapedZero = []byte(`\u0000`)
|
||||
|
||||
// LoadPrefs loads a legacy relaynode config file into Prefs
|
||||
// with sensible migration defaults set.
|
||||
func LoadPrefs(filename string) (*Prefs, error) {
|
||||
// LoadPrefsWindows loads a legacy relaynode config file into Prefs with
|
||||
// sensible migration defaults set. Windows-only.
|
||||
func LoadPrefsWindows(filename string) (*Prefs, error) {
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LoadPrefs open: %w", err) // err includes path
|
||||
@@ -881,8 +901,8 @@ func LoadPrefs(filename string) (*Prefs, error) {
|
||||
// to log in again. (better than crashing)
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
p, err := PrefsFromBytes(data)
|
||||
if err != nil {
|
||||
p := NewPrefs()
|
||||
if err := PrefsFromBytes(data, p); err != nil {
|
||||
return nil, fmt.Errorf("LoadPrefs(%q) decode: %w", filename, err)
|
||||
}
|
||||
return p, nil
|
||||
|
||||
@@ -38,7 +38,6 @@ func TestPrefsEqual(t *testing.T) {
|
||||
prefsHandles := []string{
|
||||
"ControlURL",
|
||||
"RouteAll",
|
||||
"AllowSingleHosts",
|
||||
"ExitNodeID",
|
||||
"ExitNodeIP",
|
||||
"InternalExitNodePrior",
|
||||
@@ -56,6 +55,7 @@ func TestPrefsEqual(t *testing.T) {
|
||||
"Egg",
|
||||
"AdvertiseRoutes",
|
||||
"NoSNAT",
|
||||
"NoStatefulFiltering",
|
||||
"NetfilterMode",
|
||||
"OperatorUser",
|
||||
"ProfileName",
|
||||
@@ -64,6 +64,7 @@ func TestPrefsEqual(t *testing.T) {
|
||||
"PostureChecking",
|
||||
"NetfilterKind",
|
||||
"DriveShares",
|
||||
"AllowSingleHosts",
|
||||
"Persist",
|
||||
}
|
||||
if have := fieldsOf(reflect.TypeFor[Prefs]()); !reflect.DeepEqual(have, prefsHandles) {
|
||||
@@ -122,18 +123,6 @@ func TestPrefsEqual(t *testing.T) {
|
||||
&Prefs{RouteAll: true},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Prefs{AllowSingleHosts: true},
|
||||
&Prefs{AllowSingleHosts: false},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Prefs{AllowSingleHosts: true},
|
||||
&Prefs{AllowSingleHosts: true},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Prefs{ExitNodeID: "n1234"},
|
||||
&Prefs{},
|
||||
@@ -372,9 +361,10 @@ func checkPrefs(t *testing.T, p Prefs) {
|
||||
if p.Equals(p2) {
|
||||
t.Fatalf("p == p2\n")
|
||||
}
|
||||
p2b, err = PrefsFromBytes(p2.ToBytes())
|
||||
p2b = new(Prefs)
|
||||
err = PrefsFromBytes(p2.ToBytes(), p2b)
|
||||
if err != nil {
|
||||
t.Fatalf("PrefsFromBytes(p2) failed\n")
|
||||
t.Fatalf("PrefsFromBytes(p2) failed: bytes=%q; err=%v\n", p2.ToBytes(), err)
|
||||
}
|
||||
p2p := p2.Pretty()
|
||||
p2bp := p2b.Pretty()
|
||||
@@ -425,46 +415,43 @@ func TestPrefsPretty(t *testing.T) {
|
||||
{
|
||||
Prefs{},
|
||||
"linux",
|
||||
"Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off Persist=nil}",
|
||||
"Prefs{ra=false dns=false want=false routes=[] nf=off update=off Persist=nil}",
|
||||
},
|
||||
{
|
||||
Prefs{},
|
||||
"windows",
|
||||
"Prefs{ra=false mesh=false dns=false want=false update=off Persist=nil}",
|
||||
"Prefs{ra=false dns=false want=false update=off Persist=nil}",
|
||||
},
|
||||
{
|
||||
Prefs{ShieldsUp: true},
|
||||
"windows",
|
||||
"Prefs{ra=false mesh=false dns=false want=false shields=true update=off Persist=nil}",
|
||||
"Prefs{ra=false dns=false want=false shields=true update=off Persist=nil}",
|
||||
},
|
||||
{
|
||||
Prefs{AllowSingleHosts: true},
|
||||
Prefs{},
|
||||
"windows",
|
||||
"Prefs{ra=false dns=false want=false update=off Persist=nil}",
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
NotepadURLs: true,
|
||||
AllowSingleHosts: true,
|
||||
NotepadURLs: true,
|
||||
},
|
||||
"windows",
|
||||
"Prefs{ra=false dns=false want=false notepad=true update=off Persist=nil}",
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
AllowSingleHosts: true,
|
||||
WantRunning: true,
|
||||
ForceDaemon: true, // server mode
|
||||
WantRunning: true,
|
||||
ForceDaemon: true, // server mode
|
||||
},
|
||||
"windows",
|
||||
"Prefs{ra=false dns=false want=true server=true update=off Persist=nil}",
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
AllowSingleHosts: true,
|
||||
WantRunning: true,
|
||||
ControlURL: "http://localhost:1234",
|
||||
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
||||
WantRunning: true,
|
||||
ControlURL: "http://localhost:1234",
|
||||
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
||||
},
|
||||
"darwin",
|
||||
`Prefs{ra=false dns=false want=true tags=tag:foo,tag:bar url="http://localhost:1234" update=off Persist=nil}`,
|
||||
@@ -474,7 +461,7 @@ func TestPrefsPretty(t *testing.T) {
|
||||
Persist: &persist.Persist{},
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off Persist{lm=, o=, n= u=""}}`,
|
||||
`Prefs{ra=false dns=false want=false routes=[] nf=off update=off Persist{lm=, o=, n= u=""}}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
@@ -483,21 +470,21 @@ func TestPrefsPretty(t *testing.T) {
|
||||
},
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off Persist{lm=, o=, n=[B1VKl] u=""}}`,
|
||||
`Prefs{ra=false dns=false want=false routes=[] nf=off update=off Persist{lm=, o=, n=[B1VKl] u=""}}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
ExitNodeIP: netip.MustParseAddr("1.2.3.4"),
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false exit=1.2.3.4 lan=false routes=[] nf=off update=off Persist=nil}`,
|
||||
`Prefs{ra=false dns=false want=false exit=1.2.3.4 lan=false routes=[] nf=off update=off Persist=nil}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
ExitNodeID: tailcfg.StableNodeID("myNodeABC"),
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false exit=myNodeABC lan=false routes=[] nf=off update=off Persist=nil}`,
|
||||
`Prefs{ra=false dns=false want=false exit=myNodeABC lan=false routes=[] nf=off update=off Persist=nil}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
@@ -505,21 +492,21 @@ func TestPrefsPretty(t *testing.T) {
|
||||
ExitNodeAllowLANAccess: true,
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false exit=myNodeABC lan=true routes=[] nf=off update=off Persist=nil}`,
|
||||
`Prefs{ra=false dns=false want=false exit=myNodeABC lan=true routes=[] nf=off update=off Persist=nil}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
ExitNodeAllowLANAccess: true,
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off Persist=nil}`,
|
||||
`Prefs{ra=false dns=false want=false routes=[] nf=off update=off Persist=nil}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
Hostname: "foo",
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off host="foo" update=off Persist=nil}`,
|
||||
`Prefs{ra=false dns=false want=false routes=[] nf=off host="foo" update=off Persist=nil}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
@@ -529,7 +516,7 @@ func TestPrefsPretty(t *testing.T) {
|
||||
},
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=check Persist=nil}`,
|
||||
`Prefs{ra=false dns=false want=false routes=[] nf=off update=check Persist=nil}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
@@ -539,7 +526,7 @@ func TestPrefsPretty(t *testing.T) {
|
||||
},
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=on Persist=nil}`,
|
||||
`Prefs{ra=false dns=false want=false routes=[] nf=off update=on Persist=nil}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
@@ -548,7 +535,7 @@ func TestPrefsPretty(t *testing.T) {
|
||||
},
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off appconnector=advertise Persist=nil}`,
|
||||
`Prefs{ra=false dns=false want=false routes=[] nf=off update=off appconnector=advertise Persist=nil}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
@@ -557,21 +544,21 @@ func TestPrefsPretty(t *testing.T) {
|
||||
},
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off Persist=nil}`,
|
||||
`Prefs{ra=false dns=false want=false routes=[] nf=off update=off Persist=nil}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
NetfilterKind: "iptables",
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off netfilterKind=iptables update=off Persist=nil}`,
|
||||
`Prefs{ra=false dns=false want=false routes=[] nf=off netfilterKind=iptables update=off Persist=nil}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
NetfilterKind: "",
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off update=off Persist=nil}`,
|
||||
`Prefs{ra=false dns=false want=false routes=[] nf=off update=off Persist=nil}`,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
@@ -585,7 +572,7 @@ func TestPrefsPretty(t *testing.T) {
|
||||
func TestLoadPrefsNotExist(t *testing.T) {
|
||||
bogusFile := fmt.Sprintf("/tmp/not-exist-%d", time.Now().UnixNano())
|
||||
|
||||
p, err := LoadPrefs(bogusFile)
|
||||
p, err := LoadPrefsWindows(bogusFile)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// expected.
|
||||
return
|
||||
@@ -607,7 +594,7 @@ func TestLoadPrefsFileWithZeroInIt(t *testing.T) {
|
||||
f.Close()
|
||||
defer os.Remove(path)
|
||||
|
||||
p, err := LoadPrefs(path)
|
||||
p, err := LoadPrefsWindows(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// expected.
|
||||
return
|
||||
@@ -631,8 +618,9 @@ func TestMaskedPrefsSetsInternal(t *testing.T) {
|
||||
func TestMaskedPrefsFields(t *testing.T) {
|
||||
have := map[string]bool{}
|
||||
for _, f := range fieldsOf(reflect.TypeFor[Prefs]()) {
|
||||
if f == "Persist" {
|
||||
// This one can't be edited.
|
||||
switch f {
|
||||
case "Persist", "AllowSingleHosts":
|
||||
// These can't be edited.
|
||||
continue
|
||||
}
|
||||
have[f] = true
|
||||
@@ -751,13 +739,12 @@ func TestMaskedPrefsPretty(t *testing.T) {
|
||||
{
|
||||
m: &MaskedPrefs{
|
||||
Prefs: Prefs{
|
||||
Hostname: "bar",
|
||||
OperatorUser: "galaxybrain",
|
||||
AllowSingleHosts: true,
|
||||
RouteAll: false,
|
||||
ExitNodeID: "foo",
|
||||
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
||||
NetfilterMode: preftype.NetfilterNoDivert,
|
||||
Hostname: "bar",
|
||||
OperatorUser: "galaxybrain",
|
||||
RouteAll: false,
|
||||
ExitNodeID: "foo",
|
||||
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
||||
NetfilterMode: preftype.NetfilterNoDivert,
|
||||
},
|
||||
RouteAllSet: true,
|
||||
HostnameSet: true,
|
||||
@@ -1062,3 +1049,24 @@ func TestNotifyPrefsJSONRoundtrip(t *testing.T) {
|
||||
t.Fatal("Prefs should not be valid after deserialization")
|
||||
}
|
||||
}
|
||||
|
||||
// Verify that our Prefs type writes out an AllowSingleHosts field so we can
|
||||
// downgrade to older versions that require it.
|
||||
func TestPrefsDowngrade(t *testing.T) {
|
||||
var p Prefs
|
||||
j, err := json.Marshal(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
type oldPrefs struct {
|
||||
AllowSingleHosts bool
|
||||
}
|
||||
var op oldPrefs
|
||||
if err := json.Unmarshal(j, &op); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !op.AllowSingleHosts {
|
||||
t.Fatal("AllowSingleHosts should be true")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -626,7 +626,7 @@ func (v ServeConfigView) HasAllowFunnel() bool {
|
||||
}()
|
||||
}
|
||||
|
||||
// FindFunnel reports whether target exists in in either the background AllowFunnel
|
||||
// FindFunnel reports whether target exists in either the background AllowFunnel
|
||||
// or any of the foreground configs.
|
||||
func (v ServeConfigView) HasFunnelForTarget(target HostPort) bool {
|
||||
if v.AllowFunnel().Get(target) {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -30,6 +31,10 @@ func New(_ logger.Logf, secretName string) (*Store, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if os.Getenv("TS_KUBERNETES_READ_API_SERVER_ADDRESS_FROM_ENV") == "true" {
|
||||
// Derive the API server address from the environment variables
|
||||
c.SetURL(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
|
||||
}
|
||||
canPatch, _, err := c.CheckSecretPermissions(context.Background(), secretName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package kube
|
||||
|
||||
const (
|
||||
Alpha1Version = "v1alpha1"
|
||||
|
||||
DNSRecordsCMName = "dnsrecords"
|
||||
DNSRecordsCMKey = "records.json"
|
||||
)
|
||||
|
||||
type Records struct {
|
||||
// Version is the version of this Records configuration. Version is
|
||||
// written by the operator, i.e when it first populates the Records.
|
||||
// k8s-nameserver must verify that it knows how to parse a given
|
||||
// version.
|
||||
Version string `json:"version"`
|
||||
// IP4 contains a mapping of DNS names to IPv4 address(es).
|
||||
IP4 map[string][]string `json:"ip4"`
|
||||
}
|
||||
49
k8s-operator/utils.go
Normal file
49
k8s-operator/utils.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !plan9
|
||||
|
||||
package kube
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
const (
|
||||
Alpha1Version = "v1alpha1"
|
||||
|
||||
DNSRecordsCMName = "dnsrecords"
|
||||
DNSRecordsCMKey = "records.json"
|
||||
)
|
||||
|
||||
type Records struct {
|
||||
// Version is the version of this Records configuration. Version is
|
||||
// written by the operator, i.e when it first populates the Records.
|
||||
// k8s-nameserver must verify that it knows how to parse a given
|
||||
// version.
|
||||
Version string `json:"version"`
|
||||
// IP4 contains a mapping of DNS names to IPv4 address(es).
|
||||
IP4 map[string][]string `json:"ip4"`
|
||||
}
|
||||
|
||||
// TailscaledConfigFileNameForCap returns a tailscaled config file name in
|
||||
// format expected by containerboot for the given CapVer.
|
||||
func TailscaledConfigFileNameForCap(cap tailcfg.CapabilityVersion) string {
|
||||
if cap < 95 {
|
||||
return "tailscaled"
|
||||
}
|
||||
return fmt.Sprintf("cap-%v.hujson", cap)
|
||||
}
|
||||
|
||||
// CapVerFromFileName parses the capability version from a tailscaled
|
||||
// config file name previously generated by TailscaledConfigFileNameForCap.
|
||||
func CapVerFromFileName(name string) (tailcfg.CapabilityVersion, error) {
|
||||
if name == "tailscaled" {
|
||||
return 0, nil
|
||||
}
|
||||
var cap tailcfg.CapabilityVersion
|
||||
_, err := fmt.Sscanf(name, "cap-%d.hujson", &cap)
|
||||
return cap, err
|
||||
}
|
||||
@@ -26,8 +26,7 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.19.0/LICENSE))
|
||||
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.19.0/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/bits-and-blooms/bitset](https://pkg.go.dev/github.com/bits-and-blooms/bitset) ([BSD-3-Clause](https://github.com/bits-and-blooms/bitset/blob/v1.13.0/LICENSE))
|
||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.7.0/LICENSE))
|
||||
- [github.com/coreos/go-systemd/v22/dbus](https://pkg.go.dev/github.com/coreos/go-systemd/v22/dbus) ([Apache-2.0](https://github.com/coreos/go-systemd/blob/v22.5.0/LICENSE))
|
||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/65c67c9f46e6/LICENSE))
|
||||
- [github.com/djherbis/times](https://pkg.go.dev/github.com/djherbis/times) ([MIT](https://github.com/djherbis/times/blob/v1.6.0/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.5.0/LICENSE))
|
||||
- [github.com/gaissmai/bart](https://pkg.go.dev/github.com/gaissmai/bart) ([MIT](https://github.com/gaissmai/bart/blob/v0.4.1/LICENSE))
|
||||
@@ -36,7 +35,7 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
|
||||
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
|
||||
- [github.com/google/nftables](https://pkg.go.dev/github.com/google/nftables) ([Apache-2.0](https://github.com/google/nftables/blob/5e242ec57806/LICENSE))
|
||||
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.5.0/LICENSE))
|
||||
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE))
|
||||
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.2.0/LICENSE))
|
||||
- [github.com/illarion/gonotify](https://pkg.go.dev/github.com/illarion/gonotify) ([MIT](https://github.com/illarion/gonotify/blob/v1.0.1/LICENSE))
|
||||
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/8c70d406f6d2/LICENSE))
|
||||
@@ -61,7 +60,7 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
- [github.com/tailscale/peercred](https://pkg.go.dev/github.com/tailscale/peercred) ([BSD-3-Clause](https://github.com/tailscale/peercred/blob/b535050b2aa4/LICENSE))
|
||||
- [github.com/tailscale/tailscale-android/libtailscale](https://pkg.go.dev/github.com/tailscale/tailscale-android/libtailscale) ([BSD-3-Clause](https://github.com/tailscale/tailscale-android/blob/HEAD/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/64040e66467d/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/03c5a0ccf754/LICENSE))
|
||||
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/62b9a7c569f9/LICENSE))
|
||||
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
|
||||
- [github.com/u-root/uio](https://pkg.go.dev/github.com/u-root/uio) ([BSD-3-Clause](https://github.com/u-root/uio/blob/a3c409a6018e/LICENSE))
|
||||
|
||||
@@ -12,25 +12,24 @@ See also the dependencies in the [Tailscale CLI][].
|
||||
|
||||
|
||||
- [filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519) ([BSD-3-Clause](https://github.com/FiloSottile/edwards25519/blob/v1.1.0/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.24.1/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.26.6/config/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/credentials](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.16.16/credentials/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/ec2/imds) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.14.11/feature/ec2/imds/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.2.10/internal/configsources/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.5.10/internal/endpoints/v2/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/ini](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/ini) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.7.3/internal/ini/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.24.1/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/accept-encoding/v1.10.4/service/internal/accept-encoding/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.10.10/service/internal/presigned-url/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.26.1/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.27.11/config/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/credentials](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.17.11/credentials/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/ec2/imds) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.16.1/feature/ec2/imds/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.3.5/internal/configsources/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.6.5/internal/endpoints/v2/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/ini](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/ini) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.8.0/internal/ini/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.26.1/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/accept-encoding/v1.11.2/service/internal/accept-encoding/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.11.7/service/internal/presigned-url/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/ssm](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssm) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.45.0/service/ssm/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sso](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sso) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.18.7/service/sso/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssooidc) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.21.7/service/ssooidc/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.26.7/service/sts/LICENSE.txt))
|
||||
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.19.0/LICENSE))
|
||||
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.19.0/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sso](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sso) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.20.5/service/sso/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssooidc) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.23.4/service/ssooidc/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.28.6/service/sts/LICENSE.txt))
|
||||
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.20.2/LICENSE))
|
||||
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.20.2/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/bits-and-blooms/bitset](https://pkg.go.dev/github.com/bits-and-blooms/bitset) ([BSD-3-Clause](https://github.com/bits-and-blooms/bitset/blob/v1.13.0/LICENSE))
|
||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.7.0/LICENSE))
|
||||
- [github.com/coreos/go-systemd/v22/dbus](https://pkg.go.dev/github.com/coreos/go-systemd/v22/dbus) ([Apache-2.0](https://github.com/coreos/go-systemd/blob/v22.5.0/LICENSE))
|
||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/65c67c9f46e6/LICENSE))
|
||||
- [github.com/digitalocean/go-smbios/smbios](https://pkg.go.dev/github.com/digitalocean/go-smbios/smbios) ([Apache-2.0](https://github.com/digitalocean/go-smbios/blob/390a4f403a8e/LICENSE.md))
|
||||
- [github.com/djherbis/times](https://pkg.go.dev/github.com/djherbis/times) ([MIT](https://github.com/djherbis/times/blob/v1.6.0/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.5.0/LICENSE))
|
||||
@@ -59,13 +58,14 @@ See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/miekg/dns](https://pkg.go.dev/github.com/miekg/dns) ([BSD-3-Clause](https://github.com/miekg/dns/blob/v1.1.58/LICENSE))
|
||||
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
|
||||
- [github.com/pierrec/lz4/v4](https://pkg.go.dev/github.com/pierrec/lz4/v4) ([BSD-3-Clause](https://github.com/pierrec/lz4/blob/v4.1.21/LICENSE))
|
||||
- [github.com/prometheus-community/pro-bing](https://pkg.go.dev/github.com/prometheus-community/pro-bing) ([MIT](https://github.com/prometheus-community/pro-bing/blob/v0.4.0/LICENSE))
|
||||
- [github.com/safchain/ethtool](https://pkg.go.dev/github.com/safchain/ethtool) ([Apache-2.0](https://github.com/safchain/ethtool/blob/v0.3.0/LICENSE))
|
||||
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/7ce1f622c780/LICENSE))
|
||||
- [github.com/tailscale/goupnp](https://pkg.go.dev/github.com/tailscale/goupnp) ([BSD-2-Clause](https://github.com/tailscale/goupnp/blob/c64d0f06ea05/LICENSE))
|
||||
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
- [github.com/tailscale/peercred](https://pkg.go.dev/github.com/tailscale/peercred) ([BSD-3-Clause](https://github.com/tailscale/peercred/blob/b535050b2aa4/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/64040e66467d/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/03c5a0ccf754/LICENSE))
|
||||
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/62b9a7c569f9/LICENSE))
|
||||
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
|
||||
- [github.com/u-root/uio](https://pkg.go.dev/github.com/u-root/uio) ([BSD-3-Clause](https://github.com/u-root/uio/blob/a3c409a6018e/LICENSE))
|
||||
@@ -74,12 +74,12 @@ See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/fdeea329fbba/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.21.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/1b970713:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.23.0:LICENSE))
|
||||
- [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.6.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.18.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.18.0:LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.22.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/fe59bbe5:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.24.0:LICENSE))
|
||||
- [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.7.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.19.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.19.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.14.0:LICENSE))
|
||||
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.5.0:LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/ee1e1f6070e3/LICENSE))
|
||||
@@ -92,5 +92,5 @@ See also the dependencies in the [Tailscale CLI][].
|
||||
- [Sparkle](https://sparkle-project.org/) ([MIT](https://github.com/sparkle-project/Sparkle/blob/2.x/LICENSE))
|
||||
- [wireguard-apple](https://git.zx2c4.com/wireguard-apple) ([MIT](https://git.zx2c4.com/wireguard-apple/tree/COPYING))
|
||||
- [apple-oss-distributions/configd](https://github.com/apple-oss-distributions/configd) ([APSL](https://github.com/apple-oss-distributions/configd/blob/main/APPLE_LICENSE))
|
||||
- [WebDAV-Swift](https://github.com/skjiisa/WebDAV-Swift) ([BSD](https://github.com/skjiisa/WebDAV-Swift#BSD-2-Clause-1-ov-file)
|
||||
|
||||
- [WebDAV-Swift](https://github.com/skjiisa/WebDAV-Swift) ([BSD](https://github.com/skjiisa/WebDAV-Swift#BSD-2-Clause-1-ov-file))
|
||||
- [CachedAsyncImage](https://github.com/lorenzofiamingo/swiftui-cached-async-image) ([MIT](https://github.com/lorenzofiamingo/swiftui-cached-async-image/blob/main/LICENSE.md))
|
||||
@@ -34,8 +34,7 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.19.0/LICENSE))
|
||||
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.19.0/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/bits-and-blooms/bitset](https://pkg.go.dev/github.com/bits-and-blooms/bitset) ([BSD-3-Clause](https://github.com/bits-and-blooms/bitset/blob/v1.13.0/LICENSE))
|
||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.7.0/LICENSE))
|
||||
- [github.com/coreos/go-systemd/v22/dbus](https://pkg.go.dev/github.com/coreos/go-systemd/v22/dbus) ([Apache-2.0](https://github.com/coreos/go-systemd/blob/v22.5.0/LICENSE))
|
||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/65c67c9f46e6/LICENSE))
|
||||
- [github.com/creack/pty](https://pkg.go.dev/github.com/creack/pty) ([MIT](https://github.com/creack/pty/blob/v1.1.21/LICENSE))
|
||||
- [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/a09d6be7affa/LICENSE))
|
||||
- [github.com/digitalocean/go-smbios/smbios](https://pkg.go.dev/github.com/digitalocean/go-smbios/smbios) ([Apache-2.0](https://github.com/digitalocean/go-smbios/blob/390a4f403a8e/LICENSE.md))
|
||||
@@ -48,7 +47,7 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
|
||||
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
|
||||
- [github.com/google/nftables](https://pkg.go.dev/github.com/google/nftables) ([Apache-2.0](https://github.com/google/nftables/blob/5e242ec57806/LICENSE))
|
||||
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.5.0/LICENSE))
|
||||
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.6.0/LICENSE))
|
||||
- [github.com/gorilla/csrf](https://pkg.go.dev/github.com/gorilla/csrf) ([BSD-3-Clause](https://github.com/gorilla/csrf/blob/v1.7.2/LICENSE))
|
||||
- [github.com/gorilla/securecookie](https://pkg.go.dev/github.com/gorilla/securecookie) ([BSD-3-Clause](https://github.com/gorilla/securecookie/blob/v1.1.2/LICENSE))
|
||||
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.2.0/LICENSE))
|
||||
@@ -74,6 +73,7 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/peterbourgon/ff/v3](https://pkg.go.dev/github.com/peterbourgon/ff/v3) ([Apache-2.0](https://github.com/peterbourgon/ff/blob/v3.4.0/LICENSE))
|
||||
- [github.com/pierrec/lz4/v4](https://pkg.go.dev/github.com/pierrec/lz4/v4) ([BSD-3-Clause](https://github.com/pierrec/lz4/blob/v4.1.21/LICENSE))
|
||||
- [github.com/pkg/sftp](https://pkg.go.dev/github.com/pkg/sftp) ([BSD-2-Clause](https://github.com/pkg/sftp/blob/v1.13.6/LICENSE))
|
||||
- [github.com/prometheus-community/pro-bing](https://pkg.go.dev/github.com/prometheus-community/pro-bing) ([MIT](https://github.com/prometheus-community/pro-bing/blob/v0.4.0/LICENSE))
|
||||
- [github.com/safchain/ethtool](https://pkg.go.dev/github.com/safchain/ethtool) ([Apache-2.0](https://github.com/safchain/ethtool/blob/v0.3.0/LICENSE))
|
||||
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
|
||||
- [github.com/tailscale/certstore](https://pkg.go.dev/github.com/tailscale/certstore) ([MIT](https://github.com/tailscale/certstore/blob/d3fa0460f47e/LICENSE.md))
|
||||
@@ -84,7 +84,7 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/tailscale/peercred](https://pkg.go.dev/github.com/tailscale/peercred) ([BSD-3-Clause](https://github.com/tailscale/peercred/blob/b535050b2aa4/LICENSE))
|
||||
- [github.com/tailscale/web-client-prebuilt](https://pkg.go.dev/github.com/tailscale/web-client-prebuilt) ([BSD-3-Clause](https://github.com/tailscale/web-client-prebuilt/blob/5db17b287bf1/LICENSE))
|
||||
- [github.com/tailscale/wf](https://pkg.go.dev/github.com/tailscale/wf) ([BSD-3-Clause](https://github.com/tailscale/wf/blob/6fbb0a674ee6/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/64040e66467d/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/03c5a0ccf754/LICENSE))
|
||||
- [github.com/tailscale/xnet/webdav](https://pkg.go.dev/github.com/tailscale/xnet/webdav) ([BSD-3-Clause](https://github.com/tailscale/xnet/blob/62b9a7c569f9/LICENSE))
|
||||
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
|
||||
- [github.com/toqueteos/webbrowser](https://pkg.go.dev/github.com/toqueteos/webbrowser) ([MIT](https://github.com/toqueteos/webbrowser/blob/v1.2.0/LICENSE.md))
|
||||
|
||||
@@ -13,23 +13,23 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/alexbrainman/sspi](https://pkg.go.dev/github.com/alexbrainman/sspi) ([BSD-3-Clause](https://github.com/alexbrainman/sspi/blob/1a75b4708caa/LICENSE))
|
||||
- [github.com/apenwarr/fixconsole](https://pkg.go.dev/github.com/apenwarr/fixconsole) ([Apache-2.0](https://github.com/apenwarr/fixconsole/blob/5a9f6489cc29/LICENSE))
|
||||
- [github.com/apenwarr/w32](https://pkg.go.dev/github.com/apenwarr/w32) ([BSD-3-Clause](https://github.com/apenwarr/w32/blob/aa00fece76ab/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.24.1/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.26.6/config/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/credentials](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.16.16/credentials/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/ec2/imds) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.14.11/feature/ec2/imds/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.2.10/internal/configsources/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.5.10/internal/endpoints/v2/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/ini](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/ini) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.7.3/internal/ini/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.24.1/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/accept-encoding/v1.10.4/service/internal/accept-encoding/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.10.10/service/internal/presigned-url/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.26.1/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.27.11/config/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/credentials](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.17.11/credentials/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/ec2/imds) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.16.1/feature/ec2/imds/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.3.5/internal/configsources/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.6.5/internal/endpoints/v2/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/ini](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/ini) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.8.0/internal/ini/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.26.1/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/accept-encoding/v1.11.2/service/internal/accept-encoding/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.11.7/service/internal/presigned-url/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/ssm](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssm) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.45.0/service/ssm/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sso](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sso) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.18.7/service/sso/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssooidc) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.21.7/service/ssooidc/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.26.7/service/sts/LICENSE.txt))
|
||||
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.19.0/LICENSE))
|
||||
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.19.0/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.7.0/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sso](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sso) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.20.5/service/sso/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssooidc) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.23.4/service/ssooidc/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.28.6/service/sts/LICENSE.txt))
|
||||
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.20.2/LICENSE))
|
||||
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.20.2/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/65c67c9f46e6/LICENSE))
|
||||
- [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/b75a8a7d7eb0/LICENSE))
|
||||
- [github.com/djherbis/times](https://pkg.go.dev/github.com/djherbis/times) ([MIT](https://github.com/djherbis/times/blob/v1.6.0/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.5.0/LICENSE))
|
||||
@@ -66,14 +66,14 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/fdeea329fbba/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.21.0:LICENSE))
|
||||
- [golang.org/x/exp/constraints](https://pkg.go.dev/golang.org/x/exp/constraints) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/1b970713:LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.22.0:LICENSE))
|
||||
- [golang.org/x/exp/constraints](https://pkg.go.dev/golang.org/x/exp/constraints) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/fe59bbe5:LICENSE))
|
||||
- [golang.org/x/image/bmp](https://pkg.go.dev/golang.org/x/image/bmp) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.15.0:LICENSE))
|
||||
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.14.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.23.0:LICENSE))
|
||||
- [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.6.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.18.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.18.0:LICENSE))
|
||||
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.17.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.24.0:LICENSE))
|
||||
- [golang.org/x/sync](https://pkg.go.dev/golang.org/x/sync) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.7.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.19.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.19.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.14.0:LICENSE))
|
||||
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))
|
||||
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user