Compare commits

..

1 Commits

Author SHA1 Message Date
Brad Fitzpatrick
a794630f60 wgengine/magicsock: add controlknob tunable for session timeout experiments
Updates #TODO

Change-Id: Ifb7ee2b69545cbc457aa2bf4c4744f431edb36e2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-10-06 06:59:17 -07:00
70 changed files with 828 additions and 1738 deletions

4
api.md
View File

@@ -209,6 +209,10 @@ You can also [list all devices in the tailnet](#list-tailnet-devices) to get the
"192.68.0.21:59128"
],
// derp (string) is the IP:port of the DERP server currently being used.
// Learn about DERP servers at https://tailscale.com/kb/1232/.
"derp":"",
// mappingVariesByDestIP (boolean) is 'true' if the host's NAT mappings
// vary based on the destination IP.
"mappingVariesByDestIP":false,

View File

@@ -30,7 +30,6 @@ import (
"github.com/google/uuid"
"tailscale.com/clientupdate/distsign"
"tailscale.com/types/logger"
"tailscale.com/util/cmpver"
"tailscale.com/util/winutil"
"tailscale.com/version"
"tailscale.com/version/distro"
@@ -78,10 +77,6 @@ type Arguments struct {
AppStore bool
// Logf is a logger for update progress messages.
Logf logger.Logf
// Stdout and Stderr should be used for output instead of os.Stdout and
// os.Stderr.
Stdout io.Writer
Stderr io.Writer
// Confirm is called when a new version is available and should return true
// if this new version should be installed. When Confirm returns false, the
// update is aborted.
@@ -113,12 +108,6 @@ func NewUpdater(args Arguments) (*Updater, error) {
up := Updater{
Arguments: args,
}
if up.Stdout == nil {
up.Stdout = os.Stdout
}
if up.Stderr == nil {
up.Stderr = os.Stderr
}
up.Update = up.getUpdateFunction()
if up.Update == nil {
return nil, errors.ErrUnsupported
@@ -212,13 +201,9 @@ func Update(args Arguments) error {
}
func (up *Updater) confirm(ver string) bool {
switch cmpver.Compare(version.Short(), ver) {
case 0:
if version.Short() == ver {
up.Logf("already running %v; no update needed", ver)
return false
case 1:
up.Logf("installed version %v is newer than the latest available version %v; no update needed", version.Short(), ver)
return false
}
if up.Confirm != nil {
return up.Confirm(ver)
@@ -271,9 +256,9 @@ func (up *Updater) updateSynology() error {
// connected over tailscale ssh and this parent process dies. Otherwise, if
// you abort synopkg install mid-way, tailscaled is not restarted.
cmd := exec.Command("nohup", "synopkg", "install", spkPath)
// Don't attach cmd.Stdout to Stdout because nohup will redirect that into
// nohup.out file. synopkg doesn't have any progress output anyway, it just
// spits out a JSON result when done.
// Don't attach cmd.Stdout to os.Stdout because nohup will redirect that
// into nohup.out file. synopkg doesn't have any progress output anyway, it
// just spits out a JSON result when done.
out, err := cmd.CombinedOutput()
if err != nil {
if dsmVersion == 6 && bytes.Contains(out, []byte("error = [290]")) {
@@ -384,15 +369,15 @@ func (up *Updater) updateDebLike() error {
// we're not updating them:
"-o", "APT::Get::List-Cleanup=0",
)
cmd.Stdout = up.Stdout
cmd.Stderr = up.Stderr
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}
cmd = exec.Command("apt-get", "install", "--yes", "--allow-downgrades", "tailscale="+ver)
cmd.Stdout = up.Stdout
cmd.Stderr = up.Stderr
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}
@@ -506,8 +491,8 @@ func (up *Updater) updateFedoraLike(packageManager string) func() error {
}
cmd := exec.Command(packageManager, "install", "--assumeyes", fmt.Sprintf("tailscale-%s-1", ver))
cmd.Stdout = up.Stdout
cmd.Stderr = up.Stderr
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}
@@ -592,8 +577,8 @@ func (up *Updater) updateAlpineLike() (err error) {
}
cmd := exec.Command("apk", "upgrade", "tailscale")
cmd.Stdout = up.Stdout
cmd.Stderr = up.Stderr
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed tailscale update using apk: %w", err)
}
@@ -649,8 +634,8 @@ func (up *Updater) updateMacAppStore() error {
}
cmd := exec.Command("sudo", "softwareupdate", "--install", newTailscale)
cmd.Stdout = up.Stdout
cmd.Stderr = up.Stderr
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("can't install App Store update for Tailscale: %w", err)
}
@@ -741,8 +726,8 @@ func (up *Updater) updateWindows() error {
cmd := exec.Command(selfCopy, "update")
cmd.Env = append(os.Environ(), winMSIEnv+"="+msiTarget)
cmd.Stdout = up.Stderr
cmd.Stderr = up.Stderr
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
if err := cmd.Start(); err != nil {
return err
@@ -758,8 +743,8 @@ func (up *Updater) installMSI(msi string) error {
for tries := 0; tries < 2; tries++ {
cmd := exec.Command("msiexec.exe", "/i", filepath.Base(msi), "/quiet", "/promptrestart", "/qn")
cmd.Dir = filepath.Dir(msi)
cmd.Stdout = up.Stdout
cmd.Stderr = up.Stderr
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
err = cmd.Run()
if err == nil {
@@ -772,8 +757,8 @@ func (up *Updater) installMSI(msi string) error {
// Assume it's a downgrade, which msiexec won't permit. Uninstall our current version first.
up.Logf("Uninstalling current version %q for downgrade...", uninstallVersion)
cmd = exec.Command("msiexec.exe", "/x", msiUUIDForVersion(uninstallVersion), "/norestart", "/qn")
cmd.Stdout = up.Stdout
cmd.Stderr = up.Stderr
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
err = cmd.Run()
up.Logf("msiexec uninstall: %v", err)
@@ -861,8 +846,8 @@ func (up *Updater) updateFreeBSD() (err error) {
}
cmd := exec.Command("pkg", "upgrade", "tailscale")
cmd.Stdout = up.Stdout
cmd.Stderr = up.Stderr
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed tailscale update using pkg: %w", err)
}

View File

@@ -1,3 +1,3 @@
-----BEGIN ROOT PUBLIC KEY-----
ZjjKhUHBtLNRSO1dhOTjrXJGJ8lDe1594WM2XDuheVQ=
Muw5GkO5mASsJ7k6kS+svfuanr6XcW9I7fPGtyqOTeI=
-----END ROOT PUBLIC KEY-----

View File

@@ -28,7 +28,6 @@ func main() {
ephemeral := flag.Bool("ephemeral", false, "allocate an ephemeral authkey")
preauth := flag.Bool("preauth", true, "set the authkey as pre-authorized")
tags := flag.String("tags", "", "comma-separated list of tags to apply to the authkey")
expiry := flag.Duration("expiry", 0, "amount of time until authkey expires, for example 24h.")
flag.Parse()
clientID := os.Getenv("TS_API_CLIENT_ID")
@@ -66,7 +65,7 @@ func main() {
},
}
authkey, _, err := tsClient.CreateKeyWithExpiry(ctx, caps, *expiry)
authkey, _, err := tsClient.CreateKey(ctx, caps)
if err != nil {
log.Fatal(err.Error())
}

View File

@@ -75,7 +75,6 @@ func main() {
wgPort = fs.Int("wg-listen-port", 0, "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
promoteHTTPS = fs.Bool("promote-https", true, "promote HTTP to HTTPS")
debugPort = fs.Int("debug-port", 8893, "Listening port for debug/metrics endpoint")
hostname = fs.String("hostname", "", "Hostname to register the service under")
)
err := ff.Parse(fs, os.Args[1:], ff.WithEnvVarPrefix("TS_APPC"))
@@ -90,7 +89,6 @@ func main() {
var s server
s.ts.Port = uint16(*wgPort)
s.ts.Hostname = *hostname
defer s.ts.Close()
lc, err := s.ts.LocalClient()

View File

@@ -139,11 +139,6 @@ var debugCmd = &ffcli.Command{
Exec: localAPIAction("break-derp-conns"),
ShortHelp: "break any open DERP connections from the daemon",
},
{
Name: "pick-new-derp",
Exec: localAPIAction("pick-new-derp"),
ShortHelp: "switch to some other random DERP home region for a short time",
},
{
Name: "force-netmap-update",
Exec: localAPIAction("force-netmap-update"),

View File

@@ -49,7 +49,6 @@ type setArgsT struct {
forceDaemon bool
updateCheck bool
updateApply bool
postureChecking bool
}
func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
@@ -67,8 +66,6 @@ func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
setf.BoolVar(&setArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
setf.BoolVar(&setArgs.updateCheck, "update-check", true, "HIDDEN: notify about available Tailscale updates")
setf.BoolVar(&setArgs.updateApply, "auto-update", false, "HIDDEN: automatically update to the latest available version")
setf.BoolVar(&setArgs.postureChecking, "posture-checking", false, "HIDDEN: allow management plane to gather device posture information")
if safesocket.GOOSUsesPeerCreds(goos) {
setf.StringVar(&setArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
}
@@ -111,7 +108,6 @@ func runSet(ctx context.Context, args []string) (retErr error) {
Check: setArgs.updateCheck,
Apply: setArgs.updateApply,
},
PostureChecking: setArgs.postureChecking,
},
}

View File

@@ -114,7 +114,6 @@ func newUpFlagSet(goos string, upArgs *upArgsT, cmd string) *flag.FlagSet {
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. \"10.0.0.0/8,192.168.0.0/24\") or empty string to not advertise routes")
upf.BoolVar(&upArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
if safesocket.GOOSUsesPeerCreds(goos) {
upf.StringVar(&upArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
}
@@ -726,7 +725,6 @@ func init() {
addPrefFlagMapping("nickname", "ProfileName")
addPrefFlagMapping("update-check", "AutoUpdate")
addPrefFlagMapping("auto-update", "AutoUpdate")
addPrefFlagMapping("posture-checking", "PostureChecking")
}
func addPrefFlagMapping(flagName string, prefNames ...string) {

View File

@@ -63,9 +63,7 @@ func runUpdate(ctx context.Context, args []string) error {
err := clientupdate.Update(clientupdate.Arguments{
Version: ver,
AppStore: updateArgs.appStore,
Logf: func(f string, a ...any) { printf(f+"\n", a...) },
Stdout: Stdout,
Stderr: Stderr,
Logf: func(format string, args ...any) { fmt.Printf(format+"\n", args...) },
Confirm: confirmUpdate,
})
if errors.Is(err, errors.ErrUnsupported) {

View File

@@ -158,7 +158,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/types/views from tailscale.com/tailcfg+
tailscale.com/util/clientmetric from tailscale.com/net/netcheck+
tailscale.com/util/cloudenv from tailscale.com/net/dnscache+
tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy+
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
tailscale.com/util/cmpx from tailscale.com/cmd/tailscale/cli+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+

View File

@@ -86,7 +86,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
W 💣 github.com/dblohm7/wingoes/com/automation from tailscale.com/util/osdiag/internal/wsc
W github.com/dblohm7/wingoes/internal from github.com/dblohm7/wingoes/com
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/osdiag+
LW 💣 github.com/digitalocean/go-smbios/smbios from tailscale.com/posture
github.com/fxamacker/cbor/v2 from tailscale.com/tka
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
@@ -293,7 +292,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
tailscale.com/paths from tailscale.com/ipn/ipnlocal+
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
tailscale.com/posture from tailscale.com/ipn/ipnlocal
tailscale.com/proxymap from tailscale.com/tsd+
tailscale.com/safesocket from tailscale.com/client/tailscale+
tailscale.com/smallzstd from tailscale.com/control/controlclient+
@@ -331,7 +329,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/types/views from tailscale.com/ipn/ipnlocal+
tailscale.com/util/clientmetric from tailscale.com/control/controlclient+
tailscale.com/util/cloudenv from tailscale.com/net/dns/resolver+
tailscale.com/util/cmpver from tailscale.com/net/dns+
LW tailscale.com/util/cmpver from tailscale.com/net/dns+
tailscale.com/util/cmpx from tailscale.com/derp/derphttp+
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics+
@@ -356,7 +354,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/util/set from tailscale.com/health+
tailscale.com/util/singleflight from tailscale.com/control/controlclient+
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
tailscale.com/util/syspolicy from tailscale.com/cmd/tailscaled+
W tailscale.com/util/syspolicy from tailscale.com/cmd/tailscaled
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
tailscale.com/util/systemd from tailscale.com/control/controlclient+
tailscale.com/util/testenv from tailscale.com/ipn/ipnlocal+
@@ -388,7 +386,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from github.com/tailscale/golang-x-crypto/ssh+
LD golang.org/x/crypto/ed25519 from github.com/tailscale/golang-x-crypto/ssh
LD golang.org/x/crypto/ed25519 from golang.org/x/crypto/ssh+
golang.org/x/crypto/hkdf from crypto/tls+
golang.org/x/crypto/nacl/box from tailscale.com/types/key
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box

View File

@@ -7,7 +7,9 @@ package controlknobs
import (
"slices"
"strconv"
"sync/atomic"
"time"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
@@ -52,6 +54,10 @@ type Knobs struct {
// DisableDNSForwarderTCPRetries is whether the DNS forwarder should
// skip retrying truncated queries over TCP.
DisableDNSForwarderTCPRetries atomic.Bool
// MagicsockSessionActiveTimeout is an alternate magicsock session timeout
// duration to use. If zero or unset, the default is used.
MagicsockSessionActiveTimeout syncs.AtomicValue[time.Duration]
}
// UpdateFromNodeAttributes updates k (if non-nil) based on the provided self
@@ -91,6 +97,17 @@ func (k *Knobs) UpdateFromNodeAttributes(selfNodeAttrs []tailcfg.NodeCapability,
k.DisableDeltaUpdates.Store(disableDeltaUpdates)
k.PeerMTUEnable.Store(peerMTUEnable)
k.DisableDNSForwarderTCPRetries.Store(dnsForwarderDisableTCPRetries)
var timeout time.Duration
if vv := capMap[tailcfg.NodeAttrMagicsockSessionTimeout]; len(vv) > 0 {
if v, _ := strconv.Unquote(string(vv[0])); v != "" {
timeout, _ = time.ParseDuration(v)
timeout = max(timeout, 0)
}
}
if was := k.MagicsockSessionActiveTimeout.Load(); was != timeout {
k.MagicsockSessionActiveTimeout.Store(timeout)
}
}
// AsDebugJSON returns k as something that can be marshalled with json.Marshal
@@ -109,5 +126,6 @@ func (k *Knobs) AsDebugJSON() map[string]any {
"DisableDeltaUpdates": k.DisableDeltaUpdates.Load(),
"PeerMTUEnable": k.PeerMTUEnable.Load(),
"DisableDNSForwarderTCPRetries": k.DisableDNSForwarderTCPRetries.Load(),
"MagicsockSessionActiveTimeout": k.MagicsockSessionActiveTimeout.Load().String(),
}
}

View File

@@ -261,7 +261,7 @@ func parsePong(ver uint8, p []byte) (m *Pong, err error) {
func MessageSummary(m Message) string {
switch m := m.(type) {
case *Ping:
return fmt.Sprintf("ping tx=%x padding=%v", m.TxID[:6], m.Padding)
return fmt.Sprintf("ping tx=%x", m.TxID[:6])
case *Pong:
return fmt.Sprintf("pong tx=%x", m.TxID[:6])
case *CallMeMaybe:

View File

@@ -1,63 +0,0 @@
#!/bin/sh
# Copyright (c) Tailscale Inc & AUTHORS
# SPDX-License-Identifier: BSD-3-Clause
### BEGIN INIT INFO
# Provides: tailscaled
# Required-Start:
# Required-Stop:
# Default-Start:
# Default-Stop:
# Short-Description: Tailscale Mesh Wireguard VPN
### END INIT INFO
set -e
# /etc/init.d/tailscale: start and stop the Tailscale VPN service
test -x /usr/sbin/tailscaled || exit 0
umask 022
. /lib/lsb/init-functions
# Are we running from init?
run_by_init() {
([ "$previous" ] && [ "$runlevel" ]) || [ "$runlevel" = S ]
}
export PATH="${PATH:+$PATH:}/usr/sbin:/sbin"
case "$1" in
start)
log_daemon_msg "Starting Tailscale VPN" "tailscaled" || true
if start-stop-daemon --start --oknodo --name tailscaled -m --pidfile /run/tailscaled.pid --background \
--exec /usr/sbin/tailscaled -- \
--state=/var/lib/tailscale/tailscaled.state \
--socket=/run/tailscale/tailscaled.sock \
--port 41641;
then
log_end_msg 0 || true
else
log_end_msg 1 || true
fi
;;
stop)
log_daemon_msg "Stopping Tailscale VPN" "tailscaled" || true
if start-stop-daemon --stop --remove-pidfile --pidfile /run/tailscaled.pid --exec /usr/sbin/tailscaled; then
log_end_msg 0 || true
else
log_end_msg 1 || true
fi
;;
status)
status_of_proc -p /run/tailscaled.pid /usr/sbin/tailscaled tailscaled && exit 0 || exit $?
;;
*)
log_action_msg "Usage: /etc/init.d/tailscaled {start|stop|status}" || true
exit 1
esac
exit 0

View File

@@ -115,4 +115,4 @@
in
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
}
# nix-direnv cache busting line: sha256-v3/3bVAK/ni0LZ+GPY+dnbdCdvFQUknPxur7u9Cm8Gw=
# nix-direnv cache busting line: sha256-tCc7+umCKgOmKXbElnCmDI4ntPvvHldkxi+RwQuj9ng=

9
go.mod
View File

@@ -20,7 +20,6 @@ require (
github.com/creack/pty v1.1.18
github.com/dave/jennifer v1.7.0
github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e
github.com/dsnet/try v0.0.3
github.com/evanw/esbuild v0.19.4
github.com/frankban/quicktest v1.14.5
@@ -77,14 +76,14 @@ require (
go.uber.org/zap v1.26.0
go4.org/mem v0.0.0-20220726221520-4f986261bf13
go4.org/netipx v0.0.0-20230824141953-6213f710f925
golang.org/x/crypto v0.14.0
golang.org/x/crypto v0.13.0
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
golang.org/x/mod v0.12.0
golang.org/x/net v0.17.0
golang.org/x/net v0.15.0
golang.org/x/oauth2 v0.12.0
golang.org/x/sync v0.3.0
golang.org/x/sys v0.13.0
golang.org/x/term v0.13.0
golang.org/x/sys v0.12.0
golang.org/x/term v0.12.0
golang.org/x/time v0.3.0
golang.org/x/tools v0.13.0
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2

View File

@@ -1 +1 @@
sha256-v3/3bVAK/ni0LZ+GPY+dnbdCdvFQUknPxur7u9Cm8Gw=
sha256-tCc7+umCKgOmKXbElnCmDI4ntPvvHldkxi+RwQuj9ng=

18
go.sum
View File

@@ -233,8 +233,6 @@ github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077 h1:WphxHslVftszsr0
github.com/dblohm7/wingoes v0.0.0-20230929194252-e994401fc077/go.mod h1:6NCrWM5jRefaG7iN0iMShPalLsljHWBh9v1zxM2f8Xs=
github.com/denis-tingaikin/go-header v0.4.3 h1:tEaZKAlqql6SKCY++utLmkPLd6K8IBM20Ha7UVm+mtU=
github.com/denis-tingaikin/go-header v0.4.3/go.mod h1:0wOCWuN71D5qIgE2nz9KrKmuYBAC2Mra5RassOIQ2/c=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q=
github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A=
github.com/docker/cli v24.0.6+incompatible h1:fF+XCQCgJjjQNIMjzaSmiKJSCcfcXb3TWTcc7GAneOY=
github.com/docker/cli v24.0.6+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
@@ -986,8 +984,8 @@ golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -1082,8 +1080,8 @@ golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -1179,8 +1177,8 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -1189,8 +1187,8 @@ golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@@ -1 +1 @@
d1c91593484a1db2d4de2564f2ef2669814af9c8
f242beecd311476f6e6b9fa3052e253e2301e170

View File

@@ -52,7 +52,6 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
OperatorUser string
ProfileName string
AutoUpdate AutoUpdatePrefs
PostureChecking bool
Persist *persist.Persist
}{})

View File

@@ -87,7 +87,6 @@ func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.Netfilte
func (v PrefsView) OperatorUser() string { return v.ж.OperatorUser }
func (v PrefsView) ProfileName() string { return v.ж.ProfileName }
func (v PrefsView) AutoUpdate() AutoUpdatePrefs { return v.ж.AutoUpdate }
func (v PrefsView) PostureChecking() bool { return v.ж.PostureChecking }
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.
@@ -114,7 +113,6 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
OperatorUser string
ProfileName string
AutoUpdate AutoUpdatePrefs
PostureChecking bool
Persist *persist.Persist
}{})

View File

@@ -24,12 +24,10 @@ import (
"tailscale.com/clientupdate"
"tailscale.com/envknob"
"tailscale.com/net/sockstats"
"tailscale.com/posture"
"tailscale.com/tailcfg"
"tailscale.com/util/clientmetric"
"tailscale.com/util/goroutines"
"tailscale.com/util/httpm"
"tailscale.com/util/syspolicy"
"tailscale.com/version"
)
@@ -69,14 +67,6 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
} else {
http.Error(w, "no log flusher wired up", http.StatusInternalServerError)
}
case "/posture/identity":
switch r.Method {
case httpm.GET:
b.handleC2NPostureIdentityGet(w, r)
default:
http.Error(w, "bad method", http.StatusMethodNotAllowed)
return
}
case "/debug/goroutines":
w.Header().Set("Content-Type", "text/plain")
w.Write(goroutines.ScrubbedGoroutineDump(true))
@@ -225,37 +215,6 @@ func (b *LocalBackend) handleC2NUpdatePost(w http.ResponseWriter, r *http.Reques
}()
}
func (b *LocalBackend) handleC2NPostureIdentityGet(w http.ResponseWriter, r *http.Request) {
b.logf("c2n: GET /posture/identity received")
res := tailcfg.C2NPostureIdentityResponse{}
// Only collect serial numbers if enabled on the client,
// this will first check syspolicy, MDM settings like Registry
// on Windows or defaults on macOS. If they are not set, it falls
// back to the cli-flag, `--posture-checking`.
enabled, err := syspolicy.GetBoolean(syspolicy.PostureChecking, b.Prefs().PostureChecking())
if err != nil {
enabled = b.Prefs().PostureChecking()
b.logf("c2n: failed to read PostureChecking from syspolicy, returning default from CLI: %s; got error: %s", enabled, err)
}
if enabled {
sns, err := posture.GetSerialNumbers(b.logf)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
res.SerialNumbers = sns
} else {
res.PostureDisabled = true
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
func (b *LocalBackend) newC2NUpdateResponse() tailcfg.C2NUpdateResponse {
// If NewUpdater does not return an error, we can update the installation.
// Exception: When version.IsMacSysExt returns true, we don't support that

View File

@@ -239,6 +239,7 @@ type LocalBackend struct {
peerAPIServer *peerAPIServer // or nil
peerAPIListeners []*peerAPIListener
loginFlags controlclient.LoginFlags
incomingFiles map[*incomingFile]bool
fileWaiters set.HandleSet[context.CancelFunc] // of wake-up funcs
notifyWatchers set.HandleSet[*watchSession]
lastStatusTime time.Time // status.AsOf value of the last processed status update
@@ -2156,12 +2157,6 @@ func (b *LocalBackend) DebugForceNetmapUpdate() {
b.setNetMapLocked(nm)
}
// DebugPickNewDERP forwards to magicsock.Conn.DebugPickNewDERP.
// See its docs.
func (b *LocalBackend) DebugPickNewDERP() error {
return b.sys.MagicSock.Get().DebugPickNewDERP()
}
// send delivers n to the connected frontend and any API watchers from
// LocalBackend.WatchNotifications (via the LocalAPI).
//
@@ -2218,7 +2213,10 @@ func (b *LocalBackend) sendFileNotify() {
// Make sure we always set n.IncomingFiles non-nil so it gets encoded
// in JSON to clients. They distinguish between empty and non-nil
// to know whether a Notify should be able about files.
n.IncomingFiles = apiSrv.taildrop.IncomingFiles()
n.IncomingFiles = make([]ipn.PartialFile, 0)
for f := range b.incomingFiles {
n.IncomingFiles = append(n.IncomingFiles, f.PartialFile())
}
b.mu.Unlock()
sort.Slice(n.IncomingFiles, func(i, j int) bool {
@@ -3551,11 +3549,11 @@ func (b *LocalBackend) initPeerAPIListener() {
ps := &peerAPIServer{
b: b,
taildrop: &taildrop.Handler{
Logf: b.logf,
Clock: b.clock,
Dir: fileRoot,
DirectFileMode: b.directFileRoot != "",
AvoidFinalRename: !b.directFileDoFinalRename,
Logf: b.logf,
Clock: b.clock,
RootDir: fileRoot,
DirectFileMode: b.directFileRoot != "",
DirectFileDoFinalRename: b.directFileDoFinalRename,
},
}
if dm, ok := b.sys.DNSManager.GetOK(); ok {
@@ -4592,6 +4590,19 @@ func (b *LocalBackend) SetDNS(ctx context.Context, name, value string) error {
return cc.SetDNS(ctx, req)
}
func (b *LocalBackend) registerIncomingFile(inf *incomingFile, active bool) {
b.mu.Lock()
defer b.mu.Unlock()
if b.incomingFiles == nil {
b.incomingFiles = make(map[*incomingFile]bool)
}
if active {
b.incomingFiles[inf] = true
} else {
delete(b.incomingFiles, inf)
}
}
func peerAPIPorts(peer tailcfg.NodeView) (p4, p6 uint16) {
svcs := peer.Hostinfo().Services()
for i := range svcs.LenIter() {

View File

@@ -15,6 +15,7 @@ import (
"net"
"net/http"
"net/netip"
"net/url"
"os"
"runtime"
"slices"
@@ -38,8 +39,10 @@ import (
"tailscale.com/net/sockstats"
"tailscale.com/tailcfg"
"tailscale.com/taildrop"
"tailscale.com/tstime"
"tailscale.com/types/views"
"tailscale.com/util/clientmetric"
"tailscale.com/version/distro"
"tailscale.com/wgengine/filter"
)
@@ -583,6 +586,64 @@ func (h *peerAPIHandler) handleServeSockStats(w http.ResponseWriter, r *http.Req
fmt.Fprintln(w, "</pre>")
}
type incomingFile struct {
clock tstime.Clock
name string // "foo.jpg"
started time.Time
size int64 // or -1 if unknown; never 0
w io.Writer // underlying writer
sendFileNotify func() // called when done
partialPath string // non-empty in direct mode
mu sync.Mutex
copied int64
done bool
lastNotify time.Time
}
func (f *incomingFile) markAndNotifyDone() {
f.mu.Lock()
f.done = true
f.mu.Unlock()
f.sendFileNotify()
}
func (f *incomingFile) Write(p []byte) (n int, err error) {
n, err = f.w.Write(p)
var needNotify bool
defer func() {
if needNotify {
f.sendFileNotify()
}
}()
if n > 0 {
f.mu.Lock()
defer f.mu.Unlock()
f.copied += int64(n)
now := f.clock.Now()
if f.lastNotify.IsZero() || now.Sub(f.lastNotify) > time.Second {
f.lastNotify = now
needNotify = true
}
}
return n, err
}
func (f *incomingFile) PartialFile() ipn.PartialFile {
f.mu.Lock()
defer f.mu.Unlock()
return ipn.PartialFile{
Name: f.name,
Started: f.started,
DeclaredSize: f.size,
Received: f.copied,
PartialPath: f.partialPath,
Done: f.done,
}
}
// canPutFile reports whether h can put a file ("Taildrop") to this node.
func (h *peerAPIHandler) canPutFile() bool {
if h.peerNode.UnsignedPeerAPIOnly() {
@@ -626,6 +687,10 @@ func (h *peerAPIHandler) peerHasCap(wantCap tailcfg.PeerCapability) bool {
}
func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
if !envknob.CanTaildrop() {
http.Error(w, "Taildrop disabled on device", http.StatusForbidden)
return
}
if !h.canPutFile() {
http.Error(w, "Taildrop access denied", http.StatusForbidden)
return
@@ -634,12 +699,117 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
http.Error(w, "file sharing not enabled by Tailscale admin", http.StatusForbidden)
return
}
t0 := h.ps.b.clock.Now()
n, ok := h.ps.taildrop.HandlePut(w, r)
if ok {
d := h.ps.b.clock.Since(t0).Round(time.Second / 10)
h.logf("got put of %s in %v from %v/%v", approxSize(n), d, h.remoteAddr.Addr(), h.peerNode.ComputedName)
if r.Method != "PUT" {
http.Error(w, "expected method PUT", http.StatusMethodNotAllowed)
return
}
if mayDeref(h.ps.taildrop).RootDir == "" {
http.Error(w, taildrop.ErrNoTaildrop.Error(), http.StatusInternalServerError)
return
}
if distro.Get() == distro.Unraid && !h.ps.taildrop.DirectFileMode {
http.Error(w, "Taildrop folder not configured or accessible", http.StatusInternalServerError)
return
}
rawPath := r.URL.EscapedPath()
suffix, ok := strings.CutPrefix(rawPath, "/v0/put/")
if !ok {
http.Error(w, "misconfigured internals", 500)
return
}
if suffix == "" {
http.Error(w, "empty filename", 400)
return
}
if strings.Contains(suffix, "/") {
http.Error(w, "directories not supported", 400)
return
}
baseName, err := url.PathUnescape(suffix)
if err != nil {
http.Error(w, "bad path encoding", 400)
return
}
dstFile, ok := h.ps.taildrop.DiskPath(baseName)
if !ok {
http.Error(w, "bad filename", 400)
return
}
t0 := h.ps.b.clock.Now()
// TODO(bradfitz): prevent same filename being sent by two peers at once
// prevent same filename being sent twice
if _, err := os.Stat(dstFile); err == nil {
http.Error(w, "file exists", http.StatusConflict)
return
}
partialFile := dstFile + taildrop.PartialSuffix
f, err := os.Create(partialFile)
if err != nil {
h.logf("put Create error: %v", taildrop.RedactErr(err))
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var success bool
defer func() {
if !success {
os.Remove(partialFile)
}
}()
var finalSize int64
var inFile *incomingFile
if r.ContentLength != 0 {
inFile = &incomingFile{
clock: h.ps.b.clock,
name: baseName,
started: h.ps.b.clock.Now(),
size: r.ContentLength,
w: f,
sendFileNotify: h.ps.b.sendFileNotify,
}
if h.ps.taildrop.DirectFileMode {
inFile.partialPath = partialFile
}
h.ps.b.registerIncomingFile(inFile, true)
defer h.ps.b.registerIncomingFile(inFile, false)
n, err := io.Copy(inFile, r.Body)
if err != nil {
err = taildrop.RedactErr(err)
f.Close()
h.logf("put Copy error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
finalSize = n
}
if err := taildrop.RedactErr(f.Close()); err != nil {
h.logf("put Close error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if h.ps.taildrop.DirectFileMode && !h.ps.taildrop.DirectFileDoFinalRename {
if inFile != nil { // non-zero length; TODO: notify even for zero length
inFile.markAndNotifyDone()
}
} else {
if err := os.Rename(partialFile, dstFile); err != nil {
err = taildrop.RedactErr(err)
h.logf("put final rename: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
d := h.ps.b.clock.Since(t0).Round(time.Second / 10)
h.logf("got put of %s in %v from %v/%v", approxSize(finalSize), d, h.remoteAddr.Addr(), h.peerNode.ComputedName)
// TODO: set modtime
// TODO: some real response
success = true
io.WriteString(w, "{}\n")
h.ps.taildrop.KnownEmpty.Store(false)
h.ps.b.sendFileNotify()
}
func approxSize(n int64) string {

View File

@@ -5,6 +5,7 @@ package ipnlocal
import (
"bytes"
"errors"
"fmt"
"io"
"io/fs"
@@ -14,6 +15,7 @@ import (
"net/netip"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
@@ -66,7 +68,7 @@ func bodyNotContains(sub string) check {
func fileHasSize(name string, size int) check {
return func(t *testing.T, e *peerAPITestEnv) {
root := e.ph.ps.taildrop.Dir
root := e.ph.ps.taildrop.RootDir
if root == "" {
t.Errorf("no rootdir; can't check whether %q has size %v", name, size)
return
@@ -82,7 +84,7 @@ func fileHasSize(name string, size int) check {
func fileHasContents(name string, want string) check {
return func(t *testing.T, e *peerAPITestEnv) {
root := e.ph.ps.taildrop.Dir
root := e.ph.ps.taildrop.RootDir
if root == "" {
t.Errorf("no rootdir; can't check contents of %q", name)
return
@@ -492,12 +494,9 @@ func TestHandlePeerAPI(t *testing.T) {
if !tt.omitRoot {
rootDir = t.TempDir()
if e.ph.ps.taildrop == nil {
e.ph.ps.taildrop = &taildrop.Handler{
Logf: e.logBuf.Logf,
Clock: &tstest.Clock{},
}
e.ph.ps.taildrop = &taildrop.Handler{}
}
e.ph.ps.taildrop.Dir = rootDir
e.ph.ps.taildrop.RootDir = rootDir
}
for _, req := range tt.reqs {
e.rr = httptest.NewRecorder()
@@ -537,9 +536,9 @@ func TestFileDeleteRace(t *testing.T) {
clock: &tstest.Clock{},
},
taildrop: &taildrop.Handler{
Logf: t.Logf,
Clock: &tstest.Clock{},
Dir: dir,
Logf: t.Logf,
Clock: &tstest.Clock{},
RootDir: dir,
},
}
ph := &peerAPIHandler{
@@ -580,6 +579,92 @@ func TestFileDeleteRace(t *testing.T) {
}
}
// Tests "foo.jpg.deleted" marks (for Windows).
func TestDeletedMarkers(t *testing.T) {
dir := t.TempDir()
ps := &peerAPIServer{
b: &LocalBackend{
logf: t.Logf,
capFileSharing: true,
},
taildrop: &taildrop.Handler{
RootDir: dir,
},
}
nothingWaiting := func() {
t.Helper()
ps.taildrop.KnownEmpty.Store(false)
if ps.taildrop.HasFilesWaiting() {
t.Fatal("unexpected files waiting")
}
}
touch := func(base string) {
t.Helper()
if err := taildrop.TouchFile(filepath.Join(dir, base)); err != nil {
t.Fatal(err)
}
}
wantEmptyTempDir := func() {
t.Helper()
if fis, err := os.ReadDir(dir); err != nil {
t.Fatal(err)
} else if len(fis) > 0 && runtime.GOOS != "windows" {
for _, fi := range fis {
t.Errorf("unexpected file in tempdir: %q", fi.Name())
}
}
}
nothingWaiting()
wantEmptyTempDir()
touch("foo.jpg.deleted")
nothingWaiting()
wantEmptyTempDir()
touch("foo.jpg.deleted")
touch("foo.jpg")
nothingWaiting()
wantEmptyTempDir()
touch("foo.jpg.deleted")
touch("foo.jpg")
wf, err := ps.taildrop.WaitingFiles()
if err != nil {
t.Fatal(err)
}
if len(wf) != 0 {
t.Fatalf("WaitingFiles = %d; want 0", len(wf))
}
wantEmptyTempDir()
touch("foo.jpg.deleted")
touch("foo.jpg")
if rc, _, err := ps.taildrop.OpenFile("foo.jpg"); err == nil {
rc.Close()
t.Fatal("unexpected foo.jpg open")
}
wantEmptyTempDir()
// And verify basics still work in non-deleted cases.
touch("foo.jpg")
touch("bar.jpg.deleted")
if wf, err := ps.taildrop.WaitingFiles(); err != nil {
t.Error(err)
} else if len(wf) != 1 {
t.Errorf("WaitingFiles = %d; want 1", len(wf))
} else if wf[0].Name != "foo.jpg" {
t.Errorf("unexpected waiting file %+v", wf[0])
}
if rc, _, err := ps.taildrop.OpenFile("foo.jpg"); err != nil {
t.Fatal(err)
} else {
rc.Close()
}
}
func TestPeerAPIReplyToDNSQueries(t *testing.T) {
var h peerAPIHandler
@@ -634,3 +719,67 @@ func TestPeerAPIReplyToDNSQueries(t *testing.T) {
t.Errorf("unexpectedly IPv6 deny; wanted to be a DNS server")
}
}
func TestRedactErr(t *testing.T) {
testCases := []struct {
name string
err func() error
want string
}{
{
name: "PathError",
err: func() error {
return &os.PathError{
Op: "open",
Path: "/tmp/sensitive.txt",
Err: fs.ErrNotExist,
}
},
want: `open redacted.41360718: file does not exist`,
},
{
name: "LinkError",
err: func() error {
return &os.LinkError{
Op: "symlink",
Old: "/tmp/sensitive.txt",
New: "/tmp/othersensitive.txt",
Err: fs.ErrNotExist,
}
},
want: `symlink redacted.41360718 redacted.6bcf093a: file does not exist`,
},
{
name: "something else",
err: func() error { return errors.New("i am another error type") },
want: `i am another error type`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// For debugging
var i int
for err := tc.err(); err != nil; err = errors.Unwrap(err) {
t.Logf("%d: %T @ %p", i, err, err)
i++
}
t.Run("Root", func(t *testing.T) {
got := taildrop.RedactErr(tc.err()).Error()
if got != tc.want {
t.Errorf("err = %q; want %q", got, tc.want)
}
})
t.Run("Wrapped", func(t *testing.T) {
wrapped := fmt.Errorf("wrapped error: %w", tc.err())
want := "wrapped error: " + tc.want
got := taildrop.RedactErr(wrapped).Error()
if got != want {
t.Errorf("err = %q; want %q", got, want)
}
})
})
}
}

View File

@@ -408,32 +408,18 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
}
func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
h.serveWhoIsWithBackend(w, r, h.b)
}
// localBackendWhoIsMethods is the subset of ipn.LocalBackend as needed
// by the localapi WhoIs method.
type localBackendWhoIsMethods interface {
WhoIs(netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
PeerCaps(netip.Addr) tailcfg.PeerCapMap
}
func (h *Handler) serveWhoIsWithBackend(w http.ResponseWriter, r *http.Request, b localBackendWhoIsMethods) {
if !h.PermitRead {
http.Error(w, "whois access denied", http.StatusForbidden)
return
}
b := h.b
var ipp netip.AddrPort
if v := r.FormValue("addr"); v != "" {
if ip, err := netip.ParseAddr(v); err == nil {
ipp = netip.AddrPortFrom(ip, 0)
} else {
var err error
ipp, err = netip.ParseAddrPort(v)
if err != nil {
http.Error(w, "invalid 'addr' parameter", 400)
return
}
var err error
ipp, err = netip.ParseAddrPort(v)
if err != nil {
http.Error(w, "invalid 'addr' parameter", 400)
return
}
} else {
http.Error(w, "missing 'addr' parameter", 400)
@@ -447,9 +433,7 @@ func (h *Handler) serveWhoIsWithBackend(w http.ResponseWriter, r *http.Request,
res := &apitype.WhoIsResponse{
Node: n.AsStruct(), // always non-nil per WhoIsResponse contract
UserProfile: &u, // always non-nil per WhoIsResponse contract
}
if n.Addresses().Len() > 0 {
res.CapMap = b.PeerCaps(n.Addresses().At(0).Addr())
CapMap: b.PeerCaps(ipp.Addr()),
}
j, err := json.MarshalIndent(res, "", "\t")
if err != nil {
@@ -582,8 +566,6 @@ func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
if err == nil {
return
}
case "pick-new-derp":
err = h.b.DebugPickNewDERP()
case "":
err = fmt.Errorf("missing parameter 'action'")
default:

View File

@@ -9,15 +9,11 @@ import (
"io"
"net/http"
"net/http/httptest"
"net/netip"
"net/url"
"strings"
"testing"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/hostinfo"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
)
@@ -81,68 +77,3 @@ func TestSetPushDeviceToken(t *testing.T) {
t.Errorf("hostinfo.PushDeviceToken=%q, want %q", got, want)
}
}
type whoIsBackend struct {
whoIs func(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
peerCaps map[netip.Addr]tailcfg.PeerCapMap
}
func (b whoIsBackend) WhoIs(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
return b.whoIs(ipp)
}
func (b whoIsBackend) PeerCaps(ip netip.Addr) tailcfg.PeerCapMap {
return b.peerCaps[ip]
}
// Tests that the WhoIs handler accepts either IPs or IP:ports.
//
// From https://github.com/tailscale/tailscale/pull/9714 (a PR that is effectively a bug report)
func TestWhoIsJustIP(t *testing.T) {
h := &Handler{
PermitRead: true,
}
for _, input := range []string{"100.101.102.103", "127.0.0.1:123"} {
rec := httptest.NewRecorder()
t.Run(input, func(t *testing.T) {
b := whoIsBackend{
whoIs: func(ipp netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool) {
if !strings.Contains(input, ":") {
want := netip.MustParseAddrPort("100.101.102.103:0")
if ipp != want {
t.Fatalf("backend called with %v; want %v", ipp, want)
}
}
return (&tailcfg.Node{
ID: 123,
Addresses: []netip.Prefix{
netip.MustParsePrefix("100.101.102.103/32"),
},
}).View(),
tailcfg.UserProfile{ID: 456, DisplayName: "foo"},
true
},
peerCaps: map[netip.Addr]tailcfg.PeerCapMap{
netip.MustParseAddr("100.101.102.103"): map[tailcfg.PeerCapability][]tailcfg.RawMessage{
"foo": {`"bar"`},
},
},
}
h.serveWhoIsWithBackend(rec, httptest.NewRequest("GET", "/v0/whois?addr="+url.QueryEscape(input), nil), b)
var res apitype.WhoIsResponse
if err := json.Unmarshal(rec.Body.Bytes(), &res); err != nil {
t.Fatal(err)
}
if got, want := res.Node.ID, tailcfg.NodeID(123); got != want {
t.Errorf("res.Node.ID=%v, want %v", got, want)
}
if got, want := res.UserProfile.DisplayName, "foo"; got != want {
t.Errorf("res.UserProfile.DisplayName=%q, want %q", got, want)
}
if got, want := len(res.CapMap), 1; got != want {
t.Errorf("capmap size=%v, want %v", got, want)
}
})
}
}

View File

@@ -200,10 +200,6 @@ type Prefs struct {
// AutoUpdatePrefs docs for more details.
AutoUpdate AutoUpdatePrefs
// PostureChecking enables the collection of information used for device
// posture checks.
PostureChecking bool
// 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.
@@ -250,7 +246,6 @@ type MaskedPrefs struct {
OperatorUserSet bool `json:",omitempty"`
ProfileNameSet bool `json:",omitempty"`
AutoUpdateSet bool `json:",omitempty"`
PostureCheckingSet bool `json:",omitempty"`
}
// ApplyEdits mutates p, assigning fields from m.Prefs for each MaskedPrefs
@@ -444,8 +439,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
compareStrings(p.AdvertiseTags, p2.AdvertiseTags) &&
p.Persist.Equals(p2.Persist) &&
p.ProfileName == p2.ProfileName &&
p.AutoUpdate == p2.AutoUpdate &&
p.PostureChecking == p2.PostureChecking
p.AutoUpdate == p2.AutoUpdate
}
func (au AutoUpdatePrefs) Pretty() string {

View File

@@ -57,7 +57,6 @@ func TestPrefsEqual(t *testing.T) {
"OperatorUser",
"ProfileName",
"AutoUpdate",
"PostureChecking",
"Persist",
}
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
@@ -305,16 +304,6 @@ func TestPrefsEqual(t *testing.T) {
&Prefs{AutoUpdate: AutoUpdatePrefs{Check: true, Apply: false}},
true,
},
{
&Prefs{PostureChecking: true},
&Prefs{PostureChecking: true},
true,
},
{
&Prefs{PostureChecking: true},
&Prefs{PostureChecking: false},
false,
},
}
for i, tt := range tests {
got := tt.a.Equals(tt.b)

View File

@@ -37,7 +37,6 @@ Some packages may only be included on certain architectures or operating systems
- [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/creack/pty](https://pkg.go.dev/github.com/creack/pty) ([MIT](https://github.com/creack/pty/blob/v1.1.18/LICENSE))
- [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/e994401fc077/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/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/go-ole/go-ole](https://pkg.go.dev/github.com/go-ole/go-ole) ([MIT](https://github.com/go-ole/go-ole/blob/v1.3.0/LICENSE))
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/76236955d466/LICENSE))
@@ -86,13 +85,13 @@ Some packages may only be included on certain architectures or operating systems
- [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/6213f710f925/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.14.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.13.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/92128663:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/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.15.0:LICENSE))
- [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](https://cs.opensource.google/go/x/oauth2/+/v0.12.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.3.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.13.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.13.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.12.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.12.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.13.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.3.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))

View File

@@ -6,7 +6,6 @@
package dns
import (
"bytes"
"os/exec"
)
@@ -14,17 +13,13 @@ func resolvconfStyle() string {
if _, err := exec.LookPath("resolvconf"); err != nil {
return ""
}
output, err := exec.Command("resolvconf", "--version").CombinedOutput()
if err != nil {
if _, err := exec.Command("resolvconf", "--version").CombinedOutput(); err != nil {
// Debian resolvconf doesn't understand --version, and
// exits with a specific error code.
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 99 {
return "debian"
}
}
if bytes.HasPrefix(output, []byte("Debian resolvconf")) {
return "debian"
}
// Treat everything else as openresolv, by far the more popular implementation.
return "openresolv"
}

View File

@@ -79,16 +79,14 @@ const (
safeTUNMTU TUNMTU = 1280
)
// WireMTUsToProbe is a list of the on-the-wire MTUs we want to probe. Each time
// magicsock discovery begins, it will send a set of pings, one of each size
// listed below.
var WireMTUsToProbe = []WireMTU{
WireMTU(safeTUNMTU), // Tailscale over Tailscale :)
TUNToWireMTU(safeTUNMTU), // Smallest MTU allowed for IPv6, current default
1400, // Most common MTU minus a few bytes for tunnels
1500, // Most common MTU
8000, // Should fit inside all jumbo frame sizes
9000, // Most jumbo frames are this size or larger
// MaxProbedWireMTU is the largest MTU we will test for path MTU
// discovery.
var MaxProbedWireMTU WireMTU = 9000
func init() {
if MaxProbedWireMTU > WireMTU(maxTUNMTU) {
MaxProbedWireMTU = WireMTU(maxTUNMTU)
}
}
// wgHeaderLen is the length of all the headers Wireguard adds to a packet
@@ -127,7 +125,7 @@ func WireToTUNMTU(w WireMTU) TUNMTU {
// MTU. It is also the path MTU that we default to if we have no
// information about the path to a peer.
//
// 1. If set, the value of TS_DEBUG_MTU clamped to a maximum of MaxTUNMTU
// 1. If set, the value of TS_DEBUG_MTU clamped to a maximum of MaxTunMTU
// 2. If TS_DEBUG_ENABLE_PMTUD is set, the maximum size MTU we probe, minus wg overhead
// 3. If TS_DEBUG_ENABLE_PMTUD is not set, the Safe MTU
func DefaultTUNMTU() TUNMTU {
@@ -137,23 +135,12 @@ func DefaultTUNMTU() TUNMTU {
debugPMTUD, _ := envknob.LookupBool("TS_DEBUG_ENABLE_PMTUD")
if debugPMTUD {
// TODO: While we are just probing MTU but not generating PTB,
// this has to continue to return the safe MTU. When we add the
// code to generate PTB, this will be:
//
// return WireToTUNMTU(maxProbedWireMTU)
return safeTUNMTU
return WireToTUNMTU(MaxProbedWireMTU)
}
return safeTUNMTU
}
// SafeWireMTU returns the wire MTU that is safe to use if we have no
// information about the path MTU to this peer.
func SafeWireMTU() WireMTU {
return TUNToWireMTU(safeTUNMTU)
}
// DefaultWireMTU returns the default TUN MTU, adjusted for wireguard
// overhead.
func DefaultWireMTU() WireMTU {

View File

@@ -39,18 +39,15 @@ func TestDefaultTunMTU(t *testing.T) {
t.Errorf("default TUN MTU = %d, want %d, clamping failed", DefaultTUNMTU(), maxTUNMTU)
}
// If PMTUD is enabled, the MTU should default to the safe MTU, but only
// if the user hasn't requested a specific MTU.
//
// TODO: When PMTUD is generating PTB responses, this will become the
// largest MTU we probe.
// If PMTUD is enabled, the MTU should default to the largest probed
// MTU, but only if the user hasn't requested a specific MTU.
os.Setenv("TS_DEBUG_MTU", "")
os.Setenv("TS_DEBUG_ENABLE_PMTUD", "true")
if DefaultTUNMTU() != safeTUNMTU {
t.Errorf("default TUN MTU = %d, want %d", DefaultTUNMTU(), safeTUNMTU)
if DefaultTUNMTU() != WireToTUNMTU(MaxProbedWireMTU) {
t.Errorf("default TUN MTU = %d, want %d", DefaultTUNMTU(), WireToTUNMTU(MaxProbedWireMTU))
}
// TS_DEBUG_MTU should take precedence over TS_DEBUG_ENABLE_PMTUD.
mtu = WireToTUNMTU(MaxPacketSize - 1)
mtu = WireToTUNMTU(MaxProbedWireMTU - 1)
os.Setenv("TS_DEBUG_MTU", strconv.Itoa(int(mtu)))
if DefaultTUNMTU() != mtu {
t.Errorf("default TUN MTU = %d, want %d", DefaultTUNMTU(), mtu)

View File

@@ -673,16 +673,16 @@ func (c *natFamilyConfig) selectSrcIP(oldSrc, dst netip.Addr) netip.Addr {
// natConfigFromWGConfig generates a natFamilyConfig from nm,
// for the indicated address family.
// If NAT is not required for that address family, it returns nil.
func natConfigFromWGConfig(wcfg *wgcfg.Config, addrFam ipproto.Version) *natFamilyConfig {
func natConfigFromWGConfig(wcfg *wgcfg.Config, addrFam ipproto.IPProtoVersion) *natFamilyConfig {
if wcfg == nil {
return nil
}
var nativeAddr netip.Addr
switch addrFam {
case ipproto.Version4:
case ipproto.IPProtoVersion4:
nativeAddr = findV4(wcfg.Addresses)
case ipproto.Version6:
case ipproto.IPProtoVersion6:
nativeAddr = findV6(wcfg.Addresses)
}
if !nativeAddr.IsValid() {
@@ -703,8 +703,8 @@ func natConfigFromWGConfig(wcfg *wgcfg.Config, addrFam ipproto.Version) *natFami
isExitNode := slices.Contains(p.AllowedIPs, tsaddr.AllIPv4()) || slices.Contains(p.AllowedIPs, tsaddr.AllIPv6())
if isExitNode {
hasMasqAddrsForFamily := false ||
(addrFam == ipproto.Version4 && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid()) ||
(addrFam == ipproto.Version6 && p.V6MasqAddr != nil && p.V6MasqAddr.IsValid())
(addrFam == ipproto.IPProtoVersion4 && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid()) ||
(addrFam == ipproto.IPProtoVersion6 && p.V6MasqAddr != nil && p.V6MasqAddr.IsValid())
if hasMasqAddrsForFamily {
exitNodeRequiresMasq = true
}
@@ -714,10 +714,10 @@ func natConfigFromWGConfig(wcfg *wgcfg.Config, addrFam ipproto.Version) *natFami
for i := range wcfg.Peers {
p := &wcfg.Peers[i]
var addrToUse netip.Addr
if addrFam == ipproto.Version4 && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid() {
if addrFam == ipproto.IPProtoVersion4 && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid() {
addrToUse = *p.V4MasqAddr
mak.Set(&listenAddrs, addrToUse, struct{}{})
} else if addrFam == ipproto.Version6 && p.V6MasqAddr != nil && p.V6MasqAddr.IsValid() {
} else if addrFam == ipproto.IPProtoVersion6 && p.V6MasqAddr != nil && p.V6MasqAddr.IsValid() {
addrToUse = *p.V6MasqAddr
mak.Set(&listenAddrs, addrToUse, struct{}{})
} else if exitNodeRequiresMasq {
@@ -741,7 +741,7 @@ func natConfigFromWGConfig(wcfg *wgcfg.Config, addrFam ipproto.Version) *natFami
// SetNetMap is called when a new NetworkMap is received.
func (t *Wrapper) SetWGConfig(wcfg *wgcfg.Config) {
v4, v6 := natConfigFromWGConfig(wcfg, ipproto.Version4), natConfigFromWGConfig(wcfg, ipproto.Version6)
v4, v6 := natConfigFromWGConfig(wcfg, ipproto.IPProtoVersion4), natConfigFromWGConfig(wcfg, ipproto.IPProtoVersion6)
var cfg *natConfig
if v4 != nil || v6 != nil {
cfg = &natConfig{v4: v4, v6: v6}

View File

@@ -617,7 +617,7 @@ func TestNATCfg(t *testing.T) {
p.AllowedIPs = append(p.AllowedIPs, otherAllowedIPs...)
return p
}
test := func(addrFam ipproto.Version) {
test := func(addrFam ipproto.IPProtoVersion) {
var (
noIP netip.Addr
@@ -635,7 +635,7 @@ func TestNATCfg(t *testing.T) {
exitRoute = netip.MustParsePrefix("0.0.0.0/0")
publicIP = netip.MustParseAddr("8.8.8.8")
)
if addrFam == ipproto.Version6 {
if addrFam == ipproto.IPProtoVersion6 {
selfNativeIP = netip.MustParseAddr("fd7a:115c:a1e0::a")
selfEIP1 = netip.MustParseAddr("fd7a:115c:a1e0::1a")
selfEIP2 = netip.MustParseAddr("fd7a:115c:a1e0::1b")
@@ -817,8 +817,8 @@ func TestNATCfg(t *testing.T) {
})
}
}
test(ipproto.Version4)
test(ipproto.Version6)
test(ipproto.IPProtoVersion4)
test(ipproto.IPProtoVersion6)
}
// TestCaptureHook verifies that the Wrapper.captureHook callback is called

View File

@@ -1,74 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build cgo && darwin && !ios
package posture
// #cgo LDFLAGS: -framework CoreFoundation -framework IOKit
// #include <CoreFoundation/CoreFoundation.h>
// #include <IOKit/IOKitLib.h>
//
// #if __MAC_OS_X_VERSION_MIN_REQUIRED < 120000
// #define kIOMainPortDefault kIOMasterPortDefault
// #endif
//
// const char *
// getSerialNumber()
// {
// CFMutableDictionaryRef matching = IOServiceMatching("IOPlatformExpertDevice");
// if (!matching) {
// return "err: failed to create dictionary to match IOServices";
// }
//
// io_service_t service = IOServiceGetMatchingService(kIOMainPortDefault, matching);
// if (!service) {
// return "err: failed to look up registered IOService objects that match a matching dictionary";
// }
//
// CFStringRef serialNumberRef = IORegistryEntryCreateCFProperty(
// service,
// CFSTR("IOPlatformSerialNumber"),
// kCFAllocatorDefault,
// 0
// );
// if (!serialNumberRef) {
// return "err: failed to look up serial number in IORegistry";
// }
//
// CFIndex length = CFStringGetLength(serialNumberRef);
// CFIndex max_size = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8) + 1;
// char *serialNumberBuf = (char *)malloc(max_size);
//
// bool result = CFStringGetCString(serialNumberRef, serialNumberBuf, max_size, kCFStringEncodingUTF8);
//
// CFRelease(serialNumberRef);
// IOObjectRelease(service);
//
// if (!result) {
// free(serialNumberBuf);
//
// return "err: failed to convert serial number reference to string";
// }
//
// return serialNumberBuf;
// }
import "C"
import (
"fmt"
"strings"
"tailscale.com/types/logger"
)
// GetSerialNumber returns the platform serial sumber as reported by IOKit.
func GetSerialNumbers(_ logger.Logf) ([]string, error) {
csn := C.getSerialNumber()
serialNumber := C.GoString(csn)
if err, ok := strings.CutPrefix(serialNumber, "err: "); ok {
return nil, fmt.Errorf("failed to get serial number from IOKit: %s", err)
}
return []string{serialNumber}, nil
}

View File

@@ -1,37 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build cgo && darwin && !ios
package posture
import (
"fmt"
"testing"
"tailscale.com/types/logger"
"tailscale.com/util/cibuild"
)
func TestGetSerialNumberMac(t *testing.T) {
// Do not run this test on CI, it can only be ran on macOS
// and we currenty only use Linux runners.
if cibuild.On() {
t.Skip()
}
sns, err := GetSerialNumbers(logger.Discard)
if err != nil {
t.Fatalf("failed to get serial number: %s", err)
}
if len(sns) != 1 {
t.Errorf("expected list of one serial number, got %v", sns)
}
if len(sns[0]) <= 0 {
t.Errorf("expected a serial number with more than zero characters, got %s", sns[0])
}
fmt.Printf("serials: %v\n", sns)
}

View File

@@ -1,143 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Build on Windows, Linux and *BSD
//go:build windows || (linux && !android) || freebsd || openbsd || dragonfly || netbsd
package posture
import (
"errors"
"fmt"
"strings"
"github.com/digitalocean/go-smbios/smbios"
"tailscale.com/types/logger"
"tailscale.com/util/multierr"
)
// getByteFromSmbiosStructure retrieves a 8-bit unsigned integer at the given specOffset.
func getByteFromSmbiosStructure(s *smbios.Structure, specOffset int) uint8 {
// the `Formatted` byte slice is missing the first 4 bytes of the structure that are stripped out as header info.
// so we need to subtract 4 from the offset mentioned in the SMBIOS documentation to get the right value.
index := specOffset - 4
if index >= len(s.Formatted) || index < 0 {
return 0
}
return s.Formatted[index]
}
// getStringFromSmbiosStructure retrieves a string at the given specOffset.
// Returns an empty string if no string was present.
func getStringFromSmbiosStructure(s *smbios.Structure, specOffset int) (string, error) {
index := getByteFromSmbiosStructure(s, specOffset)
if index == 0 || int(index) > len(s.Strings) {
return "", errors.New("specified offset does not exist in smbios structure")
}
str := s.Strings[index-1]
trimmed := strings.TrimSpace(str)
return trimmed, nil
}
// Product Table (Type 1) structure
// https://web.archive.org/web/20220126173219/https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_3.1.1.pdf
// Page 34 and onwards.
const (
// Serial is present at the same offset in all IDs
serialNumberOffset = 0x07
productID = 1
baseboardID = 2
chassisID = 3
)
var (
idToTableName = map[int]string{
1: "product",
2: "baseboard",
3: "chassis",
}
validTables []string
numOfTables int
)
func init() {
for _, table := range idToTableName {
validTables = append(validTables, table)
}
numOfTables = len(validTables)
}
// serialFromSmbiosStructure extracts a serial number from a product,
// baseboard or chassis SMBIOS table.
func serialFromSmbiosStructure(s *smbios.Structure) (string, error) {
id := s.Header.Type
if (id != productID) && (id != baseboardID) && (id != chassisID) {
return "", fmt.Errorf(
"cannot get serial table type %d, supported tables are %v",
id,
validTables,
)
}
serial, err := getStringFromSmbiosStructure(s, serialNumberOffset)
if err != nil {
return "", fmt.Errorf(
"failed to get serial from %s table: %w",
idToTableName[int(s.Header.Type)],
err,
)
}
return serial, nil
}
func GetSerialNumbers(logf logger.Logf) ([]string, error) {
// Find SMBIOS data in operating system-specific location.
rc, _, err := smbios.Stream()
if err != nil {
return nil, fmt.Errorf("failed to open dmi/smbios stream: %w", err)
}
defer rc.Close()
// Decode SMBIOS structures from the stream.
d := smbios.NewDecoder(rc)
ss, err := d.Decode()
if err != nil {
return nil, fmt.Errorf("failed to decode dmi/smbios structures: %w", err)
}
serials := make([]string, 0, numOfTables)
errs := make([]error, 0, numOfTables)
for _, s := range ss {
switch s.Header.Type {
case productID, baseboardID, chassisID:
serial, err := serialFromSmbiosStructure(s)
if err != nil {
errs = append(errs, err)
continue
}
serials = append(serials, serial)
}
}
err = multierr.New(errs...)
// if there were no serial numbers, check if any errors were
// returned and combine them.
if len(serials) == 0 && err != nil {
return nil, err
}
logf("got serial numbers %v (errors: %s)", serials, err)
return serials, nil
}

View File

@@ -1,38 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Build on Windows, Linux and *BSD
//go:build windows || (linux && !android) || freebsd || openbsd || dragonfly || netbsd
package posture
import (
"fmt"
"testing"
"tailscale.com/types/logger"
)
func TestGetSerialNumberNotMac(t *testing.T) {
// This test is intentionally skipped as it will
// require root on Linux to get access to the serials.
// The test case is intended for local testing.
// Comment out skip for local testing.
t.Skip()
sns, err := GetSerialNumbers(logger.Discard)
if err != nil {
t.Fatalf("failed to get serial number: %s", err)
}
if len(sns) == 0 {
t.Fatalf("expected at least one serial number, got %v", sns)
}
if len(sns[0]) <= 0 {
t.Errorf("expected a serial number with more than zero characters, got %s", sns[0])
}
fmt.Printf("serials: %v\n", sns)
}

View File

@@ -1,24 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// ios: Apple does not allow getting serials on iOS
// android: not implemented
// js: not implemented
// plan9: not implemented
// solaris: currently unsupported by go-smbios:
// https://github.com/digitalocean/go-smbios/pull/21
//go:build ios || android || solaris || plan9 || js || wasm || (darwin && !cgo)
package posture
import (
"errors"
"tailscale.com/types/logger"
)
// GetSerialNumber returns client machine serial number(s).
func GetSerialNumbers(_ logger.Logf) ([]string, error) {
return nil, errors.New("not implemented")
}

View File

@@ -1,16 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package posture
import (
"testing"
"tailscale.com/types/logger"
)
func TestGetSerialNumber(t *testing.T) {
// ensure GetSerialNumbers is implemented
// or covered by a stub on a given platform.
_, _ = GetSerialNumbers(logger.Discard)
}

View File

@@ -16,4 +16,4 @@
) {
src = ./.;
}).shellNix
# nix-direnv cache busting line: sha256-v3/3bVAK/ni0LZ+GPY+dnbdCdvFQUknPxur7u9Cm8Gw=
# nix-direnv cache busting line: sha256-tCc7+umCKgOmKXbElnCmDI4ntPvvHldkxi+RwQuj9ng=

View File

@@ -192,26 +192,6 @@ func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
return actual, loaded
}
// LoadOrInit returns the value for the given key if it exists
// otherwise f is called to construct the value to be set.
// The lock is held for the duration to prevent duplicate initialization.
func (m *Map[K, V]) LoadOrInit(key K, f func() V) (actual V, loaded bool) {
if actual, loaded := m.Load(key); loaded {
return actual, loaded
}
m.mu.Lock()
defer m.mu.Unlock()
if actual, loaded = m.m[key]; loaded {
return actual, loaded
}
loaded = false
actual = f()
mak.Set(&m.m, key, actual)
return actual, loaded
}
func (m *Map[K, V]) LoadAndDelete(key K) (value V, loaded bool) {
m.mu.Lock()
defer m.mu.Unlock()

View File

@@ -91,11 +91,8 @@ func TestMap(t *testing.T) {
if v, ok := m.LoadOrStore("two", 2); v != 2 || ok {
t.Errorf(`LoadOrStore("two", 2) = (%v, %v), want (2, false)`, v, ok)
}
if v, ok := m.LoadOrInit("three", func() int { return 3 }); v != 3 || ok {
t.Errorf(`LoadOrInit("three", 3) = (%v, %v), want (3, true)`, v, ok)
}
got := map[string]int{}
want := map[string]int{"one": 1, "two": 2, "three": 3}
want := map[string]int{"one": 1, "two": 2}
m.Range(func(k string, v int) bool {
got[k] = v
return true
@@ -109,7 +106,6 @@ func TestMap(t *testing.T) {
if v, ok := m.LoadAndDelete("two"); v != 0 || ok {
t.Errorf(`LoadAndDelete("two) = (%v, %v), want (0, false)`, v, ok)
}
m.Delete("three")
m.Delete("one")
m.Delete("noexist")
got = map[string]int{}

View File

@@ -52,15 +52,3 @@ type C2NUpdateResponse struct {
// Started indicates whether the update has started.
Started bool
}
// C2NPostureIdentityResponse contains either a set of identifying serial number
// from the client or a boolean indicating that the machine has opted out of
// posture collection.
type C2NPostureIdentityResponse struct {
// SerialNumbers is a list of serial numbers of the client machine.
SerialNumbers []string `json:",omitempty"`
// PostureDisabled indicates if the machine has opted out of
// device posture collection.
PostureDisabled bool `json:",omitempty"`
}

View File

@@ -2123,6 +2123,11 @@ const (
// NodeAttrDNSForwarderDisableTCPRetries disables retrying truncated
// DNS queries over TCP if the response is truncated.
NodeAttrDNSForwarderDisableTCPRetries NodeCapability = "dns-forwarder-disable-tcp-retries"
// NodeAttrMagicsockSessionTimeout sets the magicsock session timeout.
// It must have an associated string value, formatted by time.Duration.String
// and parsable by time.ParseDuration. If invalid or unset, the default is used.
NodeAttrMagicsockSessionTimeout NodeCapability = "magicsock-session-timeout"
)
// SetDNSRequest is a request to add a DNS record.
@@ -2436,22 +2441,6 @@ type QueryFeatureResponse struct {
ShouldWait bool `json:",omitempty"`
}
// WebClientAuthResponse is the response to a web client authentication request
// sent to "/machine/webclient/action" or "/machine/webclient/wait".
// See client/web for usage.
type WebClientAuthResponse struct {
// Message, if non-empty, provides a message for the user.
Message string `json:",omitempty"`
// Complete is true when the session authentication has been completed.
Complete bool `json:",omitempty"`
// URL is the link for the user to visit to authenticate the session.
//
// When empty, there is no action for the user to take.
URL string `json:",omitempty"`
}
// OverTLSPublicKeyResponse is the JSON response to /key?v=<n>
// over HTTPS (regular TLS) to the Tailscale control plane server,
// where the 'v' argument is the client's current capability version

View File

@@ -19,13 +19,13 @@ import (
"tailscale.com/logtail/backoff"
)
// HasFilesWaiting reports whether any files are buffered in [Handler.Dir].
// This always returns false when [Handler.DirectFileMode] is false.
// HasFilesWaiting reports whether any files are buffered in the
// tailscaled daemon storage.
func (s *Handler) HasFilesWaiting() bool {
if s == nil || s.Dir == "" || s.DirectFileMode {
if s == nil || s.RootDir == "" || s.DirectFileMode {
return false
}
if s.knownEmpty.Load() {
if s.KnownEmpty.Load() {
// Optimization: this is usually empty, so avoid opening
// the directory and checking. We can't cache the actual
// has-files-or-not values as the macOS/iOS client might
@@ -33,7 +33,7 @@ func (s *Handler) HasFilesWaiting() bool {
// keep this negative cache.
return false
}
f, err := os.Open(s.Dir)
f, err := os.Open(s.RootDir)
if err != nil {
return false
}
@@ -42,7 +42,7 @@ func (s *Handler) HasFilesWaiting() bool {
des, err := f.ReadDir(10)
for _, de := range des {
name := de.Name()
if strings.HasSuffix(name, partialSuffix) {
if strings.HasSuffix(name, PartialSuffix) {
continue
}
if name, ok := strings.CutSuffix(name, deletedSuffix); ok { // for Windows + tests
@@ -51,22 +51,22 @@ func (s *Handler) HasFilesWaiting() bool {
// as the OS may return "foo.jpg.deleted" before "foo.jpg"
// and we don't want to delete the ".deleted" file before
// enumerating to the "foo.jpg" file.
defer tryDeleteAgain(filepath.Join(s.Dir, name))
defer tryDeleteAgain(filepath.Join(s.RootDir, name))
continue
}
if de.Type().IsRegular() {
_, err := os.Stat(filepath.Join(s.Dir, name+deletedSuffix))
_, err := os.Stat(filepath.Join(s.RootDir, name+deletedSuffix))
if os.IsNotExist(err) {
return true
}
if err == nil {
tryDeleteAgain(filepath.Join(s.Dir, name))
tryDeleteAgain(filepath.Join(s.RootDir, name))
continue
}
}
}
if err == io.EOF {
s.knownEmpty.Store(true)
s.KnownEmpty.Store(true)
}
if err != nil {
break
@@ -76,19 +76,22 @@ func (s *Handler) HasFilesWaiting() bool {
}
// WaitingFiles returns the list of files that have been sent by a
// peer that are waiting in [Handler.Dir].
// This always returns nil when [Handler.DirectFileMode] is false.
// peer that are waiting in the buffered "pick up" directory owned by
// the Tailscale daemon.
//
// As a side effect, it also does any lazy deletion of files as
// required by Windows.
func (s *Handler) WaitingFiles() (ret []apitype.WaitingFile, err error) {
if s == nil {
return nil, errNilHandler
}
if s.Dir == "" {
return nil, errNoTaildrop
if s.RootDir == "" {
return nil, ErrNoTaildrop
}
if s.DirectFileMode {
return nil, nil
}
f, err := os.Open(s.Dir)
f, err := os.Open(s.RootDir)
if err != nil {
return nil, err
}
@@ -98,7 +101,7 @@ func (s *Handler) WaitingFiles() (ret []apitype.WaitingFile, err error) {
des, err := f.ReadDir(10)
for _, de := range des {
name := de.Name()
if strings.HasSuffix(name, partialSuffix) {
if strings.HasSuffix(name, PartialSuffix) {
continue
}
if name, ok := strings.CutSuffix(name, deletedSuffix); ok { // for Windows + tests
@@ -140,7 +143,7 @@ func (s *Handler) WaitingFiles() (ret []apitype.WaitingFile, err error) {
// Maybe Windows is done virus scanning the file we tried
// to delete a long time ago and will let us delete it now.
for name := range deleted {
tryDeleteAgain(filepath.Join(s.Dir, name))
tryDeleteAgain(filepath.Join(s.RootDir, name))
}
}
sort.Slice(ret, func(i, j int) bool { return ret[i].Name < ret[j].Name })
@@ -161,19 +164,17 @@ func tryDeleteAgain(fullPath string) {
}
}
// DeleteFile deletes a file of the given baseName from [Handler.Dir].
// This method is only allowed when [Handler.DirectFileMode] is false.
func (s *Handler) DeleteFile(baseName string) error {
if s == nil {
return errNilHandler
}
if s.Dir == "" {
return errNoTaildrop
if s.RootDir == "" {
return ErrNoTaildrop
}
if s.DirectFileMode {
return errors.New("deletes not allowed in direct mode")
}
path, ok := s.diskPath(baseName)
path, ok := s.DiskPath(baseName)
if !ok {
return errors.New("bad filename")
}
@@ -183,7 +184,7 @@ func (s *Handler) DeleteFile(baseName string) error {
for {
err := os.Remove(path)
if err != nil && !os.IsNotExist(err) {
err = redactErr(err)
err = RedactErr(err)
// Put a retry loop around deletes on Windows. Windows
// file descriptor closes are effectively asynchronous,
// as a bunch of hooks run on/after close, and we can't
@@ -202,7 +203,7 @@ func (s *Handler) DeleteFile(baseName string) error {
bo.BackOff(context.Background(), err)
continue
}
if err := touchFile(path + deletedSuffix); err != nil {
if err := TouchFile(path + deletedSuffix); err != nil {
logf("peerapi: failed to leave deleted marker: %v", err)
}
}
@@ -213,27 +214,25 @@ func (s *Handler) DeleteFile(baseName string) error {
}
}
func touchFile(path string) error {
func TouchFile(path string) error {
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
return redactErr(err)
return RedactErr(err)
}
return f.Close()
}
// OpenFile opens a file of the given baseName from [Handler.Dir].
// This method is only allowed when [Handler.DirectFileMode] is false.
func (s *Handler) OpenFile(baseName string) (rc io.ReadCloser, size int64, err error) {
if s == nil {
return nil, 0, errNilHandler
}
if s.Dir == "" {
return nil, 0, errNoTaildrop
if s.RootDir == "" {
return nil, 0, ErrNoTaildrop
}
if s.DirectFileMode {
return nil, 0, errors.New("opens not allowed in direct mode")
}
path, ok := s.diskPath(baseName)
path, ok := s.DiskPath(baseName)
if !ok {
return nil, 0, errors.New("bad filename")
}
@@ -243,12 +242,12 @@ func (s *Handler) OpenFile(baseName string) (rc io.ReadCloser, size int64, err e
}
f, err := os.Open(path)
if err != nil {
return nil, 0, redactErr(err)
return nil, 0, RedactErr(err)
}
fi, err := f.Stat()
if err != nil {
f.Close()
return nil, 0, redactErr(err)
return nil, 0, RedactErr(err)
}
return f, fi.Size(), nil
}

View File

@@ -1,184 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package taildrop
import (
"io"
"net/http"
"net/url"
"os"
"strings"
"sync"
"time"
"tailscale.com/envknob"
"tailscale.com/tstime"
"tailscale.com/version/distro"
)
type incomingFile struct {
clock tstime.Clock
name string // "foo.jpg"
started time.Time
size int64 // or -1 if unknown; never 0
w io.Writer // underlying writer
sendFileNotify func() // called when done
partialPath string // non-empty in direct mode
mu sync.Mutex
copied int64
done bool
lastNotify time.Time
}
func (f *incomingFile) markAndNotifyDone() {
f.mu.Lock()
f.done = true
f.mu.Unlock()
f.sendFileNotify()
}
func (f *incomingFile) Write(p []byte) (n int, err error) {
n, err = f.w.Write(p)
var needNotify bool
defer func() {
if needNotify {
f.sendFileNotify()
}
}()
if n > 0 {
f.mu.Lock()
defer f.mu.Unlock()
f.copied += int64(n)
now := f.clock.Now()
if f.lastNotify.IsZero() || now.Sub(f.lastNotify) > time.Second {
f.lastNotify = now
needNotify = true
}
}
return n, err
}
// HandlePut receives a file.
// It handles an HTTP PUT request to the "/v0/put/{filename}" endpoint,
// where {filename} is a base filename.
// It returns the number of bytes received and whether it was received successfully.
func (h *Handler) HandlePut(w http.ResponseWriter, r *http.Request) (finalSize int64, success bool) {
if !envknob.CanTaildrop() {
http.Error(w, "Taildrop disabled on device", http.StatusForbidden)
return finalSize, success
}
if r.Method != "PUT" {
http.Error(w, "expected method PUT", http.StatusMethodNotAllowed)
return finalSize, success
}
if h == nil || h.Dir == "" {
http.Error(w, errNoTaildrop.Error(), http.StatusInternalServerError)
return finalSize, success
}
if distro.Get() == distro.Unraid && !h.DirectFileMode {
http.Error(w, "Taildrop folder not configured or accessible", http.StatusInternalServerError)
return finalSize, success
}
rawPath := r.URL.EscapedPath()
suffix, ok := strings.CutPrefix(rawPath, "/v0/put/")
if !ok {
http.Error(w, "misconfigured internals", http.StatusInternalServerError)
return finalSize, success
}
if suffix == "" {
http.Error(w, "empty filename", http.StatusBadRequest)
return finalSize, success
}
if strings.Contains(suffix, "/") {
http.Error(w, "directories not supported", http.StatusBadRequest)
return finalSize, success
}
baseName, err := url.PathUnescape(suffix)
if err != nil {
http.Error(w, "bad path encoding", http.StatusBadRequest)
return finalSize, success
}
dstFile, ok := h.diskPath(baseName)
if !ok {
http.Error(w, "bad filename", http.StatusBadRequest)
return finalSize, success
}
// TODO(bradfitz): prevent same filename being sent by two peers at once
// prevent same filename being sent twice
if _, err := os.Stat(dstFile); err == nil {
http.Error(w, "file exists", http.StatusConflict)
return finalSize, success
}
partialFile := dstFile + partialSuffix
f, err := os.Create(partialFile)
if err != nil {
h.Logf("put Create error: %v", redactErr(err))
http.Error(w, err.Error(), http.StatusInternalServerError)
return finalSize, success
}
defer func() {
if !success {
os.Remove(partialFile)
}
}()
var inFile *incomingFile
sendFileNotify := h.SendFileNotify
if sendFileNotify == nil {
sendFileNotify = func() {} // avoid nil panics below
}
if r.ContentLength != 0 {
inFile = &incomingFile{
clock: h.Clock,
name: baseName,
started: h.Clock.Now(),
size: r.ContentLength,
w: f,
sendFileNotify: sendFileNotify,
}
if h.DirectFileMode {
inFile.partialPath = partialFile
}
h.incomingFiles.Store(inFile, struct{}{})
defer h.incomingFiles.Delete(inFile)
n, err := io.Copy(inFile, r.Body)
if err != nil {
err = redactErr(err)
f.Close()
h.Logf("put Copy error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return finalSize, success
}
finalSize = n
}
if err := redactErr(f.Close()); err != nil {
h.Logf("put Close error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return finalSize, success
}
if h.DirectFileMode && h.AvoidFinalRename {
if inFile != nil { // non-zero length; TODO: notify even for zero length
inFile.markAndNotifyDone()
}
} else {
if err := os.Rename(partialFile, dstFile); err != nil {
err = redactErr(err)
h.Logf("put final rename: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return finalSize, success
}
}
// TODO: set modtime
// TODO: some real response
success = true
io.WriteString(w, "{}\n")
h.knownEmpty.Store(false)
sendFileNotify()
return finalSize, success
}

View File

@@ -1,12 +1,6 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package taildrop contains the implementation of the Taildrop
// functionality including sending and retrieving files.
// This package does not validate permissions, the caller should
// be responsible for ensuring correct authorization.
//
// For related documentation see: http://go/taildrop-how-does-it-work
package taildrop
import (
@@ -21,61 +15,44 @@ import (
"unicode"
"unicode/utf8"
"tailscale.com/ipn"
"tailscale.com/syncs"
"tailscale.com/tstime"
"tailscale.com/types/logger"
"tailscale.com/util/multierr"
)
// Handler manages the state for receiving and managing taildropped files.
type Handler struct {
Logf logger.Logf
Clock tstime.Clock
// Dir is the directory to store received files.
// This main either be the final location for the files
// or just a temporary staging directory (see DirectFileMode).
Dir string
RootDir string // empty means file receiving unavailable
// DirectFileMode reports whether we are writing files
// directly to a download directory, rather than writing them to
// a temporary staging directory.
//
// The following methods:
// - HasFilesWaiting
// - WaitingFiles
// - DeleteFile
// - OpenFile
// have no purpose in DirectFileMode.
// They are only used to check whether files are in the staging directory,
// copy them out, and then delete them.
// DirectFileMode is whether we're writing files directly to a
// download directory (as *.partial files), rather than making
// the frontend retrieve it over localapi HTTP and write it
// somewhere itself. This is used on the GUI macOS versions
// and on Synology.
// In DirectFileMode, the peerapi doesn't do the final rename
// from "foo.jpg.partial" to "foo.jpg" unless
// directFileDoFinalRename is set.
DirectFileMode bool
// AvoidFinalRename specifies whether in DirectFileMode
// we should avoid renaming "foo.jpg.partial" to "foo.jpg" after reception.
AvoidFinalRename bool
// DirectFileDoFinalRename is whether in directFileMode we
// additionally move the *.direct file to its final name after
// it's received.
DirectFileDoFinalRename bool
// SendFileNotify is called periodically while a file is actively
// receiving the contents for the file. There is a final call
// to the function when reception completes.
// It is not called if nil.
SendFileNotify func()
knownEmpty atomic.Bool
incomingFiles syncs.Map[*incomingFile, struct{}]
KnownEmpty atomic.Bool
}
var (
errNilHandler = errors.New("handler unavailable; not listening")
errNoTaildrop = errors.New("Taildrop disabled; no storage directory")
ErrNoTaildrop = errors.New("Taildrop disabled; no storage directory")
)
const (
// partialSuffix is the suffix appended to files while they're
// PartialSuffix is the suffix appended to files while they're
// still in the process of being transferred.
partialSuffix = ".partial"
PartialSuffix = ".partial"
// deletedSuffix is the suffix for a deleted marker file
// that's placed next to a file (without the suffix) that we
@@ -107,7 +84,7 @@ func validFilenameRune(r rune) bool {
return unicode.IsPrint(r)
}
func (s *Handler) diskPath(baseName string) (fullPath string, ok bool) {
func (s *Handler) DiskPath(baseName string) (fullPath string, ok bool) {
if !utf8.ValidString(baseName) {
return "", false
}
@@ -122,7 +99,7 @@ func (s *Handler) diskPath(baseName string) (fullPath string, ok bool) {
if clean != baseName ||
clean == "." || clean == ".." ||
strings.HasSuffix(clean, deletedSuffix) ||
strings.HasSuffix(clean, partialSuffix) {
strings.HasSuffix(clean, PartialSuffix) {
return "", false
}
for _, r := range baseName {
@@ -133,28 +110,7 @@ func (s *Handler) diskPath(baseName string) (fullPath string, ok bool) {
if !filepath.IsLocal(baseName) {
return "", false
}
return filepath.Join(s.Dir, baseName), true
}
func (s *Handler) IncomingFiles() []ipn.PartialFile {
// Make sure we always set n.IncomingFiles non-nil so it gets encoded
// in JSON to clients. They distinguish between empty and non-nil
// to know whether a Notify should be able about files.
files := make([]ipn.PartialFile, 0)
s.incomingFiles.Range(func(f *incomingFile, _ struct{}) bool {
f.mu.Lock()
defer f.mu.Unlock()
files = append(files, ipn.PartialFile{
Name: f.name,
Started: f.started,
DeclaredSize: f.size,
Received: f.copied,
PartialPath: f.partialPath,
Done: f.done,
})
return true
})
return files
return filepath.Join(s.RootDir, baseName), true
}
type redactedErr struct {
@@ -180,7 +136,7 @@ func redactString(s string) string {
return string(b)
}
func redactErr(root error) error {
func RedactErr(root error) error {
// redactStrings is a list of sensitive strings that were redacted.
// It is not sufficient to just snub out sensitive fields in Go errors
// since some wrapper errors like fmt.Errorf pre-cache the error string,

View File

@@ -1,155 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package taildrop
import (
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
"runtime"
"testing"
)
// Tests "foo.jpg.deleted" marks (for Windows).
func TestDeletedMarkers(t *testing.T) {
dir := t.TempDir()
h := &Handler{Dir: dir}
nothingWaiting := func() {
t.Helper()
h.knownEmpty.Store(false)
if h.HasFilesWaiting() {
t.Fatal("unexpected files waiting")
}
}
touch := func(base string) {
t.Helper()
if err := touchFile(filepath.Join(dir, base)); err != nil {
t.Fatal(err)
}
}
wantEmptyTempDir := func() {
t.Helper()
if fis, err := os.ReadDir(dir); err != nil {
t.Fatal(err)
} else if len(fis) > 0 && runtime.GOOS != "windows" {
for _, fi := range fis {
t.Errorf("unexpected file in tempdir: %q", fi.Name())
}
}
}
nothingWaiting()
wantEmptyTempDir()
touch("foo.jpg.deleted")
nothingWaiting()
wantEmptyTempDir()
touch("foo.jpg.deleted")
touch("foo.jpg")
nothingWaiting()
wantEmptyTempDir()
touch("foo.jpg.deleted")
touch("foo.jpg")
wf, err := h.WaitingFiles()
if err != nil {
t.Fatal(err)
}
if len(wf) != 0 {
t.Fatalf("WaitingFiles = %d; want 0", len(wf))
}
wantEmptyTempDir()
touch("foo.jpg.deleted")
touch("foo.jpg")
if rc, _, err := h.OpenFile("foo.jpg"); err == nil {
rc.Close()
t.Fatal("unexpected foo.jpg open")
}
wantEmptyTempDir()
// And verify basics still work in non-deleted cases.
touch("foo.jpg")
touch("bar.jpg.deleted")
if wf, err := h.WaitingFiles(); err != nil {
t.Error(err)
} else if len(wf) != 1 {
t.Errorf("WaitingFiles = %d; want 1", len(wf))
} else if wf[0].Name != "foo.jpg" {
t.Errorf("unexpected waiting file %+v", wf[0])
}
if rc, _, err := h.OpenFile("foo.jpg"); err != nil {
t.Fatal(err)
} else {
rc.Close()
}
}
func TestRedactErr(t *testing.T) {
testCases := []struct {
name string
err func() error
want string
}{
{
name: "PathError",
err: func() error {
return &os.PathError{
Op: "open",
Path: "/tmp/sensitive.txt",
Err: fs.ErrNotExist,
}
},
want: `open redacted.41360718: file does not exist`,
},
{
name: "LinkError",
err: func() error {
return &os.LinkError{
Op: "symlink",
Old: "/tmp/sensitive.txt",
New: "/tmp/othersensitive.txt",
Err: fs.ErrNotExist,
}
},
want: `symlink redacted.41360718 redacted.6bcf093a: file does not exist`,
},
{
name: "something else",
err: func() error { return errors.New("i am another error type") },
want: `i am another error type`,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// For debugging
var i int
for err := tc.err(); err != nil; err = errors.Unwrap(err) {
t.Logf("%d: %T @ %p", i, err, err)
i++
}
t.Run("Root", func(t *testing.T) {
got := redactErr(tc.err()).Error()
if got != tc.want {
t.Errorf("err = %q; want %q", got, tc.want)
}
})
t.Run("Wrapped", func(t *testing.T) {
wrapped := fmt.Errorf("wrapped error: %w", tc.err())
want := "wrapped error: " + tc.want
got := redactErr(wrapped).Error()
if got != want {
t.Errorf("err = %q; want %q", got, want)
}
})
})
}
}

View File

@@ -32,10 +32,7 @@ if [[ -d "$toolchain" ]]; then
# A toolchain exists, but is it recent enough to compile gocross? If not,
# wipe it out so that the next if block fetches a usable one.
want_go_minor=$(grep -E '^go ' "go.mod" | cut -f2 -d'.')
have_go_minor=""
if [[ -f "$toolchain/VERSION" ]]; then
have_go_minor=$(head -1 "$toolchain/VERSION" | cut -f2 -d'.')
fi
have_go_minor=$(head -1 "$toolchain/VERSION" | cut -f2 -d'.')
# Shortly before stable releases, we run release candidate
# toolchains, which have a non-numeric suffix on the version
# number. Remove the rc qualifier, we just care about the minor

View File

@@ -13,19 +13,8 @@ import (
"github.com/google/go-cmp/cmp"
)
// ResourceCheck takes a snapshot of the current goroutines and registers a
// cleanup on tb to verify that after the rest, all goroutines created by the
// test go away. (well, at least that the count matches. Maybe in the future it
// can look at specific routines).
//
// It panics if called from a parallel test.
func ResourceCheck(tb testing.TB) {
tb.Helper()
// Set an environment variable (anything at all) just for the
// side effect of tb.Setenv panicking if we're in a parallel test.
tb.Setenv("TS_CHECKING_RESOURCES", "1")
startN, startStacks := goroutines()
tb.Cleanup(func() {
if tb.Failed() {

View File

@@ -6,23 +6,23 @@ package ipproto
import "fmt"
// Version describes the IP address version.
type Version uint8
// IPProtoVersion describes the IP address version.
type IPProtoVersion uint8
// Valid Version values.
// Valid IPProtoVersion values.
const (
Version4 = 4
Version6 = 6
IPProtoVersion4 = 4
IPProtoVersion6 = 6
)
func (p Version) String() string {
func (p IPProtoVersion) String() string {
switch p {
case Version4:
case IPProtoVersion4:
return "IPv4"
case Version6:
case IPProtoVersion6:
return "IPv6"
default:
return fmt.Sprintf("Version-%d", int(p))
return fmt.Sprintf("IPProtoVersion-%d", int(p))
}
}

View File

@@ -22,20 +22,15 @@ import (
"fmt"
"strconv"
"strings"
"unicode"
)
func isnum(r rune) bool {
return r >= '0' && r <= '9'
}
func notnum(r rune) bool {
return !isnum(r)
}
// Compare returns an integer comparing two strings as version
// numbers. The result will be 0 if v1==v2, -1 if v1 < v2, and +1 if
// v1 > v2.
func Compare(v1, v2 string) int {
notNumber := func(r rune) bool { return !unicode.IsNumber(r) }
var (
f1, f2 string
n1, n2 uint64
@@ -43,16 +38,16 @@ func Compare(v1, v2 string) int {
)
for v1 != "" || v2 != "" {
// Compare the non-numeric character run lexicographically.
f1, v1 = splitPrefixFunc(v1, notnum)
f2, v2 = splitPrefixFunc(v2, notnum)
f1, v1 = splitPrefixFunc(v1, notNumber)
f2, v2 = splitPrefixFunc(v2, notNumber)
if res := strings.Compare(f1, f2); res != 0 {
return res
}
// Compare the numeric character run numerically.
f1, v1 = splitPrefixFunc(v1, isnum)
f2, v2 = splitPrefixFunc(v2, isnum)
f1, v1 = splitPrefixFunc(v1, unicode.IsNumber)
f2, v2 = splitPrefixFunc(v2, unicode.IsNumber)
// ParseUint refuses to parse empty strings, which would only
// happen if we reached end-of-string. We follow the Debian

View File

@@ -1,13 +1,9 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package cmpver_test
package cmpver
import (
"testing"
"tailscale.com/util/cmpver"
)
import "testing"
func TestCompare(t *testing.T) {
tests := []struct {
@@ -91,16 +87,6 @@ func TestCompare(t *testing.T) {
v2: "0.96-105",
want: 1,
},
{
// Though ۱ and ۲ both satisfy unicode.IsNumber, our previous use
// of strconv.ParseUint with these characters would have lead us to
// panic. We're now only looking at ascii numbers, so test these are
// compared as text.
name: "only ascii numbers",
v1: "۱۱", // 2x EXTENDED ARABIC-INDIC DIGIT ONE
v2: "۲", // 1x EXTENDED ARABIC-INDIC DIGIT TWO
want: -1,
},
// A few specific OS version tests below.
{
@@ -161,17 +147,17 @@ func TestCompare(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := cmpver.Compare(test.v1, test.v2)
got := Compare(test.v1, test.v2)
if got != test.want {
t.Errorf("Compare(%v, %v) = %v, want %v", test.v1, test.v2, got, test.want)
}
// Reversing the comparison should reverse the outcome.
got2 := cmpver.Compare(test.v2, test.v1)
got2 := Compare(test.v2, test.v1)
if got2 != -test.want {
t.Errorf("Compare(%v, %v) = %v, want %v", test.v2, test.v1, got2, -test.want)
}
// Check that version comparison does not allocate.
if n := testing.AllocsPerRun(100, func() { cmpver.Compare(test.v1, test.v2) }); n > 0 {
if n := testing.AllocsPerRun(100, func() { Compare(test.v1, test.v2) }); n > 0 {
t.Errorf("Compare(%v, %v) got %v allocs per run", test.v1, test.v2, n)
}
})

View File

@@ -1,110 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux
package linuxfw
import (
"tailscale.com/envknob"
"tailscale.com/hostinfo"
"tailscale.com/types/logger"
"tailscale.com/version/distro"
)
func detectFirewallMode(logf logger.Logf) FirewallMode {
if distro.Get() == distro.Gokrazy {
// Reduce startup logging on gokrazy. There's no way to do iptables on
// gokrazy anyway.
logf("GoKrazy should use nftables.")
hostinfo.SetFirewallMode("nft-gokrazy")
return FirewallModeNfTables
}
envMode := envknob.String("TS_DEBUG_FIREWALL_MODE")
// We now use iptables as default and have "auto" and "nftables" as
// options for people to test further.
switch envMode {
case "auto":
return pickFirewallModeFromInstalledRules(logf, linuxFWDetector{})
case "nftables":
logf("envknob TS_DEBUG_FIREWALL_MODE=nftables set")
hostinfo.SetFirewallMode("nft-forced")
return FirewallModeNfTables
case "iptables":
logf("envknob TS_DEBUG_FIREWALL_MODE=iptables set")
hostinfo.SetFirewallMode("ipt-forced")
default:
logf("default choosing iptables")
hostinfo.SetFirewallMode("ipt-default")
}
return FirewallModeIPTables
}
// tableDetector abstracts helpers to detect the firewall mode.
// It is implemented for testing purposes.
type tableDetector interface {
iptDetect() (int, error)
nftDetect() (int, error)
}
type linuxFWDetector struct{}
// iptDetect returns the number of iptables rules in the current namespace.
func (l linuxFWDetector) iptDetect() (int, error) {
return detectIptables()
}
// nftDetect returns the number of nftables rules in the current namespace.
func (l linuxFWDetector) nftDetect() (int, error) {
return detectNetfilter()
}
// pickFirewallModeFromInstalledRules returns the firewall mode to use based on
// the environment and the system's capabilities.
func pickFirewallModeFromInstalledRules(logf logger.Logf, det tableDetector) FirewallMode {
if distro.Get() == distro.Gokrazy {
// Reduce startup logging on gokrazy. There's no way to do iptables on
// gokrazy anyway.
return FirewallModeNfTables
}
iptAva, nftAva := true, true
iptRuleCount, err := det.iptDetect()
if err != nil {
logf("detect iptables rule: %v", err)
iptAva = false
}
nftRuleCount, err := det.nftDetect()
if err != nil {
logf("detect nftables rule: %v", err)
nftAva = false
}
logf("nftables rule count: %d, iptables rule count: %d", nftRuleCount, iptRuleCount)
switch {
case nftRuleCount > 0 && iptRuleCount == 0:
logf("nftables is currently in use")
hostinfo.SetFirewallMode("nft-inuse")
return FirewallModeNfTables
case iptRuleCount > 0 && nftRuleCount == 0:
logf("iptables is currently in use")
hostinfo.SetFirewallMode("ipt-inuse")
return FirewallModeIPTables
case nftAva:
// if both iptables and nftables are available but
// neither/both are currently used, use nftables.
logf("nftables is available")
hostinfo.SetFirewallMode("nft")
return FirewallModeNfTables
case iptAva:
logf("iptables is available")
hostinfo.SetFirewallMode("ipt")
return FirewallModeIPTables
default:
// if neither iptables nor nftables are available, use iptablesRunner as a dummy
// runner which exists but won't do anything. Creating iptablesRunner errors only
// if the iptables command is missing or doesnt support "--version", as long as it
// can determine a version then itll carry on.
hostinfo.SetFirewallMode("ipt-fb")
return FirewallModeIPTables
}
}

View File

@@ -23,13 +23,13 @@ func DebugIptables(logf logger.Logf) error {
return nil
}
// detectIptables returns the number of iptables rules that are present in the
// DetectIptables returns the number of iptables rules that are present in the
// system, ignoring the default "ACCEPT" rule present in the standard iptables
// chains.
//
// It only returns an error when there is no iptables binary, or when iptables -S
// fails. In all other cases, it returns the number of non-default rules.
func detectIptables() (int, error) {
func DetectIptables() (int, error) {
// run "iptables -S" to get the list of rules using iptables
// exec.Command returns an error if the binary is not found
cmd := exec.Command("iptables", "-S")

View File

@@ -45,11 +45,11 @@ func checkIP6TablesExists() error {
return nil
}
// newIPTablesRunner constructs a NetfilterRunner that programs iptables rules.
// NewIPTablesRunner constructs a NetfilterRunner that programs iptables rules.
// If the underlying iptables library fails to initialize, that error is
// returned. The runner probes for IPv6 support once at initialization time and
// if not found, no IPv6 rules will be modified for the lifetime of the runner.
func newIPTablesRunner(logf logger.Logf) (*iptablesRunner, error) {
func NewIPTablesRunner(logf logger.Logf) (*iptablesRunner, error) {
ipt4, err := iptables.NewWithProtocol(iptables.ProtocolIPv4)
if err != nil {
return nil, err
@@ -79,12 +79,12 @@ func newIPTablesRunner(logf logger.Logf) (*iptablesRunner, error) {
return &iptablesRunner{ipt4, ipt6, supportsV6, supportsV6NAT}, nil
}
// HasIPV6 reports true if the system supports IPv6.
// HasIPV6 returns true if the system supports IPv6.
func (i *iptablesRunner) HasIPV6() bool {
return i.v6Available
}
// HasIPV6NAT reports true if the system supports IPv6 NAT.
// HasIPV6NAT returns true if the system supports IPv6 NAT.
func (i *iptablesRunner) HasIPV6NAT() bool {
return i.v6NATAvailable
}
@@ -254,12 +254,6 @@ func (i *iptablesRunner) addBase4(tunname string) error {
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
}
// Explicitly allow all other inbound traffic to the tun interface
args = []string{"-i", tunname, "-j", "ACCEPT"}
if err := i.ipt4.Append("filter", "ts-input", args...); err != nil {
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
}
// Forward all traffic from the Tailscale interface, and drop
// traffic to the tailscale interface by default. We use packet
// marks here so both filter/FORWARD and nat/POSTROUTING can match
@@ -297,13 +291,7 @@ func (i *iptablesRunner) addBase6(tunname string) error {
// TODO: only allow traffic from Tailscale's ULA range to come
// from tailscale0.
// Explicitly allow all other inbound traffic to the tun interface
args := []string{"-i", tunname, "-j", "ACCEPT"}
if err := i.ipt6.Append("filter", "ts-input", args...); err != nil {
return fmt.Errorf("adding %v in v6/filter/ts-input: %w", args, err)
}
args = []string{"-i", tunname, "-j", "MARK", "--set-mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask}
args := []string{"-i", tunname, "-j", "MARK", "--set-mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask}
if err := i.ipt6.Append("filter", "ts-forward", args...); err != nil {
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
}

View File

@@ -261,7 +261,6 @@ func TestAddAndDeleteBase(t *testing.T) {
}
tsRulesCommon := []fakeRule{ // table/chain/rule
{"filter", "ts-input", []string{"-i", tunname, "-j", "ACCEPT"}},
{"filter", "ts-forward", []string{"-i", tunname, "-j", "MARK", "--set-mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask}},
{"filter", "ts-forward", []string{"-m", "mark", "--mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask, "-j", "ACCEPT"}},
{"filter", "ts-forward", []string{"-o", tunname, "-j", "ACCEPT"}},

View File

@@ -25,16 +25,16 @@ func DebugNetfilter(logf logger.Logf) error {
}
// DetectNetfilter is not supported on non-Linux platforms.
func detectNetfilter() (int, error) {
func DetectNetfilter() (int, error) {
return 0, ErrUnsupported
}
// DebugIptables is not supported on non-Linux platforms.
func debugIptables(logf logger.Logf) error {
func DebugIptables(logf logger.Logf) error {
return ErrUnsupported
}
// DetectIptables is not supported on non-Linux platforms.
func detectIptables() (int, error) {
func DetectIptables() (int, error) {
return 0, ErrUnsupported
}

View File

@@ -103,8 +103,8 @@ func DebugNetfilter(logf logger.Logf) error {
return nil
}
// detectNetfilter returns the number of nftables rules present in the system.
func detectNetfilter() (int, error) {
// DetectNetfilter returns the number of nftables rules present in the system.
func DetectNetfilter() (int, error) {
conn, err := nftables.New()
if err != nil {
return 0, FWModeNotSupportedError{

View File

@@ -175,67 +175,9 @@ func createChainIfNotExist(c *nftables.Conn, cinfo chainInfo) error {
return nil
}
// NetfilterRunner abstracts helpers to run netfilter commands. It is
// implemented by linuxfw.IPTablesRunner and linuxfw.NfTablesRunner.
type NetfilterRunner interface {
// AddLoopbackRule adds a rule to permit loopback traffic to addr. This rule
// is added only if it does not already exist.
AddLoopbackRule(addr netip.Addr) error
// DelLoopbackRule removes the rule added by AddLoopbackRule.
DelLoopbackRule(addr netip.Addr) error
// AddHooks adds rules to conventional chains like "FORWARD", "INPUT" and
// "POSTROUTING" to jump from those chains to tailscale chains.
AddHooks() error
// DelHooks deletes rules added by AddHooks.
DelHooks(logf logger.Logf) error
// AddChains creates custom Tailscale chains.
AddChains() error
// DelChains removes chains added by AddChains.
DelChains() error
// AddBase adds rules reused by different other rules.
AddBase(tunname string) error
// DelBase removes rules added by AddBase.
DelBase() error
// AddSNATRule adds the netfilter rule to SNAT incoming traffic over
// the Tailscale interface destined for local subnets. An error is
// returned if the rule already exists.
AddSNATRule() error
// DelSNATRule removes the rule added by AddSNATRule.
DelSNATRule() error
// HasIPV6 reports true if the system supports IPv6.
HasIPV6() bool
// HasIPV6NAT reports true if the system supports IPv6 NAT.
HasIPV6NAT() bool
}
// New creates a NetfilterRunner using either nftables or iptables.
// As nftables is still experimental, iptables will be used unless TS_DEBUG_USE_NETLINK_NFTABLES is set.
func New(logf logger.Logf) (NetfilterRunner, error) {
mode := detectFirewallMode(logf)
switch mode {
case FirewallModeIPTables:
return newIPTablesRunner(logf)
case FirewallModeNfTables:
return newNfTablesRunner(logf)
default:
return nil, fmt.Errorf("unknown firewall mode %v", mode)
}
}
// newNfTablesRunner creates a new nftablesRunner without guaranteeing
// NewNfTablesRunner creates a new nftablesRunner without guaranteeing
// the existence of the tables and chains.
func newNfTablesRunner(logf logger.Logf) (*nftablesRunner, error) {
func NewNfTablesRunner(logf logger.Logf) (*nftablesRunner, error) {
conn, err := nftables.New()
if err != nil {
return nil, fmt.Errorf("nftables connection: %w", err)
@@ -289,7 +231,7 @@ func newLoadSaddrExpr(proto nftables.TableFamily, destReg uint32) (expr.Any, err
}
}
// HasIPV6 reports true if the system supports IPv6.
// HasIPV6 returns true if the system supports IPv6.
func (n *nftablesRunner) HasIPV6() bool {
return n.v6Available
}
@@ -935,38 +877,6 @@ func addAcceptOutgoingPacketRule(conn *nftables.Conn, table *nftables.Table, cha
return nil
}
// createAcceptIncomingPacketRule creates a rule to accept incoming packets to
// the given interface.
func createAcceptIncomingPacketRule(table *nftables.Table, chain *nftables.Chain, tunname string) *nftables.Rule {
return &nftables.Rule{
Table: table,
Chain: chain,
Exprs: []expr.Any{
&expr.Meta{Key: expr.MetaKeyIIFNAME, Register: 1},
&expr.Cmp{
Op: expr.CmpOpEq,
Register: 1,
Data: []byte(tunname),
},
&expr.Counter{},
&expr.Verdict{
Kind: expr.VerdictAccept,
},
},
}
}
func addAcceptIncomingPacketRule(conn *nftables.Conn, table *nftables.Table, chain *nftables.Chain, tunname string) error {
rule := createAcceptIncomingPacketRule(table, chain, tunname)
_ = conn.AddRule(rule)
if err := conn.Flush(); err != nil {
return fmt.Errorf("flush add rule: %w", err)
}
return nil
}
// AddBase adds some basic processing rules.
func (n *nftablesRunner) AddBase(tunname string) error {
if err := n.addBase4(tunname); err != nil {
@@ -994,9 +904,6 @@ func (n *nftablesRunner) addBase4(tunname string) error {
if err = addDropCGNATRangeRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {
return fmt.Errorf("add drop cgnat range rule v4: %w", err)
}
if err = addAcceptIncomingPacketRule(conn, n.nft4.Filter, inputChain, tunname); err != nil {
return fmt.Errorf("add accept incoming packet rule v4: %w", err)
}
forwardChain, err := getChainFromTable(conn, n.nft4.Filter, chainNameForward)
if err != nil {
@@ -1030,14 +937,6 @@ func (n *nftablesRunner) addBase4(tunname string) error {
func (n *nftablesRunner) addBase6(tunname string) error {
conn := n.conn
inputChain, err := getChainFromTable(conn, n.nft6.Filter, chainNameInput)
if err != nil {
return fmt.Errorf("get input chain v4: %v", err)
}
if err = addAcceptIncomingPacketRule(conn, n.nft6.Filter, inputChain, tunname); err != nil {
return fmt.Errorf("add accept incoming packet rule v6: %w", err)
}
forwardChain, err := getChainFromTable(conn, n.nft6.Filter, chainNameForward)
if err != nil {
return fmt.Errorf("get forward chain v6: %w", err)

View File

@@ -7,7 +7,6 @@ package linuxfw
import (
"bytes"
"errors"
"fmt"
"net/netip"
"os"
@@ -376,38 +375,6 @@ func TestAddAcceptOutgoingPacketRule(t *testing.T) {
}
}
func TestAddAcceptIncomingPacketRule(t *testing.T) {
proto := nftables.TableFamilyIPv4
want := [][]byte{
// batch begin
[]byte("\x00\x00\x00\x0a"),
// nft add table ip ts-filter-test
[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x08\x00\x02\x00\x00\x00\x00\x00"),
// nft add chain ip ts-filter-test ts-input-test { type filter hook input priority 0\; }
[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x12\x00\x03\x00\x74\x73\x2d\x69\x6e\x70\x75\x74\x2d\x74\x65\x73\x74\x00\x00\x00\x14\x00\x04\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x0b\x00\x07\x00\x66\x69\x6c\x74\x65\x72\x00\x00"),
// nft add rule ip ts-filter-test ts-input-test iifname "testTunn" counter accept
[]byte("\x02\x00\x00\x00\x13\x00\x01\x00\x74\x73\x2d\x66\x69\x6c\x74\x65\x72\x2d\x74\x65\x73\x74\x00\x00\x12\x00\x02\x00\x74\x73\x2d\x69\x6e\x70\x75\x74\x2d\x74\x65\x73\x74\x00\x00\x00\xb4\x00\x04\x80\x24\x00\x01\x80\x09\x00\x01\x00\x6d\x65\x74\x61\x00\x00\x00\x00\x14\x00\x02\x80\x08\x00\x02\x00\x00\x00\x00\x06\x08\x00\x01\x00\x00\x00\x00\x01\x30\x00\x01\x80\x08\x00\x01\x00\x63\x6d\x70\x00\x24\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01\x08\x00\x02\x00\x00\x00\x00\x00\x10\x00\x03\x80\x0c\x00\x01\x00\x74\x65\x73\x74\x54\x75\x6e\x6e\x2c\x00\x01\x80\x0c\x00\x01\x00\x63\x6f\x75\x6e\x74\x65\x72\x00\x1c\x00\x02\x80\x0c\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0c\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x30\x00\x01\x80\x0e\x00\x01\x00\x69\x6d\x6d\x65\x64\x69\x61\x74\x65\x00\x00\x00\x1c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x00\x10\x00\x02\x80\x0c\x00\x02\x80\x08\x00\x01\x00\x00\x00\x00\x01"),
// batch end
[]byte("\x00\x00\x00\x0a"),
}
testConn := newTestConn(t, want)
table := testConn.AddTable(&nftables.Table{
Family: proto,
Name: "ts-filter-test",
})
chain := testConn.AddChain(&nftables.Chain{
Name: "ts-input-test",
Table: table,
Type: nftables.ChainTypeFilter,
Hooknum: nftables.ChainHookInput,
Priority: nftables.ChainPriorityFilter,
})
err := addAcceptIncomingPacketRule(testConn, table, chain, "testTunn")
if err != nil {
t.Fatal(err)
}
}
func TestAddMatchSubnetRouteMarkRuleMasq(t *testing.T) {
proto := nftables.TableFamilyIPv4
want := [][]byte{
@@ -947,63 +914,3 @@ func TestNFTAddAndDelHookRule(t *testing.T) {
t.Fatalf("expected 0 rule in POSTROUTING chain, got %v", len(postroutingChainRules))
}
}
type testFWDetector struct {
iptRuleCount, nftRuleCount int
iptErr, nftErr error
}
func (t *testFWDetector) iptDetect() (int, error) {
return t.iptRuleCount, t.iptErr
}
func (t *testFWDetector) nftDetect() (int, error) {
return t.nftRuleCount, t.nftErr
}
func TestPickFirewallModeFromInstalledRules(t *testing.T) {
tests := []struct {
name string
det *testFWDetector
want FirewallMode
}{
{
name: "using iptables legacy",
det: &testFWDetector{iptRuleCount: 1},
want: FirewallModeIPTables,
},
{
name: "using nftables",
det: &testFWDetector{nftRuleCount: 1},
want: FirewallModeNfTables,
},
{
name: "using both iptables and nftables",
det: &testFWDetector{iptRuleCount: 2, nftRuleCount: 2},
want: FirewallModeNfTables,
},
{
name: "not using any firewall, both available",
det: &testFWDetector{},
want: FirewallModeNfTables,
},
{
name: "not using any firewall, iptables available only",
det: &testFWDetector{iptRuleCount: 1, nftErr: errors.New("nft error")},
want: FirewallModeIPTables,
},
{
name: "not using any firewall, nftables available only",
det: &testFWDetector{iptErr: errors.New("iptables error"), nftRuleCount: 1},
want: FirewallModeNfTables,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := pickFirewallModeFromInstalledRules(t.Logf, tt.det)
if got != tt.want {
t.Errorf("chooseFireWallMode() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -32,8 +32,4 @@ const (
// The default is 0 unless otherwise stated.
LogSCMInteractions Key = "LogSCMInteractions"
FlushDNSOnSessionUnlock Key = "FlushDNSOnSessionUnlock"
// Boolean key that indicates if posture checking is enabled and the client shall gather
// posture data.
PostureChecking Key = "PostureChecking"
)

View File

@@ -35,16 +35,6 @@ import (
"tailscale.com/util/ringbuffer"
)
var mtuProbePingSizesV4 []int
var mtuProbePingSizesV6 []int
func init() {
for _, m := range tstun.WireMTUsToProbe {
mtuProbePingSizesV4 = append(mtuProbePingSizesV4, pktLenToPingSize(m, false))
mtuProbePingSizesV6 = append(mtuProbePingSizesV6, pktLenToPingSize(m, true))
}
}
// endpoint is a wireguard/conn.Endpoint. In wireguard-go and kernel WireGuard
// there is only one endpoint for a peer, but in Tailscale we distribute a
// number of possible endpoints for a peer which would include the all the
@@ -93,6 +83,13 @@ type endpoint struct {
isWireguardOnly bool // whether the endpoint is WireGuard only
}
func (ep *endpoint) sessionActiveTimeout() time.Duration {
if ep == nil {
return sessionActiveTimeoutDefault
}
return ep.c.sessionActiveTimeout()
}
// endpointDisco is the current disco key and short string for an endpoint. This
// structure is immutable.
type endpointDisco struct {
@@ -114,6 +111,8 @@ type sentPing struct {
// a endpoint. (The subject is the endpoint.endpointState
// map key)
type endpointState struct {
ep *endpoint
// all fields guarded by endpoint.mu
// lastPing is the last (outgoing) ping time.
@@ -179,7 +178,7 @@ func (st *endpointState) shouldDeleteLocked() bool {
return st.index == indexSentinelDeleted
default:
// This was an endpoint discovered at runtime.
return time.Since(st.lastGotPing) > sessionActiveTimeout
return time.Since(st.lastGotPing) > st.ep.sessionActiveTimeout()
}
}
@@ -384,7 +383,7 @@ func (de *endpoint) addrForPingSizeLocked(now mono.Time, size int) (udpAddr, der
udpAddr = de.bestAddr.AddrPort
pathMTU := de.bestAddr.wireMTU
requestedMTU := pingSizeToPktLen(size, udpAddr.Addr().Is6())
requestedMTU := pingSizeToPktLen(size, udpAddr.Addr())
mtuOk := requestedMTU <= pathMTU
if udpAddr.IsValid() && mtuOk {
@@ -421,7 +420,7 @@ func (de *endpoint) heartbeat() {
return
}
if mono.Since(de.lastSend) > sessionActiveTimeout {
if mono.Since(de.lastSend) > de.c.sessionActiveTimeout() {
// Session's idle. Stop heartbeating.
de.c.dlogf("[v1] magicsock: disco: ending heartbeats for idle session to %v (%v)", de.publicKey.ShortString(), de.discoShort())
return
@@ -633,12 +632,6 @@ func (de *endpoint) sendDiscoPing(ep netip.AddrPort, discoKey key.DiscoPublic, t
}, logLevel)
if !sent {
de.forgetDiscoPing(txid)
return
}
if size != 0 {
metricSentDiscoPeerMTUProbes.Add(1)
metricSentDiscoPeerMTUProbeBytes.Add(int64(pingSizeToPktLen(size, ep.Addr().Is6())))
}
}
@@ -682,41 +675,21 @@ func (de *endpoint) startDiscoPingLocked(ep netip.AddrPort, now mono.Time, purpo
st.lastPing = now
}
// If we are doing a discovery ping or a CLI ping with no specified size
// to a non DERP address, then probe the MTU. Otherwise just send the
// one specified ping.
// Default to sending a single ping of the specified size
sizes := []int{size}
if de.c.PeerMTUEnabled() {
isDerp := ep.Addr() == tailcfg.DerpMagicIPAddr
if !isDerp && ((purpose == pingDiscovery) || (purpose == pingCLI && size == 0)) {
de.c.dlogf("[v1] magicsock: starting MTU probe")
sizes = mtuProbePingSizesV4
if ep.Addr().Is6() {
sizes = mtuProbePingSizesV6
}
}
txid := stun.NewTxID()
de.sentPing[txid] = sentPing{
to: ep,
at: now,
timer: time.AfterFunc(pingTimeoutDuration, func() { de.discoPingTimeout(txid) }),
purpose: purpose,
res: res,
cb: cb,
}
logLevel := discoLog
if purpose == pingHeartbeat {
logLevel = discoVerboseLog
}
for _, s := range sizes {
txid := stun.NewTxID()
de.sentPing[txid] = sentPing{
to: ep,
at: now,
timer: time.AfterFunc(pingTimeoutDuration, func() { de.discoPingTimeout(txid) }),
purpose: purpose,
res: res,
cb: cb,
size: s,
}
go de.sendDiscoPing(ep, epDisco.key, txid, s, logLevel)
}
go de.sendDiscoPing(ep, epDisco.key, txid, size, logLevel)
}
// sendDiscoPingsLocked starts pinging all of ep's endpoints.
@@ -912,7 +885,7 @@ func (de *endpoint) setEndpointsLocked(eps interface {
if st, ok := de.endpointState[ipp]; ok {
st.index = int16(i)
} else {
de.endpointState[ipp] = &endpointState{index: int16(i)}
de.endpointState[ipp] = &endpointState{ep: de, index: int16(i)}
newIpps = append(newIpps, ipp)
}
}
@@ -960,6 +933,7 @@ func (de *endpoint) addCandidateEndpoint(ep netip.AddrPort, forRxPingTxID stun.T
// Newly discovered endpoint. Exciting!
de.c.dlogf("[v1] magicsock: disco: adding %v as candidate endpoint for %v (%s)", ep, de.discoShort(), de.publicKey.ShortString())
de.endpointState[ep] = &endpointState{
ep: de,
lastGotPing: time.Now(),
lastGotPingTxID: forRxPingTxID,
}
@@ -1018,13 +992,13 @@ func (de *endpoint) noteConnectivityChange() {
// pingSizeToPktLen calculates the minimum path MTU that would permit
// a disco ping message of length size to reach its target at
// addr. size is the length of the entire disco message including
// disco headers. If size is zero, assume it is the safe wire MTU.
func pingSizeToPktLen(size int, is6 bool) tstun.WireMTU {
// disco headers. If size is zero, assume it is the default MTU.
func pingSizeToPktLen(size int, addr netip.Addr) tstun.WireMTU {
if size == 0 {
return tstun.SafeWireMTU()
return tstun.DefaultWireMTU()
}
headerLen := ipv4.HeaderLen
if is6 {
if addr.Is6() {
headerLen = ipv6.HeaderLen
}
headerLen += 8 // UDP header length
@@ -1035,12 +1009,12 @@ func pingSizeToPktLen(size int, is6 bool) tstun.WireMTU {
// create a disco ping message whose on-the-wire length is exactly mtu
// bytes long. If mtu is zero or less than the minimum ping size, then
// no MTU probe is desired and return zero for an unpadded ping.
func pktLenToPingSize(mtu tstun.WireMTU, is6 bool) int {
func pktLenToPingSize(mtu tstun.WireMTU, addr netip.Addr) int {
if mtu == 0 {
return 0
}
headerLen := ipv4.HeaderLen
if is6 {
if addr.Is6() {
headerLen = ipv6.HeaderLen
}
headerLen += 8 // UDP header length
@@ -1068,15 +1042,6 @@ func (de *endpoint) handlePongConnLocked(m *disco.Pong, di *discoInfo, src netip
knownTxID = true // for naked returns below
de.removeSentDiscoPingLocked(m.TxID, sp)
pktLen := int(pingSizeToPktLen(sp.size, sp.to.Addr().Is6()))
if sp.size != 0 {
m := getPeerMTUsProbedMetric(tstun.WireMTU(pktLen))
m.Add(1)
if metricMaxPeerMTUProbed.Value() < int64(pktLen) {
metricMaxPeerMTUProbed.Set(int64(pktLen))
}
}
now := mono.Now()
latency := now.Sub(sp.at)
@@ -1098,7 +1063,7 @@ func (de *endpoint) handlePongConnLocked(m *disco.Pong, di *discoInfo, src netip
}
if sp.purpose != pingHeartbeat {
de.c.dlogf("[v1] magicsock: disco: %v<-%v (%v, %v) got pong tx=%x latency=%v pktlen=%v pong.src=%v%v", de.c.discoShort, de.discoShort(), de.publicKey.ShortString(), src, m.TxID[:6], latency.Round(time.Millisecond), pktLen, m.Src, logger.ArgWriter(func(bw *bufio.Writer) {
de.c.dlogf("[v1] magicsock: disco: %v<-%v (%v, %v) got pong tx=%x latency=%v pktlen=%v pong.src=%v%v", de.c.discoShort, de.discoShort(), de.publicKey.ShortString(), src, m.TxID[:6], latency.Round(time.Millisecond), pingSizeToPktLen(sp.size, sp.to.Addr()), m.Src, logger.ArgWriter(func(bw *bufio.Writer) {
if sp.to != src {
fmt.Fprintf(bw, " ping.to=%v", sp.to)
}
@@ -1116,9 +1081,9 @@ func (de *endpoint) handlePongConnLocked(m *disco.Pong, di *discoInfo, src netip
// Promote this pong response to our current best address if it's lower latency.
// TODO(bradfitz): decide how latency vs. preference order affects decision
if !isDerp {
thisPong := addrQuality{sp.to, latency, tstun.WireMTU(pingSizeToPktLen(sp.size, sp.to.Addr().Is6()))}
thisPong := addrQuality{sp.to, latency, pingSizeToPktLen(sp.size, sp.to.Addr())}
if betterAddr(thisPong, de.bestAddr) {
de.c.logf("magicsock: disco: node %v %v now using %v mtu=%v tx=%x", de.publicKey.ShortString(), de.discoShort(), sp.to, thisPong.wireMTU, m.TxID[:6])
de.c.logf("magicsock: disco: node %v %v now using %v mtu %v", de.publicKey.ShortString(), de.discoShort(), sp.to, thisPong.wireMTU)
de.debugUpdates.Add(EndpointChange{
When: time.Now(),
What: "handlePingLocked-bestAddr-update",
@@ -1306,7 +1271,7 @@ func (de *endpoint) populatePeerStatus(ps *ipnstate.PeerStatus) {
now := mono.Now()
ps.LastWrite = de.lastSend.WallTime()
ps.Active = now.Sub(de.lastSend) < sessionActiveTimeout
ps.Active = now.Sub(de.lastSend) < de.c.sessionActiveTimeout()
if udpAddr, derpAddr, _ := de.addrForSendLocked(now); udpAddr.IsValid() && !derpAddr.IsValid() {
ps.CurAddr = udpAddr.String()

View File

@@ -42,7 +42,6 @@ import (
"tailscale.com/net/portmapper"
"tailscale.com/net/sockstats"
"tailscale.com/net/stun"
"tailscale.com/net/tstun"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
@@ -1568,7 +1567,7 @@ func (c *Conn) handlePingLocked(dm *disco.Ping, src netip.AddrPort, di *discoInf
if numNodes > 1 {
pingNodeSrcStr = "[one-of-multi]"
}
c.dlogf("[v1] magicsock: disco: %v<-%v (%v, %v) got ping tx=%x padding=%v", c.discoShort, di.discoShort, pingNodeSrcStr, src, dm.TxID[:6], dm.Padding)
c.dlogf("[v1] magicsock: disco: %v<-%v (%v, %v) got ping tx=%x", c.discoShort, di.discoShort, pingNodeSrcStr, src, dm.TxID[:6])
}
ipDst := src
@@ -2186,7 +2185,7 @@ func (c *Conn) shouldDoPeriodicReSTUNLocked() bool {
if debugReSTUNStopOnIdle() {
c.logf("magicsock: periodicReSTUN: idle for %v", idleFor.Round(time.Second))
}
if idleFor > sessionActiveTimeout {
if idleFor > c.sessionActiveTimeout() {
if c.controlKnobs != nil && c.controlKnobs.ForceBackgroundSTUN.Load() {
// Overridden by control.
return true
@@ -2658,11 +2657,11 @@ func (c *Conn) SetStatistics(stats *connstats.Statistics) {
}
const (
// sessionActiveTimeout is how long since the last activity we
// sessionActiveTimeoutDefault is how long since the last activity we
// try to keep an established endpoint peering alive.
// It's also the idle time at which we stop doing STUN queries to
// keep NAT mappings alive.
sessionActiveTimeout = 45 * time.Second
sessionActiveTimeoutDefault = 45 * time.Second
// upgradeInterval is how often we try to upgrade to a better path
// even if we have some non-DERP route that works.
@@ -2716,33 +2715,6 @@ func (c *Conn) getPinger() *ping.Pinger {
})
}
// DebugPickNewDERP picks a new DERP random home temporarily (even if just for
// seconds) and reports it to control. It exists to test DERP home changes and
// netmap deltas, etc. It serves no useful user purpose.
func (c *Conn) DebugPickNewDERP() error {
c.mu.Lock()
defer c.mu.Unlock()
dm := c.derpMap
if dm == nil {
return errors.New("no derpmap")
}
if c.netInfoLast == nil {
return errors.New("no netinfo")
}
for _, r := range dm.Regions {
if r.RegionID == c.myDerp {
continue
}
c.logf("magicsock: [debug] switching derp home to random %v (%v)", r.RegionID, r.RegionCode)
go c.setNearestDERP(r.RegionID)
ni2 := c.netInfoLast.Clone()
ni2.PreferredDERP = r.RegionID
c.callNetInfoCallbackLocked(ni2)
return nil
}
return errors.New("too few regions")
}
// portableTrySetSocketBuffer sets SO_SNDBUF and SO_RECVBUF on pconn to socketBufferSize,
// logging an error if it occurs.
func portableTrySetSocketBuffer(pconn nettype.PacketConn, logf logger.Logf) {
@@ -2757,6 +2729,15 @@ func portableTrySetSocketBuffer(pconn nettype.PacketConn, logf logger.Logf) {
}
}
func (c *Conn) sessionActiveTimeout() time.Duration {
if ck := c.controlKnobs; ck != nil {
if v := ck.MagicsockSessionActiveTimeout.Load(); v != 0 {
return v
}
}
return sessionActiveTimeoutDefault
}
// derpStr replaces DERP IPs in s with "derp-".
func derpStr(s string) string { return strings.ReplaceAll(s, "127.3.3.40:", "derp-") }
@@ -2826,18 +2807,16 @@ var (
metricRecvDataIPv6 = clientmetric.NewCounter("magicsock_recv_data_ipv6")
// Disco packets
metricSendDiscoUDP = clientmetric.NewCounter("magicsock_disco_send_udp")
metricSendDiscoDERP = clientmetric.NewCounter("magicsock_disco_send_derp")
metricSentDiscoUDP = clientmetric.NewCounter("magicsock_disco_sent_udp")
metricSentDiscoDERP = clientmetric.NewCounter("magicsock_disco_sent_derp")
metricSentDiscoPing = clientmetric.NewCounter("magicsock_disco_sent_ping")
metricSentDiscoPong = clientmetric.NewCounter("magicsock_disco_sent_pong")
metricSentDiscoPeerMTUProbes = clientmetric.NewCounter("magicsock_disco_sent_peer_mtu_probes")
metricSentDiscoPeerMTUProbeBytes = clientmetric.NewCounter("magicsock_disco_sent_peer_mtu_probe_bytes")
metricSentDiscoCallMeMaybe = clientmetric.NewCounter("magicsock_disco_sent_callmemaybe")
metricRecvDiscoBadPeer = clientmetric.NewCounter("magicsock_disco_recv_bad_peer")
metricRecvDiscoBadKey = clientmetric.NewCounter("magicsock_disco_recv_bad_key")
metricRecvDiscoBadParse = clientmetric.NewCounter("magicsock_disco_recv_bad_parse")
metricSendDiscoUDP = clientmetric.NewCounter("magicsock_disco_send_udp")
metricSendDiscoDERP = clientmetric.NewCounter("magicsock_disco_send_derp")
metricSentDiscoUDP = clientmetric.NewCounter("magicsock_disco_sent_udp")
metricSentDiscoDERP = clientmetric.NewCounter("magicsock_disco_sent_derp")
metricSentDiscoPing = clientmetric.NewCounter("magicsock_disco_sent_ping")
metricSentDiscoPong = clientmetric.NewCounter("magicsock_disco_sent_pong")
metricSentDiscoCallMeMaybe = clientmetric.NewCounter("magicsock_disco_sent_callmemaybe")
metricRecvDiscoBadPeer = clientmetric.NewCounter("magicsock_disco_recv_bad_peer")
metricRecvDiscoBadKey = clientmetric.NewCounter("magicsock_disco_recv_bad_key")
metricRecvDiscoBadParse = clientmetric.NewCounter("magicsock_disco_recv_bad_parse")
metricRecvDiscoUDP = clientmetric.NewCounter("magicsock_disco_recv_udp")
metricRecvDiscoDERP = clientmetric.NewCounter("magicsock_disco_recv_derp")
@@ -2855,18 +2834,4 @@ var (
// Disco packets received bpf read path
metricRecvDiscoPacketIPv4 = clientmetric.NewCounter("magicsock_disco_recv_bpf_ipv4")
metricRecvDiscoPacketIPv6 = clientmetric.NewCounter("magicsock_disco_recv_bpf_ipv6")
// metricMaxPeerMTUProbed is the largest peer path MTU we successfully probed.
metricMaxPeerMTUProbed = clientmetric.NewGauge("magicsock_max_peer_mtu_probed")
// metricRecvDiscoPeerMTUProbesByMTU collects the number of times we
// received an peer MTU probe response for a given MTU size.
// TODO: add proper support for label maps in clientmetrics
metricRecvDiscoPeerMTUProbesByMTU syncs.Map[string, *clientmetric.Metric]
)
func getPeerMTUsProbedMetric(mtu tstun.WireMTU) *clientmetric.Metric {
key := fmt.Sprintf("magicsock_recv_disco_peer_mtu_probes_by_mtu_%d", mtu)
mm, _ := metricRecvDiscoPeerMTUProbesByMTU.LoadOrInit(key, func() *clientmetric.Metric { return clientmetric.NewCounter(key) })
return mm
}

View File

@@ -705,8 +705,6 @@ func TestDiscokeyChange(t *testing.T) {
}
func TestActiveDiscovery(t *testing.T) {
tstest.ResourceCheck(t)
t.Run("simple_internet", func(t *testing.T) {
t.Parallel()
mstun := &natlab.Machine{Name: "stun"}
@@ -902,6 +900,7 @@ func newPinger(t *testing.T, logf logger.Logf, src, dst *magicStack) (cleanup fu
// get exercised.
func testActiveDiscovery(t *testing.T, d *devices) {
tstest.PanicOnLog()
tstest.ResourceCheck(t)
tlogf, setT := makeNestable(t)
setT(t)
@@ -2852,7 +2851,7 @@ func TestAddrForSendLockedForWireGuardOnly(t *testing.T) {
}
for _, epd := range test.ep {
endpoint.endpointState[epd.addrPort] = &endpointState{}
endpoint.endpointState[epd.addrPort] = &endpointState{ep: endpoint}
}
udpAddr, _, shouldPing := endpoint.addrForSendLocked(testTime)
if udpAddr.IsValid() != test.validAddr {
@@ -2936,7 +2935,7 @@ func TestAddrForPingSizeLocked(t *testing.T) {
},
{
desc: "ping_size_too_big_for_trusted_UDP_addr_should_start_discovery_and_send_to_DERP",
size: pktLenToPingSize(1501, validUdpAddr.Addr().Is6()),
size: pktLenToPingSize(1501, validUdpAddr.Addr()),
mtu: 1500,
bestAddr: true,
bestAddrTrusted: true,
@@ -2945,7 +2944,7 @@ func TestAddrForPingSizeLocked(t *testing.T) {
},
{
desc: "ping_size_too_big_for_untrusted_UDP_addr_should_start_discovery_and_send_to_DERP",
size: pktLenToPingSize(1501, validUdpAddr.Addr().Is6()),
size: pktLenToPingSize(1501, validUdpAddr.Addr()),
mtu: 1500,
bestAddr: true,
bestAddrTrusted: false,
@@ -2954,7 +2953,7 @@ func TestAddrForPingSizeLocked(t *testing.T) {
},
{
desc: "ping_size_small_enough_for_trusted_UDP_addr_should_send_to_UDP_and_not_DERP",
size: pktLenToPingSize(1500, validUdpAddr.Addr().Is6()),
size: pktLenToPingSize(1500, validUdpAddr.Addr()),
mtu: 1500,
bestAddr: true,
bestAddrTrusted: true,
@@ -2963,7 +2962,7 @@ func TestAddrForPingSizeLocked(t *testing.T) {
},
{
desc: "ping_size_small_enough_for_untrusted_UDP_addr_should_send_to_UDP_and_DERP",
size: pktLenToPingSize(1500, validUdpAddr.Addr().Is6()),
size: pktLenToPingSize(1500, validUdpAddr.Addr()),
mtu: 1500,
bestAddr: true,
bestAddrTrusted: false,

View File

@@ -5,8 +5,6 @@
package magicsock
import "tailscale.com/net/tstun"
// Peer path MTU routines shared by platforms that implement it.
// DontFragSetting returns true if at least one of the underlying sockets of
@@ -104,9 +102,6 @@ func (c *Conn) UpdatePMTUD() {
_ = c.setDontFragment("udp6", false)
newStatus = false
}
if debugPMTUD() {
c.logf("magicsock: peermtu: peer MTU probes are %v", tstun.WireMTUsToProbe)
}
c.peerMTUEnabled.Store(newStatus)
c.resetEndpointStates()
}

View File

@@ -22,6 +22,7 @@ import (
"golang.org/x/sys/unix"
"golang.org/x/time/rate"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
"tailscale.com/net/netmon"
"tailscale.com/types/logger"
"tailscale.com/types/preftype"
@@ -36,6 +37,145 @@ const (
netfilterOn = preftype.NetfilterOn
)
// netfilterRunner abstracts helpers to run netfilter commands. It is
// implemented by linuxfw.IPTablesRunner and linuxfw.NfTablesRunner.
type netfilterRunner interface {
AddLoopbackRule(addr netip.Addr) error
DelLoopbackRule(addr netip.Addr) error
AddHooks() error
DelHooks(logf logger.Logf) error
AddChains() error
DelChains() error
AddBase(tunname string) error
DelBase() error
AddSNATRule() error
DelSNATRule() error
HasIPV6() bool
HasIPV6NAT() bool
}
// tableDetector abstracts helpers to detect the firewall mode.
// It is implemented for testing purposes.
type tableDetector interface {
iptDetect() (int, error)
nftDetect() (int, error)
}
type linuxFWDetector struct{}
// iptDetect returns the number of iptables rules in the current namespace.
func (l *linuxFWDetector) iptDetect() (int, error) {
return linuxfw.DetectIptables()
}
// nftDetect returns the number of nftables rules in the current namespace.
func (l *linuxFWDetector) nftDetect() (int, error) {
return linuxfw.DetectNetfilter()
}
// chooseFireWallMode returns the firewall mode to use based on the
// environment and the system's capabilities.
func chooseFireWallMode(logf logger.Logf, det tableDetector) linuxfw.FirewallMode {
if distro.Get() == distro.Gokrazy {
// Reduce startup logging on gokrazy. There's no way to do iptables on
// gokrazy anyway.
return linuxfw.FirewallModeNfTables
}
iptAva, nftAva := true, true
iptRuleCount, err := det.iptDetect()
if err != nil {
logf("detect iptables rule: %v", err)
iptAva = false
}
nftRuleCount, err := det.nftDetect()
if err != nil {
logf("detect nftables rule: %v", err)
nftAva = false
}
logf("nftables rule count: %d, iptables rule count: %d", nftRuleCount, iptRuleCount)
switch {
case nftRuleCount > 0 && iptRuleCount == 0:
logf("nftables is currently in use")
hostinfo.SetFirewallMode("nft-inuse")
return linuxfw.FirewallModeNfTables
case iptRuleCount > 0 && nftRuleCount == 0:
logf("iptables is currently in use")
hostinfo.SetFirewallMode("ipt-inuse")
return linuxfw.FirewallModeIPTables
case nftAva:
// if both iptables and nftables are available but
// neither/both are currently used, use nftables.
logf("nftables is available")
hostinfo.SetFirewallMode("nft")
return linuxfw.FirewallModeNfTables
case iptAva:
logf("iptables is available")
hostinfo.SetFirewallMode("ipt")
return linuxfw.FirewallModeIPTables
default:
// if neither iptables nor nftables are available, use iptablesRunner as a dummy
// runner which exists but won't do anything. Creating iptablesRunner errors only
// if the iptables command is missing or doesnt support "--version", as long as it
// can determine a version then itll carry on.
hostinfo.SetFirewallMode("ipt-fb")
return linuxfw.FirewallModeIPTables
}
}
// newNetfilterRunner creates a netfilterRunner using either nftables or iptables.
// As nftables is still experimental, iptables will be used unless TS_DEBUG_USE_NETLINK_NFTABLES is set.
func newNetfilterRunner(logf logger.Logf) (netfilterRunner, error) {
tableDetector := &linuxFWDetector{}
var mode linuxfw.FirewallMode
// We now use iptables as default and have "auto" and "nftables" as
// options for people to test further.
switch {
case distro.Get() == distro.Gokrazy:
// Reduce startup logging on gokrazy. There's no way to do iptables on
// gokrazy anyway.
logf("GoKrazy should use nftables.")
hostinfo.SetFirewallMode("nft-gokrazy")
mode = linuxfw.FirewallModeNfTables
case envknob.String("TS_DEBUG_FIREWALL_MODE") == "nftables":
logf("envknob TS_DEBUG_FIREWALL_MODE=nftables set")
hostinfo.SetFirewallMode("nft-forced")
mode = linuxfw.FirewallModeNfTables
case envknob.String("TS_DEBUG_FIREWALL_MODE") == "auto":
mode = chooseFireWallMode(logf, tableDetector)
case envknob.String("TS_DEBUG_FIREWALL_MODE") == "iptables":
logf("envknob TS_DEBUG_FIREWALL_MODE=iptables set")
hostinfo.SetFirewallMode("ipt-forced")
mode = linuxfw.FirewallModeIPTables
default:
logf("default choosing iptables")
hostinfo.SetFirewallMode("ipt-default")
mode = linuxfw.FirewallModeIPTables
}
var nfr netfilterRunner
var err error
switch mode {
case linuxfw.FirewallModeIPTables:
logf("using iptables")
nfr, err = linuxfw.NewIPTablesRunner(logf)
if err != nil {
return nil, err
}
case linuxfw.FirewallModeNfTables:
logf("using nftables")
nfr, err = linuxfw.NewNfTablesRunner(logf)
if err != nil {
return nil, err
}
default:
return nil, fmt.Errorf("unknown firewall mode: %v", mode)
}
return nfr, nil
}
type linuxRouter struct {
closed atomic.Bool
logf func(fmt string, args ...any)
@@ -60,7 +200,7 @@ type linuxRouter struct {
// ipPolicyPrefBase is the base priority at which ip rules are installed.
ipPolicyPrefBase int
nfr linuxfw.NetfilterRunner
nfr netfilterRunner
cmd commandRunner
}
@@ -70,7 +210,7 @@ func newUserspaceRouter(logf logger.Logf, tunDev tun.Device, netMon *netmon.Moni
return nil, err
}
nfr, err := linuxfw.New(logf)
nfr, err := newNetfilterRunner(logf)
if err != nil {
return nil, err
}
@@ -82,7 +222,7 @@ func newUserspaceRouter(logf logger.Logf, tunDev tun.Device, netMon *netmon.Moni
return newUserspaceRouterAdvanced(logf, tunname, netMon, nfr, cmd)
}
func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netMon *netmon.Monitor, nfr linuxfw.NetfilterRunner, cmd commandRunner) (Router, error) {
func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netMon *netmon.Monitor, nfr netfilterRunner, cmd commandRunner) (Router, error) {
r := &linuxRouter{
logf: logf,
tunname: tunname,

View File

@@ -372,7 +372,7 @@ type fakeIPTablesRunner struct {
//we always assume ipv6 and ipv6 nat are enabled when testing
}
func newIPTablesRunner(t *testing.T) linuxfw.NetfilterRunner {
func newIPTablesRunner(t *testing.T) netfilterRunner {
return &fakeIPTablesRunner{
t: t,
ipt4: map[string][]string{
@@ -603,7 +603,7 @@ type fakeOS struct {
rules []string
//This test tests on the router level, so we will not bother
//with using iptables or nftables, chose the simpler one.
nfr linuxfw.NetfilterRunner
nfr netfilterRunner
}
func NewFakeOS(t *testing.T) *fakeOS {
@@ -1063,3 +1063,63 @@ func adjustFwmask(t *testing.T, s string) string {
return fwmaskAdjustRe.ReplaceAllString(s, "$1")
}
type testFWDetector struct {
iptRuleCount, nftRuleCount int
iptErr, nftErr error
}
func (t *testFWDetector) iptDetect() (int, error) {
return t.iptRuleCount, t.iptErr
}
func (t *testFWDetector) nftDetect() (int, error) {
return t.nftRuleCount, t.nftErr
}
func TestChooseFireWallMode(t *testing.T) {
tests := []struct {
name string
det *testFWDetector
want linuxfw.FirewallMode
}{
{
name: "using iptables legacy",
det: &testFWDetector{iptRuleCount: 1},
want: linuxfw.FirewallModeIPTables,
},
{
name: "using nftables",
det: &testFWDetector{nftRuleCount: 1},
want: linuxfw.FirewallModeNfTables,
},
{
name: "using both iptables and nftables",
det: &testFWDetector{iptRuleCount: 2, nftRuleCount: 2},
want: linuxfw.FirewallModeNfTables,
},
{
name: "not using any firewall, both available",
det: &testFWDetector{},
want: linuxfw.FirewallModeNfTables,
},
{
name: "not using any firewall, iptables available only",
det: &testFWDetector{iptRuleCount: 1, nftErr: errors.New("nft error")},
want: linuxfw.FirewallModeIPTables,
},
{
name: "not using any firewall, nftables available only",
det: &testFWDetector{iptErr: errors.New("iptables error"), nftRuleCount: 1},
want: linuxfw.FirewallModeNfTables,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := chooseFireWallMode(t.Logf, tt.det)
if got != tt.want {
t.Errorf("chooseFireWallMode() = %v, want %v", got, tt.want)
}
})
}
}