Compare commits
33 Commits
bradfitz/s
...
dgentry-au
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80e67a8c4d | ||
|
|
9f05018419 | ||
|
|
04a8b8bb8e | ||
|
|
4e083e4548 | ||
|
|
78a083e144 | ||
|
|
05a1f5bf71 | ||
|
|
56c0a75ea9 | ||
|
|
ba6ec42f6d | ||
|
|
677d486830 | ||
|
|
7f08bddfe1 | ||
|
|
00977f6de9 | ||
|
|
0ccfcb515c | ||
|
|
3749a3bbbb | ||
|
|
6b1ed732df | ||
|
|
70de16bda7 | ||
|
|
7f540042d5 | ||
|
|
d0b8bdf8f7 | ||
|
|
9eedf86563 | ||
|
|
249edaa349 | ||
|
|
893bdd729c | ||
|
|
b4e587c3bd | ||
|
|
9593cd3871 | ||
|
|
623926a25d | ||
|
|
886917c42b | ||
|
|
553f657248 | ||
|
|
6f36f8842c | ||
|
|
13767e5108 | ||
|
|
f991c8a61f | ||
|
|
498f7ec663 | ||
|
|
e4cb83b18b | ||
|
|
e6aa7b815d | ||
|
|
b7988b3825 | ||
|
|
557ddced6c |
4
api.md
4
api.md
@@ -209,10 +209,6 @@ 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,
|
||||
|
||||
@@ -30,6 +30,7 @@ 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"
|
||||
@@ -77,6 +78,10 @@ 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.
|
||||
@@ -108,6 +113,12 @@ 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
|
||||
@@ -201,9 +212,13 @@ func Update(args Arguments) error {
|
||||
}
|
||||
|
||||
func (up *Updater) confirm(ver string) bool {
|
||||
if version.Short() == ver {
|
||||
switch cmpver.Compare(version.Short(), ver) {
|
||||
case 0:
|
||||
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)
|
||||
@@ -256,9 +271,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 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.
|
||||
// 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.
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
if dsmVersion == 6 && bytes.Contains(out, []byte("error = [290]")) {
|
||||
@@ -369,15 +384,15 @@ func (up *Updater) updateDebLike() error {
|
||||
// we're not updating them:
|
||||
"-o", "APT::Get::List-Cleanup=0",
|
||||
)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd = exec.Command("apt-get", "install", "--yes", "--allow-downgrades", "tailscale="+ver)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -491,8 +506,8 @@ func (up *Updater) updateFedoraLike(packageManager string) func() error {
|
||||
}
|
||||
|
||||
cmd := exec.Command(packageManager, "install", "--assumeyes", fmt.Sprintf("tailscale-%s-1", ver))
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -577,8 +592,8 @@ func (up *Updater) updateAlpineLike() (err error) {
|
||||
}
|
||||
|
||||
cmd := exec.Command("apk", "upgrade", "tailscale")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed tailscale update using apk: %w", err)
|
||||
}
|
||||
@@ -634,8 +649,8 @@ func (up *Updater) updateMacAppStore() error {
|
||||
}
|
||||
|
||||
cmd := exec.Command("sudo", "softwareupdate", "--install", newTailscale)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("can't install App Store update for Tailscale: %w", err)
|
||||
}
|
||||
@@ -726,8 +741,8 @@ func (up *Updater) updateWindows() error {
|
||||
|
||||
cmd := exec.Command(selfCopy, "update")
|
||||
cmd.Env = append(os.Environ(), winMSIEnv+"="+msiTarget)
|
||||
cmd.Stdout = os.Stderr
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = up.Stderr
|
||||
cmd.Stderr = up.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
@@ -743,8 +758,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 = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
err = cmd.Run()
|
||||
if err == nil {
|
||||
@@ -757,8 +772,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 = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
err = cmd.Run()
|
||||
up.Logf("msiexec uninstall: %v", err)
|
||||
@@ -846,8 +861,8 @@ func (up *Updater) updateFreeBSD() (err error) {
|
||||
}
|
||||
|
||||
cmd := exec.Command("pkg", "upgrade", "tailscale")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdout = up.Stdout
|
||||
cmd.Stderr = up.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed tailscale update using pkg: %w", err)
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
-----BEGIN ROOT PUBLIC KEY-----
|
||||
Muw5GkO5mASsJ7k6kS+svfuanr6XcW9I7fPGtyqOTeI=
|
||||
ZjjKhUHBtLNRSO1dhOTjrXJGJ8lDe1594WM2XDuheVQ=
|
||||
-----END ROOT PUBLIC KEY-----
|
||||
@@ -28,6 +28,7 @@ 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")
|
||||
@@ -65,7 +66,7 @@ func main() {
|
||||
},
|
||||
}
|
||||
|
||||
authkey, _, err := tsClient.CreateKey(ctx, caps)
|
||||
authkey, _, err := tsClient.CreateKeyWithExpiry(ctx, caps, *expiry)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ 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"))
|
||||
@@ -89,6 +90,7 @@ func main() {
|
||||
|
||||
var s server
|
||||
s.ts.Port = uint16(*wgPort)
|
||||
s.ts.Hostname = *hostname
|
||||
defer s.ts.Close()
|
||||
|
||||
lc, err := s.ts.LocalClient()
|
||||
|
||||
@@ -139,6 +139,11 @@ 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"),
|
||||
|
||||
@@ -49,6 +49,7 @@ type setArgsT struct {
|
||||
forceDaemon bool
|
||||
updateCheck bool
|
||||
updateApply bool
|
||||
postureChecking bool
|
||||
}
|
||||
|
||||
func newSetFlagSet(goos string, setArgs *setArgsT) *flag.FlagSet {
|
||||
@@ -66,6 +67,8 @@ 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")
|
||||
}
|
||||
@@ -108,6 +111,7 @@ func runSet(ctx context.Context, args []string) (retErr error) {
|
||||
Check: setArgs.updateCheck,
|
||||
Apply: setArgs.updateApply,
|
||||
},
|
||||
PostureChecking: setArgs.postureChecking,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -114,6 +114,7 @@ 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")
|
||||
}
|
||||
@@ -725,6 +726,7 @@ func init() {
|
||||
addPrefFlagMapping("nickname", "ProfileName")
|
||||
addPrefFlagMapping("update-check", "AutoUpdate")
|
||||
addPrefFlagMapping("auto-update", "AutoUpdate")
|
||||
addPrefFlagMapping("posture-checking", "PostureChecking")
|
||||
}
|
||||
|
||||
func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||
|
||||
@@ -63,7 +63,9 @@ func runUpdate(ctx context.Context, args []string) error {
|
||||
err := clientupdate.Update(clientupdate.Arguments{
|
||||
Version: ver,
|
||||
AppStore: updateArgs.appStore,
|
||||
Logf: func(format string, args ...any) { fmt.Printf(format+"\n", args...) },
|
||||
Logf: func(f string, a ...any) { printf(f+"\n", a...) },
|
||||
Stdout: Stdout,
|
||||
Stderr: Stderr,
|
||||
Confirm: confirmUpdate,
|
||||
})
|
||||
if errors.Is(err, errors.ErrUnsupported) {
|
||||
|
||||
@@ -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+
|
||||
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
|
||||
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+
|
||||
|
||||
@@ -86,6 +86,7 @@ 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
|
||||
@@ -292,6 +293,7 @@ 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+
|
||||
@@ -329,7 +331,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+
|
||||
LW tailscale.com/util/cmpver from tailscale.com/net/dns+
|
||||
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+
|
||||
@@ -354,7 +356,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+
|
||||
W tailscale.com/util/syspolicy from tailscale.com/cmd/tailscaled
|
||||
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+
|
||||
@@ -386,7 +388,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 golang.org/x/crypto/ssh+
|
||||
LD golang.org/x/crypto/ed25519 from github.com/tailscale/golang-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
|
||||
|
||||
@@ -7,9 +7,7 @@ package controlknobs
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -54,10 +52,6 @@ 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
|
||||
@@ -97,17 +91,6 @@ 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
|
||||
@@ -126,6 +109,5 @@ func (k *Knobs) AsDebugJSON() map[string]any {
|
||||
"DisableDeltaUpdates": k.DisableDeltaUpdates.Load(),
|
||||
"PeerMTUEnable": k.PeerMTUEnable.Load(),
|
||||
"DisableDNSForwarderTCPRetries": k.DisableDNSForwarderTCPRetries.Load(),
|
||||
"MagicsockSessionActiveTimeout": k.MagicsockSessionActiveTimeout.Load().String(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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", m.TxID[:6])
|
||||
return fmt.Sprintf("ping tx=%x padding=%v", m.TxID[:6], m.Padding)
|
||||
case *Pong:
|
||||
return fmt.Sprintf("pong tx=%x", m.TxID[:6])
|
||||
case *CallMeMaybe:
|
||||
|
||||
63
docs/sysv/tailscale.init
Executable file
63
docs/sysv/tailscale.init
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/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
|
||||
@@ -115,4 +115,4 @@
|
||||
in
|
||||
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
|
||||
}
|
||||
# nix-direnv cache busting line: sha256-tCc7+umCKgOmKXbElnCmDI4ntPvvHldkxi+RwQuj9ng=
|
||||
# nix-direnv cache busting line: sha256-v3/3bVAK/ni0LZ+GPY+dnbdCdvFQUknPxur7u9Cm8Gw=
|
||||
|
||||
9
go.mod
9
go.mod
@@ -20,6 +20,7 @@ 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
|
||||
@@ -76,14 +77,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.13.0
|
||||
golang.org/x/crypto v0.14.0
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9
|
||||
golang.org/x/mod v0.12.0
|
||||
golang.org/x/net v0.15.0
|
||||
golang.org/x/net v0.17.0
|
||||
golang.org/x/oauth2 v0.12.0
|
||||
golang.org/x/sync v0.3.0
|
||||
golang.org/x/sys v0.12.0
|
||||
golang.org/x/term v0.12.0
|
||||
golang.org/x/sys v0.13.0
|
||||
golang.org/x/term v0.13.0
|
||||
golang.org/x/time v0.3.0
|
||||
golang.org/x/tools v0.13.0
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||
|
||||
@@ -1 +1 @@
|
||||
sha256-tCc7+umCKgOmKXbElnCmDI4ntPvvHldkxi+RwQuj9ng=
|
||||
sha256-v3/3bVAK/ni0LZ+GPY+dnbdCdvFQUknPxur7u9Cm8Gw=
|
||||
|
||||
18
go.sum
18
go.sum
@@ -233,6 +233,8 @@ 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=
|
||||
@@ -984,8 +986,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.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
|
||||
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
|
||||
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=
|
||||
@@ -1080,8 +1082,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.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
|
||||
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
|
||||
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=
|
||||
@@ -1177,8 +1179,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.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||
golang.org/x/sys v0.12.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/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=
|
||||
@@ -1187,8 +1189,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.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
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/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=
|
||||
|
||||
@@ -1 +1 @@
|
||||
f242beecd311476f6e6b9fa3052e253e2301e170
|
||||
d1c91593484a1db2d4de2564f2ef2669814af9c8
|
||||
|
||||
@@ -52,6 +52,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
|
||||
OperatorUser string
|
||||
ProfileName string
|
||||
AutoUpdate AutoUpdatePrefs
|
||||
PostureChecking bool
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ 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.
|
||||
@@ -113,6 +114,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
|
||||
OperatorUser string
|
||||
ProfileName string
|
||||
AutoUpdate AutoUpdatePrefs
|
||||
PostureChecking bool
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
|
||||
@@ -24,10 +24,12 @@ 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"
|
||||
)
|
||||
|
||||
@@ -67,6 +69,14 @@ 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))
|
||||
@@ -215,6 +225,37 @@ 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
|
||||
|
||||
@@ -239,7 +239,6 @@ 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
|
||||
@@ -2157,6 +2156,12 @@ 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).
|
||||
//
|
||||
@@ -2213,10 +2218,7 @@ 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 = make([]ipn.PartialFile, 0)
|
||||
for f := range b.incomingFiles {
|
||||
n.IncomingFiles = append(n.IncomingFiles, f.PartialFile())
|
||||
}
|
||||
n.IncomingFiles = apiSrv.taildrop.IncomingFiles()
|
||||
b.mu.Unlock()
|
||||
|
||||
sort.Slice(n.IncomingFiles, func(i, j int) bool {
|
||||
@@ -3549,11 +3551,11 @@ func (b *LocalBackend) initPeerAPIListener() {
|
||||
ps := &peerAPIServer{
|
||||
b: b,
|
||||
taildrop: &taildrop.Handler{
|
||||
Logf: b.logf,
|
||||
Clock: b.clock,
|
||||
RootDir: fileRoot,
|
||||
DirectFileMode: b.directFileRoot != "",
|
||||
DirectFileDoFinalRename: b.directFileDoFinalRename,
|
||||
Logf: b.logf,
|
||||
Clock: b.clock,
|
||||
Dir: fileRoot,
|
||||
DirectFileMode: b.directFileRoot != "",
|
||||
AvoidFinalRename: !b.directFileDoFinalRename,
|
||||
},
|
||||
}
|
||||
if dm, ok := b.sys.DNSManager.GetOK(); ok {
|
||||
@@ -4590,19 +4592,6 @@ 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() {
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
"slices"
|
||||
@@ -39,10 +38,8 @@ 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"
|
||||
)
|
||||
|
||||
@@ -586,64 +583,6 @@ 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() {
|
||||
@@ -687,10 +626,6 @@ 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
|
||||
@@ -699,117 +634,12 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "file sharing not enabled by Tailscale admin", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -5,7 +5,6 @@ package ipnlocal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
@@ -15,7 +14,6 @@ import (
|
||||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -68,7 +66,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.RootDir
|
||||
root := e.ph.ps.taildrop.Dir
|
||||
if root == "" {
|
||||
t.Errorf("no rootdir; can't check whether %q has size %v", name, size)
|
||||
return
|
||||
@@ -84,7 +82,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.RootDir
|
||||
root := e.ph.ps.taildrop.Dir
|
||||
if root == "" {
|
||||
t.Errorf("no rootdir; can't check contents of %q", name)
|
||||
return
|
||||
@@ -494,9 +492,12 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
if !tt.omitRoot {
|
||||
rootDir = t.TempDir()
|
||||
if e.ph.ps.taildrop == nil {
|
||||
e.ph.ps.taildrop = &taildrop.Handler{}
|
||||
e.ph.ps.taildrop = &taildrop.Handler{
|
||||
Logf: e.logBuf.Logf,
|
||||
Clock: &tstest.Clock{},
|
||||
}
|
||||
}
|
||||
e.ph.ps.taildrop.RootDir = rootDir
|
||||
e.ph.ps.taildrop.Dir = rootDir
|
||||
}
|
||||
for _, req := range tt.reqs {
|
||||
e.rr = httptest.NewRecorder()
|
||||
@@ -536,9 +537,9 @@ func TestFileDeleteRace(t *testing.T) {
|
||||
clock: &tstest.Clock{},
|
||||
},
|
||||
taildrop: &taildrop.Handler{
|
||||
Logf: t.Logf,
|
||||
Clock: &tstest.Clock{},
|
||||
RootDir: dir,
|
||||
Logf: t.Logf,
|
||||
Clock: &tstest.Clock{},
|
||||
Dir: dir,
|
||||
},
|
||||
}
|
||||
ph := &peerAPIHandler{
|
||||
@@ -579,92 +580,6 @@ 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
|
||||
|
||||
@@ -719,67 +634,3 @@ 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)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,18 +408,32 @@ 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 != "" {
|
||||
var err error
|
||||
ipp, err = netip.ParseAddrPort(v)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid 'addr' parameter", 400)
|
||||
return
|
||||
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
|
||||
}
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "missing 'addr' parameter", 400)
|
||||
@@ -433,7 +447,9 @@ func (h *Handler) serveWhoIs(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
|
||||
CapMap: b.PeerCaps(ipp.Addr()),
|
||||
}
|
||||
if n.Addresses().Len() > 0 {
|
||||
res.CapMap = b.PeerCaps(n.Addresses().At(0).Addr())
|
||||
}
|
||||
j, err := json.MarshalIndent(res, "", "\t")
|
||||
if err != nil {
|
||||
@@ -566,6 +582,8 @@ 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:
|
||||
|
||||
@@ -9,11 +9,15 @@ 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"
|
||||
)
|
||||
|
||||
@@ -77,3 +81,68 @@ 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,6 +200,10 @@ 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.
|
||||
@@ -246,6 +250,7 @@ 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
|
||||
@@ -439,7 +444,8 @@ 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.AutoUpdate == p2.AutoUpdate &&
|
||||
p.PostureChecking == p2.PostureChecking
|
||||
}
|
||||
|
||||
func (au AutoUpdatePrefs) Pretty() string {
|
||||
|
||||
@@ -57,6 +57,7 @@ func TestPrefsEqual(t *testing.T) {
|
||||
"OperatorUser",
|
||||
"ProfileName",
|
||||
"AutoUpdate",
|
||||
"PostureChecking",
|
||||
"Persist",
|
||||
}
|
||||
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
|
||||
@@ -304,6 +305,16 @@ 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)
|
||||
|
||||
@@ -37,6 +37,7 @@ 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))
|
||||
@@ -85,13 +86,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.13.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.14.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.15.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.17.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.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/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/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))
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
@@ -13,13 +14,17 @@ func resolvconfStyle() string {
|
||||
if _, err := exec.LookPath("resolvconf"); err != nil {
|
||||
return ""
|
||||
}
|
||||
if _, err := exec.Command("resolvconf", "--version").CombinedOutput(); err != nil {
|
||||
output, err := exec.Command("resolvconf", "--version").CombinedOutput()
|
||||
if 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"
|
||||
}
|
||||
|
||||
@@ -79,14 +79,16 @@ const (
|
||||
safeTUNMTU TUNMTU = 1280
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
// 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
|
||||
}
|
||||
|
||||
// wgHeaderLen is the length of all the headers Wireguard adds to a packet
|
||||
@@ -125,7 +127,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 {
|
||||
@@ -135,12 +137,23 @@ func DefaultTUNMTU() TUNMTU {
|
||||
|
||||
debugPMTUD, _ := envknob.LookupBool("TS_DEBUG_ENABLE_PMTUD")
|
||||
if debugPMTUD {
|
||||
return WireToTUNMTU(MaxProbedWireMTU)
|
||||
// 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 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 {
|
||||
|
||||
@@ -39,15 +39,18 @@ 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 largest probed
|
||||
// MTU, but only if the user hasn't requested a specific MTU.
|
||||
// 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.
|
||||
os.Setenv("TS_DEBUG_MTU", "")
|
||||
os.Setenv("TS_DEBUG_ENABLE_PMTUD", "true")
|
||||
if DefaultTUNMTU() != WireToTUNMTU(MaxProbedWireMTU) {
|
||||
t.Errorf("default TUN MTU = %d, want %d", DefaultTUNMTU(), WireToTUNMTU(MaxProbedWireMTU))
|
||||
if DefaultTUNMTU() != safeTUNMTU {
|
||||
t.Errorf("default TUN MTU = %d, want %d", DefaultTUNMTU(), safeTUNMTU)
|
||||
}
|
||||
// TS_DEBUG_MTU should take precedence over TS_DEBUG_ENABLE_PMTUD.
|
||||
mtu = WireToTUNMTU(MaxProbedWireMTU - 1)
|
||||
mtu = WireToTUNMTU(MaxPacketSize - 1)
|
||||
os.Setenv("TS_DEBUG_MTU", strconv.Itoa(int(mtu)))
|
||||
if DefaultTUNMTU() != mtu {
|
||||
t.Errorf("default TUN MTU = %d, want %d", DefaultTUNMTU(), mtu)
|
||||
|
||||
@@ -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.IPProtoVersion) *natFamilyConfig {
|
||||
func natConfigFromWGConfig(wcfg *wgcfg.Config, addrFam ipproto.Version) *natFamilyConfig {
|
||||
if wcfg == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var nativeAddr netip.Addr
|
||||
switch addrFam {
|
||||
case ipproto.IPProtoVersion4:
|
||||
case ipproto.Version4:
|
||||
nativeAddr = findV4(wcfg.Addresses)
|
||||
case ipproto.IPProtoVersion6:
|
||||
case ipproto.Version6:
|
||||
nativeAddr = findV6(wcfg.Addresses)
|
||||
}
|
||||
if !nativeAddr.IsValid() {
|
||||
@@ -703,8 +703,8 @@ func natConfigFromWGConfig(wcfg *wgcfg.Config, addrFam ipproto.IPProtoVersion) *
|
||||
isExitNode := slices.Contains(p.AllowedIPs, tsaddr.AllIPv4()) || slices.Contains(p.AllowedIPs, tsaddr.AllIPv6())
|
||||
if isExitNode {
|
||||
hasMasqAddrsForFamily := false ||
|
||||
(addrFam == ipproto.IPProtoVersion4 && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid()) ||
|
||||
(addrFam == ipproto.IPProtoVersion6 && p.V6MasqAddr != nil && p.V6MasqAddr.IsValid())
|
||||
(addrFam == ipproto.Version4 && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid()) ||
|
||||
(addrFam == ipproto.Version6 && p.V6MasqAddr != nil && p.V6MasqAddr.IsValid())
|
||||
if hasMasqAddrsForFamily {
|
||||
exitNodeRequiresMasq = true
|
||||
}
|
||||
@@ -714,10 +714,10 @@ func natConfigFromWGConfig(wcfg *wgcfg.Config, addrFam ipproto.IPProtoVersion) *
|
||||
for i := range wcfg.Peers {
|
||||
p := &wcfg.Peers[i]
|
||||
var addrToUse netip.Addr
|
||||
if addrFam == ipproto.IPProtoVersion4 && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid() {
|
||||
if addrFam == ipproto.Version4 && p.V4MasqAddr != nil && p.V4MasqAddr.IsValid() {
|
||||
addrToUse = *p.V4MasqAddr
|
||||
mak.Set(&listenAddrs, addrToUse, struct{}{})
|
||||
} else if addrFam == ipproto.IPProtoVersion6 && p.V6MasqAddr != nil && p.V6MasqAddr.IsValid() {
|
||||
} else if addrFam == ipproto.Version6 && 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.IPProtoVersion) *
|
||||
|
||||
// SetNetMap is called when a new NetworkMap is received.
|
||||
func (t *Wrapper) SetWGConfig(wcfg *wgcfg.Config) {
|
||||
v4, v6 := natConfigFromWGConfig(wcfg, ipproto.IPProtoVersion4), natConfigFromWGConfig(wcfg, ipproto.IPProtoVersion6)
|
||||
v4, v6 := natConfigFromWGConfig(wcfg, ipproto.Version4), natConfigFromWGConfig(wcfg, ipproto.Version6)
|
||||
var cfg *natConfig
|
||||
if v4 != nil || v6 != nil {
|
||||
cfg = &natConfig{v4: v4, v6: v6}
|
||||
|
||||
@@ -617,7 +617,7 @@ func TestNATCfg(t *testing.T) {
|
||||
p.AllowedIPs = append(p.AllowedIPs, otherAllowedIPs...)
|
||||
return p
|
||||
}
|
||||
test := func(addrFam ipproto.IPProtoVersion) {
|
||||
test := func(addrFam ipproto.Version) {
|
||||
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.IPProtoVersion6 {
|
||||
if addrFam == ipproto.Version6 {
|
||||
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.IPProtoVersion4)
|
||||
test(ipproto.IPProtoVersion6)
|
||||
test(ipproto.Version4)
|
||||
test(ipproto.Version6)
|
||||
}
|
||||
|
||||
// TestCaptureHook verifies that the Wrapper.captureHook callback is called
|
||||
|
||||
74
posture/serialnumber_macos.go
Normal file
74
posture/serialnumber_macos.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// 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
|
||||
}
|
||||
37
posture/serialnumber_macos_test.go
Normal file
37
posture/serialnumber_macos_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// 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)
|
||||
}
|
||||
143
posture/serialnumber_notmacos.go
Normal file
143
posture/serialnumber_notmacos.go
Normal file
@@ -0,0 +1,143 @@
|
||||
// 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
|
||||
}
|
||||
38
posture/serialnumber_notmacos_test.go
Normal file
38
posture/serialnumber_notmacos_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// 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)
|
||||
}
|
||||
24
posture/serialnumber_stub.go
Normal file
24
posture/serialnumber_stub.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// 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")
|
||||
}
|
||||
16
posture/serialnumber_test.go
Normal file
16
posture/serialnumber_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// 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)
|
||||
}
|
||||
@@ -16,4 +16,4 @@
|
||||
) {
|
||||
src = ./.;
|
||||
}).shellNix
|
||||
# nix-direnv cache busting line: sha256-tCc7+umCKgOmKXbElnCmDI4ntPvvHldkxi+RwQuj9ng=
|
||||
# nix-direnv cache busting line: sha256-v3/3bVAK/ni0LZ+GPY+dnbdCdvFQUknPxur7u9Cm8Gw=
|
||||
|
||||
@@ -192,6 +192,26 @@ 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()
|
||||
|
||||
@@ -91,8 +91,11 @@ 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}
|
||||
want := map[string]int{"one": 1, "two": 2, "three": 3}
|
||||
m.Range(func(k string, v int) bool {
|
||||
got[k] = v
|
||||
return true
|
||||
@@ -106,6 +109,7 @@ 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{}
|
||||
|
||||
@@ -52,3 +52,15 @@ 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"`
|
||||
}
|
||||
|
||||
@@ -2123,11 +2123,6 @@ 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.
|
||||
@@ -2441,6 +2436,22 @@ 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
|
||||
|
||||
@@ -19,13 +19,13 @@ import (
|
||||
"tailscale.com/logtail/backoff"
|
||||
)
|
||||
|
||||
// HasFilesWaiting reports whether any files are buffered in the
|
||||
// tailscaled daemon storage.
|
||||
// HasFilesWaiting reports whether any files are buffered in [Handler.Dir].
|
||||
// This always returns false when [Handler.DirectFileMode] is false.
|
||||
func (s *Handler) HasFilesWaiting() bool {
|
||||
if s == nil || s.RootDir == "" || s.DirectFileMode {
|
||||
if s == nil || s.Dir == "" || 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.RootDir)
|
||||
f, err := os.Open(s.Dir)
|
||||
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.RootDir, name))
|
||||
defer tryDeleteAgain(filepath.Join(s.Dir, name))
|
||||
continue
|
||||
}
|
||||
if de.Type().IsRegular() {
|
||||
_, err := os.Stat(filepath.Join(s.RootDir, name+deletedSuffix))
|
||||
_, err := os.Stat(filepath.Join(s.Dir, name+deletedSuffix))
|
||||
if os.IsNotExist(err) {
|
||||
return true
|
||||
}
|
||||
if err == nil {
|
||||
tryDeleteAgain(filepath.Join(s.RootDir, name))
|
||||
tryDeleteAgain(filepath.Join(s.Dir, name))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
s.KnownEmpty.Store(true)
|
||||
s.knownEmpty.Store(true)
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
@@ -76,22 +76,19 @@ func (s *Handler) HasFilesWaiting() bool {
|
||||
}
|
||||
|
||||
// WaitingFiles returns the list of files that have been sent by a
|
||||
// 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.
|
||||
// peer that are waiting in [Handler.Dir].
|
||||
// This always returns nil when [Handler.DirectFileMode] is false.
|
||||
func (s *Handler) WaitingFiles() (ret []apitype.WaitingFile, err error) {
|
||||
if s == nil {
|
||||
return nil, errNilHandler
|
||||
}
|
||||
if s.RootDir == "" {
|
||||
return nil, ErrNoTaildrop
|
||||
if s.Dir == "" {
|
||||
return nil, errNoTaildrop
|
||||
}
|
||||
if s.DirectFileMode {
|
||||
return nil, nil
|
||||
}
|
||||
f, err := os.Open(s.RootDir)
|
||||
f, err := os.Open(s.Dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -101,7 +98,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
|
||||
@@ -143,7 +140,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.RootDir, name))
|
||||
tryDeleteAgain(filepath.Join(s.Dir, name))
|
||||
}
|
||||
}
|
||||
sort.Slice(ret, func(i, j int) bool { return ret[i].Name < ret[j].Name })
|
||||
@@ -164,17 +161,19 @@ 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.RootDir == "" {
|
||||
return ErrNoTaildrop
|
||||
if s.Dir == "" {
|
||||
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")
|
||||
}
|
||||
@@ -184,7 +183,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
|
||||
@@ -203,7 +202,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)
|
||||
}
|
||||
}
|
||||
@@ -214,25 +213,27 @@ 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.RootDir == "" {
|
||||
return nil, 0, ErrNoTaildrop
|
||||
if s.Dir == "" {
|
||||
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")
|
||||
}
|
||||
@@ -242,12 +243,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
|
||||
}
|
||||
184
taildrop/send.go
Normal file
184
taildrop/send.go
Normal file
@@ -0,0 +1,184 @@
|
||||
// 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
|
||||
}
|
||||
@@ -1,6 +1,12 @@
|
||||
// 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 (
|
||||
@@ -15,44 +21,61 @@ 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
|
||||
|
||||
RootDir string // empty means file receiving unavailable
|
||||
// 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
|
||||
|
||||
// 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 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 bool
|
||||
|
||||
// DirectFileDoFinalRename is whether in directFileMode we
|
||||
// additionally move the *.direct file to its final name after
|
||||
// it's received.
|
||||
DirectFileDoFinalRename bool
|
||||
// AvoidFinalRename specifies whether in DirectFileMode
|
||||
// we should avoid renaming "foo.jpg.partial" to "foo.jpg" after reception.
|
||||
AvoidFinalRename bool
|
||||
|
||||
KnownEmpty atomic.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{}]
|
||||
}
|
||||
|
||||
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
|
||||
@@ -84,7 +107,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
|
||||
}
|
||||
@@ -99,7 +122,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 {
|
||||
@@ -110,7 +133,28 @@ func (s *Handler) DiskPath(baseName string) (fullPath string, ok bool) {
|
||||
if !filepath.IsLocal(baseName) {
|
||||
return "", false
|
||||
}
|
||||
return filepath.Join(s.RootDir, baseName), true
|
||||
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
|
||||
}
|
||||
|
||||
type redactedErr struct {
|
||||
@@ -136,7 +180,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,
|
||||
|
||||
155
taildrop/taildrop_test.go
Normal file
155
taildrop/taildrop_test.go
Normal file
@@ -0,0 +1,155 @@
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,10 @@ 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=$(head -1 "$toolchain/VERSION" | cut -f2 -d'.')
|
||||
have_go_minor=""
|
||||
if [[ -f "$toolchain/VERSION" ]]; then
|
||||
have_go_minor=$(head -1 "$toolchain/VERSION" | cut -f2 -d'.')
|
||||
fi
|
||||
# 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
|
||||
|
||||
@@ -13,8 +13,19 @@ 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() {
|
||||
|
||||
@@ -6,23 +6,23 @@ package ipproto
|
||||
|
||||
import "fmt"
|
||||
|
||||
// IPProtoVersion describes the IP address version.
|
||||
type IPProtoVersion uint8
|
||||
// Version describes the IP address version.
|
||||
type Version uint8
|
||||
|
||||
// Valid IPProtoVersion values.
|
||||
// Valid Version values.
|
||||
const (
|
||||
IPProtoVersion4 = 4
|
||||
IPProtoVersion6 = 6
|
||||
Version4 = 4
|
||||
Version6 = 6
|
||||
)
|
||||
|
||||
func (p IPProtoVersion) String() string {
|
||||
func (p Version) String() string {
|
||||
switch p {
|
||||
case IPProtoVersion4:
|
||||
case Version4:
|
||||
return "IPv4"
|
||||
case IPProtoVersion6:
|
||||
case Version6:
|
||||
return "IPv6"
|
||||
default:
|
||||
return fmt.Sprintf("IPProtoVersion-%d", int(p))
|
||||
return fmt.Sprintf("Version-%d", int(p))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,15 +22,20 @@ 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
|
||||
@@ -38,16 +43,16 @@ func Compare(v1, v2 string) int {
|
||||
)
|
||||
for v1 != "" || v2 != "" {
|
||||
// Compare the non-numeric character run lexicographically.
|
||||
f1, v1 = splitPrefixFunc(v1, notNumber)
|
||||
f2, v2 = splitPrefixFunc(v2, notNumber)
|
||||
f1, v1 = splitPrefixFunc(v1, notnum)
|
||||
f2, v2 = splitPrefixFunc(v2, notnum)
|
||||
|
||||
if res := strings.Compare(f1, f2); res != 0 {
|
||||
return res
|
||||
}
|
||||
|
||||
// Compare the numeric character run numerically.
|
||||
f1, v1 = splitPrefixFunc(v1, unicode.IsNumber)
|
||||
f2, v2 = splitPrefixFunc(v2, unicode.IsNumber)
|
||||
f1, v1 = splitPrefixFunc(v1, isnum)
|
||||
f2, v2 = splitPrefixFunc(v2, isnum)
|
||||
|
||||
// ParseUint refuses to parse empty strings, which would only
|
||||
// happen if we reached end-of-string. We follow the Debian
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cmpver
|
||||
package cmpver_test
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"tailscale.com/util/cmpver"
|
||||
)
|
||||
|
||||
func TestCompare(t *testing.T) {
|
||||
tests := []struct {
|
||||
@@ -87,6 +91,16 @@ 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.
|
||||
{
|
||||
@@ -147,17 +161,17 @@ func TestCompare(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got := Compare(test.v1, test.v2)
|
||||
got := cmpver.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 := Compare(test.v2, test.v1)
|
||||
got2 := cmpver.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() { Compare(test.v1, test.v2) }); n > 0 {
|
||||
if n := testing.AllocsPerRun(100, func() { cmpver.Compare(test.v1, test.v2) }); n > 0 {
|
||||
t.Errorf("Compare(%v, %v) got %v allocs per run", test.v1, test.v2, n)
|
||||
}
|
||||
})
|
||||
|
||||
110
util/linuxfw/detector.go
Normal file
110
util/linuxfw/detector.go
Normal file
@@ -0,0 +1,110 @@
|
||||
// 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 doesn’t support "--version", as long as it
|
||||
// can determine a version then it’ll carry on.
|
||||
hostinfo.SetFirewallMode("ipt-fb")
|
||||
return FirewallModeIPTables
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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 returns true if the system supports IPv6.
|
||||
// HasIPV6 reports true if the system supports IPv6.
|
||||
func (i *iptablesRunner) HasIPV6() bool {
|
||||
return i.v6Available
|
||||
}
|
||||
|
||||
// HasIPV6NAT returns true if the system supports IPv6 NAT.
|
||||
// HasIPV6NAT reports true if the system supports IPv6 NAT.
|
||||
func (i *iptablesRunner) HasIPV6NAT() bool {
|
||||
return i.v6NATAvailable
|
||||
}
|
||||
@@ -254,6 +254,12 @@ 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
|
||||
@@ -291,7 +297,13 @@ func (i *iptablesRunner) addBase6(tunname string) error {
|
||||
// TODO: only allow traffic from Tailscale's ULA range to come
|
||||
// from tailscale0.
|
||||
|
||||
args := []string{"-i", tunname, "-j", "MARK", "--set-mark", TailscaleSubnetRouteMark + "/" + TailscaleFwmarkMask}
|
||||
// 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}
|
||||
if err := i.ipt6.Append("filter", "ts-forward", args...); err != nil {
|
||||
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
|
||||
}
|
||||
|
||||
@@ -261,6 +261,7 @@ 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"}},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -175,9 +175,67 @@ func createChainIfNotExist(c *nftables.Conn, cinfo chainInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewNfTablesRunner creates a new nftablesRunner without guaranteeing
|
||||
// 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
|
||||
// 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)
|
||||
@@ -231,7 +289,7 @@ func newLoadSaddrExpr(proto nftables.TableFamily, destReg uint32) (expr.Any, err
|
||||
}
|
||||
}
|
||||
|
||||
// HasIPV6 returns true if the system supports IPv6.
|
||||
// HasIPV6 reports true if the system supports IPv6.
|
||||
func (n *nftablesRunner) HasIPV6() bool {
|
||||
return n.v6Available
|
||||
}
|
||||
@@ -877,6 +935,38 @@ 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 {
|
||||
@@ -904,6 +994,9 @@ 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 {
|
||||
@@ -937,6 +1030,14 @@ 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)
|
||||
|
||||
@@ -7,6 +7,7 @@ package linuxfw
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"os"
|
||||
@@ -375,6 +376,38 @@ 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{
|
||||
@@ -914,3 +947,63 @@ 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,4 +32,8 @@ 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"
|
||||
)
|
||||
|
||||
@@ -35,6 +35,16 @@ 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
|
||||
@@ -83,13 +93,6 @@ 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 {
|
||||
@@ -111,8 +114,6 @@ 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.
|
||||
@@ -178,7 +179,7 @@ func (st *endpointState) shouldDeleteLocked() bool {
|
||||
return st.index == indexSentinelDeleted
|
||||
default:
|
||||
// This was an endpoint discovered at runtime.
|
||||
return time.Since(st.lastGotPing) > st.ep.sessionActiveTimeout()
|
||||
return time.Since(st.lastGotPing) > sessionActiveTimeout
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,7 +384,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())
|
||||
requestedMTU := pingSizeToPktLen(size, udpAddr.Addr().Is6())
|
||||
mtuOk := requestedMTU <= pathMTU
|
||||
|
||||
if udpAddr.IsValid() && mtuOk {
|
||||
@@ -420,7 +421,7 @@ func (de *endpoint) heartbeat() {
|
||||
return
|
||||
}
|
||||
|
||||
if mono.Since(de.lastSend) > de.c.sessionActiveTimeout() {
|
||||
if mono.Since(de.lastSend) > 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
|
||||
@@ -632,6 +633,12 @@ 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())))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,21 +682,41 @@ func (de *endpoint) startDiscoPingLocked(ep netip.AddrPort, now mono.Time, purpo
|
||||
st.lastPing = now
|
||||
}
|
||||
|
||||
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,
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logLevel := discoLog
|
||||
if purpose == pingHeartbeat {
|
||||
logLevel = discoVerboseLog
|
||||
}
|
||||
go de.sendDiscoPing(ep, epDisco.key, txid, size, logLevel)
|
||||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// sendDiscoPingsLocked starts pinging all of ep's endpoints.
|
||||
@@ -885,7 +912,7 @@ func (de *endpoint) setEndpointsLocked(eps interface {
|
||||
if st, ok := de.endpointState[ipp]; ok {
|
||||
st.index = int16(i)
|
||||
} else {
|
||||
de.endpointState[ipp] = &endpointState{ep: de, index: int16(i)}
|
||||
de.endpointState[ipp] = &endpointState{index: int16(i)}
|
||||
newIpps = append(newIpps, ipp)
|
||||
}
|
||||
}
|
||||
@@ -933,7 +960,6 @@ 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,
|
||||
}
|
||||
@@ -992,13 +1018,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 default MTU.
|
||||
func pingSizeToPktLen(size int, addr netip.Addr) tstun.WireMTU {
|
||||
// disco headers. If size is zero, assume it is the safe wire MTU.
|
||||
func pingSizeToPktLen(size int, is6 bool) tstun.WireMTU {
|
||||
if size == 0 {
|
||||
return tstun.DefaultWireMTU()
|
||||
return tstun.SafeWireMTU()
|
||||
}
|
||||
headerLen := ipv4.HeaderLen
|
||||
if addr.Is6() {
|
||||
if is6 {
|
||||
headerLen = ipv6.HeaderLen
|
||||
}
|
||||
headerLen += 8 // UDP header length
|
||||
@@ -1009,12 +1035,12 @@ func pingSizeToPktLen(size int, addr netip.Addr) 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, addr netip.Addr) int {
|
||||
func pktLenToPingSize(mtu tstun.WireMTU, is6 bool) int {
|
||||
if mtu == 0 {
|
||||
return 0
|
||||
}
|
||||
headerLen := ipv4.HeaderLen
|
||||
if addr.Is6() {
|
||||
if is6 {
|
||||
headerLen = ipv6.HeaderLen
|
||||
}
|
||||
headerLen += 8 // UDP header length
|
||||
@@ -1042,6 +1068,15 @@ 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)
|
||||
|
||||
@@ -1063,7 +1098,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), pingSizeToPktLen(sp.size, sp.to.Addr()), 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), pktLen, m.Src, logger.ArgWriter(func(bw *bufio.Writer) {
|
||||
if sp.to != src {
|
||||
fmt.Fprintf(bw, " ping.to=%v", sp.to)
|
||||
}
|
||||
@@ -1081,9 +1116,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, pingSizeToPktLen(sp.size, sp.to.Addr())}
|
||||
thisPong := addrQuality{sp.to, latency, tstun.WireMTU(pingSizeToPktLen(sp.size, sp.to.Addr().Is6()))}
|
||||
if betterAddr(thisPong, de.bestAddr) {
|
||||
de.c.logf("magicsock: disco: node %v %v now using %v mtu %v", de.publicKey.ShortString(), de.discoShort(), sp.to, thisPong.wireMTU)
|
||||
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.debugUpdates.Add(EndpointChange{
|
||||
When: time.Now(),
|
||||
What: "handlePingLocked-bestAddr-update",
|
||||
@@ -1271,7 +1306,7 @@ func (de *endpoint) populatePeerStatus(ps *ipnstate.PeerStatus) {
|
||||
|
||||
now := mono.Now()
|
||||
ps.LastWrite = de.lastSend.WallTime()
|
||||
ps.Active = now.Sub(de.lastSend) < de.c.sessionActiveTimeout()
|
||||
ps.Active = now.Sub(de.lastSend) < sessionActiveTimeout
|
||||
|
||||
if udpAddr, derpAddr, _ := de.addrForSendLocked(now); udpAddr.IsValid() && !derpAddr.IsValid() {
|
||||
ps.CurAddr = udpAddr.String()
|
||||
|
||||
@@ -42,6 +42,7 @@ 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"
|
||||
@@ -1567,7 +1568,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", c.discoShort, di.discoShort, pingNodeSrcStr, src, dm.TxID[:6])
|
||||
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)
|
||||
}
|
||||
|
||||
ipDst := src
|
||||
@@ -2185,7 +2186,7 @@ func (c *Conn) shouldDoPeriodicReSTUNLocked() bool {
|
||||
if debugReSTUNStopOnIdle() {
|
||||
c.logf("magicsock: periodicReSTUN: idle for %v", idleFor.Round(time.Second))
|
||||
}
|
||||
if idleFor > c.sessionActiveTimeout() {
|
||||
if idleFor > sessionActiveTimeout {
|
||||
if c.controlKnobs != nil && c.controlKnobs.ForceBackgroundSTUN.Load() {
|
||||
// Overridden by control.
|
||||
return true
|
||||
@@ -2657,11 +2658,11 @@ func (c *Conn) SetStatistics(stats *connstats.Statistics) {
|
||||
}
|
||||
|
||||
const (
|
||||
// sessionActiveTimeoutDefault is how long since the last activity we
|
||||
// sessionActiveTimeout 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.
|
||||
sessionActiveTimeoutDefault = 45 * time.Second
|
||||
sessionActiveTimeout = 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.
|
||||
@@ -2715,6 +2716,33 @@ 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) {
|
||||
@@ -2729,15 +2757,6 @@ 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-") }
|
||||
|
||||
@@ -2807,16 +2826,18 @@ 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")
|
||||
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")
|
||||
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")
|
||||
|
||||
metricRecvDiscoUDP = clientmetric.NewCounter("magicsock_disco_recv_udp")
|
||||
metricRecvDiscoDERP = clientmetric.NewCounter("magicsock_disco_recv_derp")
|
||||
@@ -2834,4 +2855,18 @@ 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
|
||||
}
|
||||
|
||||
@@ -705,6 +705,8 @@ 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"}
|
||||
@@ -900,7 +902,6 @@ 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)
|
||||
@@ -2851,7 +2852,7 @@ func TestAddrForSendLockedForWireGuardOnly(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, epd := range test.ep {
|
||||
endpoint.endpointState[epd.addrPort] = &endpointState{ep: endpoint}
|
||||
endpoint.endpointState[epd.addrPort] = &endpointState{}
|
||||
}
|
||||
udpAddr, _, shouldPing := endpoint.addrForSendLocked(testTime)
|
||||
if udpAddr.IsValid() != test.validAddr {
|
||||
@@ -2935,7 +2936,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()),
|
||||
size: pktLenToPingSize(1501, validUdpAddr.Addr().Is6()),
|
||||
mtu: 1500,
|
||||
bestAddr: true,
|
||||
bestAddrTrusted: true,
|
||||
@@ -2944,7 +2945,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()),
|
||||
size: pktLenToPingSize(1501, validUdpAddr.Addr().Is6()),
|
||||
mtu: 1500,
|
||||
bestAddr: true,
|
||||
bestAddrTrusted: false,
|
||||
@@ -2953,7 +2954,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()),
|
||||
size: pktLenToPingSize(1500, validUdpAddr.Addr().Is6()),
|
||||
mtu: 1500,
|
||||
bestAddr: true,
|
||||
bestAddrTrusted: true,
|
||||
@@ -2962,7 +2963,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()),
|
||||
size: pktLenToPingSize(1500, validUdpAddr.Addr().Is6()),
|
||||
mtu: 1500,
|
||||
bestAddr: true,
|
||||
bestAddrTrusted: false,
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
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
|
||||
@@ -102,6 +104,9 @@ 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()
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ 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"
|
||||
@@ -37,145 +36,6 @@ 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 doesn’t support "--version", as long as it
|
||||
// can determine a version then it’ll 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)
|
||||
@@ -200,7 +60,7 @@ type linuxRouter struct {
|
||||
// ipPolicyPrefBase is the base priority at which ip rules are installed.
|
||||
ipPolicyPrefBase int
|
||||
|
||||
nfr netfilterRunner
|
||||
nfr linuxfw.NetfilterRunner
|
||||
cmd commandRunner
|
||||
}
|
||||
|
||||
@@ -210,7 +70,7 @@ func newUserspaceRouter(logf logger.Logf, tunDev tun.Device, netMon *netmon.Moni
|
||||
return nil, err
|
||||
}
|
||||
|
||||
nfr, err := newNetfilterRunner(logf)
|
||||
nfr, err := linuxfw.New(logf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -222,7 +82,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 netfilterRunner, cmd commandRunner) (Router, error) {
|
||||
func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netMon *netmon.Monitor, nfr linuxfw.NetfilterRunner, cmd commandRunner) (Router, error) {
|
||||
r := &linuxRouter{
|
||||
logf: logf,
|
||||
tunname: tunname,
|
||||
|
||||
@@ -372,7 +372,7 @@ type fakeIPTablesRunner struct {
|
||||
//we always assume ipv6 and ipv6 nat are enabled when testing
|
||||
}
|
||||
|
||||
func newIPTablesRunner(t *testing.T) netfilterRunner {
|
||||
func newIPTablesRunner(t *testing.T) linuxfw.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 netfilterRunner
|
||||
nfr linuxfw.NetfilterRunner
|
||||
}
|
||||
|
||||
func NewFakeOS(t *testing.T) *fakeOS {
|
||||
@@ -1063,63 +1063,3 @@ 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user