Compare commits

..

1 Commits

Author SHA1 Message Date
David Crawshaw
ad10cd71a6 tsweb: when unwrapping HTTPError, record the user-facing message also in the log
There's often some useful piece of information in there not already
repeated in the internal error.

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2020-09-17 09:57:36 -04:00
110 changed files with 1589 additions and 4978 deletions

View File

@@ -1,52 +0,0 @@
name: Windows
on:
push:
branches:
- main
pull_request:
branches:
- '*'
jobs:
test:
runs-on: windows-latest
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.15.x
- name: Checkout code
uses: actions/checkout@v2
- name: Restore Cache
uses: actions/cache@v2
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-
- name: Test
run: go test ./...
- uses: k0kubun/action-slack@v2.0.0
with:
payload: |
{
"attachments": [{
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
"color": "danger"
}]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
if: failure() && github.event_name == 'push'

1
.gitignore vendored
View File

@@ -1,6 +1,5 @@
# Binaries for programs and plugins
*~
*.tmp
*.exe
*.dll
*.so

View File

@@ -30,18 +30,6 @@ wrappers that are not open source.
go install tailscale.com/cmd/tailscale{,d}
```
If you're packaging Tailscale for distribution, use `build_dist.sh`
instead, to burn commit IDs and version info into the binaries:
```
./build_dist.sh tailscale.com/cmd/tailscale
./build_dist.sh tailscale.com/cmd/tailscaled
```
If your distro has conventions that preclude the use of
`build_dist.sh`, please do the equivalent of what it does in your
distro's way, so that bug reports contain useful version information.
We only guarantee to support the latest Go release and any Go beta or
release candidate builds (currently Go 1.15) in module mode. It might
work in earlier Go versions or in GOPATH mode, but we're making no

View File

@@ -1 +0,0 @@
1.2.9

View File

@@ -1,16 +0,0 @@
#!/usr/bin/env sh
#
# Runs `go build` with flags configured for binary distribution. All
# it does differently from `go build` is burn git commit and version
# information into the binaries, so that we can track down user
# issues.
#
# If you're packaging Tailscale for a distro, please consider using
# this script, or executing equivalent commands in your
# distro-specific build system.
set -eu
eval $(./version/version.sh)
exec go build -tags xversion -ldflags "-X tailscale.com/version.Long=${VERSION_LONG} -X tailscale.com/version.Short=${VERSION_SHORT} -X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}" "$@"

View File

@@ -33,7 +33,6 @@ var (
flagTypes = flag.String("type", "", "comma-separated list of types; required")
flagOutput = flag.String("output", "", "output file; required")
flagBuildTags = flag.String("tags", "", "compiler build tags to apply")
flagCloneFunc = flag.Bool("clonefunc", false, "add a top-level Clone func")
)
func main() {
@@ -99,27 +98,25 @@ func main() {
w := func(format string, args ...interface{}) {
fmt.Fprintf(buf, format+"\n", args...)
}
if *flagCloneFunc {
w("// Clone duplicates src into dst and reports whether it succeeded.")
w("// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,")
w("// where T is one of %s.", *flagTypes)
w("func Clone(dst, src interface{}) bool {")
w(" switch src := src.(type) {")
for _, typeName := range typeNames {
w(" case *%s:", typeName)
w(" switch dst := dst.(type) {")
w(" case *%s:", typeName)
w(" *dst = *src.Clone()")
w(" return true")
w(" case **%s:", typeName)
w(" *dst = src.Clone()")
w(" return true")
w(" }")
}
w(" }")
w(" return false")
w("}")
w("// Clone duplicates src into dst and reports whether it succeeded.")
w("// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,")
w("// where T is one of %s.", *flagTypes)
w("func Clone(dst, src interface{}) bool {")
w(" switch src := src.(type) {")
for _, typeName := range typeNames {
w(" case *%s:", typeName)
w(" switch dst := dst.(type) {")
w(" case *%s:", typeName)
w(" *dst = *src.Clone()")
w(" return true")
w(" case **%s:", typeName)
w(" *dst = src.Clone()")
w(" return true")
w(" }")
}
w(" }")
w(" return false")
w("}")
contents := new(bytes.Buffer)
fmt.Fprintf(contents, header, *flagTypes, pkg.Name)

View File

@@ -63,7 +63,7 @@ func loadConfig() config {
}
b, err := ioutil.ReadFile(*configPath)
switch {
case errors.Is(err, os.ErrNotExist):
case os.IsNotExist(err):
return writeNewConfig()
case err != nil:
log.Fatal(err)
@@ -233,7 +233,7 @@ func debugHandler(s *derp.Server) http.Handler {
f("<li><b>Hostname:</b> %v</li>\n", html.EscapeString(*hostname))
f("<li><b>Uptime:</b> %v</li>\n", tsweb.Uptime())
f("<li><b>Mesh Key:</b> %v</li>\n", s.HasMeshKey())
f("<li><b>Version:</b> %v</li>\n", html.EscapeString(version.Long))
f("<li><b>Version:</b> %v</li>\n", html.EscapeString(version.LONG))
f(`<li><a href="/debug/vars">/debug/vars</a> (Go)</li>
<li><a href="/debug/varz">/debug/varz</a> (Prometheus)</li>

View File

@@ -56,6 +56,7 @@ func runDown(ctx context.Context, args []string) error {
}
return
}
log.Printf("Notify: %#v", n)
})
bc.RequestStatus()

View File

@@ -28,6 +28,15 @@ import (
"tailscale.com/wgengine/router"
)
// globalStateKey is the ipn.StateKey that tailscaled loads on
// startup.
//
// We have to support multiple state keys for other OSes (Windows in
// particular), but right now Unix daemons run with a single
// node-global state. To keep open the option of having per-user state
// later, the global state key doesn't look like a username.
const globalStateKey = "_daemon"
var upCmd = &ffcli.Command{
Name: "up",
ShortUsage: "up [flags]",
@@ -51,6 +60,7 @@ specify any flags, options are reset to their default.
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "ACL tags to request (comma-separated, e.g. eng,montreal,ssh)")
upf.StringVar(&upArgs.authKey, "authkey", "", "node authorization key")
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
upf.BoolVar(&upArgs.enableDERP, "enable-derp", true, "enable the use of DERP servers")
if runtime.GOOS == "linux" || isBSD(runtime.GOOS) || version.OS() == "macOS" {
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)")
}
@@ -79,6 +89,7 @@ var upArgs struct {
forceReauth bool
advertiseRoutes string
advertiseTags string
enableDERP bool
snat bool
netfilterMode string
authKey string
@@ -205,9 +216,8 @@ func runUp(ctx context.Context, args []string) error {
prefs.AdvertiseRoutes = routes
prefs.AdvertiseTags = tags
prefs.NoSNAT = !upArgs.snat
prefs.DisableDERP = !upArgs.enableDERP
prefs.Hostname = upArgs.hostname
prefs.ForceDaemon = (runtime.GOOS == "windows")
if runtime.GOOS == "linux" {
switch upArgs.netfilterMode {
case "on":
@@ -231,9 +241,8 @@ func runUp(ctx context.Context, args []string) error {
startLoginInteractive := func() { loginOnce.Do(func() { bc.StartLoginInteractive() }) }
bc.SetPrefs(prefs)
opts := ipn.Options{
StateKey: ipn.GlobalDaemonStateKey,
StateKey: globalStateKey,
AuthKey: upArgs.authKey,
Notify: func(n ipn.Notify) {
if n.ErrMessage != nil {
@@ -261,22 +270,6 @@ func runUp(ctx context.Context, args []string) error {
}
},
}
// On Windows, we still run in mostly the "legacy" way that
// predated the server's StateStore. That is, we send an empty
// StateKey and send the prefs directly. Although the Windows
// supports server mode, though, the transition to StateStore
// is only half complete. Only server mode uses it, and the
// Windows service (~tailscaled) is the one that computes the
// StateKey based on the connection idenity. So for now, just
// do as the Windows GUI's always done:
if runtime.GOOS == "windows" {
// The Windows service will set this as needed based
// on our connection's identity.
opts.StateKey = ""
opts.Prefs = prefs
}
// We still have to Start right now because it's the only way to
// set up notifications and whatnot. This causes a bunch of churn
// every time the CLI touches anything.

View File

@@ -36,11 +36,10 @@ func runVersion(ctx context.Context, args []string) error {
log.Fatalf("too many non-flag arguments: %q", args)
}
if !versionArgs.daemon {
fmt.Println(version.String())
fmt.Println(version.LONG)
return nil
}
fmt.Printf("Client: %s\n", version.String())
fmt.Printf("Client: %s\n", version.LONG)
c, bc, ctx, cancel := connect(ctx)
defer cancel()

View File

@@ -5,7 +5,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscale
W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
LW github.com/go-multierror/multierror from tailscale.com/wgengine/router
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
L 💣 github.com/godbus/dbus/v5 from tailscale.com/wgengine/router/dns
@@ -16,6 +15,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli
github.com/peterbourgon/ff/v2/ffcli from tailscale.com/cmd/tailscale/cli
W 💣 github.com/tailscale/winipcfg-go from tailscale.com/net/interfaces+
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
@@ -36,7 +36,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
💣 go4.org/mem from tailscale.com/control/controlclient+
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
rsc.io/goversion/version from tailscale.com/version
tailscale.com/atomicfile from tailscale.com/ipn+
@@ -51,12 +50,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
tailscale.com/ipn/policy from tailscale.com/ipn
tailscale.com/log/logheap from tailscale.com/control/controlclient
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
tailscale.com/logtail/backoff from tailscale.com/control/controlclient
tailscale.com/metrics from tailscale.com/derp
tailscale.com/net/dnscache from tailscale.com/cmd/tailscale/cli+
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli+
tailscale.com/net/netns from tailscale.com/control/controlclient+
💣 tailscale.com/net/netns from tailscale.com/control/controlclient+
tailscale.com/net/stun from tailscale.com/net/netcheck+
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
tailscale.com/net/tsaddr from tailscale.com/ipn+
@@ -75,7 +74,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/types/opt from tailscale.com/control/controlclient+
tailscale.com/types/strbuilder from tailscale.com/wgengine/packet
tailscale.com/types/structs from tailscale.com/control/controlclient+
W tailscale.com/util/endian from tailscale.com/net/netns
tailscale.com/util/lineread from tailscale.com/control/controlclient+
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
@@ -92,26 +90,18 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from crypto/tls+
golang.org/x/crypto/hkdf from crypto/tls
golang.org/x/crypto/chacha20poly1305 from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/curve25519 from github.com/tailscale/wireguard-go/wgcfg+
golang.org/x/crypto/nacl/box from tailscale.com/control/controlclient+
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/context/ctxhttp from golang.org/x/oauth2/internal
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from net/http
golang.org/x/net/http/httpproxy from net/http
golang.org/x/net/http2/hpack from net/http
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
golang.org/x/net/dns/dnsmessage from tailscale.com/wgengine/tsdns
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net
golang.org/x/oauth2 from tailscale.com/control/controlclient+
golang.org/x/oauth2/internal from golang.org/x/oauth2
golang.org/x/sync/errgroup from tailscale.com/derp
@@ -120,11 +110,27 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
W golang.org/x/sys/windows/registry from github.com/tailscale/wireguard-go/tun/wintun+
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
golang.org/x/text/unicode/norm from github.com/tailscale/wireguard-go/tun/wintun+
W golang.org/x/text/transform from golang.org/x/text/unicode/norm
W golang.org/x/text/unicode/norm from github.com/tailscale/wireguard-go/tun/wintun
golang.org/x/time/rate from tailscale.com/types/logger+
vendor/golang.org/x/crypto/chacha20 from vendor/golang.org/x/crypto/chacha20poly1305
vendor/golang.org/x/crypto/chacha20poly1305 from crypto/tls
vendor/golang.org/x/crypto/cryptobyte from crypto/ecdsa+
vendor/golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
vendor/golang.org/x/crypto/curve25519 from crypto/tls
vendor/golang.org/x/crypto/hkdf from crypto/tls
vendor/golang.org/x/crypto/poly1305 from vendor/golang.org/x/crypto/chacha20poly1305
vendor/golang.org/x/net/dns/dnsmessage from net
vendor/golang.org/x/net/http/httpguts from net/http
vendor/golang.org/x/net/http/httpproxy from net/http
vendor/golang.org/x/net/http2/hpack from net/http
vendor/golang.org/x/net/idna from net/http+
D vendor/golang.org/x/net/route from net
vendor/golang.org/x/sys/cpu from vendor/golang.org/x/crypto/chacha20poly1305
vendor/golang.org/x/text/secure/bidirule from vendor/golang.org/x/net/idna
vendor/golang.org/x/text/transform from vendor/golang.org/x/text/secure/bidirule+
vendor/golang.org/x/text/unicode/bidi from vendor/golang.org/x/net/idna+
vendor/golang.org/x/text/unicode/norm from vendor/golang.org/x/net/idna
bufio from compress/flate+
bytes from bufio+
compress/flate from compress/gzip+
@@ -186,7 +192,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
net/http from expvar+
net/http/httptrace from github.com/tcnksm/go-httpstat+
net/http/internal from net/http
net/textproto from golang.org/x/net/http/httpguts+
net/textproto from mime/multipart+
net/url from crypto/x509+
os from crypto/rand+
os/exec from github.com/coreos/go-iptables/iptables+
@@ -197,6 +203,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
reflect from crypto/x509+
regexp from github.com/coreos/go-iptables/iptables+
regexp/syntax from regexp
LD runtime/cgo
runtime/pprof from tailscale.com/log/logheap+
sort from compress/flate+
strconv from compress/flate+
@@ -209,3 +216,4 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
unicode from bytes+
unicode/utf16 from encoding/asn1+
unicode/utf8 from bufio+
unsafe from crypto/internal/subtle+

View File

@@ -5,7 +5,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscaled
W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
LW github.com/go-multierror/multierror from tailscale.com/wgengine/router
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
L 💣 github.com/godbus/dbus/v5 from tailscale.com/wgengine/router/dns
@@ -19,6 +18,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
github.com/pborman/getopt/v2 from tailscale.com/cmd/tailscaled
W 💣 github.com/tailscale/winipcfg-go from tailscale.com/net/interfaces+
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
@@ -38,7 +39,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/conn+
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
💣 go4.org/mem from tailscale.com/control/controlclient+
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
inet.af/netaddr from tailscale.com/control/controlclient+
rsc.io/goversion/version from tailscale.com/version
tailscale.com/atomicfile from tailscale.com/ipn+
@@ -51,7 +51,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
tailscale.com/ipn/ipnstate from tailscale.com/ipn+
tailscale.com/ipn/policy from tailscale.com/ipn
tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver
tailscale.com/log/logheap from tailscale.com/control/controlclient
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled
tailscale.com/logtail from tailscale.com/logpolicy
@@ -59,9 +58,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/logtail/filch from tailscale.com/logpolicy
tailscale.com/metrics from tailscale.com/derp
tailscale.com/net/dnscache from tailscale.com/derp/derphttp+
💣 tailscale.com/net/interfaces from tailscale.com/ipn+
tailscale.com/net/interfaces from tailscale.com/ipn+
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
tailscale.com/net/netns from tailscale.com/control/controlclient+
💣 tailscale.com/net/netns from tailscale.com/control/controlclient+
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver
tailscale.com/net/stun from tailscale.com/net/netcheck+
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
@@ -76,17 +75,15 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
DW tailscale.com/tempfork/osexec from tailscale.com/portlist
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/types/empty from tailscale.com/control/controlclient+
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
tailscale.com/types/key from tailscale.com/derp+
tailscale.com/types/logger from tailscale.com/cmd/tailscaled+
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
tailscale.com/types/opt from tailscale.com/control/controlclient+
tailscale.com/types/strbuilder from tailscale.com/wgengine/packet
tailscale.com/types/structs from tailscale.com/control/controlclient+
W tailscale.com/util/endian from tailscale.com/net/netns+
tailscale.com/util/lineread from tailscale.com/control/controlclient+
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
tailscale.com/version from tailscale.com/cmd/tailscaled+
tailscale.com/version from tailscale.com/control/controlclient+
tailscale.com/version/distro from tailscale.com/control/controlclient+
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
@@ -101,11 +98,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from crypto/tls+
golang.org/x/crypto/hkdf from crypto/tls
golang.org/x/crypto/chacha20poly1305 from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/curve25519 from github.com/tailscale/wireguard-go/wgcfg+
golang.org/x/crypto/nacl/box from tailscale.com/control/controlclient+
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+
@@ -113,15 +107,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/ssh/terminal from tailscale.com/logpolicy
golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/context/ctxhttp from golang.org/x/oauth2/internal
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from net/http
golang.org/x/net/http/httpproxy from net/http
golang.org/x/net/http2/hpack from net/http
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
golang.org/x/net/dns/dnsmessage from tailscale.com/wgengine/tsdns
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net
golang.org/x/oauth2 from tailscale.com/control/controlclient+
golang.org/x/oauth2/internal from golang.org/x/oauth2
golang.org/x/sync/errgroup from tailscale.com/derp
@@ -130,11 +119,27 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
W golang.org/x/sys/windows/registry from github.com/tailscale/wireguard-go/tun/wintun+
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
golang.org/x/text/unicode/norm from github.com/tailscale/wireguard-go/tun/wintun+
W golang.org/x/text/transform from golang.org/x/text/unicode/norm
W golang.org/x/text/unicode/norm from github.com/tailscale/wireguard-go/tun/wintun
golang.org/x/time/rate from tailscale.com/types/logger+
vendor/golang.org/x/crypto/chacha20 from vendor/golang.org/x/crypto/chacha20poly1305
vendor/golang.org/x/crypto/chacha20poly1305 from crypto/tls
vendor/golang.org/x/crypto/cryptobyte from crypto/ecdsa+
vendor/golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
vendor/golang.org/x/crypto/curve25519 from crypto/tls
vendor/golang.org/x/crypto/hkdf from crypto/tls
vendor/golang.org/x/crypto/poly1305 from vendor/golang.org/x/crypto/chacha20poly1305
vendor/golang.org/x/net/dns/dnsmessage from net
vendor/golang.org/x/net/http/httpguts from net/http
vendor/golang.org/x/net/http/httpproxy from net/http
vendor/golang.org/x/net/http2/hpack from net/http
vendor/golang.org/x/net/idna from net/http+
D vendor/golang.org/x/net/route from net
vendor/golang.org/x/sys/cpu from vendor/golang.org/x/crypto/chacha20poly1305
vendor/golang.org/x/text/secure/bidirule from vendor/golang.org/x/net/idna
vendor/golang.org/x/text/transform from vendor/golang.org/x/text/secure/bidirule+
vendor/golang.org/x/text/unicode/bidi from vendor/golang.org/x/net/idna+
vendor/golang.org/x/text/unicode/norm from vendor/golang.org/x/net/idna
bufio from compress/flate+
bytes from bufio+
compress/flate from compress/gzip+
@@ -175,7 +180,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
encoding/pem from crypto/tls+
errors from bufio+
expvar from tailscale.com/derp+
flag from tailscale.com/cmd/tailscaled+
L flag from tailscale.com/net/netns
fmt from compress/flate+
hash from compress/zlib+
hash/adler32 from compress/zlib
@@ -198,7 +203,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
net/http/httptrace from github.com/tcnksm/go-httpstat+
net/http/internal from net/http
net/http/pprof from tailscale.com/cmd/tailscaled
net/textproto from golang.org/x/net/http/httpguts+
net/textproto from mime/multipart+
net/url from crypto/x509+
os from crypto/rand+
os/exec from github.com/coreos/go-iptables/iptables+
@@ -209,6 +214,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
reflect from crypto/x509+
regexp from github.com/coreos/go-iptables/iptables+
regexp/syntax from regexp
LD runtime/cgo
runtime/debug from github.com/klauspost/compress/zstd+
runtime/pprof from net/http/pprof+
runtime/trace from net/http/pprof
@@ -225,3 +231,4 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
unicode from bytes+
unicode/utf16 from encoding/asn1+
unicode/utf8 from bufio+
unsafe from crypto/internal/subtle+

View File

@@ -11,8 +11,6 @@ package main // import "tailscale.com/cmd/tailscaled"
import (
"context"
"flag"
"fmt"
"log"
"net/http"
"net/http/pprof"
@@ -20,17 +18,15 @@ import (
"os/signal"
"runtime"
"runtime/debug"
"strconv"
"syscall"
"time"
"github.com/apenwarr/fixconsole"
"github.com/pborman/getopt/v2"
"tailscale.com/ipn/ipnserver"
"tailscale.com/logpolicy"
"tailscale.com/paths"
"tailscale.com/types/flagtype"
"tailscale.com/types/logger"
"tailscale.com/version"
"tailscale.com/wgengine"
"tailscale.com/wgengine/magicsock"
"tailscale.com/wgengine/router"
@@ -75,29 +71,28 @@ func main() {
debug.SetGCPercent(10)
}
printVersion := false
flag.BoolVar(&args.cleanup, "cleanup", false, "clean up system state and exit")
flag.BoolVar(&args.fake, "fake", false, "use userspace fake tunnel+routing instead of kernel TUN interface")
flag.StringVar(&args.debug, "debug", "", "listen address ([ip]:port) of optional debug server")
flag.StringVar(&args.tunname, "tun", defaultTunName(), "tunnel interface name")
flag.Var(flagtype.PortValue(&args.port, magicsock.DefaultPort), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "path of state file")
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
// Set default values for getopt.
args.tunname = defaultTunName()
args.port = magicsock.DefaultPort
args.statepath = paths.DefaultTailscaledStateFile()
args.socketpath = paths.DefaultTailscaledSocket()
getopt.FlagLong(&args.cleanup, "cleanup", 0, "clean up system state and exit")
getopt.FlagLong(&args.fake, "fake", 0, "fake tunnel+routing instead of tuntap")
getopt.FlagLong(&args.debug, "debug", 0, "address of debug server")
getopt.FlagLong(&args.tunname, "tun", 0, "tunnel interface name")
getopt.FlagLong(&args.port, "port", 'p', "WireGuard port (0=autoselect)")
getopt.FlagLong(&args.statepath, "state", 0, "path of state file")
getopt.FlagLong(&args.socketpath, "socket", 's', "path of the service unix socket")
err := fixconsole.FixConsoleIfNeeded()
if err != nil {
log.Fatalf("fixConsoleOutput: %v", err)
}
flag.Parse()
if flag.NArg() > 0 {
log.Fatalf("tailscaled does not take non-flag arguments: %q", flag.Args())
}
if printVersion {
fmt.Println(version.String())
os.Exit(0)
getopt.Parse()
if len(getopt.Args()) > 0 {
log.Fatalf("too many non-flag arguments: %#v", getopt.Args()[0])
}
if args.statepath == "" {
@@ -125,10 +120,7 @@ func run() error {
pol.Shutdown(ctx)
}()
var logf logger.Logf = log.Printf
if v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_MEMORY")); v {
logf = logger.RusagePrefixLog(logf)
}
logf := wgengine.RusagePrefixLog(log.Printf)
logf = logger.RateLimitedFn(logf, 5*time.Second, 5, 100)
if args.cleanup {
@@ -144,7 +136,7 @@ func run() error {
var e wgengine.Engine
if args.fake {
e, err = wgengine.NewFakeUserspaceEngine(logf, args.port)
e, err = wgengine.NewFakeUserspaceEngine(logf, 0)
} else {
e, err = wgengine.NewUserspaceEngine(logf, args.tunname, args.port)
}

View File

@@ -6,7 +6,6 @@ After=network-pre.target
[Service]
EnvironmentFile=/etc/default/tailscaled
ExecStartPre=/usr/sbin/tailscaled --cleanup
ExecStart=/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port $PORT $FLAGS
ExecStopPost=/usr/sbin/tailscaled --cleanup

View File

@@ -288,7 +288,7 @@ func (c *Client) authRoutine() {
// don't send status updates for context errors,
// since context cancelation is always on purpose.
if ctx.Err() == nil {
c.sendStatus("authRoutine-report", err, "", nil)
c.sendStatus("authRoutine1", err, "", nil)
}
}
@@ -353,7 +353,7 @@ func (c *Client) authRoutine() {
c.synced = false
c.mu.Unlock()
c.sendStatus("authRoutine-wantout", nil, "", nil)
c.sendStatus("authRoutine2", nil, "", nil)
bo.BackOff(ctx, nil)
} else { // ie. goal.wantLoggedIn
c.mu.Lock()
@@ -394,7 +394,7 @@ func (c *Client) authRoutine() {
c.synced = false
c.mu.Unlock()
c.sendStatus("authRoutine-url", err, url, nil)
c.sendStatus("authRoutine3", err, url, nil)
bo.BackOff(ctx, err)
continue
}
@@ -406,7 +406,7 @@ func (c *Client) authRoutine() {
c.state = StateAuthenticated
c.mu.Unlock()
c.sendStatus("authRoutine-success", nil, "", nil)
c.sendStatus("authRoutine4", nil, "", nil)
c.cancelMapSafely()
bo.BackOff(ctx, nil)
}
@@ -528,7 +528,7 @@ func (c *Client) mapRoutine() {
c.logf("mapRoutine: netmap received: %s", state)
if stillAuthed {
c.sendStatus("mapRoutine-got-netmap", nil, "", nm)
c.sendStatus("mapRoutine2", nil, "", nm)
}
})

View File

@@ -13,7 +13,6 @@ import (
"encoding/binary"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"io/ioutil"
@@ -21,7 +20,6 @@ import (
"net/http"
"net/url"
"os"
"os/exec"
"reflect"
"runtime"
"sort"
@@ -47,19 +45,8 @@ import (
)
type Persist struct {
_ structs.Incomparable
// LegacyFrontendPrivateMachineKey is here temporarily
// (starting 2020-09-28) during migration of Windows users'
// machine keys from frontend storage to the backend. On the
// first LocalBackend.Start call, the backend will initialize
// the real (backend-owned) machine key from the frontend's
// provided value (if non-zero), picking a new random one if
// needed. This field should be considered read-only from GUI
// frontends. The real value should not be written back in
// this field, lest the frontend persist it to disk.
LegacyFrontendPrivateMachineKey wgcfg.PrivateKey `json:"PrivateMachineKey"`
_ structs.Incomparable
PrivateMachineKey wgcfg.PrivateKey
PrivateNodeKey wgcfg.PrivateKey
OldPrivateNodeKey wgcfg.PrivateKey // needed to request key rotation
Provider string
@@ -74,7 +61,7 @@ func (p *Persist) Equals(p2 *Persist) bool {
return false
}
return p.LegacyFrontendPrivateMachineKey.Equal(p2.LegacyFrontendPrivateMachineKey) &&
return p.PrivateMachineKey.Equal(p2.PrivateMachineKey) &&
p.PrivateNodeKey.Equal(p2.PrivateNodeKey) &&
p.OldPrivateNodeKey.Equal(p2.OldPrivateNodeKey) &&
p.Provider == p2.Provider &&
@@ -83,8 +70,8 @@ func (p *Persist) Equals(p2 *Persist) bool {
func (p *Persist) Pretty() string {
var mk, ok, nk wgcfg.Key
if !p.LegacyFrontendPrivateMachineKey.IsZero() {
mk = p.LegacyFrontendPrivateMachineKey.Public()
if !p.PrivateMachineKey.IsZero() {
mk = p.PrivateMachineKey.Public()
}
if !p.OldPrivateNodeKey.IsZero() {
ok = p.OldPrivateNodeKey.Public()
@@ -92,14 +79,9 @@ func (p *Persist) Pretty() string {
if !p.PrivateNodeKey.IsZero() {
nk = p.PrivateNodeKey.Public()
}
ss := func(k wgcfg.Key) string {
if k.IsZero() {
return ""
}
return k.ShortString()
}
return fmt.Sprintf("Persist{lm=%v, o=%v, n=%v u=%#v}",
ss(mk), ss(ok), ss(nk), p.LoginName)
return fmt.Sprintf("Persist{m=%v, o=%v, n=%v u=%#v}",
mk.ShortString(), ok.ShortString(), nk.ShortString(),
p.LoginName)
}
// Direct is the client that connects to a tailcontrol server for a node.
@@ -112,8 +94,6 @@ type Direct struct {
keepAlive bool
logf logger.Logf
discoPubKey tailcfg.DiscoKey
machinePrivKey wgcfg.PrivateKey
debugFlags []string
mu sync.Mutex // mutex guards the following fields
serverKey wgcfg.Key
@@ -122,25 +102,22 @@ type Direct struct {
tryingNewKey wgcfg.PrivateKey
expiry *time.Time
// hostinfo is mutated in-place while mu is held.
hostinfo *tailcfg.Hostinfo // always non-nil
endpoints []string
everEndpoints bool // whether we've ever had non-empty endpoints
localPort uint16 // or zero to mean auto
hostinfo *tailcfg.Hostinfo // always non-nil
endpoints []string
localPort uint16 // or zero to mean auto
}
type Options struct {
Persist Persist // initial persistent data
MachinePrivateKey wgcfg.PrivateKey // the machine key to use
ServerURL string // URL of the tailcontrol server
AuthKey string // optional node auth key for auto registration
TimeNow func() time.Time // time.Now implementation used by Client
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
DiscoPublicKey tailcfg.DiscoKey
NewDecompressor func() (Decompressor, error)
KeepAlive bool
Logf logger.Logf
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
DebugFlags []string // debug settings to send to control
Persist Persist // initial persistent data
ServerURL string // URL of the tailcontrol server
AuthKey string // optional node auth key for auto registration
TimeNow func() time.Time // time.Now implementation used by Client
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
DiscoPublicKey tailcfg.DiscoKey
NewDecompressor func() (Decompressor, error)
KeepAlive bool
Logf logger.Logf
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
}
type Decompressor interface {
@@ -153,9 +130,6 @@ func NewDirect(opts Options) (*Direct, error) {
if opts.ServerURL == "" {
return nil, errors.New("controlclient.New: no server URL specified")
}
if opts.MachinePrivateKey.IsZero() {
return nil, errors.New("controlclient.New: no MachinePrivateKey specified")
}
opts.ServerURL = strings.TrimRight(opts.ServerURL, "/")
serverURL, err := url.Parse(opts.ServerURL)
if err != nil {
@@ -184,7 +158,6 @@ func NewDirect(opts Options) (*Direct, error) {
c := &Direct{
httpc: httpc,
machinePrivKey: opts.MachinePrivateKey,
serverURL: opts.ServerURL,
timeNow: opts.TimeNow,
logf: opts.Logf,
@@ -193,7 +166,6 @@ func NewDirect(opts Options) (*Direct, error) {
persist: opts.Persist,
authKey: opts.AuthKey,
discoPubKey: opts.DiscoPublicKey,
debugFlags: opts.DebugFlags,
}
if opts.Hostinfo == nil {
c.SetHostinfo(NewHostinfo())
@@ -212,7 +184,7 @@ func NewHostinfo() *tailcfg.Hostinfo {
osv = osVersion()
}
return &tailcfg.Hostinfo{
IPNVersion: version.Long,
IPNVersion: version.LONG,
Hostname: hostname,
OS: version.OS(),
OSVersion: osv,
@@ -233,8 +205,6 @@ func (c *Direct) SetHostinfo(hi *tailcfg.Hostinfo) bool {
return false
}
c.hostinfo = hi.Clone()
j, _ := json.Marshal(c.hostinfo)
c.logf("HostInfo: %s", j)
return true
}
@@ -279,14 +249,16 @@ func (c *Direct) TryLogout(ctx context.Context) error {
// TODO(crawshaw): Tell the server. This node key should be
// immediately invalidated.
//if !c.persist.PrivateNodeKey.IsZero() {
//if c.persist.PrivateNodeKey != (wgcfg.PrivateKey{}) {
//}
c.persist = Persist{}
c.persist = Persist{
PrivateMachineKey: c.persist.PrivateMachineKey,
}
return nil
}
func (c *Direct) TryLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags) (url string, err error) {
c.logf("direct.TryLogin(token=%v, flags=%v)", t != nil, flags)
c.logf("direct.TryLogin(%v, %v)", t != nil, flags)
return c.doLoginOrRegen(ctx, t, flags, false, "")
}
@@ -317,8 +289,13 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
expired := c.expiry != nil && !c.expiry.IsZero() && c.expiry.Before(c.timeNow())
c.mu.Unlock()
if c.machinePrivKey.IsZero() {
return false, "", errors.New("controlclient.Direct requires a machine private key")
if persist.PrivateMachineKey == (wgcfg.PrivateKey{}) {
c.logf("Generating a new machinekey.")
mkey, err := wgcfg.NewPrivateKey()
if err != nil {
log.Fatal(err)
}
persist.PrivateMachineKey = mkey
}
if expired {
@@ -345,7 +322,7 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
var oldNodeKey wgcfg.Key
if url != "" {
} else if regen || persist.PrivateNodeKey.IsZero() {
} else if regen || persist.PrivateNodeKey == (wgcfg.PrivateKey{}) {
c.logf("Generating a new nodekey.")
persist.OldPrivateNodeKey = persist.PrivateNodeKey
key, err := wgcfg.NewPrivateKey()
@@ -358,11 +335,11 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
// Try refreshing the current key first
tryingNewKey = persist.PrivateNodeKey
}
if !persist.OldPrivateNodeKey.IsZero() {
if persist.OldPrivateNodeKey != (wgcfg.PrivateKey{}) {
oldNodeKey = persist.OldPrivateNodeKey.Public()
}
if tryingNewKey.IsZero() {
if tryingNewKey == (wgcfg.PrivateKey{}) {
log.Fatalf("tryingNewKey is empty, give up")
}
if backendLogID == "" {
@@ -383,13 +360,13 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
request.Auth.Provider = persist.Provider
request.Auth.LoginName = persist.LoginName
request.Auth.AuthKey = authKey
bodyData, err := encode(request, &serverKey, &c.machinePrivKey)
bodyData, err := encode(request, &serverKey, &persist.PrivateMachineKey)
if err != nil {
return regen, url, err
}
body := bytes.NewReader(bodyData)
u := fmt.Sprintf("%s/machine/%s", c.serverURL, c.machinePrivKey.Public().HexString())
u := fmt.Sprintf("%s/machine/%s", c.serverURL, persist.PrivateMachineKey.Public().HexString())
req, err := http.NewRequest("POST", u, body)
if err != nil {
return regen, url, err
@@ -400,20 +377,11 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
if err != nil {
return regen, url, fmt.Errorf("register request: %v", err)
}
if res.StatusCode != 200 {
msg, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
return regen, url, fmt.Errorf("register request: http %d: %.200s",
res.StatusCode, strings.TrimSpace(string(msg)))
}
c.logf("RegisterReq: returned.")
resp := tailcfg.RegisterResponse{}
if err := decode(res, &resp, &serverKey, &c.machinePrivKey); err != nil {
c.logf("error decoding RegisterResponse with server key %s and machine key %s: %v", serverKey, c.machinePrivKey.Public(), err)
if err := decode(res, &resp, &serverKey, &persist.PrivateMachineKey); err != nil {
return regen, url, fmt.Errorf("register request: %v", err)
}
// Log without PII:
c.logf("RegisterReq: got response; nodeKeyExpired=%v, machineAuthorized=%v; authURL=%v",
resp.NodeKeyExpired, resp.MachineAuthorized, resp.AuthURL != "")
if resp.NodeKeyExpired {
if regen {
@@ -489,9 +457,6 @@ func (c *Direct) newEndpoints(localPort uint16, endpoints []string) (changed boo
c.logf("client.newEndpoints(%v, %v)", localPort, endpoints)
c.localPort = localPort
c.endpoints = append(c.endpoints[:0], endpoints...)
if len(endpoints) > 0 {
c.everEndpoints = true
}
return true // changed
}
@@ -504,13 +469,6 @@ func (c *Direct) SetEndpoints(localPort uint16, endpoints []string) (changed boo
return c.newEndpoints(localPort, endpoints)
}
func inTest() bool { return flag.Lookup("test.v") != nil }
// PollNetMap makes a /map request to download the network map, calling cb with
// each new netmap.
//
// maxPolls is how many network maps to download; common values are 1
// or -1 (to keep a long-poll query open to the server).
func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkMap)) error {
c.mu.Lock()
persist := c.persist
@@ -520,7 +478,6 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
backendLogID := hostinfo.BackendLogID
localPort := c.localPort
ep := append([]string(nil), c.endpoints...)
everEndpoints := c.everEndpoints
c.mu.Unlock()
if backendLogID == "" {
@@ -528,7 +485,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
}
allowStream := maxPolls != 1
c.logf("PollNetMap: stream=%v :%v ep=%v", allowStream, localPort, ep)
c.logf("PollNetMap: stream=%v :%v %v", maxPolls, localPort, ep)
vlogf := logger.Discard
if Debug.NetMap {
@@ -536,51 +493,36 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
}
request := tailcfg.MapRequest{
Version: 5,
KeepAlive: c.keepAlive,
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
DiscoKey: c.discoPubKey,
Endpoints: ep,
Stream: allowStream,
Hostinfo: hostinfo,
DebugFlags: c.debugFlags,
}
if hostinfo != nil && ipForwardingBroken(hostinfo.RoutableIPs) {
old := request.DebugFlags
request.DebugFlags = append(old[:len(old):len(old)], "warn-ip-forwarding-off")
Version: 4,
IncludeIPv6: true,
DeltaPeers: true,
KeepAlive: c.keepAlive,
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
DiscoKey: c.discoPubKey,
Endpoints: ep,
Stream: allowStream,
Hostinfo: hostinfo,
DebugForceDisco: Debug.ForceDisco,
}
if c.newDecompressor != nil {
request.Compress = "zstd"
}
// On initial startup before we know our endpoints, set the ReadOnly flag
// to tell the control server not to distribute out our (empty) endpoints to peers.
// Presumably we'll learn our endpoints in a half second and do another post
// with useful results. The first POST just gets us the DERP map which we
// need to do the STUN queries to discover our endpoints.
// TODO(bradfitz): we skip this optimization in tests, though,
// because the e2e tests are currently hyperspecific about the
// ordering of things. The e2e tests need love.
if len(ep) == 0 && !everEndpoints && !inTest() {
request.ReadOnly = true
}
bodyData, err := encode(request, &serverKey, &c.machinePrivKey)
bodyData, err := encode(request, &serverKey, &persist.PrivateMachineKey)
if err != nil {
vlogf("netmap: encode: %v", err)
return err
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
machinePubKey := tailcfg.MachineKey(c.machinePrivKey.Public())
t0 := time.Now()
u := fmt.Sprintf("%s/machine/%s/map", serverURL, machinePubKey.HexString())
req, err := http.NewRequestWithContext(ctx, "POST", u, bytes.NewReader(bodyData))
u := fmt.Sprintf("%s/machine/%s/map", serverURL, persist.PrivateMachineKey.Public().HexString())
req, err := http.NewRequest("POST", u, bytes.NewReader(bodyData))
if err != nil {
return err
}
ctx, cancel := context.WithCancel(ctx)
defer cancel()
req = req.WithContext(ctx)
res, err := c.httpc.Do(req)
if err != nil {
@@ -591,7 +533,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
if res.StatusCode != 200 {
msg, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
return fmt.Errorf("initial fetch failed %d: %.200s",
return fmt.Errorf("initial fetch failed %d: %s",
res.StatusCode, strings.TrimSpace(string(msg)))
}
defer res.Body.Close()
@@ -630,7 +572,6 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
}()
var lastDERPMap *tailcfg.DERPMap
var lastUserProfile = map[tailcfg.UserID]tailcfg.UserProfile{}
// If allowStream, then the server will use an HTTP long poll to
// return incremental results. There is always one response right
@@ -680,9 +621,6 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
undeltaPeers(&resp, previousPeers)
previousPeers = cloneNodes(resp.Peers) // defensive/lazy clone, since this escapes to who knows where
for _, up := range resp.UserProfiles {
lastUserProfile[up.ID] = up
}
if resp.DERPMap != nil {
vlogf("netmap: new map contains DERP map")
@@ -711,7 +649,6 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
nm := &NetworkMap{
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
PrivateKey: persist.PrivateNodeKey,
MachineKey: machinePubKey,
Expiry: resp.Node.KeyExpiry,
Name: resp.Node.Name,
Addresses: resp.Node.Addresses,
@@ -720,24 +657,15 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
User: resp.Node.User,
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
Domain: resp.Domain,
Roles: resp.Roles,
DNS: resp.DNSConfig,
Hostinfo: resp.Node.Hostinfo,
PacketFilter: c.parsePacketFilter(resp.PacketFilter),
DERPMap: lastDERPMap,
Debug: resp.Debug,
}
addUserProfile := func(userID tailcfg.UserID) {
if _, dup := nm.UserProfiles[userID]; dup {
// Already populated it from a previous peer.
return
}
if up, ok := lastUserProfile[userID]; ok {
nm.UserProfiles[userID] = up
}
}
addUserProfile(nm.User)
for _, peer := range resp.Peers {
addUserProfile(peer.User)
for _, profile := range resp.UserProfiles {
nm.UserProfiles[profile.ID] = profile
}
if resp.Node.MachineAuthorized {
nm.MachineStatus = tailcfg.MachineAuthorized
@@ -789,16 +717,15 @@ func decode(res *http.Response, v interface{}, serverKey *wgcfg.Key, mkey *wgcfg
return decodeMsg(msg, v, serverKey, mkey)
}
var debugMap, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_MAP"))
var jsonEscapedZero = []byte(`\u0000`)
var dumpMapResponse, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_MAPRESPONSE"))
func (c *Direct) decodeMsg(msg []byte, v interface{}) error {
c.mu.Lock()
mkey := c.persist.PrivateMachineKey
serverKey := c.serverKey
c.mu.Unlock()
decrypted, err := decryptMsg(msg, &serverKey, &c.machinePrivKey)
decrypted, err := decryptMsg(msg, &serverKey, &mkey)
if err != nil {
return err
}
@@ -816,15 +743,11 @@ func (c *Direct) decodeMsg(msg []byte, v interface{}) error {
return err
}
}
if debugMap {
if dumpMapResponse {
var buf bytes.Buffer
json.Indent(&buf, b, "", " ")
log.Printf("MapResponse: %s", buf.Bytes())
}
if bytes.Contains(b, jsonEscapedZero) {
log.Printf("[unexpected] zero byte in controlclient.Direct.decodeMsg into %T: %q", v, b)
}
if err := json.Unmarshal(b, v); err != nil {
return fmt.Errorf("response: %v", err)
}
@@ -837,9 +760,6 @@ func decodeMsg(msg []byte, v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.Priv
if err != nil {
return err
}
if bytes.Contains(decrypted, jsonEscapedZero) {
log.Printf("[unexpected] zero byte in controlclient decodeMsg into %T: %q", v, decrypted)
}
if err := json.Unmarshal(decrypted, v); err != nil {
return fmt.Errorf("response: %v", err)
}
@@ -857,7 +777,7 @@ func decryptMsg(msg []byte, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) ([]byt
pub, pri := (*[32]byte)(serverKey), (*[32]byte)(mkey)
decrypted, ok := box.Open(nil, msg, &nonce, pub, pri)
if !ok {
return nil, fmt.Errorf("cannot decrypt response (len %d + nonce %d = %d)", len(msg), len(nonce), len(msg)+len(nonce))
return nil, fmt.Errorf("cannot decrypt response")
}
return decrypted, nil
}
@@ -867,7 +787,8 @@ func encode(v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) ([]byte
if err != nil {
return nil, err
}
if debugMap {
const debugMapRequests = false
if debugMapRequests {
if _, ok := v.(tailcfg.MapRequest); ok {
log.Printf("MapRequest: %s", b)
}
@@ -922,20 +843,25 @@ func wgIPToNetaddr(ips []wgcfg.IP) (ret []netaddr.IP) {
var Debug = initDebug()
type debug struct {
NetMap bool
ProxyDNS bool
OnlyDisco bool
Disco bool
NetMap bool
ProxyDNS bool
OnlyDisco bool
Disco bool
ForceDisco bool // ask control server to not filter out our disco key
}
func initDebug() debug {
use := os.Getenv("TS_DEBUG_USE_DISCO")
return debug{
NetMap: envBool("TS_DEBUG_NETMAP"),
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
OnlyDisco: use == "only",
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
d := debug{
NetMap: envBool("TS_DEBUG_NETMAP"),
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
OnlyDisco: os.Getenv("TS_DEBUG_USE_DISCO") == "only",
ForceDisco: os.Getenv("TS_DEBUG_USE_DISCO") == "only" || envBool("TS_DEBUG_USE_DISCO"),
}
if d.ForceDisco || os.Getenv("TS_DEBUG_USE_DISCO") == "" {
// This is now defaults to on.
d.Disco = true
}
return d
}
func envBool(k string) bool {
@@ -1070,34 +996,3 @@ func TrimWGConfig() opt.Bool {
v, _ := controlTrimWGConfig.Load().(opt.Bool)
return v
}
// ipForwardingBroken reports whether the system's IP forwarding is disabled
// and will definitely not work for the routes provided.
//
// It should not return false positives.
func ipForwardingBroken(routes []wgcfg.CIDR) bool {
if len(routes) == 0 {
// Nothing to route, so no need to warn.
return false
}
if runtime.GOOS != "linux" {
// We only do subnet routing on Linux for now.
// It might work on darwin/macOS when building from source, so
// don't return true for other OSes. We can OS-based warnings
// already in the admin panel.
return false
}
out, err := ioutil.ReadFile("/proc/sys/net/ipv4/ip_forward")
if err != nil {
// Try another way.
out, err = exec.Command("sysctl", "-n", "net.ipv4.ip_forward").Output()
}
if err != nil {
// Oh well, we tried. This is just for debugging.
// We don't want false positives.
// TODO: maybe we want a different warning for inability to check?
return false
}
return strings.TrimSpace(string(out)) == "0"
// TODO: also check IPv6 if 'routes' contains any IPv6 routes
}

View File

@@ -26,11 +26,8 @@ func init() {
func osVersionLinux() string {
dist := distro.Get()
propFile := "/etc/os-release"
switch dist {
case distro.Synology:
if dist == distro.Synology {
propFile = "/etc.defaults/VERSION"
case distro.OpenWrt:
propFile = "/etc/openwrt_release"
}
m := map[string]string{}
@@ -39,7 +36,7 @@ func osVersionLinux() string {
if eq == -1 {
return nil
}
k, v := string(line[:eq]), strings.Trim(string(line[eq+1:]), `"'`)
k, v := string(line[:eq]), strings.Trim(string(line[eq+1:]), `"`)
m[k] = v
return nil
})
@@ -81,11 +78,8 @@ func osVersionLinux() string {
return fmt.Sprintf("%s%s", v, attr)
}
}
switch dist {
case distro.Synology:
if dist == distro.Synology {
return fmt.Sprintf("Synology %s%s", m["productversion"], attr)
case distro.OpenWrt:
return fmt.Sprintf("OpenWrt %s%s", m["DISTRIB_RELEASE"], attr)
}
return fmt.Sprintf("Other%s", attr)
}

View File

@@ -30,7 +30,6 @@ type NetworkMap struct {
Addresses []wgcfg.CIDR
LocalPort uint16 // used for debugging
MachineStatus tailcfg.MachineStatus
MachineKey tailcfg.MachineKey
Peers []*tailcfg.Node // sorted by Node.ID
DNS tailcfg.DNSConfig
Hostinfo tailcfg.Hostinfo
@@ -50,6 +49,7 @@ type NetworkMap struct {
// TODO(crawshaw): reduce UserProfiles to []tailcfg.UserProfile?
// There are lots of ways to slice this data, leave it up to users.
UserProfiles map[tailcfg.UserID]tailcfg.UserProfile
Roles []tailcfg.Role
// TODO(crawshaw): Groups []tailcfg.Group
// TODO(crawshaw): Capabilities []tailcfg.Capability
}
@@ -75,15 +75,6 @@ func (nm *NetworkMap) Concise() string {
func (nm *NetworkMap) printConciseHeader(buf *strings.Builder) {
fmt.Fprintf(buf, "netmap: self: %v auth=%v",
nm.NodeKey.ShortString(), nm.MachineStatus)
login := nm.UserProfiles[nm.User].LoginName
if login == "" {
if nm.User.IsZero() {
login = "?"
} else {
login = fmt.Sprint(nm.User)
}
}
fmt.Fprintf(buf, " u=%s", login)
if nm.LocalPort != 0 {
fmt.Fprintf(buf, " port=%v", nm.LocalPort)
}
@@ -101,7 +92,6 @@ func (a *NetworkMap) equalConciseHeader(b *NetworkMap) bool {
if a.NodeKey != b.NodeKey ||
a.MachineStatus != b.MachineStatus ||
a.LocalPort != b.LocalPort ||
a.User != b.User ||
len(a.Addresses) != len(b.Addresses) {
return false
}

View File

@@ -51,7 +51,7 @@ func TestNetworkMapConcise(t *testing.T) {
},
},
},
want: "netmap: self: [AQEBA] auth=machine-unknown u=? []\n [AgICA] D2 : 192.168.0.100:12 192.168.0.100:12354\n [AwMDA] D4 : 10.2.0.100:12 10.1.0.100:12345\n",
want: "netmap: self: [AQEBA] auth=machine-unknown []\n [AgICA] D2 : 192.168.0.100:12 192.168.0.100:12354\n [AwMDA] D4 : 10.2.0.100:12 10.1.0.100:12345\n",
},
{
name: "debug_non_nil",
@@ -59,7 +59,7 @@ func TestNetworkMapConcise(t *testing.T) {
NodeKey: testNodeKey(1),
Debug: &tailcfg.Debug{},
},
want: "netmap: self: [AQEBA] auth=machine-unknown u=? debug={} []\n",
want: "netmap: self: [AQEBA] auth=machine-unknown debug={} []\n",
},
{
name: "debug_values",
@@ -67,7 +67,7 @@ func TestNetworkMapConcise(t *testing.T) {
NodeKey: testNodeKey(1),
Debug: &tailcfg.Debug{LogHeapPprof: true},
},
want: "netmap: self: [AQEBA] auth=machine-unknown u=? debug={\"LogHeapPprof\":true} []\n",
want: "netmap: self: [AQEBA] auth=machine-unknown debug={\"LogHeapPprof\":true} []\n",
},
} {
t.Run(tt.name, func(t *testing.T) {
@@ -135,7 +135,7 @@ func TestConciseDiffFrom(t *testing.T) {
},
},
},
want: "-netmap: self: [AQEBA] auth=machine-unknown u=? []\n+netmap: self: [AgICA] auth=machine-unknown u=? []\n",
want: "-netmap: self: [AQEBA] auth=machine-unknown []\n+netmap: self: [AgICA] auth=machine-unknown []\n",
},
{
name: "peer_add",

View File

@@ -12,7 +12,7 @@ import (
)
func TestPersistEqual(t *testing.T) {
persistHandles := []string{"LegacyFrontendPrivateMachineKey", "PrivateNodeKey", "OldPrivateNodeKey", "Provider", "LoginName"}
persistHandles := []string{"PrivateMachineKey", "PrivateNodeKey", "OldPrivateNodeKey", "Provider", "LoginName"}
if have := fieldsOf(reflect.TypeOf(Persist{})); !reflect.DeepEqual(have, persistHandles) {
t.Errorf("Persist.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
have, persistHandles)
@@ -36,13 +36,13 @@ func TestPersistEqual(t *testing.T) {
{&Persist{}, &Persist{}, true},
{
&Persist{LegacyFrontendPrivateMachineKey: k1},
&Persist{LegacyFrontendPrivateMachineKey: newPrivate()},
&Persist{PrivateMachineKey: k1},
&Persist{PrivateMachineKey: newPrivate()},
false,
},
{
&Persist{LegacyFrontendPrivateMachineKey: k1},
&Persist{LegacyFrontendPrivateMachineKey: k1},
&Persist{PrivateMachineKey: k1},
&Persist{PrivateMachineKey: k1},
true,
},

View File

@@ -1291,7 +1291,7 @@ func (s *Server) ExpVar() expvar.Var {
m.Set("multiforwarder_deleted", &s.multiForwarderDeleted)
m.Set("packet_forwarder_delete_other_value", &s.removePktForwardOther)
var expvarVersion expvar.String
expvarVersion.Set(version.Long)
expvarVersion.Set(version.LONG)
m.Set("version", &expvarVersion)
return m
}

15
go.mod
View File

@@ -9,7 +9,6 @@ require (
github.com/coreos/go-iptables v0.4.5
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
github.com/gliderlabs/ssh v0.2.2
github.com/go-multierror/multierror v1.0.2
github.com/go-ole/go-ole v1.2.4
github.com/godbus/dbus/v5 v5.0.3
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e
@@ -22,19 +21,19 @@ require (
github.com/miekg/dns v1.1.30
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3
github.com/peterbourgon/ff/v2 v2.0.0
github.com/tailscale/depaware v0.0.0-20201003033024-5d95aab075be
github.com/tailscale/wireguard-go v0.0.0-20201021041318-a6168fd06b3f
github.com/tailscale/depaware v0.0.0-20200914232109-e09ee10c1824
github.com/tailscale/winipcfg-go v0.0.0-20200916205758-decb9ee8e170
github.com/tailscale/wireguard-go v0.0.0-20200902185615-1997cf6f9fe4
github.com/tcnksm/go-httpstat v0.2.0
github.com/toqueteos/webbrowser v1.2.0
go4.org/mem v0.0.0-20200706164138-185c595c3ecc
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899
golang.org/x/net v0.0.0-20200822124328-c89045814202
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201004085714-dd60d0447f81
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425
honnef.co/go/tools v0.0.1-2020.1.4
inet.af/netaddr v0.0.0-20200810144936-56928fe48a98
rsc.io/goversion v1.2.0

71
go.sum
View File

@@ -3,8 +3,11 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Masterminds/semver/v3 v3.0.3 h1:znjIyLfpXEDQjOIEWh+ehwpTU14UzUPub3c3sm36u14=
github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI=
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 h1:P5U+E4x5OkVEKQDklVPmzs71WM56RTTRqV4OrDC//Y4=
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro=
@@ -27,8 +30,6 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjr
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
github.com/go-multierror/multierror v1.0.2 h1:AwsKbEXkmf49ajdFJgcFXqSG0aLo0HEyAE9zk9JguJo=
github.com/go-multierror/multierror v1.0.2/go.mod h1:U7SZR/D9jHgt2nkSj8XcbCWdmVM2igraCHQ3HC1HiKY=
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
@@ -60,8 +61,6 @@ github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lxn/walk v0.0.0-20191128110447-55ccb3a9f5c1/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20191128105842-2da648fda5b4/go.mod h1:ouWl4wViUNh8tPSIwxTVMuS014WakR1hqvBc2I0bMoA=
github.com/mattn/go-zglob v0.0.1 h1:xsEx/XUoVlI6yXjqBK062zYhRTZltCNmYPx6v+8DNaY=
github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
@@ -86,19 +85,24 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b h1:+gCnWOZV8Z/8jehJ2CdqB47Z3S+SREmQcuXkRFLNsiI=
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tailscale/depaware v0.0.0-20201003033024-5d95aab075be h1:ZKe3kVGbu/goUVxXcaCPbQ4b0STQ5NsCpG90CG6mw/c=
github.com/tailscale/depaware v0.0.0-20201003033024-5d95aab075be/go.mod h1:jissDaJNHiyV2tFdr3QyNEfsZrax/i2yQiSO+CljThI=
github.com/tailscale/wireguard-go v0.0.0-20200921221757-11a958a67bdd h1:yEWpro9EdxGgkt24NInVnONIJxRLURH5c37Ki5+06EE=
github.com/tailscale/wireguard-go v0.0.0-20200921221757-11a958a67bdd/go.mod h1:WXq+IkSOJGIgfF1XW+4z4oW+LX/TXzU9DcKlT5EZLi4=
github.com/tailscale/wireguard-go v0.0.0-20201008164108-2c83f43a9859 h1:Z7bXXCYRg/8sjSyKTk0V8Yso/gQjNvPb10DBemKuz+A=
github.com/tailscale/wireguard-go v0.0.0-20201008164108-2c83f43a9859/go.mod h1:WXq+IkSOJGIgfF1XW+4z4oW+LX/TXzU9DcKlT5EZLi4=
github.com/tailscale/wireguard-go v0.0.0-20201021041318-a6168fd06b3f h1:KMx58dbn2YCutzOvjNHgmvbwQH7nGE8H+J42Nenjl/M=
github.com/tailscale/wireguard-go v0.0.0-20201021041318-a6168fd06b3f/go.mod h1:WXq+IkSOJGIgfF1XW+4z4oW+LX/TXzU9DcKlT5EZLi4=
github.com/tailscale/depaware v0.0.0-20200909185729-8ca448326e3a h1:PjVmKyzFfgQrdrmX7kpRkKXkvwMZP/MF3nJT/WJyjW8=
github.com/tailscale/depaware v0.0.0-20200909185729-8ca448326e3a/go.mod h1:H0k9mKUzaDpb22Zn2FiSzY3zeRbAiZ7wUFxKJ7kp8GE=
github.com/tailscale/depaware v0.0.0-20200910145248-cb751026f10d h1:tjLqVTL0IZdV2kBvM2WqCjug6IBJTWOYiM8wqPk2Xp0=
github.com/tailscale/depaware v0.0.0-20200910145248-cb751026f10d/go.mod h1:H0k9mKUzaDpb22Zn2FiSzY3zeRbAiZ7wUFxKJ7kp8GE=
github.com/tailscale/depaware v0.0.0-20200914201916-3f1070fd0d55 h1:hLAgSpbb0rfOq9jziQbvOsOarpfTDxnFbG8kG6INFeY=
github.com/tailscale/depaware v0.0.0-20200914201916-3f1070fd0d55/go.mod h1:nyzwKFaLuckPu3dAJHH7B6lMi4xDBWzD0r3pEpGZm2Y=
github.com/tailscale/depaware v0.0.0-20200914232109-e09ee10c1824 h1:MD/YQ8xI070ZqFC3SnLAlhPPUNfeRKErQaAaXc/r4dQ=
github.com/tailscale/depaware v0.0.0-20200914232109-e09ee10c1824/go.mod h1:nyzwKFaLuckPu3dAJHH7B6lMi4xDBWzD0r3pEpGZm2Y=
github.com/tailscale/winipcfg-go v0.0.0-20200413171540-609dcf2df55f h1:uFj5bslHsMzxIM8UTjAhq4VXeo6GfNW91rpoh/WMJaY=
github.com/tailscale/winipcfg-go v0.0.0-20200413171540-609dcf2df55f/go.mod h1:x880GWw5fvrl2DVTQ04ttXQD4DuppTt1Yz6wLibbjNE=
github.com/tailscale/winipcfg-go v0.0.0-20200916205758-decb9ee8e170 h1:vJ0twi0120W/LKiDxzXROSVx1F4pIKZBQqvtPahnH60=
github.com/tailscale/winipcfg-go v0.0.0-20200916205758-decb9ee8e170/go.mod h1:x880GWw5fvrl2DVTQ04ttXQD4DuppTt1Yz6wLibbjNE=
github.com/tailscale/wireguard-go v0.0.0-20200902185615-1997cf6f9fe4 h1:UiTXdZChEWxxci7bx+jS9OyHQx2IA8zmMWQqp5wfP7c=
github.com/tailscale/wireguard-go v0.0.0-20200902185615-1997cf6f9fe4/go.mod h1:WXq+IkSOJGIgfF1XW+4z4oW+LX/TXzU9DcKlT5EZLi4=
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
@@ -107,22 +111,16 @@ github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go4.org/mem v0.0.0-20200706164138-185c595c3ecc h1:paujszgN6SpsO/UsXC7xax3gQAKz/XQKCYZLQdU34Tw=
go4.org/mem v0.0.0-20200706164138-185c595c3ecc/go.mod h1:NEYvpHWemiG/E5UWfaN5QAIGZeT1sa0Z2UNk6oeMb/k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88=
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
@@ -130,57 +128,48 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 h1:WQ8q63x+f/zpC8Ac1s9wLElVoHhm32p6tudrU72n1QA=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190310054646-10058d7d4faa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 h1:5B6i6EAiSYyejWfvc5Rc9BbI3rzIsrrXfAQBWnYfn+w=
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d h1:QQrM/CCYEzTs91GZylDCQjGHudbPTxF/1fvXdVh5lMo=
golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d h1:/iIZNFGxc/a7C3yWjGcnboV+Tkc7mxr+p6fDztwoxuM=
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20201001230009-b5b87423c93b/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d h1:vWQvJ/Z0Lu+9/8oQ/pAYXNzbc7CMnBl+tULGVHOy3oE=
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wireguard v0.0.20200321-0.20200715051853-507f148e1c42 h1:SrR1hmxGKKarHEEDvaHxatwnqE3uT+7jvMcin6SHOkw=
golang.zx2c4.com/wireguard v0.0.20200321-0.20200715051853-507f148e1c42/go.mod h1:GJvYs5O24/ASlwPiRklVnjMx2xQzrOic0DuU6GvYJL4=
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201004085714-dd60d0447f81 h1:cT2oWlz8v9g7bjFZclT362akxJJfGv9d7ccKu6GQUbA=
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201004085714-dd60d0447f81/go.mod h1:GaK5zcgr5XE98WaRzIDilumDBp5/yP8j2kG/LCDnvAM=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -21,7 +21,6 @@ type State int
const (
NoState = State(iota)
InUseOtherUser
NeedsLogin
NeedsMachineAuth
Stopped
@@ -34,14 +33,8 @@ const (
const GoogleIDTokenType = "ts_android_google_login"
func (s State) String() string {
return [...]string{
"NoState",
"InUseOtherUser",
"NeedsLogin",
"NeedsMachineAuth",
"Stopped",
"Starting",
"Running"}[s]
return [...]string{"NoState", "NeedsLogin", "NeedsMachineAuth",
"Stopped", "Starting", "Running"}[s]
}
// EngineStatus contains WireGuard engine stats.
@@ -60,7 +53,7 @@ type EngineStatus struct {
type Notify struct {
_ structs.Incomparable
Version string // version number of IPN backend
ErrMessage *string // critical error message, if any; for InUseOtherUser, the details
ErrMessage *string // critical error message, if any
LoginFinished *empty.Message // event: non-nil when login process succeeded
State *State // current IPN state has changed
Prefs *Prefs // preferences were changed
@@ -88,14 +81,14 @@ type Notify struct {
// shared by several consecutive users. Ideally we would just use the
// username of the connected frontend as the StateKey.
//
// Various platforms currently set StateKey in different ways:
//
// * the macOS/iOS GUI apps set it to "ipn-go-bridge"
// * the Android app sets it to "ipn-android"
// * on Windows, it's the empty string (in client mode) or, via
// LocalBackend.userID, a string like "user-$USER_ID" (used in
// server mode).
// * on Linux/etc, it's always "_daemon" (ipn.GlobalDaemonStateKey)
// However, on Windows, there seems to be no safe way to figure out
// the owning user of a process connected over IPC mechanisms
// (sockets, named pipes). So instead, on Windows, we use a
// capability-oriented system where the frontend generates a random
// identifier for itself, and uses that as the StateKey when talking
// to the backend. That way, while we can't identify an OS user by
// name, we can tell two different users apart, because they'll have
// different opaque state keys (and no access to each others's keys).
type StateKey string
type Options struct {
@@ -104,8 +97,7 @@ type Options struct {
// StateKey and Prefs together define the state the backend should
// use:
// - StateKey=="" && Prefs!=nil: use Prefs for internal state,
// don't persist changes in the backend, except for the machine key
// for migration purposes.
// don't persist changes in the backend.
// - StateKey!="" && Prefs==nil: load the given backend-side
// state and use/update that.
// - StateKey!="" && Prefs!=nil: like the previous case, but do

View File

@@ -7,10 +7,9 @@ package ipnserver
import (
"bufio"
"context"
"errors"
"fmt"
"html"
"io"
"io/ioutil"
"log"
"net"
"net/http"
@@ -19,7 +18,6 @@ import (
"os/signal"
"os/user"
"runtime"
"strings"
"sync"
"syscall"
"time"
@@ -27,7 +25,6 @@ import (
"inet.af/netaddr"
"tailscale.com/control/controlclient"
"tailscale.com/ipn"
"tailscale.com/log/filelogger"
"tailscale.com/logtail/backoff"
"tailscale.com/net/netstat"
"tailscale.com/safesocket"
@@ -71,12 +68,6 @@ type Options struct {
// its existing state, and accepts new frontend connections. If
// false, the server dumps its state and becomes idle.
//
// This is effectively whether the platform is in "server
// mode" by default. On Linux, it's true; on Windows, it's
// false. But on some platforms (currently only Windows), the
// "server mode" can be overridden at runtime with a change in
// Prefs.ForceDaemon/WantRunning.
//
// To support CLI connections (notably, "tailscale status"),
// the actual definition of "disconnect" is when the
// connection count transitions from 1 to 0.
@@ -90,183 +81,30 @@ type Options struct {
// server is an IPN backend and its set of 0 or more active connections
// talking to an IPN backend.
type server struct {
b *ipn.LocalBackend
logf logger.Logf
// resetOnZero is whether to call bs.Reset on transition from
// 1->0 connections. That is, this is whether the backend is
// being run in "client mode" that requires an active GUI
// connection (such as on Windows by default). Even if this
// is true, the ForceDaemon pref can override this.
resetOnZero bool
resetOnZero bool // call bs.Reset on transition from 1->0 connections
bsMu sync.Mutex // lock order: bsMu, then mu
bs *ipn.BackendServer
mu sync.Mutex
serverModeUser *user.User // or nil if not in server mode
lastUserID string // tracks last userid; on change, Reset state for paranoia
allClients map[net.Conn]connIdentity // HTTP or IPN
clients map[net.Conn]bool // subset of allClients; only IPN protocol
disconnectSub map[chan<- struct{}]struct{} // keys are subscribers of disconnects
}
// connIdentity represents the owner of a localhost TCP connection.
type connIdentity struct {
Unknown bool
Pid int
UserID string
User *user.User
}
// getConnIdentity returns the localhost TCP connection's identity information
// (pid, userid, user). If it's not Windows (for now), it returns a nil error
// and a ConnIdentity with Unknown set true. It's only an error if we expected
// to be able to map it and couldn't.
func (s *server) getConnIdentity(c net.Conn) (ci connIdentity, err error) {
if runtime.GOOS != "windows" { // for now; TODO: expand to other OSes
return connIdentity{Unknown: true}, nil
}
la, err := netaddr.ParseIPPort(c.LocalAddr().String())
if err != nil {
return ci, fmt.Errorf("parsing local address: %w", err)
}
ra, err := netaddr.ParseIPPort(c.RemoteAddr().String())
if err != nil {
return ci, fmt.Errorf("parsing local remote: %w", err)
}
if !la.IP.IsLoopback() || !ra.IP.IsLoopback() {
return ci, errors.New("non-loopback connection")
}
tab, err := netstat.Get()
if err != nil {
return ci, fmt.Errorf("failed to get local connection table: %w", err)
}
pid := peerPid(tab.Entries, la, ra)
if pid == 0 {
return ci, errors.New("no local process found matching localhost connection")
}
ci.Pid = pid
uid, err := pidowner.OwnerOfPID(pid)
if err != nil {
var hint string
if runtime.GOOS == "windows" {
hint = " (WSL?)"
}
return ci, fmt.Errorf("failed to map connection's pid to a user%s: %w", hint, err)
}
ci.UserID = uid
u, err := s.lookupUserFromID(uid)
if err != nil {
return ci, fmt.Errorf("failed to look up user from userid: %w", err)
}
ci.User = u
return ci, nil
}
func (s *server) lookupUserFromID(uid string) (*user.User, error) {
u, err := user.LookupId(uid)
if err != nil && runtime.GOOS == "windows" && errors.Is(err, syscall.Errno(0x534)) {
s.logf("[warning] issue 869: os/user.LookupId failed; ignoring")
// Work around https://github.com/tailscale/tailscale/issues/869 for
// now. We don't strictly need the username. It's just a nice-to-have.
// So make up a *user.User if their machine is broken in this way.
return &user.User{
Uid: uid,
Username: "unknown-user-" + uid,
Name: "unknown user " + uid,
}, nil
}
return u, err
}
// blockWhileInUse blocks while until either a Read from conn fails
// (i.e. it's closed) or until the server is able to accept ci as a
// user.
func (s *server) blockWhileInUse(conn io.Reader, ci connIdentity) {
s.logf("blocking client while server in use; connIdentity=%v", ci)
connDone := make(chan struct{})
go func() {
io.Copy(ioutil.Discard, conn)
close(connDone)
}()
ch := make(chan struct{}, 1)
s.registerDisconnectSub(ch, true)
defer s.registerDisconnectSub(ch, false)
for {
select {
case <-connDone:
s.logf("blocked client Read completed; connIdentity=%v", ci)
return
case <-ch:
s.mu.Lock()
err := s.checkConnIdentityLocked(ci)
s.mu.Unlock()
if err == nil {
s.logf("unblocking client, server is free; connIdentity=%v", ci)
// Server is now available again for a new user.
// TODO(bradfitz): keep this connection alive. But for
// now just return and have our caller close the connection
// (which unblocks the io.Copy goroutine we started above)
// and then the client (e.g. Windows) will reconnect and
// discover that it works.
return
}
}
}
mu sync.Mutex
clients map[net.Conn]bool
}
func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
// First see if it's an HTTP request.
br := bufio.NewReader(c)
// First see if it's an HTTP request.
c.SetReadDeadline(time.Now().Add(time.Second))
peek, _ := br.Peek(4)
c.SetReadDeadline(time.Time{})
isHTTPReq := string(peek) == "GET "
ci, err := s.addConn(c, isHTTPReq)
if err != nil {
if isHTTPReq {
fmt.Fprintf(c, "HTTP/1.0 500 Nope\r\nContent-Type: text/plain\r\nX-Content-Type-Options: nosniff\r\n\r\n%s\n", err.Error())
c.Close()
return
}
defer c.Close()
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
bs := ipn.NewBackendServer(logf, nil, serverToClient)
_, occupied := err.(inUseOtherUserError)
if occupied {
bs.SendInUseOtherUserErrorMessage(err.Error())
s.blockWhileInUse(c, ci)
} else {
bs.SendErrorMessage(err.Error())
time.Sleep(time.Second)
}
if string(peek) == "GET " {
http.Serve(&oneConnListener{altReaderNetConn{br, c}}, localhostHandler(c))
return
}
// Tell the LocalBackend about the identity we're now running as.
s.b.SetCurrentUserID(ci.UserID)
if isHTTPReq {
httpServer := http.Server{
// Localhost connections are cheap; so only do
// keep-alives for a short period of time, as these
// active connections lock the server into only serving
// that user. If the user has this page open, we don't
// want another switching user to be locked out for
// minutes. 5 seconds is enough to let browser hit
// favicon.ico and such.
IdleTimeout: 5 * time.Second,
ErrorLog: logger.StdLogger(logf),
Handler: s.localhostHandler(ci),
}
httpServer.Serve(&oneConnListener{&protoSwitchConn{s: s, br: br, Conn: c}})
return
}
defer s.removeAndCloseConn(c)
s.addConn(c)
logf("incoming control connection")
defer s.removeAndCloseConn(c)
for ctx.Err() == nil {
msg, err := ipn.ReadMsg(br)
if err != nil {
@@ -287,126 +125,25 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
}
}
// inUseOtherUserError is the error type for when the server is in use
// by a different local user.
type inUseOtherUserError struct{ error }
func (e inUseOtherUserError) Unwrap() error { return e.error }
// checkConnIdentityLocked checks whether the provided identity is
// allowed to connect to the server.
//
// The returned error, when non-nil, will be of type inUseOtherUserError.
//
// s.mu must be held.
func (s *server) checkConnIdentityLocked(ci connIdentity) error {
// If clients are already connected, verify they're the same user.
// This mostly matters on Windows at the moment.
if len(s.allClients) > 0 {
var active connIdentity
for _, active = range s.allClients {
break
}
if ci.UserID != active.UserID {
//lint:ignore ST1005 we want to capitalize Tailscale here
return inUseOtherUserError{fmt.Errorf("Tailscale already in use by %s, pid %d", active.User.Username, active.Pid)}
}
}
if su := s.serverModeUser; su != nil && ci.UserID != su.Uid {
//lint:ignore ST1005 we want to capitalize Tailscale here
return inUseOtherUserError{fmt.Errorf("Tailscale already in use by %s", su.Username)}
}
return nil
}
// registerDisconnectSub adds ch as a subscribe to connection disconnect
// events. If add is false, the subscriber is removed.
func (s *server) registerDisconnectSub(ch chan<- struct{}, add bool) {
func (s *server) addConn(c net.Conn) {
s.mu.Lock()
defer s.mu.Unlock()
if add {
if s.disconnectSub == nil {
s.disconnectSub = make(map[chan<- struct{}]struct{})
}
s.disconnectSub[ch] = struct{}{}
} else {
delete(s.disconnectSub, ch)
}
}
// addConn adds c to the server's list of clients.
//
// If the returned error is of type inUseOtherUserError then the
// returned connIdentity is also valid.
func (s *server) addConn(c net.Conn, isHTTP bool) (ci connIdentity, err error) {
ci, err = s.getConnIdentity(c)
if err != nil {
return
}
// If the connected user changes, reset the backend server state to make
// sure node keys don't leak between users.
var doReset bool
defer func() {
if doReset {
s.logf("identity changed; resetting server")
s.bsMu.Lock()
s.bs.Reset()
s.bsMu.Unlock()
}
}()
s.mu.Lock()
defer s.mu.Unlock()
if s.clients == nil {
s.clients = map[net.Conn]bool{}
}
if s.allClients == nil {
s.allClients = map[net.Conn]connIdentity{}
}
if err := s.checkConnIdentityLocked(ci); err != nil {
return ci, err
}
if !isHTTP {
s.clients[c] = true
}
s.allClients[c] = ci
if s.lastUserID != ci.UserID {
if s.lastUserID != "" {
doReset = true
}
s.lastUserID = ci.UserID
}
return ci, nil
s.clients[c] = true
}
func (s *server) removeAndCloseConn(c net.Conn) {
s.mu.Lock()
delete(s.clients, c)
delete(s.allClients, c)
remain := len(s.allClients)
for sub := range s.disconnectSub {
select {
case sub <- struct{}{}:
default:
}
}
remain := len(s.clients)
s.mu.Unlock()
if remain == 0 && s.resetOnZero {
if s.b.InServerMode() {
s.logf("client disconnected; staying alive in server mode")
} else {
s.logf("client disconnected; stopping server")
s.bsMu.Lock()
s.bs.Reset()
s.bsMu.Unlock()
}
s.bsMu.Lock()
s.bs.Reset()
s.bsMu.Unlock()
}
c.Close()
}
@@ -421,48 +158,9 @@ func (s *server) stopAll() {
s.clients = nil
}
// setServerModeUserLocked is called when we're in server mode but our s.serverModeUser is nil.
//
// s.mu must be held
func (s *server) setServerModeUserLocked() {
var ci connIdentity
var ok bool
for _, ci = range s.allClients {
ok = true
break
}
if !ok {
s.logf("ipnserver: [unexpected] now in server mode, but no connected client")
return
}
if ci.Unknown {
return
}
if ci.User != nil {
s.logf("ipnserver: now in server mode; user=%v", ci.User.Username)
s.serverModeUser = ci.User
} else {
s.logf("ipnserver: [unexpected] now in server mode, but nil User")
}
}
func (s *server) writeToClients(b []byte) {
inServerMode := s.b.InServerMode()
s.mu.Lock()
defer s.mu.Unlock()
if inServerMode {
if s.serverModeUser == nil {
s.setServerModeUserLocked()
}
} else {
if s.serverModeUser != nil {
s.logf("ipnserver: no longer in server mode")
s.serverModeUser = nil
}
}
for c := range s.clients {
ipn.WriteMsg(c, b)
}
@@ -480,7 +178,6 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
}
server := &server{
logf: logf,
resetOnZero: !opts.SurviveDisconnects,
}
@@ -497,11 +194,12 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
logf("Listening on %v", listen.Addr())
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
eng, err := getEngine()
if err != nil {
logf("ipnserver: initial getEngine call: %v", err)
logf("Initial getEngine call: %v", err)
for i := 1; ctx.Err() == nil; i++ {
c, err := listen.Accept()
if err != nil {
@@ -509,14 +207,14 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
bo.BackOff(ctx, err)
continue
}
logf("ipnserver: try%d: trying getEngine again...", i)
logf("%d: trying getEngine again...", i)
eng, err = getEngine()
if err == nil {
logf("%d: GetEngine worked; exiting failure loop", i)
unservedConn = c
break
}
logf("ipnserver%d: getEngine failed again: %v", i, err)
logf("%d: getEngine failed again: %v", i, err)
errMsg := err.Error()
go func() {
defer c.Close()
@@ -537,24 +235,6 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
if err != nil {
return fmt.Errorf("ipn.NewFileStore(%q): %v", opts.StatePath, err)
}
if opts.AutostartStateKey == "" {
autoStartKey, err := store.ReadState(ipn.ServerModeStartKey)
if err != nil && err != ipn.ErrStateNotExist {
return fmt.Errorf("calling ReadState on %s: %w", opts.StatePath, err)
}
key := string(autoStartKey)
if strings.HasPrefix(key, "user-") {
uid := strings.TrimPrefix(key, "user-")
u, err := server.lookupUserFromID(uid)
if err != nil {
logf("ipnserver: found server mode auto-start key %q; failed to load user: %v", key, err)
} else {
logf("ipnserver: found server mode auto-start key %q (user %s)", key, u.Username)
server.serverModeUser = u
}
opts.AutostartStateKey = ipn.StateKey(key)
}
}
} else {
store = &ipn.MemoryStore{}
}
@@ -570,16 +250,18 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
if opts.DebugMux != nil {
opts.DebugMux.HandleFunc("/debug/ipn", func(w http.ResponseWriter, r *http.Request) {
serveHTMLStatus(w, b)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
st := b.Status()
// TODO(bradfitz): add LogID and opts to st?
st.WriteHTML(w)
})
}
server.b = b
server.bs = ipn.NewBackendServer(logf, b, server.writeToClients)
if opts.AutostartStateKey != "" {
server.bs.GotCommand(&ipn.Command{
Version: version.Long,
Version: version.LONG,
Start: &ipn.StartArgs{
Opts: ipn.Options{
StateKey: opts.AutostartStateKey,
@@ -610,11 +292,6 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
return ctx.Err()
}
// BabysitProc runs the current executable as a child process with the
// provided args, capturing its output, writing it to files, and
// restarting the process on any crashes.
//
// It's only currently (2020-10-29) used on Windows.
func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
executable, err := os.Executable()
@@ -622,14 +299,6 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
panic("cannot determine executable: " + err.Error())
}
if runtime.GOOS == "windows" {
if len(args) != 2 && args[0] != "/subproc" {
panic(fmt.Sprintf("unexpected arguments %q", args))
}
logID := args[1]
logf = filelogger.New("tailscale-service", logID, logf)
}
var proc struct {
mu sync.Mutex
p *os.Process
@@ -767,40 +436,54 @@ func (l *oneConnListener) Addr() net.Addr { return dummyAddr("unused-address") }
func (a dummyAddr) Network() string { return string(a) }
func (a dummyAddr) String() string { return string(a) }
// protoSwitchConn is a net.Conn that's we want to speak HTTP to but
// it's already had a few bytes read from it to determine that it's
// HTTP. So we Read from its bufio.Reader. On Close, we we tell the
// server it's closed, so the server can account the who's connected.
type protoSwitchConn struct {
s *server
type altReaderNetConn struct {
r io.Reader
net.Conn
br *bufio.Reader
closeOnce sync.Once
}
func (psc *protoSwitchConn) Read(p []byte) (int, error) { return psc.br.Read(p) }
func (psc *protoSwitchConn) Close() error {
psc.closeOnce.Do(func() { psc.s.removeAndCloseConn(psc.Conn) })
return nil
}
func (a altReaderNetConn) Read(p []byte) (int, error) { return a.r.Read(p) }
func (s *server) localhostHandler(ci connIdentity) http.Handler {
func localhostHandler(c net.Conn) http.Handler {
la, lerr := netaddr.ParseIPPort(c.LocalAddr().String())
ra, rerr := netaddr.ParseIPPort(c.RemoteAddr().String())
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if ci.Unknown {
io.WriteString(w, "<html><title>Tailscale</title><body><h1>Tailscale</h1>This is the local Tailscale daemon.")
fmt.Fprintf(w, "<html><body><h1>tailscale</h1>\n")
if lerr != nil || rerr != nil {
io.WriteString(w, "failed to parse remote address")
return
}
serveHTMLStatus(w, s.b)
if !la.IP.IsLoopback() || !ra.IP.IsLoopback() {
io.WriteString(w, "not loopback")
return
}
tab, err := netstat.Get()
if err == netstat.ErrNotImplemented {
io.WriteString(w, "status page not available on "+runtime.GOOS)
return
}
if err != nil {
io.WriteString(w, "failed to get netstat table")
return
}
pid := peerPid(tab.Entries, la, ra)
if pid == 0 {
io.WriteString(w, "peer pid not found")
return
}
uid, err := pidowner.OwnerOfPID(pid)
if err != nil {
io.WriteString(w, "owner of peer pid not found")
return
}
u, err := user.LookupId(uid)
if err != nil {
io.WriteString(w, "User lookup failed")
return
}
fmt.Fprintf(w, "Hello, %s", html.EscapeString(u.Username))
})
}
func serveHTMLStatus(w http.ResponseWriter, b *ipn.LocalBackend) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
st := b.Status()
// TODO(bradfitz): add LogID and opts to st?
st.WriteHTML(w)
}
func peerPid(entries []netstat.Entry, la, ra netaddr.IPPort) int {
for _, e := range entries {
if e.Local == ra && e.Remote == la {

View File

@@ -7,6 +7,8 @@ package ipnserver_test
import (
"context"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
@@ -23,7 +25,11 @@ func TestRunMultipleAccepts(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
td := t.TempDir()
td, err := ioutil.TempDir("", "TestRunMultipleAccepts")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(td)
socketPath := filepath.Join(td, "tailscale.sock")
logf := func(format string, args ...interface{}) {

View File

@@ -5,12 +5,9 @@
package ipn
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"runtime"
"strings"
"sync"
"time"
@@ -37,15 +34,6 @@ import (
"tailscale.com/wgengine/tsdns"
)
var controlDebugFlags = getControlDebugFlags()
func getControlDebugFlags() []string {
if e := os.Getenv("TS_DEBUG_CONTROL_FLAGS"); e != "" {
return strings.Split(e, ",")
}
return nil
}
// LocalBackend is the glue between the major pieces of the Tailscale
// network software: the cloud control plane (via controlclient), the
// network data plane (via wgengine), and the user-facing UIs and CLIs
@@ -62,39 +50,32 @@ type LocalBackend struct {
ctxCancel context.CancelFunc // cancels ctx
logf logger.Logf // general logging
keyLogf logger.Logf // for printing list of peers on change
statsLogf logger.Logf // for printing peers stats on change
e wgengine.Engine
store StateStore
backendLogID string
portpoll *portlist.Poller // may be nil
portpollOnce sync.Once // guards starting readPoller
gotPortPollRes chan struct{} // closed upon first readPoller result
serverURL string // tailcontrol URL
portpollOnce sync.Once
serverURL string // tailcontrol URL
newDecompressor func() (controlclient.Decompressor, error)
filterHash string
// The mutex protects the following elements.
mu sync.Mutex
notify func(Notify)
c *controlclient.Client
stateKey StateKey // computed in part from user-provided value
userID string // current controlling user ID (for Windows, primarily)
prefs *Prefs
inServerMode bool
machinePrivKey wgcfg.PrivateKey
state State
mu sync.Mutex
notify func(Notify)
c *controlclient.Client
stateKey StateKey
prefs *Prefs
state State
// hostinfo is mutated in-place while mu is held.
hostinfo *tailcfg.Hostinfo
// netMap is not mutated in-place once set.
netMap *controlclient.NetworkMap
activeLogin string // last logged LoginName from netMap
engineStatus EngineStatus
endpoints []string
blocked bool
authURL string
interact bool
prevIfState *interfaces.State
interact int
// statusLock must be held before calling statusChanged.Wait() or
// statusChanged.Broadcast().
@@ -119,17 +100,15 @@ func NewLocalBackend(logf logger.Logf, logid string, store StateStore, e wgengin
}
b := &LocalBackend{
ctx: ctx,
ctxCancel: cancel,
logf: logf,
keyLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
statsLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
e: e,
store: store,
backendLogID: logid,
state: NoState,
portpoll: portpoll,
gotPortPollRes: make(chan struct{}),
ctx: ctx,
ctxCancel: cancel,
logf: logf,
keyLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
e: e,
store: store,
backendLogID: logid,
state: NoState,
portpoll: portpoll,
}
e.SetLinkChangeCallback(b.linkChange)
b.statusChanged = sync.NewCond(&b.statusLock)
@@ -137,31 +116,15 @@ func NewLocalBackend(logf logger.Logf, logid string, store StateStore, e wgengin
return b, nil
}
// linkChange is called (in a new goroutine) by wgengine when its link monitor
// detects a network change.
func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
b.mu.Lock()
defer b.mu.Unlock()
hadPAC := b.prevIfState.HasPAC()
b.prevIfState = ifst
networkUp := ifst.AnyInterfaceUp()
if b.c != nil {
go b.c.SetPaused(b.state == Stopped || !networkUp)
}
// If the PAC-ness of the network changed, reconfig wireguard+route to
// add/remove subnets.
if hadPAC != ifst.HasPAC() {
b.logf("linkChange: in state %v; PAC changed from %v->%v", b.state, hadPAC, ifst.HasPAC())
switch b.state {
case NoState, Stopped:
// Do nothing.
default:
go b.authReconfig()
}
}
// TODO(bradfitz): on a major link change, ask controlclient
// whether its host (e.g. login.tailscale.com) is reachable.
// If not, down the world and poll for a bit. Windows' WinHTTP
// service might be unable to resolve its WPAD PAC URL if we
// have DNS/routes configured. So we need to remove that DNS
// and those routes to let it figure out its proxy
// settings. Once it's back up and happy, then we can resume
// and our connection to the control server would work again.
}
// Shutdown halts the backend and all its sub-components. The backend
@@ -269,14 +232,8 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
b.prefs.Persist = st.Persist.Clone()
}
}
if temporarilySetMachineKeyInPersist() && b.prefs.Persist != nil &&
b.prefs.Persist.LegacyFrontendPrivateMachineKey.IsZero() {
b.prefs.Persist.LegacyFrontendPrivateMachineKey = b.machinePrivKey
prefsChanged = true
}
if st.NetMap != nil {
b.setNetMapLocked(st.NetMap)
b.netMap = st.NetMap
}
if st.URL != "" {
b.authURL = st.URL
@@ -315,16 +272,23 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
b.updateFilter(st.NetMap, prefs)
b.e.SetNetworkMap(st.NetMap)
if !dnsMapsEqual(st.NetMap, netMap) {
b.updateDNSMap(st.NetMap)
}
b.e.SetDERPMap(st.NetMap.DERPMap)
disableDERP := prefs != nil && prefs.DisableDERP
if disableDERP {
b.e.SetDERPMap(nil)
} else {
b.e.SetDERPMap(st.NetMap.DERPMap)
}
b.send(Notify{NetMap: st.NetMap})
}
if st.URL != "" {
b.logf("Received auth URL: %.20v...", st.URL)
if interact {
if interact > 0 {
b.popBrowserAuthNow()
}
}
@@ -346,8 +310,9 @@ func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) {
return
}
es := b.parseWgStatus(s)
b.mu.Lock()
es := b.parseWgStatusLocked(s)
c := b.c
b.engineStatus = es
b.endpoints = append([]string{}, s.LocalAddrs...)
@@ -415,44 +380,18 @@ func (b *LocalBackend) Start(opts Options) error {
return fmt.Errorf("loading requested state: %v", err)
}
b.inServerMode = b.prefs.ForceDaemon
b.serverURL = b.prefs.ControlURL
hostinfo.RoutableIPs = append(hostinfo.RoutableIPs, b.prefs.AdvertiseRoutes...)
hostinfo.RequestTags = append(hostinfo.RequestTags, b.prefs.AdvertiseTags...)
if b.inServerMode || runtime.GOOS == "windows" {
b.logf("Start: serverMode=%v", b.inServerMode)
}
applyPrefsToHostinfo(hostinfo, b.prefs)
b.notify = opts.Notify
b.setNetMapLocked(nil)
b.netMap = nil
persist := b.prefs.Persist
machinePrivKey := b.machinePrivKey
b.mu.Unlock()
b.updateFilter(nil, nil)
if b.portpoll != nil {
b.portpollOnce.Do(func() {
go b.portpoll.Run(b.ctx)
go b.readPoller()
// Give the poller a second to get results to
// prevent it from restarting our map poll
// HTTP request (via doSetHostinfoFilterServices >
// cli.SetHostinfo). In practice this is very quick.
t0 := time.Now()
timer := time.NewTimer(time.Second)
select {
case <-b.gotPortPollRes:
b.logf("got initial portlist info in %v", time.Since(t0).Round(time.Millisecond))
timer.Stop()
case <-timer.C:
b.logf("timeout waiting for initial portlist")
}
})
}
var discoPublic tailcfg.DiscoKey
if controlclient.Debug.Disco {
discoPublic = b.e.DiscoPublicKey()
@@ -464,22 +403,29 @@ func (b *LocalBackend) Start(opts Options) error {
persist = &controlclient.Persist{}
}
cli, err := controlclient.New(controlclient.Options{
MachinePrivateKey: machinePrivKey,
Logf: logger.WithPrefix(b.logf, "control: "),
Persist: *persist,
ServerURL: b.serverURL,
AuthKey: opts.AuthKey,
Hostinfo: hostinfo,
KeepAlive: true,
NewDecompressor: b.newDecompressor,
HTTPTestClient: opts.HTTPTestClient,
DiscoPublicKey: discoPublic,
DebugFlags: controlDebugFlags,
Logf: logger.WithPrefix(b.logf, "control: "),
Persist: *persist,
ServerURL: b.serverURL,
AuthKey: opts.AuthKey,
Hostinfo: hostinfo,
KeepAlive: true,
NewDecompressor: b.newDecompressor,
HTTPTestClient: opts.HTTPTestClient,
DiscoPublicKey: discoPublic,
})
if err != nil {
return err
}
// At this point, we have finished using hostinfo without synchronization,
// so it is safe to start readPoller which concurrently writes to it.
if b.portpoll != nil {
b.portpollOnce.Do(func() {
go b.portpoll.Run(b.ctx)
go b.readPoller()
})
}
b.mu.Lock()
b.c = cli
endpoints := b.endpoints
@@ -495,12 +441,6 @@ func (b *LocalBackend) Start(opts Options) error {
b.mu.Lock()
prefs := b.prefs.Clone()
if temporarilySetMachineKeyInPersist() && prefs.Persist != nil &&
prefs.Persist.LegacyFrontendPrivateMachineKey.IsZero() {
prefs.Persist.LegacyFrontendPrivateMachineKey = b.machinePrivKey
}
b.mu.Unlock()
blid := b.backendLogID
@@ -634,7 +574,6 @@ func (b *LocalBackend) updateDNSMap(netMap *controlclient.NetworkMap) {
// readPoller is a goroutine that receives service lists from
// b.portpoll and propagates them into the controlclient's HostInfo.
func (b *LocalBackend) readPoller() {
n := 0
for {
ports, ok := <-b.portpoll.C
if !ok {
@@ -661,11 +600,6 @@ func (b *LocalBackend) readPoller() {
b.mu.Unlock()
b.doSetHostinfoFilterServices(hi)
n++
if n == 1 {
close(b.gotPortPollRes)
}
}
}
@@ -677,7 +611,7 @@ func (b *LocalBackend) send(n Notify) {
b.mu.Unlock()
if notify != nil {
n.Version = version.Long
n.Version = version.LONG
notify(n)
} else {
b.logf("nil notify callback; dropping %+v", n)
@@ -689,7 +623,7 @@ func (b *LocalBackend) send(n Notify) {
func (b *LocalBackend) popBrowserAuthNow() {
b.mu.Lock()
url := b.authURL
b.interact = false
b.interact = 0
b.authURL = ""
b.mu.Unlock()
@@ -703,171 +637,48 @@ func (b *LocalBackend) popBrowserAuthNow() {
}
}
// initMachineKeyLocked is called to initialize b.machinePrivKey.
//
// b.prefs must already be initialized.
// b.stateKey should be set too, but just for nicer log messages.
// b.mu must be held.
func (b *LocalBackend) initMachineKeyLocked() (err error) {
if temporarilySetMachineKeyInPersist() {
defer func() {
if err != nil {
return
}
if b.prefs != nil && b.prefs.Persist != nil {
b.prefs.Persist.LegacyFrontendPrivateMachineKey = b.machinePrivKey
}
}()
}
if !b.machinePrivKey.IsZero() {
// Already set.
return nil
}
var legacyMachineKey wgcfg.PrivateKey
if b.prefs.Persist != nil {
legacyMachineKey = b.prefs.Persist.LegacyFrontendPrivateMachineKey
}
keyText, err := b.store.ReadState(MachineKeyStateKey)
if err == nil {
if err := b.machinePrivKey.UnmarshalText(keyText); err != nil {
return fmt.Errorf("invalid key in %s key of %v: %w", MachineKeyStateKey, b.store, err)
}
if b.machinePrivKey.IsZero() {
return fmt.Errorf("invalid zero key stored in %v key of %v", MachineKeyStateKey, b.store)
}
if !legacyMachineKey.IsZero() && !bytes.Equal(legacyMachineKey[:], b.machinePrivKey[:]) {
b.logf("frontend-provided legacy machine key ignored; used value from server state")
}
return nil
}
if err != ErrStateNotExist {
return fmt.Errorf("error reading %v key of %v: %w", MachineKeyStateKey, b.store, err)
}
// If we didn't find one already on disk and the prefs already
// have a legacy machine key, use that. Otherwise generate a
// new one.
if !legacyMachineKey.IsZero() {
if b.stateKey == "" {
b.logf("using frontend-provided legacy machine key")
} else {
b.logf("using legacy machine key from state key %q", b.stateKey)
}
b.machinePrivKey = legacyMachineKey
} else {
b.logf("generating new machine key")
var err error
b.machinePrivKey, err = wgcfg.NewPrivateKey()
if err != nil {
return fmt.Errorf("initializing new machine key: %w", err)
}
}
keyText, _ = b.machinePrivKey.MarshalText()
if err := b.store.WriteState(MachineKeyStateKey, keyText); err != nil {
b.logf("error writing machine key to store: %v", err)
return err
}
b.logf("machine key written to store")
return nil
}
// writeServerModeStartState stores the ServerModeStartKey value based on the current
// user and prefs. If userID is blank or prefs is blank, no work is done.
//
// b.mu may either be held or not.
func (b *LocalBackend) writeServerModeStartState(userID string, prefs *Prefs) {
if userID == "" || prefs == nil {
return
}
if prefs.ForceDaemon {
stateKey := StateKey("user-" + userID)
if err := b.store.WriteState(ServerModeStartKey, []byte(stateKey)); err != nil {
b.logf("WriteState error: %v", err)
}
// It's important we do this here too, even if it looks
// redundant with the one in the 'if stateKey != ""'
// check block above. That one won't fire in the case
// where the Windows client started up in client mode.
// This happens when we transition into server mode:
if err := b.store.WriteState(stateKey, prefs.ToBytes()); err != nil {
b.logf("WriteState error: %v", err)
}
} else {
if err := b.store.WriteState(ServerModeStartKey, nil); err != nil {
b.logf("WriteState error: %v", err)
}
}
}
// loadStateLocked sets b.prefs and b.stateKey based on a complex
// combination of key, prefs, and legacyPath. b.mu must be held when
// calling.
func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath string) (err error) {
func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath string) error {
if prefs == nil && key == "" {
panic("state key and prefs are both unset")
}
// Optimistically set stateKey (for initMachineKeyLocked's
// logging), but revert it if we return an error so a later SetPrefs
// call can't pick it up if it's bogus.
b.stateKey = key
defer func() {
if err != nil {
b.stateKey = ""
}
}()
if key == "" {
// Frontend owns the state, we just need to obey it.
//
// If the frontend (e.g. on Windows) supplied the
// optional/legacy machine key then it's used as the
// value instead of making up a new one.
b.logf("using frontend prefs: %s", prefs.Pretty())
// Frontend fully owns the state, we just need to obey it.
b.logf("Using frontend prefs")
b.prefs = prefs.Clone()
if err := b.initMachineKeyLocked(); err != nil {
return fmt.Errorf("initMachineKeyLocked: %w", err)
}
b.writeServerModeStartState(b.userID, b.prefs)
b.stateKey = ""
return nil
}
if prefs != nil {
// Backend owns the state, but frontend is trying to migrate
// state into the backend.
b.logf("importing frontend prefs into backend store; frontend prefs: %s", prefs.Pretty())
b.logf("Importing frontend prefs into backend store")
if err := b.store.WriteState(key, prefs.ToBytes()); err != nil {
return fmt.Errorf("store.WriteState: %v", err)
}
}
b.logf("using backend prefs")
b.logf("Using backend prefs")
bs, err := b.store.ReadState(key)
if err != nil {
if errors.Is(err, ErrStateNotExist) {
if legacyPath != "" {
b.prefs, err = LoadPrefs(legacyPath)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
b.logf("failed to load legacy prefs: %v", err)
}
b.logf("Failed to load legacy prefs: %v", err)
b.prefs = NewPrefs()
} else {
b.logf("imported prefs from relaynode for %q: %v", key, b.prefs.Pretty())
b.logf("Imported state from relaynode for %q", key)
}
} else {
b.prefs = NewPrefs()
b.logf("created empty state for %q: %s", key, b.prefs.Pretty())
}
if err := b.initMachineKeyLocked(); err != nil {
return fmt.Errorf("initMachineKeyLocked: %w", err)
b.logf("Created empty state for %q", key)
}
b.stateKey = key
return nil
}
return fmt.Errorf("store.ReadState(%q): %v", key, err)
@@ -876,10 +687,7 @@ func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath st
if err != nil {
return fmt.Errorf("PrefsFromBytes: %v", err)
}
b.logf("backend prefs for %q: %s", key, b.prefs.Pretty())
if err := b.initMachineKeyLocked(); err != nil {
return fmt.Errorf("initMachineKeyLocked: %w", err)
}
b.stateKey = key
return nil
}
@@ -891,12 +699,6 @@ func (b *LocalBackend) State() State {
return b.state
}
func (b *LocalBackend) InServerMode() bool {
b.mu.Lock()
defer b.mu.Unlock()
return b.inServerMode
}
// getEngineStatus returns a copy of b.engineStatus.
//
// TODO(bradfitz): remove this and use Status() throughout.
@@ -924,7 +726,7 @@ func (b *LocalBackend) Login(token *oauth2.Token) {
func (b *LocalBackend) StartLoginInteractive() {
b.mu.Lock()
b.assertClientLocked()
b.interact = true
b.interact++
url := b.authURL
c := b.c
b.mu.Unlock()
@@ -955,7 +757,7 @@ func (b *LocalBackend) FakeExpireAfter(x time.Duration) {
if e.IsZero() || time.Until(e) > x {
mapCopy.Expiry = time.Now().Add(x)
}
b.setNetMapLocked(&mapCopy)
b.netMap = &mapCopy
b.send(Notify{NetMap: b.netMap})
}
@@ -970,33 +772,30 @@ func (b *LocalBackend) Ping(ipStr string) {
})
}
// parseWgStatusLocked returns an EngineStatus based on s.
//
// b.mu must be held; mostly because the caller is about to anyway, and doing so
// gives us slightly better guarantees about the two peers stats lines not
// being intermixed if there are concurrent calls to our caller.
func (b *LocalBackend) parseWgStatusLocked(s *wgengine.Status) (ret EngineStatus) {
var peerStats, peerKeys strings.Builder
func (b *LocalBackend) parseWgStatus(s *wgengine.Status) (ret EngineStatus) {
var (
peerStats []string
peerKeys []string
)
ret.LiveDERPs = s.DERPs
ret.LivePeers = map[tailcfg.NodeKey]wgengine.PeerStatus{}
for _, p := range s.Peers {
if !p.LastHandshake.IsZero() {
fmt.Fprintf(&peerStats, "%d/%d ", p.RxBytes, p.TxBytes)
fmt.Fprintf(&peerKeys, "%s ", p.NodeKey.ShortString())
peerStats = append(peerStats, fmt.Sprintf("%d/%d", p.RxBytes, p.TxBytes))
ret.NumLive++
ret.LivePeers[p.NodeKey] = p
peerKeys = append(peerKeys, p.NodeKey.ShortString())
}
ret.RBytes += p.RxBytes
ret.WBytes += p.TxBytes
}
// [GRINDER STATS LINES] - please don't remove (used for log parsing)
if peerStats.Len() > 0 {
b.keyLogf("peer keys: %s", strings.TrimSpace(peerKeys.String()))
b.statsLogf("v%v peers: %v", version.Long, strings.TrimSpace(peerStats.String()))
if len(peerStats) > 0 {
// [GRINDER STATS LINE] - please don't remove (used for log parsing)
b.keyLogf("peer keys: %s", strings.Join(peerKeys, " "))
// [GRINDER STATS LINE] - please don't remove (used for log parsing)
b.logf("v%v peers: %v", version.LONG, strings.Join(peerStats, " "))
}
return ret
}
@@ -1013,12 +812,6 @@ func (b *LocalBackend) shieldsAreUp() bool {
return b.prefs.ShieldsUp
}
func (b *LocalBackend) SetCurrentUserID(uid string) {
b.mu.Lock()
b.userID = uid
b.mu.Unlock()
}
func (b *LocalBackend) SetWantRunning(wantRunning bool) {
b.mu.Lock()
new := b.prefs.Clone()
@@ -1033,8 +826,8 @@ func (b *LocalBackend) SetWantRunning(wantRunning bool) {
// SetPrefs saves new user preferences and propagates them throughout
// the system. Implements Backend.
func (b *LocalBackend) SetPrefs(newp *Prefs) {
if newp == nil {
func (b *LocalBackend) SetPrefs(new *Prefs) {
if new == nil {
panic("SetPrefs got nil prefs")
}
@@ -1043,64 +836,51 @@ func (b *LocalBackend) SetPrefs(newp *Prefs) {
netMap := b.netMap
stateKey := b.stateKey
oldp := b.prefs
newp.Persist = oldp.Persist // caller isn't allowed to override this
b.prefs = newp
b.inServerMode = newp.ForceDaemon
old := b.prefs
new.Persist = old.Persist // caller isn't allowed to override this
b.prefs = new
// We do this to avoid holding the lock while doing everything else.
newp = b.prefs.Clone()
new = b.prefs.Clone()
oldHi := b.hostinfo
newHi := oldHi.Clone()
newHi.RoutableIPs = append([]wgcfg.CIDR(nil), b.prefs.AdvertiseRoutes...)
applyPrefsToHostinfo(newHi, newp)
applyPrefsToHostinfo(newHi, new)
b.hostinfo = newHi
hostInfoChanged := !oldHi.Equal(newHi)
userID := b.userID
b.mu.Unlock()
if stateKey != "" {
if err := b.store.WriteState(stateKey, newp.ToBytes()); err != nil {
if err := b.store.WriteState(stateKey, new.ToBytes()); err != nil {
b.logf("Failed to save new controlclient state: %v", err)
}
}
b.writeServerModeStartState(userID, newp)
// [GRINDER STATS LINE] - please don't remove (used for log parsing)
b.logf("SetPrefs: %v", newp.Pretty())
if netMap != nil {
if login := netMap.UserProfiles[netMap.User].LoginName; login != "" {
if newp.Persist == nil {
b.logf("active login: %s", login)
} else if newp.Persist.LoginName != login {
// Corp issue 461: sometimes the wrong prefs are
// logged; the frontend isn't always getting
// notified (to update its prefs/persist) on
// account switch. Log this while we figure it
// out.
b.logf("active login: %s ([unexpected] corp#461, not %s)", newp.Persist.LoginName)
}
}
}
b.logf("SetPrefs: %v", new.Pretty())
if oldp.ShieldsUp != newp.ShieldsUp || hostInfoChanged {
if old.ShieldsUp != new.ShieldsUp || hostInfoChanged {
b.doSetHostinfoFilterServices(newHi)
}
b.updateFilter(netMap, newp)
b.updateFilter(netMap, new)
if netMap != nil {
turnDERPOff := new.DisableDERP && !old.DisableDERP
turnDERPOn := !new.DisableDERP && old.DisableDERP
if turnDERPOff {
b.e.SetDERPMap(nil)
} else if turnDERPOn && netMap != nil {
b.e.SetDERPMap(netMap.DERPMap)
}
if oldp.WantRunning != newp.WantRunning {
if old.WantRunning != new.WantRunning {
b.stateMachine()
} else {
b.authReconfig()
}
b.send(Notify{Prefs: newp})
b.send(Notify{Prefs: new})
}
// doSetHostinfoFilterServices calls SetHostinfo on the controlclient,
@@ -1153,8 +933,6 @@ func (b *LocalBackend) authReconfig() {
blocked := b.blocked
uc := b.prefs
nm := b.netMap
hasPAC := b.prevIfState.HasPAC()
disableSubnetsIfPAC := nm != nil && nm.Debug != nil && nm.Debug.DisableSubnetsIfPAC.EqualBool(true)
b.mu.Unlock()
if blocked {
@@ -1179,12 +957,6 @@ func (b *LocalBackend) authReconfig() {
if uc.AllowSingleHosts {
flags |= controlclient.AllowSingleHosts
}
if hasPAC && disableSubnetsIfPAC {
if flags&controlclient.AllowSubnetRoutes != 0 {
b.logf("authReconfig: have PAC; disabling subnet routes")
flags &^= controlclient.AllowSubnetRoutes
}
}
cfg, err := nm.WGCfg(b.logf, flags)
if err != nil {
@@ -1334,7 +1106,6 @@ func (b *LocalBackend) enterState(newState State) {
prefs := b.prefs
notify := b.notify
bc := b.c
networkUp := b.prevIfState.AnyInterfaceUp()
b.mu.Unlock()
if state == newState {
@@ -1347,7 +1118,7 @@ func (b *LocalBackend) enterState(newState State) {
}
if bc != nil {
bc.SetPaused(newState == Stopped || !networkUp)
bc.SetPaused(newState == Stopped)
}
switch newState {
@@ -1475,7 +1246,7 @@ func (b *LocalBackend) requestEngineStatusAndWait() {
func (b *LocalBackend) Logout() {
b.mu.Lock()
c := b.c
b.setNetMapLocked(nil)
b.netMap = nil
b.mu.Unlock()
if c == nil {
@@ -1492,7 +1263,7 @@ func (b *LocalBackend) Logout() {
c.Logout()
b.mu.Lock()
b.setNetMapLocked(nil)
b.netMap = nil
b.mu.Unlock()
b.stateMachine()
@@ -1521,52 +1292,19 @@ func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) {
c.SetNetInfo(ni)
}
func (b *LocalBackend) setNetMapLocked(nm *controlclient.NetworkMap) {
var login string
if nm != nil {
login = nm.UserProfiles[nm.User].LoginName
if login == "" {
login = "<missing-profile>"
}
}
b.netMap = nm
if login != b.activeLogin {
b.logf("active login: %v", login)
b.activeLogin = login
}
}
// TestOnlyPublicKeys returns the current machine and node public
// keys. Used in tests only to facilitate automated node authorization
// in the test harness.
func (b *LocalBackend) TestOnlyPublicKeys() (machineKey tailcfg.MachineKey, nodeKey tailcfg.NodeKey) {
b.mu.Lock()
prefs := b.prefs
machinePrivKey := b.machinePrivKey
b.mu.Unlock()
if prefs == nil || machinePrivKey.IsZero() {
if prefs == nil {
return
}
mk := machinePrivKey.Public()
mk := prefs.Persist.PrivateMachineKey.Public()
nk := prefs.Persist.PrivateNodeKey.Public()
return tailcfg.MachineKey(mk), tailcfg.NodeKey(nk)
}
// temporarilySetMachineKeyInPersist reports whether we should set
// the machine key in Prefs.Persist.LegacyFrontendPrivateMachineKey
// for the frontend to write out to its preferences for use later.
//
// TODO: remove this in Tailscale 1.3.x (so it effectively always
// returns false). It just exists so users can downgrade from 1.2.x to
// 1.0.x. But eventually we want to stop sending the machine key to
// clients. We can't do that until 1.0.x is no longer supported.
func temporarilySetMachineKeyInPersist() bool {
//lint:ignore S1008 for comments
if runtime.GOOS == "darwin" || runtime.GOOS == "android" {
// iOS, macOS, Android users can't downgrade anyway.
return false
}
return true
}

View File

@@ -83,9 +83,7 @@ func TestLocalLogLines(t *testing.T) {
}},
LocalAddrs: []string{"idk an address"},
}
lb.mu.Lock()
lb.parseWgStatusLocked(status)
lb.mu.Unlock()
lb.parseWgStatus(status)
t.Run("after_peers", testWantRemain())
}

View File

@@ -5,7 +5,6 @@
package ipn
import (
"bytes"
"encoding/binary"
"encoding/json"
"errors"
@@ -20,8 +19,6 @@ import (
"tailscale.com/version"
)
var jsonEscapedZero = []byte(`\u0000`)
type NoArgs struct{}
type StartArgs struct {
@@ -83,14 +80,11 @@ func NewBackendServer(logf logger.Logf, b Backend, sendNotifyMsg func(b []byte))
}
func (bs *BackendServer) send(n Notify) {
n.Version = version.Long
n.Version = version.LONG
b, err := json.Marshal(n)
if err != nil {
log.Fatalf("Failed json.Marshal(notify): %v\n%#v", err, n)
}
if bytes.Contains(b, jsonEscapedZero) {
log.Printf("[unexpected] zero byte in BackendServer.send notify message: %q", b)
}
bs.sendNotifyMsg(b)
}
@@ -98,17 +92,6 @@ func (bs *BackendServer) SendErrorMessage(msg string) {
bs.send(Notify{ErrMessage: &msg})
}
// SendInUseOtherUserErrorMessage sends a Notify message to the client that
// both sets the state to 'InUseOtherUser' and sets the associated reason
// to msg.
func (bs *BackendServer) SendInUseOtherUserErrorMessage(msg string) {
inUse := InUseOtherUser
bs.send(Notify{
State: &inUse,
ErrMessage: &msg,
})
}
// GotCommandMsg parses the incoming message b as a JSON Command and
// calls GotCommand with it.
func (bs *BackendServer) GotCommandMsg(b []byte) error {
@@ -123,14 +106,14 @@ func (bs *BackendServer) GotCommandMsg(b []byte) error {
}
func (bs *BackendServer) GotFakeCommand(cmd *Command) error {
cmd.Version = version.Long
cmd.Version = version.LONG
return bs.GotCommand(cmd)
}
func (bs *BackendServer) GotCommand(cmd *Command) error {
if cmd.Version != version.Long && !cmd.AllowVersionSkew {
if cmd.Version != version.LONG && !cmd.AllowVersionSkew {
vs := fmt.Sprintf("GotCommand: Version mismatch! frontend=%#v backend=%#v",
cmd.Version, version.Long)
cmd.Version, version.LONG)
bs.logf("%s", vs)
// ignore the command, but send a message back to the
// caller so it can realize the version mismatch too.
@@ -210,16 +193,13 @@ func (bc *BackendClient) GotNotifyMsg(b []byte) {
// not interesting
return
}
if bytes.Contains(b, jsonEscapedZero) {
log.Printf("[unexpected] zero byte in BackendClient.GotNotifyMsg message: %q", b)
}
n := Notify{}
if err := json.Unmarshal(b, &n); err != nil {
log.Fatalf("BackendClient.Notify: cannot decode message (length=%d)\n%#v", len(b), string(b))
}
if n.Version != version.Long && !bc.AllowVersionSkew {
if n.Version != version.LONG && !bc.AllowVersionSkew {
vs := fmt.Sprintf("GotNotify: Version mismatch! frontend=%#v backend=%#v",
version.Long, n.Version)
version.LONG, n.Version)
bc.logf("%s", vs)
// delete anything in the notification except the version,
// to prevent incorrect operation.
@@ -234,14 +214,11 @@ func (bc *BackendClient) GotNotifyMsg(b []byte) {
}
func (bc *BackendClient) send(cmd Command) {
cmd.Version = version.Long
cmd.Version = version.LONG
b, err := json.Marshal(cmd)
if err != nil {
log.Fatalf("Failed json.Marshal(cmd): %v\n%#v\n", err, cmd)
}
if bytes.Contains(b, jsonEscapedZero) {
log.Printf("[unexpected] zero byte in BackendClient.send command: %q", b)
}
bc.sendCommandMsg(b)
}

View File

@@ -11,8 +11,6 @@ import (
"log"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/tailscale/wireguard-go/wgcfg"
"tailscale.com/atomicfile"
@@ -20,57 +18,44 @@ import (
"tailscale.com/wgengine/router"
)
//go:generate go run tailscale.com/cmd/cloner -type=Prefs -output=prefs_clone.go
// Prefs are the user modifiable settings of the Tailscale node agent.
type Prefs struct {
// ControlURL is the URL of the control server to use.
ControlURL string
// RouteAll specifies whether to accept subnet and default routes
// advertised by other nodes on the Tailscale network.
RouteAll bool
// AllowSingleHosts specifies whether to install routes for each
// node IP on the tailscale network, in addition to a route for
// the whole network.
// This corresponds to the "tailscale up --host-routes" value,
// which defaults to true.
//
// TODO(danderson): why do we have this? It dumps a lot of stuff
// into the routing table, and a single network route _should_ be
// all that we need. But when I turn this off in my tailscaled,
// packets stop flowing. What's up with that?
AllowSingleHosts bool
// CorpDNS specifies whether to install the Tailscale network's
// DNS configuration, if it exists.
CorpDNS bool
// WantRunning indicates whether networking should be active on
// this node.
WantRunning bool
// ShieldsUp indicates whether to block all incoming connections,
// regardless of the control-provided packet filter. If false, we
// use the packet filter as provided. If true, we block incoming
// connections.
ShieldsUp bool
// AdvertiseTags specifies groups that this node wants to join, for
// purposes of ACL enforcement. These can be referenced from the ACL
// security policy. Note that advertising a tag doesn't guarantee that
// the control server will allow you to take on the rights for that
// tag.
AdvertiseTags []string
// Hostname is the hostname to use for identifying the node. If
// not set, os.Hostname is used.
Hostname string
// OSVersion overrides tailcfg.Hostinfo's OSVersion.
OSVersion string
// DeviceModel overrides tailcfg.Hostinfo's DeviceModel.
DeviceModel string
@@ -82,17 +67,8 @@ type Prefs struct {
// users narrow it down a bit.
NotepadURLs bool
// ForceDaemon specifies whether a platform that normally
// operates in "client mode" (that is, requires an active user
// logged in with the GUI app running) should keep running after the
// GUI ends and/or the user logs out.
//
// The only current applicable platform is Windows. This
// forced Windows to go into "server mode" where Tailscale is
// running even with no users logged in. This might also be
// used for macOS in the future. This setting has no effect
// for Linux/etc, which always operate in daemon mode.
ForceDaemon bool `json:"ForceDaemon,omitempty"`
// DisableDERP prevents DERP from being used.
DisableDERP bool
// The following block of options only have an effect on Linux.
@@ -100,7 +76,6 @@ type Prefs struct {
// Tailscale network as reachable through the current
// node.
AdvertiseRoutes []wgcfg.CIDR
// NoSNAT specifies whether to source NAT traffic going to
// destinations in AdvertiseRoutes. The default is to apply source
// NAT, which makes the traffic appear to come from the router
@@ -112,7 +87,6 @@ type Prefs struct {
//
// Linux-only.
NoSNAT bool
// NetfilterMode specifies how much to manage netfilter rules for
// Tailscale, if at all.
NetfilterMode router.NetfilterMode
@@ -128,46 +102,16 @@ type Prefs struct {
// IsEmpty reports whether p is nil or pointing to a Prefs zero value.
func (p *Prefs) IsEmpty() bool { return p == nil || p.Equals(&Prefs{}) }
func (p *Prefs) Pretty() string { return p.pretty(runtime.GOOS) }
func (p *Prefs) pretty(goos string) string {
var sb strings.Builder
sb.WriteString("Prefs{")
fmt.Fprintf(&sb, "ra=%v ", p.RouteAll)
if !p.AllowSingleHosts {
sb.WriteString("mesh=false ")
}
fmt.Fprintf(&sb, "dns=%v want=%v ", p.CorpDNS, p.WantRunning)
if p.ForceDaemon {
sb.WriteString("server=true ")
}
if p.NotepadURLs {
sb.WriteString("notepad=true ")
}
if p.ShieldsUp {
sb.WriteString("shields=true ")
}
if len(p.AdvertiseRoutes) > 0 || goos == "linux" {
fmt.Fprintf(&sb, "routes=%v ", p.AdvertiseRoutes)
}
if len(p.AdvertiseRoutes) > 0 || p.NoSNAT {
fmt.Fprintf(&sb, "snat=%v ", !p.NoSNAT)
}
if len(p.AdvertiseTags) > 0 {
fmt.Fprintf(&sb, "tags=%s ", strings.Join(p.AdvertiseTags, ","))
}
if goos == "linux" {
fmt.Fprintf(&sb, "nf=%v ", p.NetfilterMode)
}
if p.ControlURL != "" && p.ControlURL != "https://login.tailscale.com" {
fmt.Fprintf(&sb, "url=%q ", p.ControlURL)
}
func (p *Prefs) Pretty() string {
var pp string
if p.Persist != nil {
sb.WriteString(p.Persist.Pretty())
pp = p.Persist.Pretty()
} else {
sb.WriteString("Persist=nil")
pp = "Persist=nil"
}
sb.WriteString("}")
return sb.String()
return fmt.Sprintf("Prefs{ra=%v mesh=%v dns=%v want=%v notepad=%v derp=%v shields=%v routes=%v snat=%v nf=%v %v}",
p.RouteAll, p.AllowSingleHosts, p.CorpDNS, p.WantRunning,
p.NotepadURLs, !p.DisableDERP, p.ShieldsUp, p.AdvertiseRoutes, !p.NoSNAT, p.NetfilterMode, pp)
}
func (p *Prefs) ToBytes() []byte {
@@ -193,13 +137,13 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
p.CorpDNS == p2.CorpDNS &&
p.WantRunning == p2.WantRunning &&
p.NotepadURLs == p2.NotepadURLs &&
p.DisableDERP == p2.DisableDERP &&
p.ShieldsUp == p2.ShieldsUp &&
p.NoSNAT == p2.NoSNAT &&
p.NetfilterMode == p2.NetfilterMode &&
p.Hostname == p2.Hostname &&
p.OSVersion == p2.OSVersion &&
p.DeviceModel == p2.DeviceModel &&
p.ForceDaemon == p2.ForceDaemon &&
compareIPNets(p.AdvertiseRoutes, p2.AdvertiseRoutes) &&
compareStrings(p.AdvertiseTags, p2.AdvertiseTags) &&
p.Persist.Equals(p2.Persist)
@@ -269,16 +213,26 @@ func PrefsFromBytes(b []byte, enforceDefaults bool) (*Prefs, error) {
return p, err
}
// Clone returns a deep copy of p.
func (p *Prefs) Clone() *Prefs {
// TODO: write a faster/non-Fatal-y Clone implementation?
p2, err := PrefsFromBytes(p.ToBytes(), false)
if err != nil {
log.Fatalf("Prefs was uncopyable: %v\n", err)
}
return p2
}
// LoadPrefs loads a legacy relaynode config file into Prefs
// with sensible migration defaults set.
func LoadPrefs(filename string) (*Prefs, error) {
data, err := ioutil.ReadFile(filename)
if err != nil {
return nil, fmt.Errorf("LoadPrefs open: %w", err) // err includes path
return nil, fmt.Errorf("loading prefs from %q: %v", filename, err)
}
p, err := PrefsFromBytes(data, false)
if err != nil {
return nil, fmt.Errorf("LoadPrefs(%q) decode: %w", filename, err)
return nil, fmt.Errorf("decoding prefs in %q: %v", filename, err)
}
return p, nil
}

View File

@@ -1,51 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Code generated by tailscale.com/cmd/cloner -type Prefs; DO NOT EDIT.
package ipn
import (
"github.com/tailscale/wireguard-go/wgcfg"
"tailscale.com/control/controlclient"
"tailscale.com/wgengine/router"
)
// Clone makes a deep copy of Prefs.
// The result aliases no memory with the original.
func (src *Prefs) Clone() *Prefs {
if src == nil {
return nil
}
dst := new(Prefs)
*dst = *src
dst.AdvertiseTags = append(src.AdvertiseTags[:0:0], src.AdvertiseTags...)
dst.AdvertiseRoutes = append(src.AdvertiseRoutes[:0:0], src.AdvertiseRoutes...)
if dst.Persist != nil {
dst.Persist = new(controlclient.Persist)
*dst.Persist = *src.Persist
}
return dst
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type Prefs
var _PrefsNeedsRegeneration = Prefs(struct {
ControlURL string
RouteAll bool
AllowSingleHosts bool
CorpDNS bool
WantRunning bool
ShieldsUp bool
AdvertiseTags []string
Hostname string
OSVersion string
DeviceModel string
NotepadURLs bool
ForceDaemon bool
AdvertiseRoutes []wgcfg.CIDR
NoSNAT bool
NetfilterMode router.NetfilterMode
Persist *controlclient.Persist
}{})

View File

@@ -5,12 +5,8 @@
package ipn
import (
"errors"
"fmt"
"os"
"reflect"
"testing"
"time"
"github.com/tailscale/wireguard-go/wgcfg"
"tailscale.com/control/controlclient"
@@ -28,7 +24,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
func TestPrefsEqual(t *testing.T) {
tstest.PanicOnLog()
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"}
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "DisableDERP", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"}
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
t.Errorf("Prefs.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
have, prefsHandles)
@@ -282,92 +278,3 @@ func TestPrefsPersist(t *testing.T) {
}
checkPrefs(t, p)
}
func TestPrefsPretty(t *testing.T) {
tests := []struct {
p Prefs
os string
want string
}{
{
Prefs{},
"linux",
"Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist=nil}",
},
{
Prefs{},
"windows",
"Prefs{ra=false mesh=false dns=false want=false Persist=nil}",
},
{
Prefs{ShieldsUp: true},
"windows",
"Prefs{ra=false mesh=false dns=false want=false shields=true Persist=nil}",
},
{
Prefs{AllowSingleHosts: true},
"windows",
"Prefs{ra=false dns=false want=false Persist=nil}",
},
{
Prefs{
NotepadURLs: true,
AllowSingleHosts: true,
},
"windows",
"Prefs{ra=false dns=false want=false notepad=true Persist=nil}",
},
{
Prefs{
AllowSingleHosts: true,
WantRunning: true,
ForceDaemon: true, // server mode
},
"windows",
"Prefs{ra=false dns=false want=true server=true Persist=nil}",
},
{
Prefs{
AllowSingleHosts: true,
WantRunning: true,
ControlURL: "http://localhost:1234",
AdvertiseTags: []string{"tag:foo", "tag:bar"},
},
"darwin",
`Prefs{ra=false dns=false want=true tags=tag:foo,tag:bar url="http://localhost:1234" Persist=nil}`,
},
{
Prefs{
Persist: &controlclient.Persist{},
},
"linux",
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist{lm=, o=, n= u=""}}`,
},
{
Prefs{
Persist: &controlclient.Persist{
PrivateNodeKey: wgcfg.PrivateKey{1: 1},
},
},
"linux",
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist{lm=, o=, n=[B1VKl] u=""}}`,
},
}
for i, tt := range tests {
got := tt.p.pretty(tt.os)
if got != tt.want {
t.Errorf("%d. wrong String:\n got: %s\nwant: %s\n", i, got, tt.want)
}
}
}
func TestLoadPrefsNotExist(t *testing.T) {
bogusFile := fmt.Sprintf("/tmp/not-exist-%d", time.Now().UnixNano())
p, err := LoadPrefs(bogusFile)
if errors.Is(err, os.ErrNotExist) {
// expected.
return
}
t.Fatalf("unexpected prefs=%#v, err=%v", p, err)
}

View File

@@ -5,12 +5,9 @@
package ipn
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"sync"
@@ -22,30 +19,6 @@ import (
// requested state ID doesn't exist.
var ErrStateNotExist = errors.New("no state with given ID")
const (
// MachineKeyStateKey is the key under which we store the machine key,
// in its wgcfg.PrivateKey.MarshalText representation.
MachineKeyStateKey = StateKey("_machinekey")
// GlobalDaemonStateKey is the ipn.StateKey that tailscaled
// loads on startup.
//
// We have to support multiple state keys for other OSes (Windows in
// particular), but right now Unix daemons run with a single
// node-global state. To keep open the option of having per-user state
// later, the global state key doesn't look like a username.
GlobalDaemonStateKey = StateKey("_daemon")
// ServerModeStartKey's value, if non-empty, is the value of a
// StateKey containing the prefs to start with which to start the
// server.
//
// For example, the value might be "user-1234", meaning the
// the server should start with the Prefs JSON loaded from
// StateKey "user-1234".
ServerModeStartKey = StateKey("server-mode-start-key")
)
// StateStore persists state, and produces it back on request.
type StateStore interface {
// ReadState returns the bytes associated with ID. Returns (nil,
@@ -61,8 +34,6 @@ type MemoryStore struct {
cache map[StateKey][]byte
}
func (s *MemoryStore) String() string { return "MemoryStore" }
// ReadState implements the StateStore interface.
func (s *MemoryStore) ReadState(id StateKey) ([]byte, error) {
s.mu.Lock()
@@ -96,19 +67,9 @@ type FileStore struct {
cache map[StateKey][]byte
}
func (s *FileStore) String() string { return fmt.Sprintf("FileStore(%q)", s.path) }
// NewFileStore returns a new file store that persists to path.
func NewFileStore(path string) (*FileStore, error) {
bs, err := ioutil.ReadFile(path)
// Treat an empty file as a missing file.
// (https://github.com/tailscale/tailscale/issues/895#issuecomment-723255589)
if err == nil && len(bs) == 0 {
log.Printf("ipn.NewFileStore(%q): file empty; treating it like a missing file [warning]", path)
err = os.ErrNotExist
}
if err != nil {
if os.IsNotExist(err) {
// Write out an initial file, to verify that we can write
@@ -151,9 +112,6 @@ func (s *FileStore) ReadState(id StateKey) ([]byte, error) {
func (s *FileStore) WriteState(id StateKey, bs []byte) error {
s.mu.Lock()
defer s.mu.Unlock()
if bytes.Equal(s.cache[id], bs) {
return nil
}
s.cache[id] = append([]byte(nil), bs...)
bs, err := json.MarshalIndent(s.cache, "", " ")
if err != nil {

View File

@@ -1,199 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package filelogger provides localdisk log writing & rotation, primarily for Windows
// clients. (We get this for free on other platforms.)
package filelogger
import (
"bytes"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"time"
"tailscale.com/types/logger"
)
const (
maxSize = 100 << 20
maxFiles = 50
)
// New returns a logf wrapper that appends to local disk log
// files on Windows, rotating old log files as needed to stay under
// file count & byte limits.
func New(fileBasePrefix, logID string, logf logger.Logf) logger.Logf {
if runtime.GOOS != "windows" {
panic("not yet supported on any platform except Windows")
}
if logf == nil {
panic("nil logf")
}
dir := filepath.Join(os.Getenv("LocalAppData"), "Tailscale", "Logs")
if err := os.MkdirAll(dir, 0700); err != nil {
log.Printf("failed to create local log directory; not writing logs to disk: %v", err)
return logf
}
logf("local disk logdir: %v", dir)
lfw := &logFileWriter{
fileBasePrefix: fileBasePrefix,
logID: logID,
dir: dir,
wrappedLogf: logf,
}
return lfw.Logf
}
// logFileWriter is the state for the log writer & rotator.
type logFileWriter struct {
dir string // e.g. `C:\Users\FooBarUser\AppData\Local\Tailscale\Logs`
logID string // hex logID
fileBasePrefix string // e.g. "tailscale-service" or "tailscale-gui"
wrappedLogf logger.Logf // underlying logger to send to
mu sync.Mutex // guards following
buf bytes.Buffer // scratch buffer to avoid allocs
fday civilDay // day that f was opened; zero means no file yet open
f *os.File // file currently opened for append
}
// civilDay is a year, month, and day in the local timezone.
// It's a comparable value type.
type civilDay struct {
year int
month time.Month
day int
}
func dayOf(t time.Time) civilDay {
return civilDay{t.Year(), t.Month(), t.Day()}
}
func (w *logFileWriter) Logf(format string, a ...interface{}) {
w.mu.Lock()
defer w.mu.Unlock()
w.buf.Reset()
fmt.Fprintf(&w.buf, format, a...)
if w.buf.Len() == 0 {
return
}
out := w.buf.Bytes()
w.wrappedLogf("%s", out)
// Make sure there's a final newline before we write to the log file.
if out[len(out)-1] != '\n' {
w.buf.WriteByte('\n')
out = w.buf.Bytes()
}
w.appendToFileLocked(out)
}
// out should end in a newline.
// w.mu must be held.
func (w *logFileWriter) appendToFileLocked(out []byte) {
now := time.Now()
day := dayOf(now)
if w.fday != day {
w.startNewFileLocked()
}
if w.f != nil {
// RFC3339Nano but with a fixed number (3) of nanosecond digits:
const formatPre = "2006-01-02T15:04:05"
const formatPost = "Z07:00"
fmt.Fprintf(w.f, "%s.%03d%s: %s",
now.Format(formatPre),
now.Nanosecond()/int(time.Millisecond/time.Nanosecond),
now.Format(formatPost),
out)
}
}
// startNewFileLocked opens a new log file for writing
// and also cleans up any old files.
//
// w.mu must be held.
func (w *logFileWriter) startNewFileLocked() {
var oldName string
if w.f != nil {
oldName = filepath.Base(w.f.Name())
w.f.Close()
w.f = nil
w.fday = civilDay{}
}
w.cleanLocked()
now := time.Now()
day := dayOf(now)
name := filepath.Join(w.dir, fmt.Sprintf("%s-%04d%02d%02dT%02d%02d%02d-%d.txt",
w.fileBasePrefix,
day.year,
day.month,
day.day,
now.Hour(),
now.Minute(),
now.Second(),
now.Unix()))
var err error
w.f, err = os.Create(name)
if err != nil {
w.wrappedLogf("failed to create log file: %v", err)
return
}
if oldName != "" {
fmt.Fprintf(w.f, "(logID %q; continued from log file %s)\n", w.logID, oldName)
} else {
fmt.Fprintf(w.f, "(logID %q)\n", w.logID)
}
w.fday = day
}
// cleanLocked cleans up old log files.
//
// w.mu must be held.
func (w *logFileWriter) cleanLocked() {
fis, _ := ioutil.ReadDir(w.dir)
prefix := w.fileBasePrefix + "-"
fileSize := map[string]int64{}
var files []string
var sumSize int64
for _, fi := range fis {
baseName := filepath.Base(fi.Name())
if !strings.HasPrefix(baseName, prefix) {
continue
}
size := fi.Size()
fileSize[baseName] = size
sumSize += size
files = append(files, baseName)
}
if sumSize > maxSize {
w.wrappedLogf("cleaning log files; sum byte count %d > %d", sumSize, maxSize)
}
if len(files) > maxFiles {
w.wrappedLogf("cleaning log files; number of files %d > %d", len(files), maxFiles)
}
for (sumSize > maxSize || len(files) > maxFiles) && len(files) > 0 {
target := files[0]
files = files[1:]
targetSize := fileSize[target]
targetFull := filepath.Join(w.dir, target)
err := os.Remove(targetFull)
if err != nil {
w.wrappedLogf("error cleaning log file: %v", err)
} else {
sumSize -= targetSize
w.wrappedLogf("cleaned log file %s (size %d); new bytes=%v, files=%v", targetFull, targetSize, sumSize, len(files))
}
}
}

View File

@@ -315,9 +315,6 @@ func New(collection string) *Policy {
} else {
lflags = log.LstdFlags
}
if v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_LOG_TIME")); v {
lflags = log.LstdFlags | log.Lmicroseconds
}
if runningUnderSystemd() {
// If journalctl is going to prepend its own timestamp
// anyway, no need to add one.
@@ -359,7 +356,7 @@ func New(collection string) *Policy {
newc.PrivateID = logtail.PrivateID{}
newc.Collection = collection
}
if newc.PrivateID.IsZero() {
if newc.PrivateID == (logtail.PrivateID{}) {
newc.PrivateID, err = logtail.NewPrivateID()
if err != nil {
log.Fatalf("logpolicy: NewPrivateID() should never fail")
@@ -395,7 +392,7 @@ func New(collection string) *Policy {
log.SetOutput(lw)
log.Printf("Program starting: v%v, Go %v: %#v",
version.Long,
version.LONG,
strings.TrimPrefix(runtime.Version(), "go"),
os.Args)
log.Printf("LogID: %v", newc.PublicID)

View File

@@ -8,7 +8,7 @@ import (
"fmt"
"io/ioutil"
"os"
"runtime"
"path/filepath"
"strings"
"testing"
"unicode"
@@ -56,8 +56,19 @@ func (f *filchTest) close(t *testing.T) {
}
}
func genFilePrefix(t *testing.T) (dir, prefix string) {
t.Helper()
dir, err := ioutil.TempDir("", "filch")
if err != nil {
t.Fatal(err)
}
return dir, filepath.Join(dir, "ringbuffer-")
}
func TestQueue(t *testing.T) {
filePrefix := t.TempDir()
td, filePrefix := genFilePrefix(t)
defer os.RemoveAll(td)
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
f.readEOF(t)
@@ -79,7 +90,8 @@ func TestQueue(t *testing.T) {
func TestRecover(t *testing.T) {
t.Run("empty", func(t *testing.T) {
filePrefix := t.TempDir()
td, filePrefix := genFilePrefix(t)
defer os.RemoveAll(td)
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
f.write(t, "hello")
f.read(t, "hello")
@@ -92,7 +104,8 @@ func TestRecover(t *testing.T) {
})
t.Run("cur", func(t *testing.T) {
filePrefix := t.TempDir()
td, filePrefix := genFilePrefix(t)
defer os.RemoveAll(td)
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
f.write(t, "hello")
f.close(t)
@@ -110,7 +123,8 @@ func TestRecover(t *testing.T) {
filch_test.go:129: r.ReadLine()="hello", want "world"
*/
filePrefix := t.TempDir()
td, filePrefix := genFilePrefix(t)
defer os.RemoveAll(td)
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
f.write(t, "hello")
f.read(t, "hello")
@@ -129,14 +143,6 @@ func TestRecover(t *testing.T) {
}
func TestFilchStderr(t *testing.T) {
if runtime.GOOS == "windows" {
// TODO(bradfitz): this is broken on Windows but not
// fully sure why. Investigate. But notably, the
// stderrFD variable (defined in filch.go) and set
// below is only ever read in filch_unix.go. So just
// skip this for test for now.
t.Skip("test broken on Windows")
}
pipeR, pipeW, err := os.Pipe()
if err != nil {
t.Fatal(err)
@@ -149,7 +155,8 @@ func TestFilchStderr(t *testing.T) {
stderrFD = 2
}()
filePrefix := t.TempDir()
td, filePrefix := genFilePrefix(t)
defer os.RemoveAll(td)
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: true})
f.write(t, "hello")
if _, err := fmt.Fprintf(pipeW, "filch\n"); err != nil {

View File

@@ -10,7 +10,6 @@ import (
"net"
"net/http"
"reflect"
"sort"
"strings"
"inet.af/netaddr"
@@ -161,11 +160,10 @@ type State struct {
InterfaceUp map[string]bool
// HaveV6Global is whether this machine has an IPv6 global address
// on some non-Tailscale interface that's up.
// on some interface.
HaveV6Global bool
// HaveV4 is whether the machine has some non-localhost,
// non-link-local IPv4 address on a non-Tailscale interface that's up.
// HaveV4 is whether the machine has some non-localhost IPv4 address.
HaveV4 bool
// IsExpensive is whether the current network interface is
@@ -175,104 +173,30 @@ type State struct {
// DefaultRouteInterface is the interface name for the machine's default route.
// It is not yet populated on all OSes.
// Its exact value should not be assumed to be a map key for
// the Interface maps above; it's only used for debugging.
DefaultRouteInterface string
// HTTPProxy is the HTTP proxy to use.
HTTPProxy string
// PAC is the URL to the Proxy Autoconfig URL, if applicable.
PAC string
}
func (s *State) String() string {
var sb strings.Builder
fmt.Fprintf(&sb, "interfaces.State{defaultRoute=%v ifs={", s.DefaultRouteInterface)
ifs := make([]string, 0, len(s.InterfaceUp))
for k := range s.InterfaceUp {
if allLoopbackIPs(s.InterfaceIPs[k]) {
continue
}
ifs = append(ifs, k)
}
sort.Slice(ifs, func(i, j int) bool {
upi, upj := s.InterfaceUp[ifs[i]], s.InterfaceUp[ifs[j]]
if upi != upj {
// Up sorts before down.
return upi
}
return ifs[i] < ifs[j]
})
for i, ifName := range ifs {
if i > 0 {
sb.WriteString(" ")
}
if s.InterfaceUp[ifName] {
fmt.Fprintf(&sb, "%s:[", ifName)
needSpace := false
for _, ip := range s.InterfaceIPs[ifName] {
if ip.IsLinkLocalUnicast() {
continue
}
if needSpace {
sb.WriteString(" ")
}
fmt.Fprintf(&sb, "%s", ip)
needSpace = true
}
sb.WriteString("]")
} else {
fmt.Fprintf(&sb, "%s:down", ifName)
}
}
sb.WriteString("}")
if s.IsExpensive {
sb.WriteString(" expensive")
}
if s.HTTPProxy != "" {
fmt.Fprintf(&sb, " httpproxy=%s", s.HTTPProxy)
}
if s.PAC != "" {
fmt.Fprintf(&sb, " pac=%s", s.PAC)
}
fmt.Fprintf(&sb, " v4=%v v6global=%v}", s.HaveV4, s.HaveV6Global)
return sb.String()
}
func (s *State) Equal(s2 *State) bool {
return reflect.DeepEqual(s, s2)
}
func (s *State) HasPAC() bool { return s != nil && s.PAC != "" }
// AnyInterfaceUp reports whether any interface seems like it has Internet access.
func (s *State) AnyInterfaceUp() bool {
return s != nil && (s.HaveV4 || s.HaveV6Global)
}
// RemoveTailscaleInterfaces modifes s to remove any interfaces that
// are owned by this process. (TODO: make this true; currently it
// makes the Linux-only assumption that the interface is named
// /^tailscale/)
func (s *State) RemoveTailscaleInterfaces() {
for name := range s.InterfaceIPs {
if isTailscaleInterfaceName(name) {
if name == "Tailscale" || // as it is on Windows
strings.HasPrefix(name, "tailscale") { // TODO: use --tun flag value, etc; see TODO in method doc
delete(s.InterfaceIPs, name)
delete(s.InterfaceUp, name)
}
}
}
func isTailscaleInterfaceName(name string) bool {
return name == "Tailscale" || // as it is on Windows
strings.HasPrefix(name, "tailscale") // TODO: use --tun flag value, etc; see TODO in method doc
}
// getPAC, if non-nil, returns the current PAC file URL.
var getPAC func() string
// GetState returns the state of all the current machine's network interfaces.
//
// It does not set the returned State.IsExpensive. The caller can populate that.
@@ -282,29 +206,21 @@ func GetState() (*State, error) {
InterfaceUp: make(map[string]bool),
}
if err := ForeachInterfaceAddress(func(ni Interface, ip netaddr.IP) {
ifUp := ni.IsUp()
s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], ip)
s.InterfaceUp[ni.Name] = ifUp
if ifUp && !ip.IsLoopback() && !ip.IsLinkLocalUnicast() && !isTailscaleInterfaceName(ni.Name) {
s.HaveV6Global = s.HaveV6Global || isGlobalV6(ip)
s.HaveV4 = s.HaveV4 || ip.Is4()
}
s.InterfaceUp[ni.Name] = ni.IsUp()
s.HaveV6Global = s.HaveV6Global || isGlobalV6(ip)
s.HaveV4 = s.HaveV4 || (ip.Is4() && !ip.IsLoopback())
}); err != nil {
return nil, err
}
s.DefaultRouteInterface, _ = DefaultRouteInterface()
if s.AnyInterfaceUp() {
req, err := http.NewRequest("GET", LoginEndpointForProxyDetermination, nil)
if err != nil {
return nil, err
}
if u, err := tshttpproxy.ProxyFromEnvironment(req); err == nil && u != nil {
s.HTTPProxy = u.String()
}
if getPAC != nil {
s.PAC = getPAC()
}
req, err := http.NewRequest("GET", LoginEndpointForProxyDetermination, nil)
if err != nil {
return nil, err
}
if u, err := tshttpproxy.ProxyFromEnvironment(req); err == nil && u != nil {
s.HTTPProxy = u.String()
}
return s, nil
@@ -396,15 +312,3 @@ var (
linkLocalIPv4 = mustCIDR("169.254.0.0/16")
v6Global1 = mustCIDR("2000::/3")
)
func allLoopbackIPs(ips []netaddr.IP) bool {
if len(ips) == 0 {
return false
}
for _, ip := range ips {
if !ip.IsLoopback() {
return false
}
}
return true
}

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !linux,!windows
// +build !linux
package interfaces

View File

@@ -35,7 +35,6 @@ func TestGetState(t *testing.T) {
t.Fatal(err)
}
t.Logf("Got: %#v", st)
t.Logf("As string: %s", st)
st2, err := GetState()
if err != nil {
@@ -47,9 +46,6 @@ func TestGetState(t *testing.T) {
// the two GetState calls.
t.Fatal("two States back-to-back were not equal")
}
st.RemoveTailscaleInterfaces()
t.Logf("As string without Tailscale:\n\t%s", st)
}
func TestLikelyHomeRouterIP(t *testing.T) {

View File

@@ -5,16 +5,11 @@
package interfaces
import (
"fmt"
"log"
"net/url"
"os/exec"
"syscall"
"unsafe"
"github.com/tailscale/winipcfg-go"
"go4.org/mem"
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"inet.af/netaddr"
"tailscale.com/tsconst"
"tailscale.com/util/lineread"
@@ -22,7 +17,6 @@ import (
func init() {
likelyHomeRouterIP = likelyHomeRouterIPWindows
getPAC = getPACWindows
}
/*
@@ -82,115 +76,19 @@ func likelyHomeRouterIPWindows() (ret netaddr.IP, ok bool) {
// NonTailscaleMTUs returns a map of interface LUID to interface MTU,
// for all interfaces except Tailscale tunnels.
func NonTailscaleMTUs() (map[winipcfg.LUID]uint32, error) {
mtus := map[winipcfg.LUID]uint32{}
ifs, err := NonTailscaleInterfaces()
for luid, iface := range ifs {
mtus[luid] = iface.MTU
}
return mtus, err
}
// NonTailscaleInterfaces returns a map of interface LUID to interface
// for all interfaces except Tailscale tunnels.
func NonTailscaleInterfaces() (map[winipcfg.LUID]*winipcfg.IPAdapterAddresses, error) {
ifs, err := winipcfg.GetAdaptersAddresses(windows.AF_UNSPEC, winipcfg.GAAFlagIncludeAllInterfaces)
func NonTailscaleMTUs() (map[uint64]uint32, error) {
ifs, err := winipcfg.GetInterfaces()
if err != nil {
return nil, err
}
ret := map[winipcfg.LUID]*winipcfg.IPAdapterAddresses{}
ret := map[uint64]uint32{}
for _, iface := range ifs {
if iface.Description() == tsconst.WintunInterfaceDesc {
if iface.Description == tsconst.WintunInterfaceDesc {
continue
}
ret[iface.LUID] = iface
ret[iface.Luid] = iface.Mtu
}
return ret, nil
}
// GetWindowsDefault returns the interface that has the non-Tailscale
// default route for the given address family.
//
// It returns (nil, nil) if no interface is found.
func GetWindowsDefault(family winipcfg.AddressFamily) (*winipcfg.IPAdapterAddresses, error) {
ifs, err := NonTailscaleInterfaces()
if err != nil {
return nil, err
}
routes, err := winipcfg.GetIPForwardTable2(family)
if err != nil {
return nil, err
}
bestMetric := ^uint32(0)
var bestIface *winipcfg.IPAdapterAddresses
for _, route := range routes {
iface := ifs[route.InterfaceLUID]
if route.DestinationPrefix.PrefixLength != 0 || iface == nil {
continue
}
if iface.OperStatus == winipcfg.IfOperStatusUp && route.Metric < bestMetric {
bestMetric = route.Metric
bestIface = iface
}
}
return bestIface, nil
}
func DefaultRouteInterface() (string, error) {
iface, err := GetWindowsDefault(windows.AF_INET)
if err != nil {
return "", err
}
if iface == nil {
return "(none)", nil
}
return fmt.Sprintf("%s (%s)", iface.FriendlyName(), iface.Description()), nil
}
var (
winHTTP = windows.NewLazySystemDLL("winhttp.dll")
detectAutoProxyConfigURL = winHTTP.NewProc("WinHttpDetectAutoProxyConfigUrl")
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
globalFree = kernel32.NewProc("GlobalFree")
)
const (
winHTTP_AUTO_DETECT_TYPE_DHCP = 0x00000001
winHTTP_AUTO_DETECT_TYPE_DNS_A = 0x00000002
)
func getPACWindows() string {
var res *uint16
r, _, e := detectAutoProxyConfigURL.Call(
winHTTP_AUTO_DETECT_TYPE_DHCP|winHTTP_AUTO_DETECT_TYPE_DNS_A,
uintptr(unsafe.Pointer(&res)),
)
if r == 1 {
if res == nil {
log.Printf("getPACWindows: unexpected success with nil result")
return ""
}
defer globalFree.Call(uintptr(unsafe.Pointer(res)))
s := windows.UTF16PtrToString(res)
if _, err := url.Parse(s); err != nil {
log.Printf("getPACWindows: invalid URL %q from winhttp; ignoring", s)
return ""
}
return s
}
const (
ERROR_WINHTTP_AUTODETECTION_FAILED = 12180
)
if e == syscall.Errno(ERROR_WINHTTP_AUTODETECTION_FAILED) {
// Common case on networks without advertised PAC.
return ""
}
log.Printf("getPACWindows: %T=%v", e, e) // syscall.Errno=0x....
return ""
}

View File

@@ -1,17 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package interfaces
import "testing"
func BenchmarkGetPACWindows(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
v := getPACWindows()
if i == 0 {
b.Logf("Got: %q", v)
}
}
}

View File

@@ -156,15 +156,6 @@ type Client struct {
// GetSTUNConn6 is like GetSTUNConn4, but for IPv6.
GetSTUNConn6 func() STUNConn
// SkipExternalNetwork controls whether the client should not try
// to reach things other than localhost. This is set to true
// in tests to avoid probing the local LAN's router, etc.
SkipExternalNetwork bool
// UDPBindAddr, if non-empty, is the address to listen on for UDP.
// It defaults to ":0".
UDPBindAddr string
mu sync.Mutex // guards following
nextFull bool // do a full region scan, even if last != nil
prev map[time.Time]*Report // some previous reports
@@ -781,13 +772,6 @@ func newReport() *Report {
}
}
func (c *Client) udpBindAddr() string {
if v := c.UDPBindAddr; v != "" {
return v
}
return ":0"
}
// GetReport gets a report.
//
// It may not be called concurrently with itself.
@@ -847,10 +831,8 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
}
defer rs.pc4Hair.Close()
if !c.SkipExternalNetwork {
rs.waitPortMap.Add(1)
go rs.probePortMapServices()
}
rs.waitPortMap.Add(1)
go rs.probePortMapServices()
// At least the Apple Airport Extreme doesn't allow hairpin
// sends from a private socket until it's seen traffic from
@@ -868,7 +850,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
if f := c.GetSTUNConn4; f != nil {
rs.pc4 = f()
} else {
u4, err := netns.Listener().ListenPacket(ctx, "udp4", c.udpBindAddr())
u4, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
if err != nil {
c.logf("udp4: %v", err)
return nil, err
@@ -881,7 +863,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
if f := c.GetSTUNConn6; f != nil {
rs.pc6 = f()
} else {
u6, err := netns.Listener().ListenPacket(ctx, "udp6", c.udpBindAddr())
u6, err := netns.Listener().ListenPacket(ctx, "udp6", ":0")
if err != nil {
c.logf("udp6: %v", err)
} else {
@@ -920,10 +902,8 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
rs.waitHairCheck(ctx)
c.vlogf("hairCheck done")
if !c.SkipExternalNetwork {
rs.waitPortMap.Wait()
c.vlogf("portMap done")
}
rs.waitPortMap.Wait()
c.vlogf("portMap done")
rs.stopTimers()
// Try HTTPS latency check if all STUN probes failed due to UDP presumably being blocked.

View File

@@ -50,8 +50,7 @@ func TestBasic(t *testing.T) {
defer cleanup()
c := &Client{
Logf: t.Logf,
UDPBindAddr: "127.0.0.1:0",
Logf: t.Logf,
}
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)

View File

@@ -5,37 +5,18 @@
package netns
import (
"math/bits"
"strings"
"encoding/binary"
"syscall"
"unsafe"
"github.com/tailscale/winipcfg-go"
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"tailscale.com/net/interfaces"
"tailscale.com/util/endian"
)
func interfaceIndex(iface *winipcfg.IPAdapterAddresses) uint32 {
if iface == nil {
// The zero ifidx means "unspecified". If we end up passing zero
// to bindSocket*(), it unsets the binding and lets the socket
// behave as normal again, which is what we want if there's no
// default route we can use.
return 0
}
return iface.IfIndex
}
// control binds c to the Windows interface that holds a default
// route, and is not the Tailscale WinTun interface.
func control(network, address string, c syscall.RawConn) error {
if strings.HasPrefix(address, "127.") {
// Don't bind to an interface for localhost connections,
// otherwise we get:
// connectex: The requested address is not valid in its context
// (The derphttp tests were failing)
return nil
}
canV4, canV6 := false, false
switch network {
case "tcp", "udp":
@@ -47,21 +28,21 @@ func control(network, address string, c syscall.RawConn) error {
}
if canV4 {
iface, err := interfaces.GetWindowsDefault(windows.AF_INET)
if4, err := getDefaultInterface(winipcfg.AF_INET)
if err != nil {
return err
}
if err := bindSocket4(c, interfaceIndex(iface)); err != nil {
if err := bindSocket4(c, if4); err != nil {
return err
}
}
if canV6 {
iface, err := interfaces.GetWindowsDefault(windows.AF_INET6)
if6, err := getDefaultInterface(winipcfg.AF_INET6)
if err != nil {
return err
}
if err := bindSocket6(c, interfaceIndex(iface)); err != nil {
if err := bindSocket6(c, if6); err != nil {
return err
}
}
@@ -69,27 +50,49 @@ func control(network, address string, c syscall.RawConn) error {
return nil
}
// sockoptBoundInterface is the value of IP_UNICAST_IF and IPV6_UNICAST_IF.
//
// See https://docs.microsoft.com/en-us/windows/win32/winsock/ipproto-ip-socket-options
// and https://docs.microsoft.com/en-us/windows/win32/winsock/ipproto-ipv6-socket-options
// getDefaultInterface returns the index of the interface that has the
// non-Tailscale default route for the given address family.
func getDefaultInterface(family winipcfg.AddressFamily) (ifidx uint32, err error) {
ifs, err := interfaces.NonTailscaleMTUs()
if err != nil {
return 0, err
}
routes, err := winipcfg.GetRoutes(family)
if err != nil {
return 0, err
}
bestMetric := ^uint32(0)
// The zero index means "unspecified". If we end up passing zero
// to bindSocket*(), it unsets the binding and lets the socket
// behave as normal again, which is what we want if there's no
// default route we can use.
var index uint32
for _, route := range routes {
if route.DestinationPrefix.PrefixLength != 0 || ifs[route.InterfaceLuid] == 0 {
continue
}
if route.Metric < bestMetric {
bestMetric = route.Metric
index = route.InterfaceIndex
}
}
return index, nil
}
const sockoptBoundInterface = 31
// bindSocket4 binds the given RawConn to the network interface with
// index ifidx, for IPv4 traffic only.
func bindSocket4(c syscall.RawConn, ifidx uint32) error {
// For IPv4 (but NOT IPv6) the interface index must be passed
// as a big-endian integer (regardless of platform endianness)
// because the underlying sockopt takes either an IPv4 address
// or an index shoved into IPv4 address representation (an IP
// in 0.0.0.0/8 means it's actually an index).
//
// See https://docs.microsoft.com/en-us/windows/win32/winsock/ipproto-ip-socket-options
// and IP_UNICAST_IF.
indexAsAddr := nativeToBigEndian(ifidx)
// For v4 the interface index must be passed as a big-endian
// integer, regardless of platform endianness.
index := nativeToBigEndian(ifidx)
var controlErr error
err := c.Control(func(fd uintptr) {
controlErr = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, sockoptBoundInterface, int(indexAsAddr))
controlErr = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, sockoptBoundInterface, int(index))
})
if err != nil {
return err
@@ -114,8 +117,7 @@ func bindSocket6(c syscall.RawConn, ifidx uint32) error {
// representation, suitable for passing to Windows APIs that require a
// mangled uint32.
func nativeToBigEndian(i uint32) uint32 {
if endian.Big {
return i
}
return bits.ReverseBytes32(i)
var b [4]byte
binary.BigEndian.PutUint32(b[:], i)
return *(*uint32)(unsafe.Pointer(&b[0]))
}

View File

@@ -6,15 +6,14 @@
package netstat
import (
"encoding/binary"
"errors"
"fmt"
"math/bits"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
"inet.af/netaddr"
"tailscale.com/util/endian"
)
// See https://docs.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getextendedtcptable
@@ -154,11 +153,9 @@ func state(v uint32) string {
}
func ipport4(addr uint32, port uint16) netaddr.IPPort {
if !endian.Big {
addr = bits.ReverseBytes32(addr)
}
a4 := (*[4]byte)(unsafe.Pointer(&addr))
return netaddr.IPPort{
IP: netaddr.IPv4(byte(addr>>24), byte(addr>>16), byte(addr>>8), byte(addr)),
IP: netaddr.IPv4(a4[0], a4[1], a4[2], a4[3]),
Port: port,
}
}
@@ -176,8 +173,6 @@ func ipport6(addr [16]byte, scope uint32, port uint16) netaddr.IPPort {
}
func port(v *uint32) uint16 {
if !endian.Big {
return uint16(bits.ReverseBytes32(*v) >> 16)
}
return uint16(*v >> 16)
p := (*[4]byte)(unsafe.Pointer(v))
return binary.BigEndian.Uint16(p[:2])
}

View File

@@ -30,11 +30,7 @@ func CGNATRange() netaddr.IPPrefix {
return cgnatRange.v
}
var (
cgnatRange oncePrefix
ulaRange oncePrefix
ula4To6Range oncePrefix
)
var cgnatRange oncePrefix
// TailscaleServiceIP returns the listen address of services
// provided by Tailscale itself such as the Magic DNS proxy.
@@ -48,40 +44,7 @@ var serviceIP onceIP
// IsTailscaleIP reports whether ip is an IP address in a range that
// Tailscale assigns from.
func IsTailscaleIP(ip netaddr.IP) bool {
if ip.Is4() {
return CGNATRange().Contains(ip) && !ChromeOSVMRange().Contains(ip)
}
return TailscaleULARange().Contains(ip)
}
// TailscaleULARange returns the IPv6 Unique Local Address range that
// is the superset range that Tailscale assigns out of.
func TailscaleULARange() netaddr.IPPrefix {
ulaRange.Do(func() { mustPrefix(&ulaRange.v, "fd7a:115c:a1e0::/48") })
return ulaRange.v
}
// Tailscale4To6Range returns the subset of TailscaleULARange used for
// auto-translated Tailscale ipv4 addresses.
func Tailscale4To6Range() netaddr.IPPrefix {
// This IP range has no significance, beyond being a subset of
// TailscaleULARange. The bits from /48 to /104 were picked at
// random.
ula4To6Range.Do(func() { mustPrefix(&ula4To6Range.v, "fd7a:115c:a1e0:ab12:4843:cd96:6200::/104") })
return ula4To6Range.v
}
// Tailscale4To6 returns a Tailscale IPv6 address that maps 1:1 to the
// given Tailscale IPv4 address. Returns a zero IP if ipv4 isn't a
// Tailscale IPv4 address.
func Tailscale4To6(ipv4 netaddr.IP) netaddr.IP {
if !ipv4.Is4() || !IsTailscaleIP(ipv4) {
return netaddr.IP{}
}
ret := Tailscale4To6Range().IP.As16()
v4 := ipv4.As4()
copy(ret[13:], v4[1:])
return netaddr.IPFrom16(ret)
return CGNATRange().Contains(ip) && !ChromeOSVMRange().Contains(ip)
}
func mustPrefix(v *netaddr.IPPrefix, prefix string) {

View File

@@ -10,45 +10,14 @@ import (
"net/http"
"net/url"
"os"
"sync"
"time"
)
// InvalidateCache invalidates the package-level cache for ProxyFromEnvironment.
//
// It's intended to be called on network link/routing table changes.
func InvalidateCache() {
mu.Lock()
defer mu.Unlock()
noProxyUntil = time.Time{}
}
var (
mu sync.Mutex
noProxyUntil time.Time // if non-zero, time at which ProxyFromEnvironment should check again
)
func setNoProxyUntil(d time.Duration) {
mu.Lock()
defer mu.Unlock()
noProxyUntil = time.Now().Add(d)
}
var _ = setNoProxyUntil // quiet staticcheck; Windows uses the above, more might later
// sysProxyFromEnv, if non-nil, specifies a platform-specific ProxyFromEnvironment
// func to use if http.ProxyFromEnvironment doesn't return a proxy.
// For example, WPAD PAC files on Windows.
var sysProxyFromEnv func(*http.Request) (*url.URL, error)
func ProxyFromEnvironment(req *http.Request) (*url.URL, error) {
mu.Lock()
noProxyTime := noProxyUntil
mu.Unlock()
if time.Now().Before(noProxyTime) {
return nil, nil
}
u, err := http.ProxyFromEnvironment(req)
if u != nil && err == nil {
return u, nil

View File

@@ -19,7 +19,6 @@ import (
"github.com/alexbrainman/sspi/negotiate"
"golang.org/x/sys/windows"
"tailscale.com/types/logger"
)
var (
@@ -39,13 +38,6 @@ var cachedProxy struct {
val *url.URL
}
// proxyErrorf is a rate-limited logger specifically for errors asking
// WinHTTP for the proxy information. We don't want to log about
// errors often, otherwise the log message itself will generate a new
// HTTP request which ultimately will call back into us to log again,
// forever. So for errors, we only log a bit.
var proxyErrorf = logger.RateLimitedFn(log.Printf, 10*time.Minute, 2 /* burst*/, 10 /* maxCache */)
func proxyFromWinHTTPOrCache(req *http.Request) (*url.URL, error) {
if req.URL == nil {
return nil, nil
@@ -79,31 +71,16 @@ func proxyFromWinHTTPOrCache(req *http.Request) (*url.URL, error) {
}
// See https://docs.microsoft.com/en-us/windows/win32/winhttp/error-messages
const (
ERROR_WINHTTP_AUTODETECTION_FAILED = 12180
ERROR_WINHTTP_UNABLE_TO_DOWNLOAD_SCRIPT = 12167
)
const ERROR_WINHTTP_AUTODETECTION_FAILED = 12180
if err == syscall.Errno(ERROR_WINHTTP_AUTODETECTION_FAILED) {
setNoProxyUntil(10 * time.Second)
return nil, nil
}
if err == windows.ERROR_INVALID_PARAMETER {
// Seen on Windows 8.1. (https://github.com/tailscale/tailscale/issues/879)
// TODO(bradfitz): figure this out.
setNoProxyUntil(time.Hour)
proxyErrorf("tshttpproxy: winhttp: GetProxyForURL(%q): ERROR_INVALID_PARAMETER [unexpected]", urlStr)
return nil, nil
}
proxyErrorf("tshttpproxy: winhttp: GetProxyForURL(%q): %v/%#v", urlStr, err, err)
if err == syscall.Errno(ERROR_WINHTTP_UNABLE_TO_DOWNLOAD_SCRIPT) {
setNoProxyUntil(10 * time.Second)
return nil, nil
}
log.Printf("tshttpproxy: winhttp: GetProxyForURL(%q): %v/%#v", urlStr, err, err)
return nil, err
case <-ctx.Done():
cachedProxy.Lock()
defer cachedProxy.Unlock()
proxyErrorf("tshttpproxy: winhttp: GetProxyForURL(%q): timeout; using cached proxy %v", urlStr, cachedProxy.val)
log.Printf("tshttpproxy: winhttp: GetProxyForURL(%q): timeout; using cached proxy %v", urlStr, cachedProxy.val)
return cachedProxy.val, nil
}
}
@@ -111,7 +88,7 @@ func proxyFromWinHTTPOrCache(req *http.Request) (*url.URL, error) {
func proxyFromWinHTTP(ctx context.Context, urlStr string) (proxy *url.URL, err error) {
whi, err := winHTTPOpen()
if err != nil {
proxyErrorf("winhttp: Open: %v", err)
log.Printf("winhttp: Open: %v", err)
return nil, err
}
defer whi.Close()

View File

@@ -8,7 +8,6 @@ import (
"syscall"
"time"
"golang.org/x/sys/windows"
exec "tailscale.com/tempfork/osexec"
)
@@ -20,9 +19,6 @@ func listPorts() (List, error) {
}
func addProcesses(pl []Port) ([]Port, error) {
if t := windows.GetCurrentProcessToken(); !t.IsElevated() {
return listPortsNetstat("-na")
}
return listPortsNetstat("-nab")
}

View File

@@ -4,7 +4,7 @@
package tailcfg
//go:generate go run tailscale.com/cmd/cloner --type=User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse --clonefunc=true --output=tailcfg_clone.go
//go:generate go run tailscale.com/cmd/cloner -type=User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig -output=tailcfg_clone.go
import (
"bytes"
@@ -27,34 +27,14 @@ type ID int64
type UserID ID
func (u UserID) IsZero() bool {
return u == 0
}
type LoginID ID
func (u LoginID) IsZero() bool {
return u == 0
}
type NodeID ID
func (u NodeID) IsZero() bool {
return u == 0
}
type GroupID ID
func (u GroupID) IsZero() bool {
return u == 0
}
type RoleID ID
func (u RoleID) IsZero() bool {
return u == 0
}
type CapabilityID ID
// MachineKey is the curve25519 public key for a machine.
@@ -134,7 +114,7 @@ type UserProfile struct {
LoginName string // "alice@smith.com"; for display purposes only (provider is not listed)
DisplayName string // "Alice Smith"
ProfilePicURL string
Roles []RoleID // deprecated; clients should not rely on Roles
Roles []RoleID
}
type Node struct {
@@ -259,7 +239,7 @@ type Service struct {
_ structs.Incomparable
Proto ServiceProto // TCP or UDP
Port uint16 // port number service is listening on
Description string `json:",omitempty"` // text description of service
Description string // text description of service
// TODO(apenwarr): allow advertising services on subnet IPs?
// TODO(apenwarr): add "tags" here for each service?
@@ -275,13 +255,13 @@ type Hostinfo struct {
// TODO(crawshaw): mark all these fields ",omitempty" when all the
// iOS apps are updated with the latest swift version of this struct.
IPNVersion string // version of this code
FrontendLogID string `json:",omitempty"` // logtail ID of frontend instance
BackendLogID string `json:",omitempty"` // logtail ID of backend instance
FrontendLogID string // logtail ID of frontend instance
BackendLogID string // logtail ID of backend instance
OS string // operating system the client runs on (a version.OS value)
OSVersion string `json:",omitempty"` // operating system version, with optional distro prefix ("Debian 10.4", "Windows 10 Pro 10.0.19041")
DeviceModel string `json:",omitempty"` // mobile phone model ("Pixel 3a", "iPhone 11 Pro")
OSVersion string // operating system version, with optional distro prefix ("Debian 10.4", "Windows 10 Pro 10.0.19041")
DeviceModel string // mobile phone model ("Pixel 3a", "iPhone 11 Pro")
Hostname string // name of the host the client runs on
GoArch string `json:",omitempty"` // the host's GOARCH value (of the running binary)
GoArch string // the host's GOARCH value (of the running binary)
RoutableIPs []wgcfg.CIDR `json:",omitempty"` // set of IP ranges this client can route
RequestTags []string `json:",omitempty"` // set of ACL tags this node wants to claim
Services []Service `json:",omitempty"` // services advertised by this machine
@@ -328,7 +308,7 @@ type NetInfo struct {
PreferredDERP int
// LinkType is the current link type, if known.
LinkType string `json:",omitempty"` // "wired", "wifi", "mobile" (LTE, 4G, 3G, etc)
LinkType string // "wired", "wifi", "mobile" (LTE, 4G, 3G, etc)
// DERPLatency is the fastest recent time to reach various
// DERP STUN servers, in seconds. The map key is the
@@ -461,51 +441,22 @@ type RegisterResponse struct {
// using the local machine key, and sent to:
// https://login.tailscale.com/machine/<mkey hex>/map
type MapRequest struct {
// Version is incremented whenever the client code changes enough that
// we want to signal to the control server that we're capable of something
// different.
//
// History of versions:
// 3: implicit compression, keep-alives
// 4: opt-in keep-alives via KeepAlive field, opt-in compression via Compress
// 5: 2020-10-19, implies IncludeIPv6, DeltaPeers/DeltaUserProfiles, supports MagicDNS
Version int
Version int // current version is 4
Compress string // "zstd" or "" (no compression)
KeepAlive bool // whether server should send keep-alives back to us
NodeKey NodeKey
DiscoKey DiscoKey
Endpoints []string // caller's endpoints (IPv4 or IPv6)
IncludeIPv6 bool `json:",omitempty"` // include IPv6 endpoints in returned Node Endpoints (for Version 4 clients)
IncludeIPv6 bool // include IPv6 endpoints in returned Node Endpoints
DeltaPeers bool // whether the 2nd+ network map in response should be deltas, using PeersChanged, PeersRemoved
Stream bool // if true, multiple MapResponse objects are returned
Hostinfo *Hostinfo
// ReadOnly is whether the client just wants to fetch the
// MapResponse, without updating their Endpoints. The
// Endpoints field will be ignored and LastSeen will not be
// updated and peers will not be notified of changes.
//
// The intended use is for clients to discover the DERP map at
// start-up before their first real endpoint update.
ReadOnly bool `json:",omitempty"`
// OmitPeers is whether the client is okay with the Peers list
// being omitted in the response. (For example, a client on
// start up using ReadOnly to get the DERP map.)
OmitPeers bool `json:",omitempty"`
// DebugFlags is a list of strings specifying debugging and
// development features to enable in handling this map
// request. The values are deliberately unspecified, as they get
// added and removed all the time during development, and offer no
// compatibility promise. To roll out semantic changes, bump
// Version instead.
//
// Current DebugFlags values are:
// * "warn-ip-forwarding-off": client is trying to be a subnet
// router but their IP forwarding is broken.
// * "v6-overlay": IPv6 development flag to have control send
// v6 node addrs
DebugFlags []string `json:",omitempty"`
// DebugForceDisco is a temporary flag during the deployment
// of magicsock active discovery. It says that that the client
// has environment variables explicitly turning discovery on,
// so control should not disable it.
DebugForceDisco bool `json:"debugForceDisco,omitempty"`
}
// PortRange represents a range of UDP or TCP port numbers.
@@ -519,42 +470,23 @@ var PortRangeAny = PortRange{0, 65535}
// NetPortRange represents a single subnet:portrange.
type NetPortRange struct {
_ structs.Incomparable
IP string // "*" means all
Bits *int // backward compatibility: if missing, means "all" bits
IP string
Bits *int // backward compatibility: if missing, means "all" bits
Ports PortRange
}
// FilterRule represents one rule in a packet filter.
//
// A rule is logically a set of source CIDRs to match (described by
// SrcIPs and SrcBits), and a set of destination targets that are then
// allowed if a source IP is mathces of those CIDRs.
type FilterRule struct {
// SrcIPs are the source IPs/networks to match.
// The special value "*" means to match all.
SrcIPs []string
// SrcBits values correspond to the SrcIPs above.
//
// If present at the same index, it changes the SrcIP above to
// be a network with /n CIDR bits. If the slice is nil or
// insufficiently long, the default value (for an IPv4
// address) for a position is 32, as if the SrcIPs above were
// a /32 mask. For a "*" SrcIPs value, the corresponding
// SrcBits value is ignored.
// TODO: for IPv6, clarify default bits length.
SrcBits []int
// DstPorts are the port ranges to allow once a source IP
// matches (is in the CIDR described by SrcIPs & SrcBits).
SrcIPs []string
SrcBits []int
DstPorts []NetPortRange
}
var FilterAllowAll = []FilterRule{
{
FilterRule{
SrcIPs: []string{"*"},
SrcBits: nil,
DstPorts: []NetPortRange{{
DstPorts: []NetPortRange{NetPortRange{
IP: "*",
Bits: nil,
Ports: PortRange{0, 65535},
@@ -613,8 +545,8 @@ type MapResponse struct {
// ACLs
Domain string
PacketFilter []FilterRule
UserProfiles []UserProfile // as of 1.1.541: may be new or updated user profiles only
Roles []Role // deprecated; clients should not rely on Roles
UserProfiles []UserProfile
Roles []Role
// TODO: Groups []Group
// TODO: Capabilities []Capability
@@ -650,15 +582,10 @@ type Debug struct {
// TrimWGConfig controls whether Tailscale does lazy, on-demand
// wireguard configuration of peers.
TrimWGConfig opt.Bool `json:",omitempty"`
// DisableSubnetsIfPAC controls whether subnet routers should be
// disabled if WPAD is present on the network.
DisableSubnetsIfPAC opt.Bool `json:",omitempty"`
}
func (k MachineKey) String() string { return fmt.Sprintf("mkey:%x", k[:]) }
func (k MachineKey) MarshalText() ([]byte, error) { return keyMarshalText("mkey:", k), nil }
func (k MachineKey) HexString() string { return fmt.Sprintf("%x", k[:]) }
func (k *MachineKey) UnmarshalText(text []byte) error { return keyUnmarshalText(k[:], "mkey:", text) }
func keyMarshalText(prefix string, k [32]byte) []byte {
@@ -688,9 +615,6 @@ func (k *NodeKey) UnmarshalText(text []byte) error { return keyUnmarshalText(k[:
// IsZero reports whether k is the zero value.
func (k NodeKey) IsZero() bool { return k == NodeKey{} }
// IsZero reports whether k is the zero value.
func (k MachineKey) IsZero() bool { return k == MachineKey{} }
func (k DiscoKey) String() string { return fmt.Sprintf("discokey:%x", k[:]) }
func (k DiscoKey) MarshalText() ([]byte, error) { return keyMarshalText("discokey:", k), nil }
func (k *DiscoKey) UnmarshalText(text []byte) error { return keyUnmarshalText(k[:], "discokey:", text) }

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Code generated by tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse; DO NOT EDIT.
// Code generated by tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig; DO NOT EDIT.
package tailcfg
@@ -28,7 +28,7 @@ func (src *User) Clone() *User {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig
var _UserNeedsRegeneration = User(struct {
ID UserID
LoginName string
@@ -60,7 +60,7 @@ func (src *Node) Clone() *Node {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig
var _NodeNeedsRegeneration = Node(struct {
ID NodeID
Name string
@@ -96,7 +96,7 @@ func (src *Hostinfo) Clone() *Hostinfo {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig
var _HostinfoNeedsRegeneration = Hostinfo(struct {
IPNVersion string
FrontendLogID string
@@ -130,7 +130,7 @@ func (src *NetInfo) Clone() *NetInfo {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig
var _NetInfoNeedsRegeneration = NetInfo(struct {
MappingVariesByDestIP opt.Bool
HairPinning opt.Bool
@@ -157,7 +157,7 @@ func (src *Group) Clone() *Group {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig
var _GroupNeedsRegeneration = Group(struct {
ID GroupID
Name string
@@ -177,7 +177,7 @@ func (src *Role) Clone() *Role {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig
var _RoleNeedsRegeneration = Role(struct {
ID RoleID
Name string
@@ -196,7 +196,7 @@ func (src *Capability) Clone() *Capability {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig
var _CapabilityNeedsRegeneration = Capability(struct {
ID CapabilityID
Type CapType
@@ -215,7 +215,7 @@ func (src *Login) Clone() *Login {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig
var _LoginNeedsRegeneration = Login(struct {
_ structs.Incomparable
ID LoginID
@@ -240,7 +240,7 @@ func (src *DNSConfig) Clone() *DNSConfig {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig
var _DNSConfigNeedsRegeneration = DNSConfig(struct {
Nameservers []netaddr.IP
Domains []string
@@ -248,31 +248,9 @@ var _DNSConfigNeedsRegeneration = DNSConfig(struct {
Proxied bool
}{})
// Clone makes a deep copy of RegisterResponse.
// The result aliases no memory with the original.
func (src *RegisterResponse) Clone() *RegisterResponse {
if src == nil {
return nil
}
dst := new(RegisterResponse)
*dst = *src
dst.User = *src.User.Clone()
return dst
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
var _RegisterResponseNeedsRegeneration = RegisterResponse(struct {
User User
Login Login
NodeKeyExpired bool
MachineAuthorized bool
AuthURL string
}{})
// Clone duplicates src into dst and reports whether it succeeded.
// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,
// where T is one of User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse.
// where T is one of User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig.
func Clone(dst, src interface{}) bool {
switch src := src.(type) {
case *User:
@@ -356,15 +334,6 @@ func Clone(dst, src interface{}) bool {
*dst = src.Clone()
return true
}
case *RegisterResponse:
switch dst := dst.(type) {
case *RegisterResponse:
*dst = *src.Clone()
return true
case **RegisterResponse:
*dst = src.Clone()
return true
}
}
return false
}

View File

@@ -7,12 +7,17 @@
package exec
import (
"io/ioutil"
"os"
"testing"
)
func TestLookPathUnixEmptyPath(t *testing.T) {
tmp := t.TempDir()
tmp, err := ioutil.TempDir("", "TestLookPathUnixEmptyPath")
if err != nil {
t.Fatal("TempDir failed: ", err)
}
defer os.RemoveAll(tmp)
wd, err := os.Getwd()
if err != nil {
t.Fatal("Getwd failed: ", err)

View File

@@ -5,8 +5,6 @@
package tstest
import (
"bytes"
"fmt"
"log"
"os"
"sync"
@@ -37,16 +35,7 @@ func UnfixLogs(t *testing.T) {
type panicLogWriter struct{}
func (panicLogWriter) Write(b []byte) (int, error) {
// Allow certain phrases for now, in the interest of getting
// CI working on Windows and not having to refactor all the
// interfaces.GetState & tshttpproxy code to allow pushing
// down a Logger yet. TODO(bradfitz): do that refactoring once
// 1.2.0 is out.
if bytes.Contains(b, []byte("tshttpproxy: ")) {
os.Stderr.Write(b)
return len(b), nil
}
panic(fmt.Sprintf("please use tailscale.com/logger.Logf instead of the log package (tried to log: %q)", b))
panic("please use tailscale.com/logger.Logf instead of the log package")
}
// PanicOnLog modifies the standard library log package's default output to

View File

@@ -40,12 +40,6 @@ func goroutineDump() (int, string) {
}
func (r *ResourceCheck) Assert(t testing.TB) {
if t.Failed() {
// Something else went wrong.
// Assume that that is responsible for the leak
// and don't pile on a bunch of extra of output.
return
}
t.Helper()
want := r.startNumRoutines

View File

@@ -157,22 +157,11 @@ type ReturnHandler interface {
ServeHTTPReturn(http.ResponseWriter, *http.Request) error
}
type HandlerOptions struct {
Quiet200s bool // if set, do not log successfully handled HTTP requests
Logf logger.Logf
Now func() time.Time // if nil, defaults to time.Now
// If non-nil, StatusCodeCounters maintains counters
// of status codes for handled responses.
// The keys are "1xx", "2xx", "3xx", "4xx", and "5xx".
StatusCodeCounters *expvar.Map
}
// StdHandler converts a ReturnHandler into a standard http.Handler.
// Handled requests are logged using logf, as are any errors. Errors
// are handled as specified by the Handler interface.
func StdHandler(h ReturnHandler, logf logger.Logf) http.Handler {
return StdHandlerOpts(h, HandlerOptions{Logf: logf, Now: time.Now})
return stdHandler(h, logf, time.Now, true)
}
// ReturnHandlerFunc is an adapter to allow the use of ordinary
@@ -189,32 +178,27 @@ func (f ReturnHandlerFunc) ServeHTTPReturn(w http.ResponseWriter, r *http.Reques
// StdHandlerNo200s is like StdHandler, but successfully handled HTTP
// requests don't write an access log entry to logf.
//
// TODO(josharian): eliminate this and StdHandler in favor of StdHandlerOpts,
// rename StdHandlerOpts to StdHandler. Will be a breaking API change.
// TODO(danderson): quick stopgap, probably want ...Options on StdHandler instead?
func StdHandlerNo200s(h ReturnHandler, logf logger.Logf) http.Handler {
return StdHandlerOpts(h, HandlerOptions{Logf: logf, Now: time.Now, Quiet200s: true})
return stdHandler(h, logf, time.Now, false)
}
// StdHandlerOpts converts a ReturnHandler into a standard http.Handler.
// Handled requests are logged using opts.Logf, as are any errors.
// Errors are handled as specified by the Handler interface.
func StdHandlerOpts(h ReturnHandler, opts HandlerOptions) http.Handler {
if opts.Now == nil {
opts.Now = time.Now
}
return retHandler{h, opts}
func stdHandler(h ReturnHandler, logf logger.Logf, now func() time.Time, log200s bool) http.Handler {
return retHandler{h, logf, now, log200s}
}
// retHandler is an http.Handler that wraps a Handler and handles errors.
type retHandler struct {
rh ReturnHandler
opts HandlerOptions
rh ReturnHandler
logf logger.Logf
timeNow func() time.Time
log200s bool
}
// ServeHTTP implements the http.Handler interface.
func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
msg := AccessLogRecord{
When: h.opts.Now(),
When: h.timeNow(),
RemoteAddr: r.RemoteAddr,
Proto: r.Proto,
TLS: r.TLS != nil,
@@ -225,7 +209,7 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Referer: r.Referer(),
}
lw := &loggingResponseWriter{ResponseWriter: w, logf: h.opts.Logf}
lw := &loggingResponseWriter{ResponseWriter: w, logf: h.logf}
err := h.rh.ServeHTTPReturn(lw, r)
hErr, hErrOK := err.(HTTPError)
@@ -235,7 +219,7 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
lw.code = 200
}
msg.Seconds = h.opts.Now().Sub(msg.When).Seconds()
msg.Seconds = h.timeNow().Sub(msg.When).Seconds()
msg.Code = lw.code
msg.Bytes = lw.bytes
@@ -261,12 +245,12 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
if lw.code != 0 {
h.opts.Logf("[unexpected] handler returned HTTPError %v, but already sent a response with code %d", hErr, lw.code)
h.logf("[unexpected] handler returned HTTPError %v, but already sent a response with code %d", hErr, lw.code)
break
}
msg.Code = hErr.Code
if msg.Code == 0 {
h.opts.Logf("[unexpected] HTTPError %v did not contain an HTTP status code, sending internal server error", hErr)
h.logf("[unexpected] HTTPError %v did not contain an HTTP status code, sending internal server error", hErr)
msg.Code = http.StatusInternalServerError
}
http.Error(lw, hErr.Msg, msg.Code)
@@ -280,13 +264,8 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
if msg.Code != 200 || !h.opts.Quiet200s {
h.opts.Logf("%s", msg)
}
if h.opts.StatusCodeCounters != nil {
key := fmt.Sprintf("%dxx", msg.Code/100)
h.opts.StatusCodeCounters.Add(key, 1)
if msg.Code != 200 || h.log200s {
h.logf("%s", msg)
}
}

View File

@@ -248,7 +248,7 @@ func TestStdHandler(t *testing.T) {
clock.Reset()
rec := noopHijacker{httptest.NewRecorder(), false}
h := StdHandlerOpts(test.rh, HandlerOptions{Logf: logf, Now: clock.Now})
h := stdHandler(test.rh, logf, clock.Now, true)
h.ServeHTTP(&rec, test.r)
res := rec.Result()
if res.StatusCode != test.wantCode {

View File

@@ -1,46 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package flagtype defines flag.Value types.
package flagtype
import (
"errors"
"flag"
"fmt"
"math"
"strconv"
"strings"
)
type portValue struct{ n *uint16 }
func PortValue(dst *uint16, defaultPort uint16) flag.Value {
*dst = defaultPort
return portValue{dst}
}
func (p portValue) String() string {
if p.n == nil {
return ""
}
return fmt.Sprint(*p.n)
}
func (p portValue) Set(v string) error {
if v == "" {
return errors.New("can't be the empty string")
}
if strings.Contains(v, ":") {
return errors.New("expecting just a port number, without a colon")
}
n, err := strconv.ParseUint(v, 10, 64) // use 64 instead of 16 to return nicer error message
if err != nil {
return fmt.Errorf("not a valid number")
}
if n > math.MaxUint16 {
return errors.New("out of range for port number")
}
*p.n = uint16(n)
return nil
}

View File

@@ -158,10 +158,7 @@ func LogOnChange(logf Logf, maxInterval time.Duration, timeNow func() time.Time)
tLastLogged = timeNow()
mu.Unlock()
// Re-stringify it (instead of using "%s", s) so something like "%s"
// doesn't end up getting rate-limited. (And can't use 's' as the pattern,
// as it might contain formatting directives.)
logf(format, args...)
logf(s)
}
}

View File

@@ -1,10 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build mips mips64 ppc64 s390x
package endian
// Big is whether the current platform is big endian.
const Big = true

View File

@@ -1,6 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package endian exports a constant about whether the machine is big endian.
package endian

View File

@@ -1,10 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build 386 amd64 arm arm64 mips64le mipsle ppc64le riscv64 wasm
package endian
// Big is whether the current platform is big endian.
const Big = false

View File

@@ -1,65 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package uniq provides removal of adjacent duplicate elements in slices.
// It is similar to the unix command uniq.
package uniq
import (
"fmt"
"reflect"
)
type badTypeError struct {
typ reflect.Type
}
func (e badTypeError) Error() string {
return fmt.Sprintf("uniq.ModifySlice's first argument must have type *[]T, got %v", e.typ)
}
// ModifySlice removes adjacent duplicate elements from the slice pointed to by sliceptr.
// It adjusts the length of the slice appropriately and zeros the tail.
// eq reports whether (*sliceptr)[i] and (*sliceptr)[j] are equal.
// ModifySlice does O(len(*sliceptr)) operations.
func ModifySlice(sliceptr interface{}, eq func(i, j int) bool) {
rvp := reflect.ValueOf(sliceptr)
if rvp.Type().Kind() != reflect.Ptr {
panic(badTypeError{rvp.Type()})
}
rv := rvp.Elem()
if rv.Type().Kind() != reflect.Slice {
panic(badTypeError{rvp.Type()})
}
length := rv.Len()
dst := 0
for i := 1; i < length; i++ {
if eq(dst, i) {
continue
}
dst++
// slice[dst] = slice[i]
rv.Index(dst).Set(rv.Index(i))
}
end := dst + 1
var zero reflect.Value
if end < length {
zero = reflect.Zero(rv.Type().Elem())
}
// for i := range slice[end:] {
// size[i] = 0/nil/{}
// }
for i := end; i < length; i++ {
// slice[i] = 0/nil/{}
rv.Index(i).Set(zero)
}
// slice = slice[:end]
if end < length {
rv.SetLen(end)
}
}

View File

@@ -1,88 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uniq_test
import (
"reflect"
"strconv"
"testing"
"tailscale.com/util/uniq"
)
func TestModifySlice(t *testing.T) {
tests := []struct {
in []int
want []int
}{
{in: []int{0, 1, 2}, want: []int{0, 1, 2}},
{in: []int{0, 1, 2, 2}, want: []int{0, 1, 2}},
{in: []int{0, 0, 1, 2}, want: []int{0, 1, 2}},
{in: []int{0, 1, 0, 2}, want: []int{0, 1, 0, 2}},
{in: []int{0}, want: []int{0}},
{in: []int{0, 0}, want: []int{0}},
{in: []int{}, want: []int{}},
}
for _, test := range tests {
in := make([]int, len(test.in))
copy(in, test.in)
uniq.ModifySlice(&test.in, func(i, j int) bool { return test.in[i] == test.in[j] })
if !reflect.DeepEqual(test.in, test.want) {
t.Errorf("uniq.Slice(%v) = %v, want %v", in, test.in, test.want)
}
start := len(test.in)
test.in = test.in[:cap(test.in)]
for i := start; i < len(in); i++ {
if test.in[i] != 0 {
t.Errorf("uniq.Slice(%v): non-0 in tail of %v at index %v", in, test.in, i)
}
}
}
}
func Benchmark(b *testing.B) {
benches := []struct {
name string
reset func(s []byte)
}{
{name: "AllDups",
reset: func(s []byte) {
for i := range s {
s[i] = '*'
}
},
},
{name: "NoDups",
reset: func(s []byte) {
for i := range s {
s[i] = byte(i)
}
},
},
}
for _, bb := range benches {
b.Run(bb.name, func(b *testing.B) {
for size := 1; size <= 4096; size *= 16 {
b.Run(strconv.Itoa(size), func(b *testing.B) {
benchmark(b, 64, bb.reset)
})
}
})
}
}
func benchmark(b *testing.B, size int64, reset func(s []byte)) {
b.ReportAllocs()
b.SetBytes(size)
s := make([]byte, size)
b.ResetTimer()
for i := 0; i < b.N; i++ {
s = s[:size]
reset(s)
uniq.ModifySlice(&s, func(i, j int) bool { return s[i] == s[j] })
}
}

3
version/.gitignore vendored
View File

@@ -1,9 +1,6 @@
describe.txt
long.txt
short.txt
gitcommit.txt
extragitcommit.txt
version-info.sh
version.h
version.xcconfig
ver.go

4
version/describe.txt.do Normal file
View File

@@ -0,0 +1,4 @@
describe=$(cd ../.. && git describe --long --abbrev=9)
echo "$describe" >$3
redo-always
redo-stamp <$3

View File

@@ -16,7 +16,6 @@ const (
Debian = Distro("debian")
Arch = Distro("arch")
Synology = Distro("synology")
OpenWrt = Distro("openwrt")
)
// Get returns the current distro, or the empty string if unknown.
@@ -37,8 +36,5 @@ func linuxDistro() Distro {
if _, err := os.Stat("/etc/arch-release"); err == nil {
return Arch
}
if _, err := os.Stat("/etc/openwrt_version"); err == nil {
return OpenWrt
}
return ""
}

4
version/long.txt.do Normal file
View File

@@ -0,0 +1,4 @@
redo-ifchange mkversion.sh describe.txt
read -r describe <describe.txt
ver=$(./mkversion.sh long "$describe")
echo "$ver" >$3

111
version/mkversion.sh Executable file
View File

@@ -0,0 +1,111 @@
#!/bin/sh
set -eu
mode=$1
describe=$2
# Git describe output overall looks like
# MAJOR.MINOR.PATCH-NUMCOMMITS-GITHASH. Depending on the tag being
# described and the state of the repo, ver can be missing PATCH,
# NUMCOMMITS, or both.
#
# Valid values look like: 1.2.3-1234-abcdef, 0.98-1234-abcdef,
# 1.0.0-abcdef, 0.99-abcdef.
ver="${describe#v}"
stem="${ver%%-*}" # Just the semver-ish bit e.g. 1.2.3, 0.98
suffix="${ver#$stem}" # The rest e.g. -23-abcdef, -abcdef
# Normalize the stem into a full major.minor.patch semver. We might
# not use all those pieces depending on what kind of version we're
# making, but it's good to have them all on hand.
case "$stem" in
*.*.*)
# Full SemVer, nothing to do
stem="$stem"
;;
*.*)
# Old style major.minor, add a .0
stem="${stem}.0"
;;
*)
echo "Unparseable version $stem" >&2
exit 1
;;
esac
major=$(echo "$stem" | cut -f1 -d.)
minor=$(echo "$stem" | cut -f2 -d.)
patch=$(echo "$stem" | cut -f3 -d.)
# Extract the change count and git ID from the suffix.
case "$suffix" in
-*-*)
# Has both a change count and a commit hash.
changecount=$(echo "$suffix" | cut -f2 -d-)
githash=$(echo "$suffix" | cut -f3 -d-)
;;
-*)
# Git hash only, change count is zero.
changecount="0"
githash=$(echo "$suffix" | cut -f2 -d-)
;;
*)
echo "Unparseable version suffix $suffix" >&2
exit 1
;;
esac
# Validate that the version data makes sense. Rules:
# - Odd number minors are unstable. Patch must be 0, and gets
# replaced by changecount.
# - Even number minors are stable. Changecount must be 0, and
# gets removed.
#
# After this section, we only use major/minor/patch, which have been
# tweaked as needed.
if expr "$minor" : "[0-9]*[13579]$" >/dev/null; then
# Unstable
if [ "$patch" != "0" ]; then
# This is a fatal error, because a non-zero patch number
# indicates that we created an unstable git tag in violation
# of our versioning policy, and we want to blow up loudly to
# get that fixed.
echo "Unstable release $describe has a non-zero patch number, which is not allowed" >&2
exit 1
fi
patch="$changecount"
else
# Stable
if [ "$changecount" != "0" ]; then
# This is a commit that's sitting between two stable
# releases. We never want to release such a commit to the
# pbulic, but it's useful to be able to build it for
# debugging. Just force the version to 0.0.0, so that we're
# forced to rely on the git commit hash.
major=0
minor=0
patch=0
fi
fi
case "$1" in
long)
echo "${major}.${minor}.${patch}-${githash}"
;;
short)
echo "${major}.${minor}.${patch}"
;;
xcode)
# CFBundleShortVersionString: the "short name" used in the App
# Store. eg. 0.92.98
echo "VERSION_NAME = ${major}.${minor}.${patch}"
# CFBundleVersion: the build number. Needs to be 3 numeric
# sections that increment for each release according to SemVer
# rules.
#
# We start counting at 100 because we submitted using raw
# build numbers before, and Apple doesn't let you start over.
# e.g. 0.98.3 -> 100.98.3
echo "VERSION_ID = $((major + 100)).${minor}.${patch}"
;;
esac

View File

@@ -7,96 +7,69 @@ package version
import (
"fmt"
"os/exec"
"runtime"
"strconv"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
)
func mkversion(t *testing.T, gitHash, otherHash string, major, minor, patch, changeCount int) (string, bool) {
func xcode(short, long string) string {
return fmt.Sprintf("VERSION_NAME = %s\nVERSION_ID = %s", short, long)
}
func mkversion(t *testing.T, mode, in string) (string, bool) {
t.Helper()
bs, err := exec.Command("./version.sh", gitHash, otherHash, strconv.Itoa(major), strconv.Itoa(minor), strconv.Itoa(patch), strconv.Itoa(changeCount)).CombinedOutput()
out := strings.TrimSpace(string(bs))
bs, err := exec.Command("./mkversion.sh", mode, in).CombinedOutput()
if err != nil {
return out, false
t.Logf("mkversion.sh output: %s", string(bs))
return "", false
}
return out, true
return strings.TrimSpace(string(bs)), true
}
func TestMkversion(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skip test on Windows, because there is no shell to execute mkversion.sh.")
}
tests := []struct {
gitHash, otherHash string
major, minor, patch, changeCount int
want string
in string
ok bool
long string
short string
xcode string
}{
{"abcdef", "", 0, 98, 0, 0, `
VERSION_SHORT="0.98.0"
VERSION_LONG="0.98.0-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH=""
VERSION_XCODE="100.98.0"
VERSION_WINRES="0,98,0,0"`},
{"abcdef", "", 0, 98, 1, 0, `
VERSION_SHORT="0.98.1"
VERSION_LONG="0.98.1-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH=""
VERSION_XCODE="100.98.1"
VERSION_WINRES="0,98,1,0"`},
{"abcdef", "", 1, 1, 0, 37, `
VERSION_SHORT="1.1.1037"
VERSION_LONG="1.1.1037-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH=""
VERSION_XCODE="101.1.1037"
VERSION_WINRES="1,1,1037,0"`},
{"abcdef", "", 1, 2, 9, 0, `
VERSION_SHORT="1.2.9"
VERSION_LONG="1.2.9-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH=""
VERSION_XCODE="101.2.9"
VERSION_WINRES="1,2,9,0"`},
{"abcdef", "", 1, 15, 0, 129, `
VERSION_SHORT="1.15.129"
VERSION_LONG="1.15.129-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH=""
VERSION_XCODE="101.15.129"
VERSION_WINRES="1,15,129,0"`},
{"abcdef", "", 1, 2, 0, 17, `
VERSION_SHORT="1.2.0"
VERSION_LONG="1.2.0-17-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH=""
VERSION_XCODE="101.2.0"
VERSION_WINRES="1,2,0,0"`},
{"abcdef", "defghi", 1, 15, 0, 129, `
VERSION_SHORT="1.15.129"
VERSION_LONG="1.15.129-tabcdef-gdefghi"
VERSION_GIT_HASH="abcdef"
VERSION_EXTRA_HASH="defghi"
VERSION_XCODE="101.15.129"
VERSION_WINRES="1,15,129,0"`},
{"abcdef", "", 0, 99, 5, 0, ""}, // unstable, patch number not allowed
{"abcdef", "", 0, 99, 5, 123, ""}, // unstable, patch number not allowed
{"v0.98-abcdef", true, "0.98.0-abcdef", "0.98.0", xcode("0.98.0", "100.98.0")},
{"v0.98.1-abcdef", true, "0.98.1-abcdef", "0.98.1", xcode("0.98.1", "100.98.1")},
{"v1.1.0-37-abcdef", true, "1.1.37-abcdef", "1.1.37", xcode("1.1.37", "101.1.37")},
{"v1.2.9-abcdef", true, "1.2.9-abcdef", "1.2.9", xcode("1.2.9", "101.2.9")},
{"v1.2.9-0-abcdef", true, "1.2.9-abcdef", "1.2.9", xcode("1.2.9", "101.2.9")},
{"v1.15.0-129-abcdef", true, "1.15.129-abcdef", "1.15.129", xcode("1.15.129", "101.15.129")},
{"v0.98-123-abcdef", true, "0.0.0-abcdef", "0.0.0", xcode("0.0.0", "100.0.0")},
{"v1.0.0-37-abcdef", true, "0.0.0-abcdef", "0.0.0", xcode("0.0.0", "100.0.0")},
{"v0.99.5-0-abcdef", false, "", "", ""}, // unstable, patch not allowed
{"v0.99.5-123-abcdef", false, "", "", ""}, // unstable, patch not allowed
{"v1-abcdef", false, "", "", ""}, // bad semver
{"v1.0", false, "", "", ""}, // missing suffix
}
for _, test := range tests {
want := strings.ReplaceAll(strings.TrimSpace(test.want), " ", "")
got, ok := mkversion(t, test.gitHash, test.otherHash, test.major, test.minor, test.patch, test.changeCount)
invoc := fmt.Sprintf("version.sh %s %s %d %d %d %d", test.gitHash, test.otherHash, test.major, test.minor, test.patch, test.changeCount)
if want == "" && ok {
t.Errorf("%s ok=true, want false", invoc)
continue
gotlong, longOK := mkversion(t, "long", test.in)
if longOK != test.ok {
t.Errorf("mkversion.sh long %q ok=%v, want %v", test.in, longOK, test.ok)
}
if diff := cmp.Diff(got, want); want != "" && diff != "" {
t.Errorf("%s wrong output (-got+want):\n%s", invoc, diff)
gotshort, shortOK := mkversion(t, "short", test.in)
if shortOK != test.ok {
t.Errorf("mkversion.sh short %q ok=%v, want %v", test.in, shortOK, test.ok)
}
gotxcode, xcodeOK := mkversion(t, "xcode", test.in)
if xcodeOK != test.ok {
t.Errorf("mkversion.sh xcode %q ok=%v, want %v", test.in, xcodeOK, test.ok)
}
if longOK && gotlong != test.long {
t.Errorf("mkversion.sh long %q: got %q, want %q", test.in, gotlong, test.long)
}
if shortOK && gotshort != test.short {
t.Errorf("mkversion.sh short %q: got %q, want %q", test.in, gotshort, test.short)
}
if xcodeOK && gotxcode != test.xcode {
t.Errorf("mkversion.sh xcode %q: got %q, want %q", test.in, gotxcode, test.xcode)
}
}
}

View File

@@ -1,25 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package version
import (
"fmt"
"runtime"
"strings"
)
func String() string {
var ret strings.Builder
ret.WriteString(Short)
ret.WriteByte('\n')
if GitCommit != "" {
fmt.Fprintf(&ret, " tailscale commit: %s\n", GitCommit)
}
if ExtraGitCommit != "" {
fmt.Fprintf(&ret, " other commit: %s\n", ExtraGitCommit)
}
fmt.Fprintf(&ret, " go version: %s\n", runtime.Version())
return strings.TrimSpace(ret.String())
}

4
version/short.txt.do Normal file
View File

@@ -0,0 +1,4 @@
redo-ifchange mkversion.sh describe.txt
read -r describe <describe.txt
ver=$(./mkversion.sh short "$describe")
echo "$ver" >$3

View File

@@ -1,9 +1,8 @@
redo-ifchange version-info.sh ver.go.in
redo-ifchange long.txt short.txt ver.go.in
. ./version-info.sh
read -r LONGVER <long.txt
read -r SHORTVER <short.txt
sed -e "s/{LONGVER}/$VERSION_LONG/g" \
-e "s/{SHORTVER}/$VERSION_SHORT/g" \
-e "s/{GITCOMMIT}/$VERSION_GIT_HASH/g" \
-e "s/{EXTRAGITCOMMIT}/$VERSION_EXTRA_HASH/g" \
sed -e "s/{LONGVER}/$LONGVER/g" \
-e "s/{SHORTVER}/$SHORTVER/g" \
<ver.go.in >$3

View File

@@ -6,9 +6,5 @@
package version
const Long = "{LONGVER}"
const Short = "{SHORTVER}"
const LONG = Long
const SHORT = Short
const GitCommit = "{GITCOMMIT}"
const ExtraGitCommit = "{EXTRAGITCOMMIT}"
const LONG = "{LONGVER}"
const SHORT = "{SHORTVER}"

View File

@@ -1,3 +0,0 @@
./version.sh ../.. >$3
redo-always
redo-stamp <$3

View File

@@ -7,35 +7,5 @@
// Package version provides the version that the binary was built at.
package version
// Long is a full version number for this build, of the form
// "x.y.z-commithash", or "date.yyyymmdd" if no actual version was
// provided.
const Long = "date.20200921"
// Short is a short version number for this build, of the form
// "x.y.z", or "date.yyyymmdd" if no actual version was provided.
const Short = Long
// LONG is a deprecated alias for Long. Don't use it.
const LONG = Long
// SHORT is a deprecated alias for Short. Don't use it.
const SHORT = Short
// GitCommit, if non-empty, is the git commit of the
// github.com/tailscale/tailscale repository at which Tailscale was
// built. Its format is the one returned by `git describe --always
// --exclude "*" --dirty --abbrev=200`.
const GitCommit = ""
// ExtraGitCommit, if non-empty, is the git commit of a "supplemental"
// repository at which Tailscale was built. Its format is the same as
// gitCommit.
//
// ExtraGitCommit is used to track the source revision when the main
// Tailscale repository is integrated into and built from another
// repository (for example, Tailscale's proprietary code, or the
// Android OSS repository). Together, GitCommit and ExtraGitCommit
// exactly describe what repositories and commits were used in a
// build.
const ExtraGitCommit = ""
const LONG = "date.20200820"
const SHORT = LONG

View File

@@ -1,9 +1,11 @@
redo-ifchange version-info.sh
redo-ifchange long.txt short.txt
read -r long <long.txt
read -r short <short.txt
. ./version-info.sh
# get it into "major.minor.patch" format
ver=$(echo "$ver" | sed -e 's/-/./')
cat >$3 <<EOF
#define TAILSCALE_VERSION_LONG "$VERSION_LONG"
#define TAILSCALE_VERSION_SHORT "$VERSION_SHORT"
#define TAILSCALE_VERSION_WIN_RES $VERSION_WINRES
EOF
(
printf '#define TAILSCALE_VERSION_LONG "%s"\n' "$long"
printf '#define TAILSCALE_VERSION_SHORT "%s"\n' "$short"
) >$3

View File

@@ -1,130 +0,0 @@
#!/bin/sh
set -eu
# Return the commitid of the given ref in the given repo dir. If the worktree
# or index is dirty, also appends -dirty.
#
# $ git_hash_dirty ../.. HEAD
# 1be01ddc6e430ca3aa9beea3587d16750efb3241-dirty
git_hash_dirty() {
(
cd "$1"
x=$(git rev-parse HEAD)
if ! git diff-index --quiet HEAD; then
x="$x-dirty"
fi
echo "$x"
)
}
case $# in
0|1)
# extra_hash_or_dir is either:
# - a git commitid
# or
# - the path to a git repo from which to calculate the real hash.
#
# It gets embedded as an additional commit hash in built
# binaries, to help us locate the exact set of tools and code
# that were used.
extra_hash_or_dir="${1:-}"
if [ -z "$extra_hash_or_dir" ]; then
# Nothing, empty extra hash is fine.
extra_hash=""
elif [ -d "$extra_hash_or_dir/.git" ]; then
extra_hash=$(git_hash_dirty "$extra_hash_or_dir" HEAD)
elif ! expr "$extra_hash_or_dir" : "^[0-9a-f]*$"; then
echo "Invalid extra hash '$extra_hash_or_dir', must be a git commit or path to a git repo" >&2
exit 1
else
extra_hash="$extra_hash_or_dir"
fi
# Load the base version and optional corresponding git hash
# from the VERSION file. If there is no git hash in the file,
# we use the hash of the last change to the VERSION file.
version_file="$(dirname $0)/../VERSION.txt"
IFS=".$IFS" read -r major minor patch base_git_hash <"$version_file"
if [ -z "$base_git_hash" ]; then
base_git_hash=$(git rev-list --max-count=1 HEAD -- "$version_file")
fi
git_hash=$(git_hash_dirty . HEAD)
# The number of extra commits between the release base to git_hash.
change_count=$(git rev-list --count HEAD "^$base_git_hash")
;;
6)
# Test mode: rather than run git commands and whatnot, take in
# all the version pieces as arguments.
git_hash=$1
extra_hash=$2
major=$3
minor=$4
patch=$5
change_count=$6
;;
*)
echo "Usage: $0 [extra-git-commitid-or-dir]"
exit 1
esac
# Shortened versions of git hashes, so that they fit neatly into an
# "elongated" but still human-readable version number.
short_git_hash=$(echo "$git_hash" | cut -c1-9)
short_extra_hash=$(echo "$extra_hash" | cut -c1-9)
# Convert major/minor/patch/change_count into an adjusted
# major/minor/patch. This block is where all our policies on
# versioning are.
if expr "$minor" : "[0-9]*[13579]$" >/dev/null; then
# Odd minor numbers are unstable builds.
if [ "$patch" != "0" ]; then
# This is a fatal error, because a non-zero patch number
# indicates that we created an unstable VERSION.txt in violation
# of our versioning policy, and we want to blow up loudly to
# get that fixed.
echo "Unstable release $major.$minor.$patch has a non-zero patch number, which is not allowed" >&2
exit 1
fi
patch="$change_count"
change_suffix=""
elif [ "$change_count" != "0" ]; then
# Even minor numbers are stable builds, but stable builds are
# supposed to have a zero change count. Therefore, we're currently
# describing a commit that's on a release branch, but hasn't been
# tagged as a patch release yet.
#
# We used to change the version number to 0.0.0 in that case, but that
# caused some features to get disabled due to the low version number.
# Instead, add yet another suffix to the version number, with a change
# count.
change_suffix="-$change_count"
else
# Even minor number with no extra changes.
change_suffix=""
fi
# Hack for 1.1: add 1000 to the patch number. We switched from using
# the proprietary repo's change_count over to using the OSS repo's
# change_count, and this was necessary to avoid a backwards jump in
# release numbers.
if [ "$major.$minor" = "1.1" ]; then
patch="$((patch + 1000))"
fi
# At this point, the version number correctly reflects our
# policies. All that remains is to output the various vars that other
# code can use to embed version data.
if [ -z "$extra_hash" ]; then
long_version_suffix="$change_suffix-t$short_git_hash"
else
long_version_suffix="$change_suffix-t$short_git_hash-g$short_extra_hash"
fi
cat <<EOF
VERSION_SHORT="$major.$minor.$patch"
VERSION_LONG="$major.$minor.$patch$long_version_suffix"
VERSION_GIT_HASH="$git_hash"
VERSION_EXTRA_HASH="$extra_hash"
VERSION_XCODE="$((major + 100)).$minor.$patch"
VERSION_WINRES="$major,$minor,$patch,0"
EOF

View File

@@ -1,14 +1,4 @@
redo-ifchange version-info.sh
. ./version-info.sh
# CFBundleShortVersionString: the "short name" used in the App Store.
# eg. 0.92.98
echo "VERSION_NAME = $VERSION_SHORT"
# CFBundleVersion: the build number. Needs to be 3 numeric sections
# that increment for each release according to SemVer rules.
#
# We start counting at 100 because we submitted using raw build
# numbers before, and Apple doesn't let you start over. e.g. 0.98.3
# -> 100.98.3
echo "VERSION_ID = $VERSION_XCODE"
redo-ifchange mkversion.sh describe.txt
read -r describe <describe.txt
ver=$(./mkversion.sh xcode "$describe")
echo "$ver" >$3

View File

@@ -6,12 +6,6 @@
package version
// Replaced at build time with the Go linker flag -X. See
// ../build_dist.sh for example usage, and version.go for field
// documentation.
var Long string = "<not set>"
var Short string = "<not set>"
var LONG = Long
var SHORT = Short
var GitCommit = ""
var ExtraGitCommit = ""
// Replaced at build time with the Go linker flag -X.
var LONG string = "<not set>"
var SHORT string = "<not set>"

View File

@@ -367,15 +367,6 @@ func (f *Filter) pre(q *packet.ParsedPacket, rf RunFlags, dir direction) Respons
f.logRateLimit(rf, q, dir, Drop, "ipv6")
return Drop
}
if q.DstIP.IsMulticast() {
f.logRateLimit(rf, q, dir, Drop, "multicast")
return Drop
}
if q.DstIP.IsMostLinkLocalUnicast() {
f.logRateLimit(rf, q, dir, Drop, "link-local-unicast")
return Drop
}
switch q.IPProto {
case packet.Unknown:
// Unknown packets are dangerous; always drop them.
@@ -418,9 +409,6 @@ func omitDropLogging(p *packet.ParsedPacket, dir direction) bool {
if ipProto == packet.IGMP {
return true
}
if p.DstIP.IsMulticast() || p.DstIP.IsMostLinkLocalUnicast() {
return true
}
case 6:
if len(b) < 40 {
return false

View File

@@ -379,24 +379,6 @@ func TestOmitDropLogging(t *testing.T) {
dir: out,
want: true,
},
{
name: "v4_multicast_out_low",
pkt: &packet.ParsedPacket{IPVersion: 4, DstIP: packet.NewIP(net.ParseIP("224.0.0.0"))},
dir: out,
want: true,
},
{
name: "v4_multicast_out_high",
pkt: &packet.ParsedPacket{IPVersion: 4, DstIP: packet.NewIP(net.ParseIP("239.255.255.255"))},
dir: out,
want: true,
},
{
name: "v4_link_local_unicast",
pkt: &packet.ParsedPacket{IPVersion: 4, DstIP: packet.NewIP(net.ParseIP("169.254.1.2"))},
dir: out,
want: true,
},
}
for _, tt := range tests {

View File

@@ -40,7 +40,6 @@ import (
"tailscale.com/derp/derphttp"
"tailscale.com/disco"
"tailscale.com/ipn/ipnstate"
"tailscale.com/logtail/backoff"
"tailscale.com/net/dnscache"
"tailscale.com/net/interfaces"
"tailscale.com/net/netcheck"
@@ -120,7 +119,6 @@ type Conn struct {
netChecker *netcheck.Client
idleFunc func() time.Duration // nil means unknown
noteRecvActivity func(tailcfg.DiscoKey) // or nil, see Options.NoteRecvActivity
simulatedNetwork bool
// bufferedIPv4From and bufferedIPv4Packet are owned by
// ReceiveIPv4, and used when both a DERP and IPv4 packet arrive
@@ -210,11 +208,6 @@ type Conn struct {
// necessarily have a netcheck.Report and don't want to skip
// logging.
noV4, noV6 syncs.AtomicBool
// networkUp is whether the network is up (some interface is up
// with IPv4 or IPv6). It's used to suppress log spam and prevent
// new connection that'll fail.
networkUp syncs.AtomicBool
}
// derpRoute is a route entry for a public key, saying that a certain
@@ -308,18 +301,11 @@ type Options struct {
// sole user just doesn't need or want it called on every
// packet, just every minute or two for Wireguard timeouts,
// and 10 seconds seems like a good trade-off between often
// enough and not too often.) The provided func is called
// while holding userspaceEngine.wgLock and likely calls
// Conn.CreateEndpoint, which acquires Conn.mu. As such, you
// should not hold Conn.mu while calling it.
// enough and not too often.) The provided func is called while
// holding userspaceEngine.wgLock and likely calls
// Conn.CreateEndpoint, which acquires Conn.mu. As such, you should
// not hold
NoteRecvActivity func(tailcfg.DiscoKey)
// SimulatedNetwork can be set true in tests to signal that
// the network is simulated and thus it's okay to bind on the
// unspecified address (which we'd normally avoid to avoid
// triggering macOS and Windows firwall dialog boxes during
// "go test").
SimulatedNetwork bool
}
func (o *Options) logf() logger.Logf {
@@ -359,7 +345,6 @@ func newConn() *Conn {
discoOfAddr: make(map[netaddr.IPPort]tailcfg.DiscoKey),
}
c.muCond = sync.NewCond(&c.mu)
c.networkUp.Set(true) // assume up until told otherwise
return c
}
@@ -377,7 +362,6 @@ func NewConn(opts Options) (*Conn, error) {
c.idleFunc = opts.IdleFunc
c.packetListener = opts.PacketListener
c.noteRecvActivity = opts.NoteRecvActivity
c.simulatedNetwork = opts.SimulatedNetwork
if err := c.initialBind(); err != nil {
return nil, err
@@ -385,9 +369,8 @@ func NewConn(opts Options) (*Conn, error) {
c.connCtx, c.connCtxCancel = context.WithCancel(context.Background())
c.netChecker = &netcheck.Client{
Logf: logger.WithPrefix(c.logf, "netcheck: "),
GetSTUNConn4: func() netcheck.STUNConn { return c.pconn4 },
SkipExternalNetwork: inTest(),
Logf: logger.WithPrefix(c.logf, "netcheck: "),
GetSTUNConn4: func() netcheck.STUNConn { return c.pconn4 },
}
if c.pconn6 != nil {
c.netChecker.GetSTUNConn6 = func() netcheck.STUNConn { return c.pconn6 }
@@ -448,7 +431,7 @@ func (c *Conn) updateEndpoints(why string) {
return
}
if c.setEndpoints(endpoints, reasons) {
if c.setEndpoints(endpoints) {
c.logEndpointChange(endpoints, reasons)
c.epFunc(endpoints)
}
@@ -456,31 +439,9 @@ func (c *Conn) updateEndpoints(why string) {
// setEndpoints records the new endpoints, reporting whether they're changed.
// It takes ownership of the slice.
func (c *Conn) setEndpoints(endpoints []string, reasons map[string]string) (changed bool) {
anySTUN := false
for _, reason := range reasons {
if reason == "stun" {
anySTUN = true
}
}
func (c *Conn) setEndpoints(endpoints []string) (changed bool) {
c.mu.Lock()
defer c.mu.Unlock()
if !anySTUN && c.derpMap == nil && !inTest() {
// Don't bother storing or reporting this yet. We
// don't have a DERP map or any STUN entries, so we're
// just starting up. A DERP map should arrive shortly
// and then we'll have more interesting endpoints to
// report. This saves a map update.
// TODO(bradfitz): this optimization is currently
// skipped during the e2e tests because they depend
// too much on the exact sequence of updates. Fix the
// tests. But a protocol rewrite might happen first.
c.logf("magicsock: ignoring pre-DERP map, STUN-less endpoint update: %v", endpoints)
return false
}
if stringsEqual(endpoints, c.lastEndpoints) {
return false
}
@@ -493,7 +454,7 @@ func (c *Conn) updateNetInfo(ctx context.Context) (*netcheck.Report, error) {
dm := c.derpMap
c.mu.Unlock()
if dm == nil || c.networkDown() {
if dm == nil {
return new(netcheck.Report), nil
}
@@ -1008,15 +969,8 @@ func (as *AddrSet) appendDests(dsts []netaddr.IPPort, b []byte) (_ []netaddr.IPP
}
var errNoDestinations = errors.New("magicsock: no destinations")
var errNetworkDown = errors.New("magicsock: network down")
func (c *Conn) networkDown() bool { return !c.networkUp.Get() }
func (c *Conn) Send(b []byte, ep conn.Endpoint) error {
if c.networkDown() {
return errNetworkDown
}
var as *AddrSet
switch v := ep.(type) {
default:
@@ -1157,10 +1111,6 @@ func (c *Conn) derpWriteChanOfAddr(addr netaddr.IPPort, peer key.Public) chan<-
}
regionID := int(addr.Port)
if c.networkDown() {
return nil
}
c.mu.Lock()
defer c.mu.Unlock()
if !c.wantDerpLocked() || c.closed {
@@ -1351,45 +1301,36 @@ func (c *Conn) runDerpReader(ctx context.Context, derpFakeAddr netaddr.IPPort, d
// peerPresent is the set of senders we know are present on this
// connection, based on messages we've received from the server.
peerPresent := map[key.Public]bool{}
bo := backoff.NewBackoff(fmt.Sprintf("derp-%d", regionID), c.logf, 5*time.Second)
for {
msg, err := dc.Recv()
if err == derphttp.ErrClientClosed {
return
}
if err != nil {
// Forget that all these peers have routes.
for peer := range peerPresent {
delete(peerPresent, peer)
c.removeDerpPeerRoute(peer, regionID, dc)
}
if err == derphttp.ErrClientClosed {
return
}
if c.networkDown() {
c.logf("magicsock: derp.Recv(derp-%d): network down, closing", regionID)
return
}
select {
case <-ctx.Done():
return
default:
}
c.ReSTUN("derp-close")
c.logf("magicsock: [%p] derp.Recv(derp-%d): %v", dc, regionID, err)
// If our DERP connection broke, it might be because our network
// conditions changed. Start that check.
c.ReSTUN("derp-recv-error")
// Back off a bit before reconnecting.
bo.BackOff(ctx, err)
// Avoid excessive spinning.
// TODO: use a backoff timer, perhaps between 10ms and 500ms?
// Don't want to sleep too long. For now 250ms seems fine.
select {
case <-ctx.Done():
return
default:
case <-time.After(250 * time.Millisecond):
}
continue
}
bo.BackOff(ctx, nil) // reset
switch m := msg.(type) {
case derp.ReceivedPacket:
pkt = m
@@ -1750,9 +1691,7 @@ func (c *Conn) sendDiscoMessage(dst netaddr.IPPort, dstKey tailcfg.NodeKey, dstD
} else if err == nil {
// Can't send. (e.g. no IPv6 locally)
} else {
if !c.networkDown() {
c.logf("magicsock: disco: failed to send %T to %v: %v", m, dst, err)
}
c.logf("magicsock: disco: failed to send %T to %v: %v", m, dst, err)
}
return sent, err
}
@@ -2017,21 +1956,6 @@ func (c *Conn) sharedDiscoKeyLocked(k tailcfg.DiscoKey) *[32]byte {
return shared
}
func (c *Conn) SetNetworkUp(up bool) {
c.mu.Lock()
defer c.mu.Unlock()
if c.networkUp.Get() == up {
return
}
c.logf("magicsock: SetNetworkUp(%v)", up)
c.networkUp.Set(up)
if !up {
c.closeAllDerpLocked("network-down")
}
}
// SetPrivateKey sets the connection's private key.
//
// This is only used to be able prove our identity when connecting to
@@ -2358,10 +2282,6 @@ func maxIdleBeforeSTUNShutdown() time.Duration {
}
func (c *Conn) shouldDoPeriodicReSTUN() bool {
if c.networkDown() {
return false
}
c.mu.Lock()
defer c.mu.Unlock()
if len(c.peerSet) == 0 {
@@ -2490,11 +2410,8 @@ func (c *Conn) listenPacket(ctx context.Context, network, addr string) (net.Pack
func (c *Conn) bind1(ruc **RebindingUDPConn, which string) error {
host := ""
if inTest() && !c.simulatedNetwork {
if inTest() {
host = "127.0.0.1"
if which == "udp6" {
host = "::1"
}
}
var pc net.PacketConn
var err error
@@ -2523,7 +2440,7 @@ func (c *Conn) bind1(ruc **RebindingUDPConn, which string) error {
// It should be followed by a call to ReSTUN.
func (c *Conn) Rebind() {
host := ""
if inTest() && !c.simulatedNetwork {
if inTest() {
host = "127.0.0.1"
}
listenCtx := context.Background() // unused without DNS name to resolve

View File

@@ -46,10 +46,6 @@ import (
"tailscale.com/wgengine/tstun"
)
func init() {
os.Setenv("IN_TS_TEST", "1")
}
// WaitReady waits until the magicsock is entirely initialized and connected
// to its home DERP server. This is normally not necessary, since magicsock
// is intended to be entirely asynchronous, but it helps eliminate race
@@ -122,7 +118,7 @@ type magicStack struct {
privateKey wgcfg.PrivateKey
epCh chan []string // endpoint updates produced by this peer
conn *Conn // the magicsock itself
tun *tuntest.ChannelTUN // TUN device to send/receive packets
tun *tuntest.ChannelTUN // tuntap device to send/receive packets
tsTun *tstun.TUN // wrapped tun that implements filtering and wgengine hooks
dev *device.Device // the wireguard-go Device that connects the previous things
}
@@ -145,7 +141,6 @@ func newMagicStack(t *testing.T, logf logger.Logf, l nettype.PacketListener, der
EndpointsFunc: func(eps []string) {
epCh <- eps
},
SimulatedNetwork: l != nettype.Std{},
})
if err != nil {
t.Fatalf("constructing magicsock: %v", err)
@@ -379,7 +374,7 @@ collectEndpoints:
func pickPort(t *testing.T) uint16 {
t.Helper()
conn, err := net.ListenPacket("udp4", "127.0.0.1:0")
conn, err := net.ListenPacket("udp4", ":0")
if err != nil {
t.Fatal(err)
}

View File

@@ -7,8 +7,6 @@ package monitor
import (
"context"
"errors"
"fmt"
"runtime"
"sync"
"syscall"
"time"
@@ -26,15 +24,9 @@ const (
)
var (
iphlpapi = syscall.NewLazyDLL("iphlpapi.dll")
notifyAddrChangeProc = iphlpapi.NewProc("NotifyAddrChange")
notifyRouteChangeProc = iphlpapi.NewProc("NotifyRouteChange")
cancelIPChangeNotifyProc = iphlpapi.NewProc("CancelIPChangeNotify")
)
const (
_STATUS_PENDING = 0x00000103 // 259
_STATUS_WAIT_0 = 0
iphlpapi = syscall.NewLazyDLL("iphlpapi.dll")
notifyAddrChangeProc = iphlpapi.NewProc("NotifyAddrChange")
notifyRouteChangeProc = iphlpapi.NewProc("NotifyRouteChange")
)
type unspecifiedMessage struct{}
@@ -51,33 +43,27 @@ type messageOrError struct {
}
type winMon struct {
ctx context.Context
cancel context.CancelFunc
messagec chan messageOrError
logf logger.Logf
pollTicker *time.Ticker
lastState *interfaces.State
closeHandle windows.Handle // signaled upon close
ctx context.Context
cancel context.CancelFunc
messagec chan messageOrError
logf logger.Logf
pollTicker *time.Ticker
lastState *interfaces.State
mu sync.Mutex
event windows.Handle
lastNetChange time.Time
inFastPoll bool // recent net change event made us go into fast polling mode (to detect proxy changes)
}
func newOSMon(logf logger.Logf) (osMon, error) {
closeHandle, err := windows.CreateEvent(nil, 1 /* manual reset */, 0 /* unsignaled */, nil /* no name */)
if err != nil {
return nil, fmt.Errorf("CreateEvent: %w", err)
}
ctx, cancel := context.WithCancel(context.Background())
m := &winMon{
logf: logf,
ctx: ctx,
cancel: cancel,
messagec: make(chan messageOrError, 1),
pollTicker: time.NewTicker(pollIntervalSlow),
closeHandle: closeHandle,
logf: logf,
ctx: ctx,
cancel: cancel,
messagec: make(chan messageOrError, 1),
pollTicker: time.NewTicker(pollIntervalSlow),
}
go m.awaitIPAndRouteChanges()
return m, nil
@@ -86,7 +72,14 @@ func newOSMon(logf logger.Logf) (osMon, error) {
func (m *winMon) Close() error {
m.cancel()
m.pollTicker.Stop()
windows.SetEvent(m.closeHandle) // wakes up any reader blocked in Receive
m.mu.Lock()
defer m.mu.Unlock()
if h := m.event; h != 0 {
// Wake up any reader blocked in Receive.
windows.SetEvent(h)
}
return nil
}
@@ -143,80 +136,52 @@ func (m *winMon) getIPOrRouteChangeMessage() (message, error) {
return nil, errClosed
}
// TODO(bradfitz): locking ourselves to an OS thread here
// likely isn't necessary, but also can't really hurt.
// We'll be blocked in windows.WaitForMultipleObjects below
// anyway, so might as well stay on this thread during the
// notify calls and cancel funcs.
// Given the past memory corruption from misuse of these APIs,
// and my continued lack of understanding of Windows APIs,
// I'll be paranoid. But perhaps we can remove this once
// we understand more.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
var o windows.Overlapped
h, err := windows.CreateEvent(nil, 1 /* true*/, 0 /* unsignaled */, nil /* no name */)
if err != nil {
m.logf("CreateEvent: %v", err)
return nil, err
}
defer windows.CloseHandle(h)
addrHandle, oaddr, cancel, err := notifyAddrChange()
m.mu.Lock()
m.event = h
m.mu.Unlock()
o.HEvent = h
err = notifyAddrChange(&h, &o)
if err != nil {
m.logf("notifyAddrChange: %v", err)
return nil, err
}
defer cancel()
routeHandle, oroute, cancel, err := notifyRouteChange()
err = notifyRouteChange(&h, &o)
if err != nil {
m.logf("notifyRouteChange: %v", err)
return nil, err
}
defer cancel()
t0 := time.Now()
eventNum, err := windows.WaitForMultipleObjects([]windows.Handle{
m.closeHandle, // eventNum 0
addrHandle, // eventNum 1
routeHandle, // eventNum 2
}, false, windows.INFINITE)
if m.ctx.Err() != nil || (err == nil && eventNum == 0) {
_, err = windows.WaitForSingleObject(o.HEvent, windows.INFINITE)
if m.ctx.Err() != nil {
return nil, errClosed
}
if err != nil {
m.logf("waitForMultipleObjects: %v", err)
m.logf("waitForSingleObject: %v", err)
return nil, err
}
d := time.Since(t0)
var eventStr string
// notifyAddrChange and notifyRouteChange both seem to return the same
// handle value. Determine which fired by looking at the "Internal" (sic)
// field of the Ovelapped instead.
// TODO(bradfitz): maybe clean this up; see TODO in callNotifyProc.
if (eventNum == 1 || eventNum == 2) && addrHandle == routeHandle {
if oaddr.Internal == _STATUS_WAIT_0 && oroute.Internal == _STATUS_PENDING {
eventStr = "addr-o" // "-o" overlapped suffix to distinguish from "addr" below
} else if oroute.Internal == _STATUS_WAIT_0 && oaddr.Internal == _STATUS_PENDING {
eventStr = "route-o"
} else {
eventStr = fmt.Sprintf("[unexpected] addr.internal=%d; route.internal=%d", oaddr.Internal, oroute.Internal)
}
} else {
switch eventNum {
case 1:
eventStr = "addr"
case 2:
eventStr = "route"
default:
eventStr = fmt.Sprintf("%d [unexpected]", eventNum)
}
}
m.logf("got windows change event after %v: evt=%s", d, eventStr)
m.logf("got windows change event after %v", d)
m.mu.Lock()
{
m.lastNetChange = time.Now()
m.event = 0
// Something changed, so assume Windows is about to
// discover its new proxy settings from WPAD, which
// seems to take a bit. Poll heavily for awhile.
m.logf("starting quick poll, waiting for WPAD change")
m.inFastPoll = true
m.pollTicker.Reset(pollIntervalFast)
}
@@ -225,46 +190,23 @@ func (m *winMon) getIPOrRouteChangeMessage() (message, error) {
return unspecifiedMessage{}, nil
}
func notifyAddrChange() (h windows.Handle, o *windows.Overlapped, cancel func(), err error) {
return callNotifyProc(notifyAddrChangeProc)
func notifyAddrChange(h *windows.Handle, o *windows.Overlapped) error {
return callNotifyProc(notifyAddrChangeProc, h, o)
}
func notifyRouteChange() (h windows.Handle, o *windows.Overlapped, cancel func(), err error) {
return callNotifyProc(notifyRouteChangeProc)
func notifyRouteChange(h *windows.Handle, o *windows.Overlapped) error {
return callNotifyProc(notifyAddrChangeProc, h, o)
}
func callNotifyProc(p *syscall.LazyProc) (h windows.Handle, o *windows.Overlapped, cancel func(), err error) {
o = new(windows.Overlapped)
// TODO(bradfitz): understand why this if-false code doesn't
// work, even though the docs online suggest we should pass an
// event in the overlapped.Hevent field.
// The docs at
// https://docs.microsoft.com/en-us/windows/win32/api/minwinbase/ns-minwinbase-overlapped
// says that o.HEvent can be zero, though, which seems to work.
// Note that the returned windows.Handle returns the same value for both
// notifyAddrChange and notifyRouteChange, which is why our caller needs
// to look at the returned Overlapped's Internal field to see which case
// fired. That's also worth understanding more.
// See crawshaw's comment at https://github.com/tailscale/tailscale/pull/944#discussion_r526469186
// too.
if false {
evt, err := windows.CreateEvent(nil, 0, 0, nil)
if err != nil {
return 0, nil, nil, err
}
o.HEvent = evt
func callNotifyProc(p *syscall.LazyProc, h *windows.Handle, o *windows.Overlapped) error {
r1, _, e1 := p.Call(uintptr(unsafe.Pointer(h)), uintptr(unsafe.Pointer(o)))
expect := uintptr(0)
if h != nil || o != nil {
const ERROR_IO_PENDING = 997
expect = ERROR_IO_PENDING
}
r1, _, e1 := syscall.Syscall(p.Addr(), 2, uintptr(unsafe.Pointer(&h)), uintptr(unsafe.Pointer(o)), 0)
// We expect ERROR_IO_PENDING.
if syscall.Errno(r1) != windows.ERROR_IO_PENDING {
return 0, nil, nil, e1
if r1 == expect {
return nil
}
cancel = func() {
cancelIPChangeNotifyProc.Call(uintptr(unsafe.Pointer(o)))
}
return h, o, cancel, nil
return e1
}

View File

@@ -39,18 +39,6 @@ func (ip IP) String() string {
return fmt.Sprintf("%d.%d.%d.%d", byte(ip>>24), byte(ip>>16), byte(ip>>8), byte(ip))
}
func (ip IP) IsMulticast() bool {
return byte(ip>>24)&0xf0 == 0xe0
}
func (ip IP) IsLinkLocalUnicast() bool {
return byte(ip>>24) == 169 && byte(ip>>16) == 254
}
func (ip IP) IsMostLinkLocalUnicast() bool {
return ip.IsLinkLocalUnicast() && ip != 0xA9FEA9FE
}
// IPProto is either a real IP protocol (ITCP, UDP, ...) or an special value like Unknown.
// If it is a real IP protocol, its value corresponds to its IP protocol number.
type IPProto uint8

View File

@@ -63,8 +63,7 @@ func (lhs Config) Equal(rhs Config) bool {
// ManagerConfig is the set of parameters from which
// a manager implementation is chosen and initialized.
type ManagerConfig struct {
// Logf is the logger for the manager to use.
// It is wrapped with a "dns: " prefix.
// logf is the logger for the manager to use.
Logf logger.Logf
// InterfaceNAme is the name of the interface with which DNS settings should be associated.
InterfaceName string

View File

@@ -9,7 +9,6 @@ package dns
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
@@ -131,7 +130,7 @@ func (m directManager) Up(config Config) error {
contents, err := ioutil.ReadFile(resolvConf)
// If the original did not exist, still back up an empty file.
// The presence of a backup file is the way we know that Up ran.
if err != nil && !errors.Is(err, os.ErrNotExist) {
if err != nil && !os.IsNotExist(err) {
return err
}
if err := atomicfile.WriteFile(backupConf, contents, 0644); err != nil {

View File

@@ -5,11 +5,9 @@
package dns
import (
"errors"
"fmt"
"os/exec"
"strings"
"syscall"
"time"
"github.com/tailscale/wireguard-go/tun"
"golang.org/x/sys/windows/registry"
@@ -48,16 +46,68 @@ func setRegistryString(path, name, value string) error {
return nil
}
func getRegistryString(path, name string) (string, error) {
key, err := registry.OpenKey(registry.LOCAL_MACHINE, path, registry.READ)
if err != nil {
return "", fmt.Errorf("opening %s: %w", path, err)
}
defer key.Close()
value, _, err := key.GetStringValue(name)
if err != nil {
return "", fmt.Errorf("getting %s[%s]: %w", path, name, err)
}
return value, nil
}
func (m windowsManager) setNameservers(basePath string, nameservers []string) error {
path := fmt.Sprintf(`%s\Interfaces\%s`, basePath, m.guid)
value := strings.Join(nameservers, ",")
return setRegistryString(path, "NameServer", value)
}
func (m windowsManager) setDomains(basePath string, domains []string) error {
path := fmt.Sprintf(`%s\Interfaces\%s`, basePath, m.guid)
value := strings.Join(domains, ",")
return setRegistryString(path, "SearchList", value)
func (m windowsManager) setDomains(path string, oldDomains, newDomains []string) error {
// We reimplement setRegistryString to ensure that we hold the key for the whole operation.
key, err := registry.OpenKey(registry.LOCAL_MACHINE, path, registry.READ|registry.SET_VALUE)
if err != nil {
return fmt.Errorf("opening %s: %w", path, err)
}
defer key.Close()
searchList, _, err := key.GetStringValue("SearchList")
if err != nil && err != registry.ErrNotExist {
return fmt.Errorf("getting %s[SearchList]: %w", path, err)
}
currentDomains := strings.Split(searchList, ",")
var domainsToSet []string
for _, domain := range currentDomains {
inOld, inNew := false, false
// The number of domains should be small,
// so this is probaly faster than constructing a map.
for _, oldDomain := range oldDomains {
if domain == oldDomain {
inOld = true
}
}
for _, newDomain := range newDomains {
if domain == newDomain {
inNew = true
}
}
if !inNew && !inOld {
domainsToSet = append(domainsToSet, domain)
}
}
domainsToSet = append(domainsToSet, newDomains...)
searchList = strings.Join(domainsToSet, ",")
if err := key.SetStringValue("SearchList", searchList); err != nil {
return fmt.Errorf("setting %s[SearchList]: %w", path, err)
}
return nil
}
func (m windowsManager) Up(config Config) error {
@@ -72,17 +122,23 @@ func (m windowsManager) Up(config Config) error {
}
}
lastSearchList, err := getRegistryString(tsRegBase, "SearchList")
if err != nil && !errors.Is(err, registry.ErrNotExist) {
return err
}
lastDomains := strings.Split(lastSearchList, ",")
if err := m.setNameservers(ipv4RegBase, ipsv4); err != nil {
return err
}
if err := m.setDomains(ipv4RegBase, config.Domains); err != nil {
if err := m.setDomains(ipv4RegBase, lastDomains, config.Domains); err != nil {
return err
}
if err := m.setNameservers(ipv6RegBase, ipsv6); err != nil {
return err
}
if err := m.setDomains(ipv6RegBase, config.Domains); err != nil {
if err := m.setDomains(ipv6RegBase, lastDomains, config.Domains); err != nil {
return err
}
@@ -91,26 +147,6 @@ func (m windowsManager) Up(config Config) error {
return err
}
// Force DNS re-registration in Active Directory. What we actually
// care about is that this command invokes the undocumented hidden
// function that forces Windows to notice that adapter settings
// have changed, which makes the DNS settings actually take
// effect.
//
// This command can take a few seconds to run, so run it async, best effort.
go func() {
t0 := time.Now()
m.logf("running ipconfig /registerdns ...")
cmd := exec.Command("ipconfig", "/registerdns")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
d := time.Since(t0).Round(time.Millisecond)
if err := cmd.Run(); err != nil {
m.logf("error running ipconfig /registerdns after %v: %v", d, err)
} else {
m.logf("ran ipconfig /registerdns in %v", d)
}
}()
return nil
}

View File

@@ -11,15 +11,14 @@ import (
"fmt"
"log"
"net"
"runtime"
"os"
"sort"
"time"
"github.com/go-multierror/multierror"
ole "github.com/go-ole/go-ole"
winipcfg "github.com/tailscale/winipcfg-go"
"github.com/tailscale/wireguard-go/tun"
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"tailscale.com/net/interfaces"
"tailscale.com/wgengine/winnet"
)
@@ -40,51 +39,51 @@ import (
// help with MTU issues compared to a static 1280B implementation.
func monitorDefaultRoutes(tun *tun.NativeTun) (*winipcfg.RouteChangeCallback, error) {
guid := tun.GUID()
ourLuid, err := winipcfg.LUIDFromGUID(&guid)
ourLuid, err := winipcfg.InterfaceGuidToLuid(&guid)
lastMtu := uint32(0)
if err != nil {
return nil, fmt.Errorf("error mapping GUID %v to LUID: %w", guid, err)
return nil, err
}
doIt := func() error {
mtu, err := getDefaultRouteMTU()
if err != nil {
return fmt.Errorf("error getting default route MTU: %w", err)
return err
}
if mtu > 0 && (lastMtu == 0 || lastMtu != mtu) {
iface, err := ourLuid.IPInterface(windows.AF_INET)
iface, err := winipcfg.GetIpInterface(ourLuid, winipcfg.AF_INET)
if err != nil {
return fmt.Errorf("error getting v4 interface: %w", err)
return err
}
iface.NLMTU = mtu - 80
iface.NlMtu = mtu - 80
// If the TUN device was created with a smaller MTU,
// though, such as 1280, we don't want to go bigger than
// configured. (See the comment on minimalMTU in the
// wgengine package.)
if min, err := tun.MTU(); err == nil && min < int(iface.NLMTU) {
iface.NLMTU = uint32(min)
if min, err := tun.MTU(); err == nil && min < int(iface.NlMtu) {
iface.NlMtu = uint32(min)
}
if iface.NLMTU < 576 {
iface.NLMTU = 576
if iface.NlMtu < 576 {
iface.NlMtu = 576
}
err = iface.Set()
if err != nil {
return fmt.Errorf("error setting v4 MTU: %w", err)
return err
}
tun.ForceMTU(int(iface.NLMTU))
iface, err = ourLuid.IPInterface(windows.AF_INET6)
tun.ForceMTU(int(iface.NlMtu))
iface, err = winipcfg.GetIpInterface(ourLuid, winipcfg.AF_INET6)
if err != nil {
if !errors.Is(err, windows.ERROR_NOT_FOUND) {
return fmt.Errorf("error getting v6 interface: %w", err)
if !isMissingIPv6Err(err) {
return err
}
} else {
iface.NLMTU = mtu - 80
if iface.NLMTU < 1280 {
iface.NLMTU = 1280
iface.NlMtu = mtu - 80
if iface.NlMtu < 1280 {
iface.NlMtu = 1280
}
err = iface.Set()
if err != nil {
return fmt.Errorf("error setting v6 MTU: %w", err)
return err
}
}
lastMtu = mtu
@@ -95,7 +94,7 @@ func monitorDefaultRoutes(tun *tun.NativeTun) (*winipcfg.RouteChangeCallback, er
if err != nil {
return nil, err
}
cb, err := winipcfg.RegisterRouteChangeCallback(func(notificationType winipcfg.MibNotificationType, route *winipcfg.MibIPforwardRow2) {
cb, err := winipcfg.RegisterRouteChangeCallback(func(notificationType winipcfg.MibNotificationType, route *winipcfg.Route) {
//fmt.Printf("MonitorDefaultRoutes: changed: %v\n", route.DestinationPrefix)
if route.DestinationPrefix.PrefixLength == 0 {
_ = doIt()
@@ -113,7 +112,7 @@ func getDefaultRouteMTU() (uint32, error) {
return 0, err
}
routes, err := winipcfg.GetIPForwardTable2(windows.AF_INET)
routes, err := winipcfg.GetRoutes(winipcfg.AF_INET)
if err != nil {
return 0, err
}
@@ -123,7 +122,7 @@ func getDefaultRouteMTU() (uint32, error) {
if route.DestinationPrefix.PrefixLength != 0 {
continue
}
routeMTU := mtus[route.InterfaceLUID]
routeMTU := mtus[route.InterfaceLuid]
if routeMTU == 0 {
continue
}
@@ -133,7 +132,7 @@ func getDefaultRouteMTU() (uint32, error) {
}
}
routes, err = winipcfg.GetIPForwardTable2(windows.AF_INET6)
routes, err = winipcfg.GetRoutes(winipcfg.AF_INET6)
if err != nil {
return 0, err
}
@@ -142,7 +141,7 @@ func getDefaultRouteMTU() (uint32, error) {
if route.DestinationPrefix.PrefixLength != 0 {
continue
}
routeMTU := mtus[route.InterfaceLUID]
routeMTU := mtus[route.InterfaceLuid]
if routeMTU == 0 {
continue
}
@@ -166,13 +165,6 @@ func setPrivateNetwork(ifcGUID *windows.GUID) (bool, error) {
categoryPrivate = 1
categoryDomain = 2
)
// Lock OS thread when using OLE, which seems to be a requirement
// from the Microsoft docs. go-ole doesn't seem to handle it automatically.
// https://github.com/tailscale/tailscale/issues/921#issuecomment-727526807
runtime.LockOSThread()
defer runtime.UnlockOSThread()
var c ole.Connection
if err := c.Initialize(); err != nil {
return false, fmt.Errorf("c.Initialize: %v", err)
@@ -222,34 +214,10 @@ func setPrivateNetwork(ifcGUID *windows.GUID) (bool, error) {
return false, nil
}
// interfaceFromGUID returns IPAdapterAddresses with specified GUID.
func interfaceFromGUID(guid *windows.GUID, flags winipcfg.GAAFlags) (*winipcfg.IPAdapterAddresses, error) {
luid, err := winipcfg.LUIDFromGUID(guid)
if err != nil {
return nil, err
}
addresses, err := winipcfg.GetAdaptersAddresses(windows.AF_UNSPEC, flags)
if err != nil {
return nil, err
}
for _, addr := range addresses {
if addr.LUID == luid {
return addr, nil
}
}
return nil, fmt.Errorf("interfaceFromGUID: interface with LUID %v (from GUID %v) not found", luid, guid)
}
func configureInterface(cfg *Config, tun *tun.NativeTun) error {
const mtu = 0
guid := tun.GUID()
iface, err := interfaceFromGUID(&guid,
// Issue 474: on early boot, when the network is still
// coming up, if the Tailscale service comes up first,
// the Tailscale adapter it finds might not have the
// IPv4 service available yet? Try this flag:
winipcfg.GAAFlagIncludeAllInterfaces,
)
iface, err := winipcfg.InterfaceFromGUID(&guid)
if err != nil {
return err
}
@@ -274,6 +242,7 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
log.Printf("setPrivateNetwork: adapter %v not found after %d tries, giving up", guid, tries)
}()
routes := []winipcfg.RouteData{}
var firstGateway4 *net.IP
var firstGateway6 *net.IP
addresses := make([]*net.IPNet, len(cfg.LocalAddrs))
@@ -288,7 +257,6 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
}
}
var routes []winipcfg.RouteData
foundDefault4 := false
foundDefault6 := false
for _, route := range cfg.Routes {
@@ -333,7 +301,7 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
routes = append(routes, r)
}
err = syncAddresses(iface, addresses)
err = iface.SyncAddresses(addresses)
if err != nil {
return err
}
@@ -352,26 +320,14 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
deduplicatedRoutes = append(deduplicatedRoutes, &routes[i])
}
// Re-read interface after syncAddresses.
iface, err = interfaceFromGUID(&guid,
// Issue 474: on early boot, when the network is still
// coming up, if the Tailscale service comes up first,
// the Tailscale adapter it finds might not have the
// IPv4 service available yet? Try this flag:
winipcfg.GAAFlagIncludeAllInterfaces,
)
if err != nil {
return err
}
var errAcc error
err = syncRoutes(iface, deduplicatedRoutes)
err = iface.SyncRoutes(deduplicatedRoutes)
if err != nil && errAcc == nil {
log.Printf("setroutes: %v", err)
errAcc = err
}
ipif, err := iface.LUID.IPInterface(windows.AF_INET)
ipif, err := iface.GetIpInterface(winipcfg.AF_INET)
if err != nil {
log.Printf("getipif: %v", err)
return err
@@ -381,17 +337,17 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
ipif.Metric = 0
}
if mtu > 0 {
ipif.NLMTU = uint32(mtu)
tun.ForceMTU(int(ipif.NLMTU))
ipif.NlMtu = uint32(mtu)
tun.ForceMTU(int(ipif.NlMtu))
}
err = ipif.Set()
if err != nil && errAcc == nil {
errAcc = err
}
ipif, err = iface.LUID.IPInterface(windows.AF_INET6)
ipif, err = iface.GetIpInterface(winipcfg.AF_INET6)
if err != nil {
if !errors.Is(err, windows.ERROR_NOT_FOUND) {
if !isMissingIPv6Err(err) {
return err
}
} else {
@@ -400,7 +356,7 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
ipif.Metric = 0
}
if mtu > 0 {
ipif.NLMTU = uint32(mtu)
ipif.NlMtu = uint32(mtu)
}
ipif.DadTransmits = 0
ipif.RouterDiscoveryBehavior = winipcfg.RouterDiscoveryDisabled
@@ -413,6 +369,22 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
return errAcc
}
// isMissingIPv6Err reports whether err is due to IPv6 not being enabled on the machine.
//
// It only currently supports the errors returned by winipcfg.Interface.GetIpInterface.
func isMissingIPv6Err(err error) bool {
if se, ok := err.(*os.SyscallError); ok {
switch se.Syscall {
case "iphlpapi.GetIpInterfaceEntry":
// ERROR_NOT_FOUND from means the address family (IPv6) is not found.
// (ERROR_FILE_NOT_FOUND means that the interface doesn't exist.)
// https://docs.microsoft.com/en-us/windows/win32/api/netioapi/nf-netioapi-getipinterfaceentry
return se.Err == windows.ERROR_NOT_FOUND
}
}
return false
}
// routeLess reports whether ri should sort before rj.
// The actual sort order doesn't appear to matter. The caller just
// wants them sorted to be able to de-dup.
@@ -434,257 +406,3 @@ func routeLess(ri, rj *winipcfg.RouteData) bool {
}
return false
}
// unwrapIP returns the shortest version of ip.
func unwrapIP(ip net.IP) net.IP {
if ip4 := ip.To4(); ip4 != nil {
return ip4
}
return ip
}
func v4Mask(m net.IPMask) net.IPMask {
if len(m) == 16 {
return m[12:]
}
return m
}
func netCompare(a, b net.IPNet) int {
aip, bip := unwrapIP(a.IP), unwrapIP(b.IP)
v := bytes.Compare(aip, bip)
if v != 0 {
return v
}
amask, bmask := a.Mask, b.Mask
if len(aip) == 4 {
amask = v4Mask(a.Mask)
bmask = v4Mask(b.Mask)
}
// narrower first
return -bytes.Compare(amask, bmask)
}
func sortNets(a []*net.IPNet) {
sort.Slice(a, func(i, j int) bool {
return netCompare(*a[i], *a[j]) == -1
})
}
// deltaNets returns the changes to turn a into b.
func deltaNets(a, b []*net.IPNet) (add, del []*net.IPNet) {
add = make([]*net.IPNet, 0, len(b))
del = make([]*net.IPNet, 0, len(a))
sortNets(a)
sortNets(b)
i := 0
j := 0
for i < len(a) && j < len(b) {
switch netCompare(*a[i], *b[j]) {
case -1:
// a < b, delete
del = append(del, a[i])
i++
case 0:
// a == b, no diff
i++
j++
case 1:
// a > b, add missing entry
add = append(add, b[j])
j++
default:
panic("unexpected compare result")
}
}
del = append(del, a[i:]...)
add = append(add, b[j:]...)
return
}
func excludeIPv6LinkLocal(in []*net.IPNet) (out []*net.IPNet) {
out = in[:0]
for _, n := range in {
if len(n.IP) == 16 && n.IP.IsLinkLocalUnicast() {
continue
}
out = append(out, n)
}
return out
}
// ipAdapterUnicastAddressToIPNet converts windows.IpAdapterUnicastAddress to net.IPNet.
func ipAdapterUnicastAddressToIPNet(u *windows.IpAdapterUnicastAddress) *net.IPNet {
ip := u.Address.IP()
w := 32
if ip.To4() == nil {
w = 128
}
return &net.IPNet{
IP: ip,
Mask: net.CIDRMask(int(u.OnLinkPrefixLength), w),
}
}
// unicastIPNets returns all unicast net.IPNet for ifc interface.
func unicastIPNets(ifc *winipcfg.IPAdapterAddresses) []*net.IPNet {
nets := make([]*net.IPNet, 0)
for addr := ifc.FirstUnicastAddress; addr != nil; addr = addr.Next {
nets = append(nets, ipAdapterUnicastAddressToIPNet(addr))
}
return nets
}
// syncAddresses incrementally sets the interface's unicast IP addresses,
// doing the minimum number of AddAddresses & DeleteAddress calls.
// This avoids the full FlushAddresses.
//
// Any IPv6 link-local addresses are not deleted.
func syncAddresses(ifc *winipcfg.IPAdapterAddresses, want []*net.IPNet) error {
var erracc error
got := unicastIPNets(ifc)
add, del := deltaNets(got, want)
del = excludeIPv6LinkLocal(del)
for _, a := range del {
err := ifc.LUID.DeleteIPAddress(*a)
if err != nil {
erracc = err
}
}
for _, a := range add {
err := ifc.LUID.AddIPAddress(*a)
if err != nil {
erracc = err
}
}
return erracc
}
func routeDataCompare(a, b *winipcfg.RouteData) int {
v := bytes.Compare(a.Destination.IP, b.Destination.IP)
if v != 0 {
return v
}
// Narrower masks first
v = bytes.Compare(a.Destination.Mask, b.Destination.Mask)
if v != 0 {
return -v
}
// No nexthop before non-empty nexthop
v = bytes.Compare(a.NextHop, b.NextHop)
if v != 0 {
return v
}
// Lower metrics first
if a.Metric < b.Metric {
return -1
} else if a.Metric > b.Metric {
return 1
}
return 0
}
func sortRouteData(a []*winipcfg.RouteData) {
sort.Slice(a, func(i, j int) bool {
return routeDataCompare(a[i], a[j]) < 0
})
}
func deltaRouteData(a, b []*winipcfg.RouteData) (add, del []*winipcfg.RouteData) {
add = make([]*winipcfg.RouteData, 0, len(b))
del = make([]*winipcfg.RouteData, 0, len(a))
sortRouteData(a)
sortRouteData(b)
i := 0
j := 0
for i < len(a) && j < len(b) {
switch routeDataCompare(a[i], b[j]) {
case -1:
// a < b, delete
del = append(del, a[i])
i++
case 0:
// a == b, no diff
i++
j++
case 1:
// a > b, add missing entry
add = append(add, b[j])
j++
default:
panic("unexpected compare result")
}
}
del = append(del, a[i:]...)
add = append(add, b[j:]...)
return
}
// getInterfaceRoutes returns all the interface's routes.
// Corresponds to GetIpForwardTable2 function, but filtered by interface.
func getInterfaceRoutes(ifc *winipcfg.IPAdapterAddresses, family winipcfg.AddressFamily) (matches []*winipcfg.MibIPforwardRow2, err error) {
routes, err := winipcfg.GetIPForwardTable2(family)
if err != nil {
return nil, err
}
for i := range routes {
if routes[i].InterfaceLUID == ifc.LUID {
matches = append(matches, &routes[i])
}
}
return
}
// syncRoutes incrementally sets multiples routes on an interface.
// This avoids a full ifc.FlushRoutes call.
func syncRoutes(ifc *winipcfg.IPAdapterAddresses, want []*winipcfg.RouteData) error {
routes, err := getInterfaceRoutes(ifc, windows.AF_INET)
if err != nil {
return err
}
got := make([]*winipcfg.RouteData, 0, len(routes))
for _, r := range routes {
got = append(got, &winipcfg.RouteData{
Destination: r.DestinationPrefix.IPNet(),
NextHop: r.NextHop.IP(),
Metric: r.Metric,
})
}
add, del := deltaRouteData(got, want)
var errs []error
for _, a := range del {
err := ifc.LUID.DeleteRoute(a.Destination, a.NextHop)
if err != nil {
dstStr := a.Destination.String()
if dstStr == "169.254.255.255/32" {
// Issue 785. Ignore these routes
// failing to delete. Harmless.
continue
}
errs = append(errs, fmt.Errorf("deleting route %v: %w", dstStr, err))
}
}
for _, a := range add {
err := ifc.LUID.AddRoute(a.Destination, a.NextHop, a.Metric)
if err != nil {
errs = append(errs, fmt.Errorf("adding route %v: %w", &a.Destination, err))
}
}
return multierror.New(errs)
}

View File

@@ -5,13 +5,11 @@
package router
import (
"fmt"
"math/rand"
"net"
"strings"
"testing"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
winipcfg "github.com/tailscale/winipcfg-go"
"inet.af/netaddr"
)
@@ -97,144 +95,3 @@ func TestRouteLessConsistent(t *testing.T) {
}
}
}
func equalNetIPs(a, b []*net.IPNet) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if netCompare(*a[i], *b[i]) != 0 {
return false
}
}
return true
}
func ipnet4(ip string, bits int) *net.IPNet {
return &net.IPNet{
IP: net.ParseIP(ip),
Mask: net.CIDRMask(bits, 32),
}
}
// each cidr can end in "[4]" to mean To4 form.
func nets(cidrs ...string) (ret []*net.IPNet) {
for _, s := range cidrs {
to4 := strings.HasSuffix(s, "[4]")
if to4 {
s = strings.TrimSuffix(s, "[4]")
}
ip, ipNet, err := net.ParseCIDR(s)
if err != nil {
panic(fmt.Sprintf("Bogus CIDR %q in test", s))
}
if to4 {
ip = ip.To4()
}
ipNet.IP = ip
ret = append(ret, ipNet)
}
return
}
func TestDeltaNets(t *testing.T) {
tests := []struct {
a, b []*net.IPNet
wantAdd, wantDel []*net.IPNet
}{
{
a: nets("1.2.3.4/24", "1.2.3.4/31", "1.2.3.3/32", "10.0.1.1/32", "100.0.1.1/32"),
b: nets("10.0.1.1/32", "100.0.2.1/32", "1.2.3.3/32", "1.2.3.4/24"),
wantAdd: nets("100.0.2.1/32"),
wantDel: nets("1.2.3.4/31", "100.0.1.1/32"),
},
{
a: nets("fe80::99d0:ec2d:b2e7:536b/64", "100.84.36.11/32"),
b: nets("100.84.36.11/32"),
wantDel: nets("fe80::99d0:ec2d:b2e7:536b/64"),
},
{
a: nets("100.84.36.11/32", "fe80::99d0:ec2d:b2e7:536b/64"),
b: nets("100.84.36.11/32"),
wantDel: nets("fe80::99d0:ec2d:b2e7:536b/64"),
},
{
a: nets("100.84.36.11/32", "fe80::99d0:ec2d:b2e7:536b/64"),
b: nets("100.84.36.11/32[4]"),
wantDel: nets("fe80::99d0:ec2d:b2e7:536b/64"),
},
{
a: excludeIPv6LinkLocal(nets("100.84.36.11/32", "fe80::99d0:ec2d:b2e7:536b/64")),
b: nets("100.84.36.11/32"),
},
{
a: []*net.IPNet{
{
IP: net.ParseIP("1.2.3.4"),
Mask: net.IPMask{0xff, 0xff, 0xff, 0xff},
},
},
b: []*net.IPNet{
{
IP: net.ParseIP("1.2.3.4"),
Mask: net.IPMask{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff},
},
},
},
}
for i, tt := range tests {
add, del := deltaNets(tt.a, tt.b)
if !equalNetIPs(add, tt.wantAdd) {
t.Errorf("[%d] add:\n got: %v\n want: %v\n", i, add, tt.wantAdd)
}
if !equalNetIPs(del, tt.wantDel) {
t.Errorf("[%d] del:\n got: %v\n want: %v\n", i, del, tt.wantDel)
}
}
}
func equalRouteDatas(a, b []*winipcfg.RouteData) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if routeDataCompare(a[i], b[i]) != 0 {
return false
}
}
return true
}
func TestDeltaRouteData(t *testing.T) {
var h0 net.IP
h1 := net.ParseIP("99.99.99.99")
h2 := net.ParseIP("99.99.9.99")
a := []*winipcfg.RouteData{
{*ipnet4("1.2.3.4", 32), h0, 1},
{*ipnet4("1.2.3.4", 24), h1, 2},
{*ipnet4("1.2.3.4", 24), h2, 1},
{*ipnet4("1.2.3.5", 32), h0, 1},
}
b := []*winipcfg.RouteData{
{*ipnet4("1.2.3.5", 32), h0, 1},
{*ipnet4("1.2.3.4", 24), h1, 2},
{*ipnet4("1.2.3.4", 24), h2, 2},
}
add, del := deltaRouteData(a, b)
wantAdd := []*winipcfg.RouteData{
{*ipnet4("1.2.3.4", 24), h2, 2},
}
wantDel := []*winipcfg.RouteData{
{*ipnet4("1.2.3.4", 32), h0, 1},
{*ipnet4("1.2.3.4", 24), h2, 1},
}
if !equalRouteDatas(add, wantAdd) {
t.Errorf("add:\n got: %v\n want: %v\n", add, wantAdd)
}
if !equalRouteDatas(del, wantDel) {
t.Errorf("del:\n got: %v\n want: %v\n", del, wantDel)
}
}

View File

@@ -12,8 +12,8 @@ import (
"tailscale.com/types/logger"
)
func newUserspaceRouter(logf logger.Logf, tunname string, dev *device.Device, tunDev tun.Device, netChanged func()) Router {
return NewFakeRouter(logf, tunname, dev, tunDev, netChanged)
func newUserspaceRouter(logf logger.Logf, tunname string, dev *device.Device, tuntap tun.Device, netChanged func()) Router {
return NewFakeRouter(logf, tunname, dev, tuntap, netChanged)
}
func cleanup(logf logger.Logf, interfaceName string) {

View File

@@ -5,16 +5,11 @@
package router
import (
"bytes"
"fmt"
"io/ioutil"
"os"
"os/exec"
"strconv"
"strings"
"github.com/coreos/go-iptables/iptables"
"github.com/go-multierror/multierror"
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
"inet.af/netaddr"
@@ -84,21 +79,16 @@ type netfilterRunner interface {
type linuxRouter struct {
logf func(fmt string, args ...interface{})
ipRuleAvailable bool
tunname string
addrs map[netaddr.IPPrefix]bool
routes map[netaddr.IPPrefix]bool
snatSubnetRoutes bool
netfilterMode NetfilterMode
// Various feature checks for the network stack.
ipRuleAvailable bool
v6Available bool
v6NATAvailable bool
dns *dns.Manager
ipt4 netfilterRunner
ipt6 netfilterRunner
cmd commandRunner
}
@@ -113,24 +103,12 @@ func newUserspaceRouter(logf logger.Logf, _ *device.Device, tunDev tun.Device) (
return nil, err
}
supportsV6 := supportsV6()
supportsV6NAT := supportsV6 && supportsV6NAT()
var ipt6 netfilterRunner
if supportsV6 {
// The iptables package probes for `ip6tables` and errors out
// if unavailable. We want that to be a non-fatal error.
ipt6, err = iptables.NewWithProtocol(iptables.ProtocolIPv6)
if err != nil {
return nil, err
}
}
return newUserspaceRouterAdvanced(logf, tunname, ipt4, ipt6, osCommandRunner{}, supportsV6, supportsV6NAT)
return newUserspaceRouterAdvanced(logf, tunname, ipt4, osCommandRunner{})
}
func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netfilter4, netfilter6 netfilterRunner, cmd commandRunner, supportsV6, supportsV6NAT bool) (Router, error) {
ipRuleAvailable := (cmd.run("ip", "rule") == nil)
func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netfilter netfilterRunner, cmd commandRunner) (Router, error) {
_, err := exec.Command("ip", "rule").Output()
ipRuleAvailable := (err == nil)
mconfig := dns.ManagerConfig{
Logf: logf,
@@ -138,18 +116,13 @@ func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netfilter4, ne
}
return &linuxRouter{
logf: logf,
tunname: tunname,
netfilterMode: NetfilterOff,
logf: logf,
ipRuleAvailable: ipRuleAvailable,
v6Available: supportsV6,
v6NATAvailable: supportsV6NAT,
ipt4: netfilter4,
ipt6: netfilter6,
cmd: cmd,
dns: dns.NewManager(mconfig),
tunname: tunname,
netfilterMode: NetfilterOff,
ipt4: netfilter,
cmd: cmd,
dns: dns.NewManager(mconfig),
}, nil
}
@@ -172,7 +145,7 @@ func (r *linuxRouter) Up() error {
func (r *linuxRouter) Close() error {
if err := r.dns.Down(); err != nil {
return fmt.Errorf("dns down: %w", err)
return fmt.Errorf("dns down: %v", err)
}
if err := r.downInterface(); err != nil {
return err
@@ -192,28 +165,23 @@ func (r *linuxRouter) Close() error {
// Set implements the Router interface.
func (r *linuxRouter) Set(cfg *Config) error {
var errs []error
if cfg == nil {
cfg = &shutdownConfig
}
if err := r.dns.Set(cfg.DNS); err != nil {
errs = append(errs, fmt.Errorf("dns set: %w", err))
}
if err := r.setNetfilterMode(cfg.NetfilterMode); err != nil {
errs = append(errs, err)
return err
}
newRoutes, err := cidrDiff("route", r.routes, cfg.Routes, r.addRoute, r.delRoute, r.logf)
if err != nil {
errs = append(errs, err)
return err
}
r.routes = newRoutes
newAddrs, err := cidrDiff("addr", r.addrs, cfg.LocalAddrs, r.addAddress, r.delAddress, r.logf)
if err != nil {
errs = append(errs, err)
return err
}
r.addrs = newAddrs
@@ -222,16 +190,20 @@ func (r *linuxRouter) Set(cfg *Config) error {
// state already correct, nothing to do.
case cfg.SNATSubnetRoutes:
if err := r.addSNATRule(); err != nil {
errs = append(errs, err)
return err
}
default:
if err := r.delSNATRule(); err != nil {
errs = append(errs, err)
return err
}
}
r.snatSubnetRoutes = cfg.SNATSubnetRoutes
return multierror.New(errs)
if err := r.dns.Set(cfg.DNS); err != nil {
return fmt.Errorf("dns set: %v", err)
}
return nil
}
// setNetfilterMode switches the router to the given netfilter
@@ -358,7 +330,6 @@ func (r *linuxRouter) setNetfilterMode(mode NetfilterMode) error {
// address is already assigned to the interface, or if the addition
// fails.
func (r *linuxRouter) addAddress(addr netaddr.IPPrefix) error {
if err := r.cmd.run("ip", "addr", "add", addr.String(), "dev", r.tunname); err != nil {
return fmt.Errorf("adding address %q to tunnel interface: %w", addr, err)
}
@@ -387,17 +358,7 @@ func (r *linuxRouter) addLoopbackRule(addr netaddr.IP) error {
if r.netfilterMode == NetfilterOff {
return nil
}
nf := r.ipt4
if addr.Is6() {
if !r.v6Available {
// IPv6 not available, ignore.
return nil
}
nf = r.ipt6
}
if err := nf.Insert("filter", "ts-input", 1, "-i", "lo", "-s", addr.String(), "-j", "ACCEPT"); err != nil {
if err := r.ipt4.Insert("filter", "ts-input", 1, "-i", "lo", "-s", addr.String(), "-j", "ACCEPT"); err != nil {
return fmt.Errorf("adding loopback allow rule for %q: %w", addr, err)
}
return nil
@@ -409,17 +370,7 @@ func (r *linuxRouter) delLoopbackRule(addr netaddr.IP) error {
if r.netfilterMode == NetfilterOff {
return nil
}
nf := r.ipt4
if addr.Is6() {
if !r.v6Available {
// IPv6 not available, ignore.
return nil
}
nf = r.ipt6
}
if err := nf.Delete("filter", "ts-input", "-i", "lo", "-s", addr.String(), "-j", "ACCEPT"); err != nil {
if err := r.ipt4.Delete("filter", "ts-input", "-i", "lo", "-s", addr.String(), "-j", "ACCEPT"); err != nil {
return fmt.Errorf("deleting loopback allow rule for %q: %w", addr, err)
}
return nil
@@ -465,13 +416,6 @@ func (r *linuxRouter) downInterface() error {
return r.cmd.run("ip", "link", "set", "dev", r.tunname, "down")
}
func (r *linuxRouter) iprouteFamilies() []string {
if r.v6Available {
return []string{"-4", "-6"}
}
return []string{"-4"}
}
// addIPRules adds the policy routing rule that avoids tailscaled
// routing loops. If the rule exists and appears to be a
// tailscale-managed rule, it is gracefully replaced.
@@ -488,60 +432,58 @@ func (r *linuxRouter) addIPRules() error {
rg := newRunGroup(nil, r.cmd)
for _, family := range r.iprouteFamilies() {
// NOTE(apenwarr): We leave spaces between each pref number.
// This is so the sysadmin can override by inserting rules in
// between if they want.
// NOTE(apenwarr): We leave spaces between each pref number.
// This is so the sysadmin can override by inserting rules in
// between if they want.
// NOTE(apenwarr): This sequence seems complicated, right?
// If we could simply have a rule that said "match packets that
// *don't* have this fwmark", then we would only need to add one
// link to table 52 and we'd be done. Unfortunately, older kernels
// and 'ip rule' implementations (including busybox), don't support
// checking for the lack of a fwmark, only the presence. The technique
// below works even on very old kernels.
// NOTE(apenwarr): This sequence seems complicated, right?
// If we could simply have a rule that said "match packets that
// *don't* have this fwmark", then we would only need to add one
// link to table 52 and we'd be done. Unfortunately, older kernels
// and 'ip rule' implementations (including busybox), don't support
// checking for the lack of a fwmark, only the presence. The technique
// below works even on very old kernels.
// Packets from us, tagged with our fwmark, first try the kernel's
// main routing table.
rg.Run(
"ip", family, "rule", "add",
"pref", tailscaleRouteTable+"10",
"fwmark", tailscaleBypassMark,
"table", "main",
)
// ...and then we try the 'default' table, for correctness,
// even though it's been empty on every Linux system I've ever seen.
rg.Run(
"ip", family, "rule", "add",
"pref", tailscaleRouteTable+"30",
"fwmark", tailscaleBypassMark,
"table", "default",
)
// If neither of those matched (no default route on this system?)
// then packets from us should be aborted rather than falling through
// to the tailscale routes, because that would create routing loops.
rg.Run(
"ip", family, "rule", "add",
"pref", tailscaleRouteTable+"50",
"fwmark", tailscaleBypassMark,
"type", "unreachable",
)
// If we get to this point, capture all packets and send them
// through to the tailscale route table. For apps other than us
// (ie. with no fwmark set), this is the first routing table, so
// it takes precedence over all the others, ie. VPN routes always
// beat non-VPN routes.
//
// NOTE(apenwarr): tables >255 are not supported in busybox, so we
// can't use a table number that aligns with the rule preferences.
rg.Run(
"ip", family, "rule", "add",
"pref", tailscaleRouteTable+"70",
"table", tailscaleRouteTable,
)
// If that didn't match, then non-fwmark packets fall through to the
// usual rules (pref 32766 and 32767, ie. main and default).
}
// Packets from us, tagged with our fwmark, first try the kernel's
// main routing table.
rg.Run(
"ip", "rule", "add",
"pref", tailscaleRouteTable+"10",
"fwmark", tailscaleBypassMark,
"table", "main",
)
// ...and then we try the 'default' table, for correctness,
// even though it's been empty on every Linux system I've ever seen.
rg.Run(
"ip", "rule", "add",
"pref", tailscaleRouteTable+"30",
"fwmark", tailscaleBypassMark,
"table", "default",
)
// If neither of those matched (no default route on this system?)
// then packets from us should be aborted rather than falling through
// to the tailscale routes, because that would create routing loops.
rg.Run(
"ip", "rule", "add",
"pref", tailscaleRouteTable+"50",
"fwmark", tailscaleBypassMark,
"type", "unreachable",
)
// If we get to this point, capture all packets and send them
// through to the tailscale route table. For apps other than us
// (ie. with no fwmark set), this is the first routing table, so
// it takes precedence over all the others, ie. VPN routes always
// beat non-VPN routes.
//
// NOTE(apenwarr): tables >255 are not supported in busybox, so we
// can't use a table number that aligns with the rule preferences.
rg.Run(
"ip", "rule", "add",
"pref", tailscaleRouteTable+"70",
"table", tailscaleRouteTable,
)
// If that didn't match, then non-fwmark packets fall through to the
// usual rules (pref 32766 and 32767, ie. main and default).
return rg.ErrAcc
}
@@ -561,105 +503,73 @@ func (r *linuxRouter) delIPRules() error {
// unknown rules during deletion.
rg := newRunGroup([]int{2, 254}, r.cmd)
for _, family := range r.iprouteFamilies() {
// When deleting rules, we want to be a bit specific (mention which
// table we were routing to) but not *too* specific (fwmarks, etc).
// That leaves us some flexibility to change these values in later
// versions without having ongoing hacks for every possible
// combination.
// When deleting rules, we want to be a bit specific (mention which
// table we were routing to) but not *too* specific (fwmarks, etc).
// That leaves us some flexibility to change these values in later
// versions without having ongoing hacks for every possible
// combination.
// Delete old-style tailscale rules
// (never released in a stable version, so we can drop this
// support eventually).
rg.Run(
"ip", family, "rule", "del",
"pref", "10000",
"table", "main",
)
// Delete new-style tailscale rules.
rg.Run(
"ip", family, "rule", "del",
"pref", tailscaleRouteTable+"10",
"table", "main",
)
rg.Run(
"ip", family, "rule", "del",
"pref", tailscaleRouteTable+"30",
"table", "default",
)
rg.Run(
"ip", family, "rule", "del",
"pref", tailscaleRouteTable+"50",
"type", "unreachable",
)
rg.Run(
"ip", family, "rule", "del",
"pref", tailscaleRouteTable+"70",
"table", tailscaleRouteTable,
)
}
// Delete old-style tailscale rules
// (never released in a stable version, so we can drop this
// support eventually).
rg.Run(
"ip", "rule", "del",
"pref", "10000",
"table", "main",
)
// Delete new-style tailscale rules.
rg.Run(
"ip", "rule", "del",
"pref", tailscaleRouteTable+"10",
"table", "main",
)
rg.Run(
"ip", "rule", "del",
"pref", tailscaleRouteTable+"30",
"table", "default",
)
rg.Run(
"ip", "rule", "del",
"pref", tailscaleRouteTable+"50",
"type", "unreachable",
)
rg.Run(
"ip", "rule", "del",
"pref", tailscaleRouteTable+"70",
"table", tailscaleRouteTable,
)
return rg.ErrAcc
}
func (r *linuxRouter) netfilterFamilies() []netfilterRunner {
if r.v6Available {
return []netfilterRunner{r.ipt4, r.ipt6}
}
return []netfilterRunner{r.ipt4}
}
// addNetfilterChains creates custom Tailscale chains in netfilter.
func (r *linuxRouter) addNetfilterChains() error {
create := func(ipt netfilterRunner, table, chain string) error {
err := ipt.ClearChain(table, chain)
create := func(table, chain string) error {
err := r.ipt4.ClearChain(table, chain)
if errCode(err) == 1 {
// nonexistent chain. let's create it!
return ipt.NewChain(table, chain)
return r.ipt4.NewChain(table, chain)
}
if err != nil {
return fmt.Errorf("setting up %s/%s: %w", table, chain, err)
}
return nil
}
for _, ipt := range r.netfilterFamilies() {
if err := create(ipt, "filter", "ts-input"); err != nil {
return err
}
if err := create(ipt, "filter", "ts-forward"); err != nil {
return err
}
}
if err := create(r.ipt4, "nat", "ts-postrouting"); err != nil {
if err := create("filter", "ts-input"); err != nil {
return err
}
if r.v6NATAvailable {
if err := create(r.ipt6, "nat", "ts-postrouting"); err != nil {
return err
}
if err := create("filter", "ts-forward"); err != nil {
return err
}
if err := create("nat", "ts-postrouting"); err != nil {
return err
}
return nil
}
// addNetfilterBase adds some basic processing rules to be
// supplemented by later calls to other helpers.
// addNetfilterBase adds with some basic processing rules to be supplemented
// by later calls to other helpers.
func (r *linuxRouter) addNetfilterBase() error {
if err := r.addNetfilterBase4(); err != nil {
return err
}
if r.v6Available {
if err := r.addNetfilterBase6(); err != nil {
return err
}
}
return nil
}
// addNetfilterBase4 adds some basic IPv4 processing rules to be
// supplemented by later calls to other helpers.
func (r *linuxRouter) addNetfilterBase4() error {
// Only allow CGNAT range traffic to come from tailscale0. There
// is an exception carved out for ranges used by ChromeOS, for
// which we fall out of the Tailscale chain.
@@ -668,11 +578,11 @@ func (r *linuxRouter) addNetfilterBase4() error {
// CGNAT range for other purposes :(.
args := []string{"!", "-i", r.tunname, "-s", tsaddr.ChromeOSVMRange().String(), "-j", "RETURN"}
if err := r.ipt4.Append("filter", "ts-input", args...); err != nil {
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
return fmt.Errorf("adding %v in filter/ts-input: %w", args, err)
}
args = []string{"!", "-i", r.tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}
if err := r.ipt4.Append("filter", "ts-input", args...); err != nil {
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
return fmt.Errorf("adding %v in filter/ts-input: %w", args, err)
}
// Forward all traffic from the Tailscale interface, and drop
@@ -688,43 +598,19 @@ func (r *linuxRouter) addNetfilterBase4() error {
// use to effectively run that same test again.
args = []string{"-i", r.tunname, "-j", "MARK", "--set-mark", tailscaleSubnetRouteMark}
if err := r.ipt4.Append("filter", "ts-forward", args...); err != nil {
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
return fmt.Errorf("adding %v in filter/ts-forward: %w", args, err)
}
args = []string{"-m", "mark", "--mark", tailscaleSubnetRouteMark, "-j", "ACCEPT"}
if err := r.ipt4.Append("filter", "ts-forward", args...); err != nil {
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
return fmt.Errorf("adding %v in filter/ts-forward: %w", args, err)
}
args = []string{"-o", r.tunname, "-s", tsaddr.CGNATRange().String(), "-j", "DROP"}
if err := r.ipt4.Append("filter", "ts-forward", args...); err != nil {
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
return fmt.Errorf("adding %v in filter/ts-forward: %w", args, err)
}
args = []string{"-o", r.tunname, "-j", "ACCEPT"}
if err := r.ipt4.Append("filter", "ts-forward", args...); err != nil {
return fmt.Errorf("adding %v in v4/filter/ts-forward: %w", args, err)
}
return nil
}
// addNetfilterBase4 adds some basic IPv6 processing rules to be
// supplemented by later calls to other helpers.
func (r *linuxRouter) addNetfilterBase6() error {
// TODO: only allow traffic from Tailscale's ULA range to come
// from tailscale0.
args := []string{"-i", r.tunname, "-j", "MARK", "--set-mark", tailscaleSubnetRouteMark}
if err := r.ipt6.Append("filter", "ts-forward", args...); err != nil {
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
}
args = []string{"-m", "mark", "--mark", tailscaleSubnetRouteMark, "-j", "ACCEPT"}
if err := r.ipt6.Append("filter", "ts-forward", args...); err != nil {
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
}
// TODO: drop forwarded traffic to tailscale0 from tailscale's ULA
// (see corresponding IPv4 CGNAT rule).
args = []string{"-o", r.tunname, "-j", "ACCEPT"}
if err := r.ipt6.Append("filter", "ts-forward", args...); err != nil {
return fmt.Errorf("adding %v in v6/filter/ts-forward: %w", args, err)
return fmt.Errorf("adding %v in filter/ts-forward: %w", args, err)
}
return nil
@@ -732,8 +618,8 @@ func (r *linuxRouter) addNetfilterBase6() error {
// delNetfilterChains removes the custom Tailscale chains from netfilter.
func (r *linuxRouter) delNetfilterChains() error {
del := func(ipt netfilterRunner, table, chain string) error {
if err := ipt.ClearChain(table, chain); err != nil {
del := func(table, chain string) error {
if err := r.ipt4.ClearChain(table, chain); err != nil {
if errCode(err) == 1 {
// nonexistent chain. That's fine, since it's
// the desired state anyway.
@@ -741,7 +627,7 @@ func (r *linuxRouter) delNetfilterChains() error {
}
return fmt.Errorf("flushing %s/%s: %w", table, chain, err)
}
if err := ipt.DeleteChain(table, chain); err != nil {
if err := r.ipt4.DeleteChain(table, chain); err != nil {
// this shouldn't fail, because if the chain didn't
// exist, we would have returned after ClearChain.
return fmt.Errorf("deleting %s/%s: %v", table, chain, err)
@@ -749,21 +635,14 @@ func (r *linuxRouter) delNetfilterChains() error {
return nil
}
for _, ipt := range r.netfilterFamilies() {
if err := del(ipt, "filter", "ts-input"); err != nil {
return err
}
if err := del(ipt, "filter", "ts-forward"); err != nil {
return err
}
}
if err := del(r.ipt4, "nat", "ts-postrouting"); err != nil {
if err := del("filter", "ts-input"); err != nil {
return err
}
if r.v6NATAvailable {
if err := del(r.ipt6, "nat", "ts-postrouting"); err != nil {
return err
}
if err := del("filter", "ts-forward"); err != nil {
return err
}
if err := del("nat", "ts-postrouting"); err != nil {
return err
}
return nil
@@ -772,8 +651,8 @@ func (r *linuxRouter) delNetfilterChains() error {
// delNetfilterBase empties but does not remove custom Tailscale chains from
// netfilter.
func (r *linuxRouter) delNetfilterBase() error {
del := func(ipt netfilterRunner, table, chain string) error {
if err := ipt.ClearChain(table, chain); err != nil {
del := func(table, chain string) error {
if err := r.ipt4.ClearChain(table, chain); err != nil {
if errCode(err) == 1 {
// nonexistent chain. That's fine, since it's
// the desired state anyway.
@@ -784,21 +663,14 @@ func (r *linuxRouter) delNetfilterBase() error {
return nil
}
for _, ipt := range r.netfilterFamilies() {
if err := del(ipt, "filter", "ts-input"); err != nil {
return err
}
if err := del(ipt, "filter", "ts-forward"); err != nil {
return err
}
}
if err := del(r.ipt4, "nat", "ts-postrouting"); err != nil {
if err := del("filter", "ts-input"); err != nil {
return err
}
if r.v6NATAvailable {
if err := del(r.ipt6, "nat", "ts-postrouting"); err != nil {
return err
}
if err := del("filter", "ts-forward"); err != nil {
return err
}
if err := del("nat", "ts-postrouting"); err != nil {
return err
}
return nil
@@ -808,38 +680,31 @@ func (r *linuxRouter) delNetfilterBase() error {
// the relevant main netfilter chains. The tailscale chains must
// already exist.
func (r *linuxRouter) addNetfilterHooks() error {
divert := func(ipt netfilterRunner, table, chain string) error {
divert := func(table, chain string) error {
tsChain := tsChain(chain)
args := []string{"-j", tsChain}
exists, err := ipt.Exists(table, chain, args...)
exists, err := r.ipt4.Exists(table, chain, args...)
if err != nil {
return fmt.Errorf("checking for %v in %s/%s: %w", args, table, chain, err)
}
if exists {
return nil
}
if err := ipt.Insert(table, chain, 1, args...); err != nil {
if err := r.ipt4.Insert(table, chain, 1, args...); err != nil {
return fmt.Errorf("adding %v in %s/%s: %w", args, table, chain, err)
}
return nil
}
for _, ipt := range r.netfilterFamilies() {
if err := divert(ipt, "filter", "INPUT"); err != nil {
return err
}
if err := divert(ipt, "filter", "FORWARD"); err != nil {
return err
}
}
if err := divert(r.ipt4, "nat", "POSTROUTING"); err != nil {
if err := divert("filter", "INPUT"); err != nil {
return err
}
if r.v6NATAvailable {
if err := divert(r.ipt6, "nat", "POSTROUTING"); err != nil {
return err
}
if err := divert("filter", "FORWARD"); err != nil {
return err
}
if err := divert("nat", "POSTROUTING"); err != nil {
return err
}
return nil
}
@@ -847,10 +712,10 @@ func (r *linuxRouter) addNetfilterHooks() error {
// delNetfilterHooks deletes the calls to tailscale's netfilter chains
// in the relevant main netfilter chains.
func (r *linuxRouter) delNetfilterHooks() error {
del := func(ipt netfilterRunner, table, chain string) error {
del := func(table, chain string) error {
tsChain := tsChain(chain)
args := []string{"-j", tsChain}
if err := ipt.Delete(table, chain, args...); err != nil {
if err := r.ipt4.Delete(table, chain, args...); err != nil {
// TODO(apenwarr): check for errCode(1) here.
// Unfortunately the error code from the iptables
// module resists unwrapping, unlike with other
@@ -862,21 +727,14 @@ func (r *linuxRouter) delNetfilterHooks() error {
return nil
}
for _, ipt := range r.netfilterFamilies() {
if err := del(ipt, "filter", "INPUT"); err != nil {
return err
}
if err := del(ipt, "filter", "FORWARD"); err != nil {
return err
}
}
if err := del(r.ipt4, "nat", "POSTROUTING"); err != nil {
if err := del("filter", "INPUT"); err != nil {
return err
}
if r.v6NATAvailable {
if err := del(r.ipt6, "nat", "POSTROUTING"); err != nil {
return err
}
if err := del("filter", "FORWARD"); err != nil {
return err
}
if err := del("nat", "POSTROUTING"); err != nil {
return err
}
return nil
}
@@ -890,12 +748,7 @@ func (r *linuxRouter) addSNATRule() error {
args := []string{"-m", "mark", "--mark", tailscaleSubnetRouteMark, "-j", "MASQUERADE"}
if err := r.ipt4.Append("nat", "ts-postrouting", args...); err != nil {
return fmt.Errorf("adding %v in v4/nat/ts-postrouting: %w", args, err)
}
if r.v6NATAvailable {
if err := r.ipt6.Append("nat", "ts-postrouting", args...); err != nil {
return fmt.Errorf("adding %v in v6/nat/ts-postrouting: %w", args, err)
}
return fmt.Errorf("adding %v in nat/ts-postrouting: %w", args, err)
}
return nil
}
@@ -909,12 +762,7 @@ func (r *linuxRouter) delSNATRule() error {
args := []string{"-m", "mark", "--mark", tailscaleSubnetRouteMark, "-j", "MASQUERADE"}
if err := r.ipt4.Delete("nat", "ts-postrouting", args...); err != nil {
return fmt.Errorf("deleting %v in v4/nat/ts-postrouting: %w", args, err)
}
if r.v6NATAvailable {
if err := r.ipt6.Delete("nat", "ts-postrouting", args...); err != nil {
return fmt.Errorf("deleting %v in v6/nat/ts-postrouting: %w", args, err)
}
return fmt.Errorf("deleting %v in nat/ts-postrouting: %w", args, err)
}
return nil
}
@@ -1002,61 +850,3 @@ func normalizeCIDR(cidr netaddr.IPPrefix) string {
func cleanup(logf logger.Logf, interfaceName string) {
// TODO(dmytro): clean up iptables.
}
// supportsV6 returns whether the system appears to have a working
// IPv6 network stack.
func supportsV6() bool {
_, err := os.Stat("/proc/sys/net/ipv6")
if os.IsNotExist(err) {
return false
}
bs, err := ioutil.ReadFile("/proc/sys/net/ipv6/conf/all/disable_ipv6")
if err != nil {
// Be conservative if we can't find the ipv6 configuration knob.
return false
}
disabled, err := strconv.ParseBool(strings.TrimSpace(string(bs)))
if err != nil {
return false
}
if disabled {
return false
}
// Older kernels don't support IPv6 policy routing.
bs, err = ioutil.ReadFile("/proc/sys/net/ipv6/conf/all/disable_policy")
if err != nil {
// Absent knob means policy routing is unsupported.
return false
}
disabled, err = strconv.ParseBool(strings.TrimSpace(string(bs)))
if err != nil {
return false
}
if disabled {
return false
}
// Some distros ship ip6tables separately from iptables.
if _, err := exec.LookPath("ip6tables"); err != nil {
return false
}
return true
}
// supportsV6NAT returns whether the system has a "nat" table in the
// IPv6 netfilter stack.
//
// The nat table was added after the initial release of ipv6
// netfilter, so some older distros ship a kernel that can't NAT IPv6
// traffic.
func supportsV6NAT() bool {
bs, err := ioutil.ReadFile("/proc/net/ip6_tables_names")
if err != nil {
// Can't read the file. Assume SNAT works.
return true
}
return bytes.Contains(bs, []byte("nat\n"))
}

View File

@@ -34,14 +34,10 @@ func mustCIDRs(ss ...string) []netaddr.IPPrefix {
func TestRouterStates(t *testing.T) {
basic := `
ip rule add -4 pref 5210 fwmark 0x80000 table main
ip rule add -4 pref 5230 fwmark 0x80000 table default
ip rule add -4 pref 5250 fwmark 0x80000 type unreachable
ip rule add -4 pref 5270 table 52
ip rule add -6 pref 5210 fwmark 0x80000 table main
ip rule add -6 pref 5230 fwmark 0x80000 table default
ip rule add -6 pref 5250 fwmark 0x80000 type unreachable
ip rule add -6 pref 5270 table 52
ip rule add pref 5210 fwmark 0x80000 table main
ip rule add pref 5230 fwmark 0x80000 table default
ip rule add pref 5250 fwmark 0x80000 type unreachable
ip rule add pref 5270 table 52
`
states := []struct {
name string
@@ -108,24 +104,17 @@ up
ip addr add 100.101.102.104/10 dev tailscale0
ip route add 10.0.0.0/8 dev tailscale0 table 52
ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic +
`v4/filter/FORWARD -j ts-forward
v4/filter/INPUT -j ts-input
v4/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v4/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v4/filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
v4/filter/ts-forward -o tailscale0 -j ACCEPT
v4/filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
v4/filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
v4/filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
v4/nat/POSTROUTING -j ts-postrouting
v4/nat/ts-postrouting -m mark --mark 0x40000 -j MASQUERADE
v6/filter/FORWARD -j ts-forward
v6/filter/INPUT -j ts-input
v6/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v6/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v6/filter/ts-forward -o tailscale0 -j ACCEPT
v6/nat/POSTROUTING -j ts-postrouting
v6/nat/ts-postrouting -m mark --mark 0x40000 -j MASQUERADE
`filter/FORWARD -j ts-forward
filter/INPUT -j ts-input
filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
filter/ts-forward -o tailscale0 -j ACCEPT
filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
nat/POSTROUTING -j ts-postrouting
nat/ts-postrouting -m mark --mark 0x40000 -j MASQUERADE
`,
},
{
@@ -140,22 +129,16 @@ up
ip addr add 100.101.102.104/10 dev tailscale0
ip route add 10.0.0.0/8 dev tailscale0 table 52
ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic +
`v4/filter/FORWARD -j ts-forward
v4/filter/INPUT -j ts-input
v4/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v4/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v4/filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
v4/filter/ts-forward -o tailscale0 -j ACCEPT
v4/filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
v4/filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
v4/filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
v4/nat/POSTROUTING -j ts-postrouting
v6/filter/FORWARD -j ts-forward
v6/filter/INPUT -j ts-input
v6/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v6/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v6/filter/ts-forward -o tailscale0 -j ACCEPT
v6/nat/POSTROUTING -j ts-postrouting
`filter/FORWARD -j ts-forward
filter/INPUT -j ts-input
filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
filter/ts-forward -o tailscale0 -j ACCEPT
filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
nat/POSTROUTING -j ts-postrouting
`,
},
@@ -173,22 +156,16 @@ up
ip addr add 100.101.102.104/10 dev tailscale0
ip route add 10.0.0.0/8 dev tailscale0 table 52
ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic +
`v4/filter/FORWARD -j ts-forward
v4/filter/INPUT -j ts-input
v4/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v4/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v4/filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
v4/filter/ts-forward -o tailscale0 -j ACCEPT
v4/filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
v4/filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
v4/filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
v4/nat/POSTROUTING -j ts-postrouting
v6/filter/FORWARD -j ts-forward
v6/filter/INPUT -j ts-input
v6/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v6/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v6/filter/ts-forward -o tailscale0 -j ACCEPT
v6/nat/POSTROUTING -j ts-postrouting
`filter/FORWARD -j ts-forward
filter/INPUT -j ts-input
filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
filter/ts-forward -o tailscale0 -j ACCEPT
filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
nat/POSTROUTING -j ts-postrouting
`,
},
{
@@ -203,22 +180,16 @@ up
ip addr add 100.101.102.104/10 dev tailscale0
ip route add 10.0.0.0/8 dev tailscale0 table 52
ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic +
`v4/filter/FORWARD -j ts-forward
v4/filter/INPUT -j ts-input
v4/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v4/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v4/filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
v4/filter/ts-forward -o tailscale0 -j ACCEPT
v4/filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
v4/filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
v4/filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
v4/nat/POSTROUTING -j ts-postrouting
v6/filter/FORWARD -j ts-forward
v6/filter/INPUT -j ts-input
v6/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v6/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v6/filter/ts-forward -o tailscale0 -j ACCEPT
v6/nat/POSTROUTING -j ts-postrouting
`filter/FORWARD -j ts-forward
filter/INPUT -j ts-input
filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
filter/ts-forward -o tailscale0 -j ACCEPT
filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
nat/POSTROUTING -j ts-postrouting
`,
},
@@ -234,16 +205,13 @@ up
ip addr add 100.101.102.104/10 dev tailscale0
ip route add 10.0.0.0/8 dev tailscale0 table 52
ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic +
`v4/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v4/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v4/filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
v4/filter/ts-forward -o tailscale0 -j ACCEPT
v4/filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
v4/filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
v4/filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
v6/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v6/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v6/filter/ts-forward -o tailscale0 -j ACCEPT
`filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
filter/ts-forward -o tailscale0 -j ACCEPT
filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
`,
},
{
@@ -258,28 +226,22 @@ up
ip addr add 100.101.102.104/10 dev tailscale0
ip route add 10.0.0.0/8 dev tailscale0 table 52
ip route add 100.100.100.100/32 dev tailscale0 table 52` + basic +
`v4/filter/FORWARD -j ts-forward
v4/filter/INPUT -j ts-input
v4/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v4/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v4/filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
v4/filter/ts-forward -o tailscale0 -j ACCEPT
v4/filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
v4/filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
v4/filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
v4/nat/POSTROUTING -j ts-postrouting
v6/filter/FORWARD -j ts-forward
v6/filter/INPUT -j ts-input
v6/filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
v6/filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
v6/filter/ts-forward -o tailscale0 -j ACCEPT
v6/nat/POSTROUTING -j ts-postrouting
`filter/FORWARD -j ts-forward
filter/INPUT -j ts-input
filter/ts-forward -i tailscale0 -j MARK --set-mark 0x40000
filter/ts-forward -m mark --mark 0x40000 -j ACCEPT
filter/ts-forward -o tailscale0 -s 100.64.0.0/10 -j DROP
filter/ts-forward -o tailscale0 -j ACCEPT
filter/ts-input -i lo -s 100.101.102.104 -j ACCEPT
filter/ts-input ! -i tailscale0 -s 100.115.92.0/23 -j RETURN
filter/ts-input ! -i tailscale0 -s 100.64.0.0/10 -j DROP
nat/POSTROUTING -j ts-postrouting
`,
},
}
fake := NewFakeOS(t)
router, err := newUserspaceRouterAdvanced(t.Logf, "tailscale0", fake.netfilter4, fake.netfilter6, fake, true, true)
router, err := newUserspaceRouterAdvanced(t.Logf, "tailscale0", fake, fake)
if err != nil {
t.Fatalf("failed to create router: %v", err)
}
@@ -313,15 +275,21 @@ v6/nat/POSTROUTING -j ts-postrouting
}
}
type fakeNetfilter struct {
t *testing.T
n map[string][]string
// fakeOS implements netfilterRunner and commandRunner, but captures
// changes without touching the OS.
type fakeOS struct {
t *testing.T
up bool
ips []string
routes []string
rules []string
netfilter map[string][]string
}
func newNetfilter(t *testing.T) *fakeNetfilter {
return &fakeNetfilter{
func NewFakeOS(t *testing.T) *fakeOS {
return &fakeOS{
t: t,
n: map[string][]string{
netfilter: map[string][]string{
"filter/INPUT": nil,
"filter/OUTPUT": nil,
"filter/FORWARD": nil,
@@ -332,118 +300,6 @@ func newNetfilter(t *testing.T) *fakeNetfilter {
}
}
func (n *fakeNetfilter) Insert(table, chain string, pos int, args ...string) error {
k := table + "/" + chain
if rules, ok := n.n[k]; ok {
if pos > len(rules)+1 {
n.t.Errorf("bad position %d in %s", pos, k)
return errExec
}
rules = append(rules, "")
copy(rules[pos:], rules[pos-1:])
rules[pos-1] = strings.Join(args, " ")
n.n[k] = rules
} else {
n.t.Errorf("unknown table/chain %s", k)
return errExec
}
return nil
}
func (n *fakeNetfilter) Append(table, chain string, args ...string) error {
k := table + "/" + chain
return n.Insert(table, chain, len(n.n[k])+1, args...)
}
func (n *fakeNetfilter) Exists(table, chain string, args ...string) (bool, error) {
k := table + "/" + chain
if rules, ok := n.n[k]; ok {
for _, rule := range rules {
if rule == strings.Join(args, " ") {
return true, nil
}
}
return false, nil
} else {
n.t.Errorf("unknown table/chain %s", k)
return false, errExec
}
}
func (n *fakeNetfilter) Delete(table, chain string, args ...string) error {
k := table + "/" + chain
if rules, ok := n.n[k]; ok {
for i, rule := range rules {
if rule == strings.Join(args, " ") {
rules = append(rules[:i], rules[i+1:]...)
n.n[k] = rules
return nil
}
}
n.t.Errorf("delete of unknown rule %q from %s", strings.Join(args, " "), k)
return errExec
} else {
n.t.Errorf("unknown table/chain %s", k)
return errExec
}
}
func (n *fakeNetfilter) ClearChain(table, chain string) error {
k := table + "/" + chain
if _, ok := n.n[k]; ok {
n.n[k] = nil
return nil
} else {
n.t.Logf("note: ClearChain: unknown table/chain %s", k)
return errors.New("exitcode:1")
}
}
func (n *fakeNetfilter) NewChain(table, chain string) error {
k := table + "/" + chain
if _, ok := n.n[k]; ok {
n.t.Errorf("table/chain %s already exists", k)
return errExec
}
n.n[k] = nil
return nil
}
func (n *fakeNetfilter) DeleteChain(table, chain string) error {
k := table + "/" + chain
if rules, ok := n.n[k]; ok {
if len(rules) != 0 {
n.t.Errorf("%s is not empty", k)
return errExec
}
delete(n.n, k)
return nil
} else {
n.t.Errorf("%s does not exist", k)
return errExec
}
}
// fakeOS implements commandRunner and provides v4 and v6
// netfilterRunners, but captures changes without touching the OS.
type fakeOS struct {
t *testing.T
up bool
ips []string
routes []string
rules []string
netfilter4 *fakeNetfilter
netfilter6 *fakeNetfilter
}
func NewFakeOS(t *testing.T) *fakeOS {
return &fakeOS{
t: t,
netfilter4: newNetfilter(t),
netfilter6: newNetfilter(t),
}
}
var errExec = errors.New("execution failed")
func (o *fakeOS) String() string {
@@ -467,30 +323,120 @@ func (o *fakeOS) String() string {
}
var chains []string
for chain := range o.netfilter4.n {
for chain := range o.netfilter {
chains = append(chains, chain)
}
sort.Strings(chains)
for _, chain := range chains {
for _, rule := range o.netfilter4.n[chain] {
fmt.Fprintf(&b, "v4/%s %s\n", chain, rule)
for _, rule := range o.netfilter[chain] {
fmt.Fprintf(&b, "%s %s\n", chain, rule)
}
}
chains = nil
for chain := range o.netfilter6.n {
chains = append(chains, chain)
}
sort.Strings(chains)
for _, chain := range chains {
for _, rule := range o.netfilter6.n[chain] {
fmt.Fprintf(&b, "v6/%s %s\n", chain, rule)
}
}
return b.String()[:len(b.String())-1]
}
func (o *fakeOS) Insert(table, chain string, pos int, args ...string) error {
k := table + "/" + chain
if rules, ok := o.netfilter[k]; ok {
if pos > len(rules)+1 {
o.t.Errorf("bad position %d in %s", pos, k)
return errExec
}
rules = append(rules, "")
copy(rules[pos:], rules[pos-1:])
rules[pos-1] = strings.Join(args, " ")
o.netfilter[k] = rules
} else {
o.t.Errorf("unknown table/chain %s", k)
return errExec
}
return nil
}
func (o *fakeOS) Append(table, chain string, args ...string) error {
k := table + "/" + chain
return o.Insert(table, chain, len(o.netfilter[k])+1, args...)
}
func (o *fakeOS) Exists(table, chain string, args ...string) (bool, error) {
k := table + "/" + chain
if rules, ok := o.netfilter[k]; ok {
for _, rule := range rules {
if rule == strings.Join(args, " ") {
return true, nil
}
}
return false, nil
} else {
o.t.Errorf("unknown table/chain %s", k)
return false, errExec
}
}
func (o *fakeOS) Delete(table, chain string, args ...string) error {
k := table + "/" + chain
if rules, ok := o.netfilter[k]; ok {
for i, rule := range rules {
if rule == strings.Join(args, " ") {
rules = append(rules[:i], rules[i+1:]...)
o.netfilter[k] = rules
return nil
}
}
o.t.Errorf("delete of unknown rule %q from %s", strings.Join(args, " "), k)
return errExec
} else {
o.t.Errorf("unknown table/chain %s", k)
return errExec
}
}
func (o *fakeOS) ListChains(table string) (ret []string, err error) {
for chain := range o.netfilter {
pfx := table + "/"
if strings.HasPrefix(chain, pfx) {
ret = append(ret, chain[len(pfx):])
}
}
return ret, nil
}
func (o *fakeOS) ClearChain(table, chain string) error {
k := table + "/" + chain
if _, ok := o.netfilter[k]; ok {
o.netfilter[k] = nil
return nil
} else {
o.t.Logf("note: ClearChain: unknown table/chain %s", k)
return errors.New("exitcode:1")
}
}
func (o *fakeOS) NewChain(table, chain string) error {
k := table + "/" + chain
if _, ok := o.netfilter[k]; ok {
o.t.Errorf("table/chain %s already exists", k)
return errExec
}
o.netfilter[k] = nil
return nil
}
func (o *fakeOS) DeleteChain(table, chain string) error {
k := table + "/" + chain
if rules, ok := o.netfilter[k]; ok {
if len(rules) != 0 {
o.t.Errorf("%s is not empty", k)
return errExec
}
delete(o.netfilter, k)
return nil
} else {
o.t.Errorf("%s does not exist", k)
return errExec
}
}
func (o *fakeOS) run(args ...string) error {
unexpected := func() error {
o.t.Errorf("unexpected invocation %q", strings.Join(args, " "))
@@ -500,20 +446,7 @@ func (o *fakeOS) run(args ...string) error {
return unexpected()
}
if len(args) == 2 && args[1] == "rule" {
// naked invocation of `ip rule` is a feature test. Return
// successfully.
return nil
}
family := ""
rest := strings.Join(args[3:], " ")
if args[1] == "-4" || args[1] == "-6" {
family = args[1]
copy(args[1:], args[2:])
args = args[:len(args)-1]
rest = family + " " + strings.Join(args[3:], " ")
}
var l *[]string
switch args[1] {

View File

@@ -5,17 +5,15 @@
package router
import (
"context"
"fmt"
"log"
"os/exec"
"sync"
"syscall"
"time"
winipcfg "github.com/tailscale/winipcfg-go"
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"tailscale.com/logtail/backoff"
"tailscale.com/types/logger"
"tailscale.com/wgengine/router/dns"
)
@@ -27,7 +25,9 @@ type winRouter struct {
wgdev *device.Device
routeChangeCallback *winipcfg.RouteChangeCallback
dns *dns.Manager
firewall *firewallTweaker
mu sync.Mutex
firewallRuleIP string // the IP rule exists for, or "" if rule doesn't exist
}
func newUserspaceRouter(logf logger.Logf, wgdev *device.Device, tundev tun.Device) (Router, error) {
@@ -49,34 +49,73 @@ func newUserspaceRouter(logf logger.Logf, wgdev *device.Device, tundev tun.Devic
tunname: tunname,
nativeTun: nativeTun,
dns: dns.NewManager(mconfig),
firewall: &firewallTweaker{logf: logger.WithPrefix(logf, "firewall: ")},
}, nil
}
func (r *winRouter) Up() error {
r.firewall.clear()
r.removeFirewallAcceptRule()
var err error
t0 := time.Now()
r.routeChangeCallback, err = monitorDefaultRoutes(r.nativeTun)
d := time.Since(t0).Round(time.Millisecond)
if err != nil {
return fmt.Errorf("monitorDefaultRoutes, after %v: %v", d, err)
log.Fatalf("MonitorDefaultRoutes: %v", err)
}
r.logf("monitorDefaultRoutes done after %v", d)
return nil
}
// removeFirewallAcceptRule removes the "Tailscale-In" firewall rule.
//
// If it doesn't already exist, this currently returns an error but TODO: it should not.
//
// So callers should ignore its error for now.
func (r *winRouter) removeFirewallAcceptRule() error {
r.mu.Lock()
defer r.mu.Unlock()
r.firewallRuleIP = ""
cmd := exec.Command("netsh", "advfirewall", "firewall", "delete", "rule", "name=Tailscale-In", "dir=in")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
return cmd.Run()
}
// addFirewallAcceptRule adds a firewall rule to allow all incoming
// traffic to the given IP (the Tailscale adapter's IP) for network
// adapters in category private. (as previously set by
// setPrivateNetwork)
//
// It returns (false, nil) if the firewall rule was already previously existed with this IP.
func (r *winRouter) addFirewallAcceptRule(ipStr string) (added bool, err error) {
r.mu.Lock()
defer r.mu.Unlock()
if ipStr == r.firewallRuleIP {
return false, nil
}
cmd := exec.Command("netsh", "advfirewall", "firewall", "add", "rule", "name=Tailscale-In", "dir=in", "action=allow", "localip="+ipStr, "profile=private", "enable=yes")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
err = cmd.Run()
if err != nil {
return false, err
}
r.firewallRuleIP = ipStr
return true, nil
}
func (r *winRouter) Set(cfg *Config) error {
if cfg == nil {
cfg = &shutdownConfig
}
var localAddrs []string
for _, la := range cfg.LocalAddrs {
localAddrs = append(localAddrs, la.String())
if len(cfg.LocalAddrs) == 1 && cfg.LocalAddrs[0].Bits == 32 {
ipStr := cfg.LocalAddrs[0].IP.String()
if ok, err := r.addFirewallAcceptRule(ipStr); err != nil {
r.logf("addFirewallRule(%q): %v", ipStr, err)
} else if ok {
r.logf("added firewall rule Tailscale-In for %v", ipStr)
}
} else {
r.removeFirewallAcceptRule()
}
r.firewall.set(localAddrs)
err := configureInterface(cfg, r.nativeTun)
if err != nil {
@@ -92,7 +131,7 @@ func (r *winRouter) Set(cfg *Config) error {
}
func (r *winRouter) Close() error {
r.firewall.clear()
r.removeFirewallAcceptRule()
if err := r.dns.Down(); err != nil {
return fmt.Errorf("dns down: %w", err)
@@ -107,110 +146,3 @@ func (r *winRouter) Close() error {
func cleanup(logf logger.Logf, interfaceName string) {
// Nothing to do here.
}
// firewallTweaker changes the Windows firewall. Normally this wouldn't be so complicated,
// but it can be REALLY SLOW to change the Windows firewall for reasons not understood.
// Like 4 minutes slow. But usually it's tens of milliseconds.
// See https://github.com/tailscale/tailscale/issues/785.
// So this tracks the desired state and runs the actual adjusting code asynchrounsly.
type firewallTweaker struct {
logf logger.Logf
mu sync.Mutex
running bool // doAsyncSet goroutine is running
known bool // firewall is in known state (in lastVal)
want []string // next value we want, or "" to delete the firewall rule
lastVal []string // last set value, if known
}
func (ft *firewallTweaker) clear() { ft.set(nil) }
// set takes the IPv4 and/or IPv6 CIDRs to allow; an empty slice
// removes the firwall rules.
//
// set takes ownership of the slice.
func (ft *firewallTweaker) set(cidrs []string) {
ft.mu.Lock()
defer ft.mu.Unlock()
if len(cidrs) == 0 {
ft.logf("marking for removal")
} else {
ft.logf("marking allowed %v", cidrs)
}
ft.want = cidrs
if ft.running {
// The doAsyncSet goroutine will check ft.want
// before returning.
return
}
ft.logf("starting netsh goroutine")
ft.running = true
go ft.doAsyncSet()
}
func (ft *firewallTweaker) runFirewall(args ...string) (time.Duration, error) {
t0 := time.Now()
args = append([]string{"advfirewall", "firewall"}, args...)
cmd := exec.Command("netsh", args...)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
err := cmd.Run()
return time.Since(t0).Round(time.Millisecond), err
}
func (ft *firewallTweaker) doAsyncSet() {
bo := backoff.NewBackoff("win-firewall", ft.logf, time.Minute)
ctx := context.Background()
ft.mu.Lock()
for { // invariant: ft.mu must be locked when beginning this block
val := ft.want
if ft.known && strsEqual(ft.lastVal, val) {
ft.running = false
ft.logf("ending netsh goroutine")
ft.mu.Unlock()
return
}
needClear := !ft.known || len(ft.lastVal) > 0 || len(val) == 0
ft.mu.Unlock()
if needClear {
ft.logf("clearing Tailscale-In firewall rules...")
// We ignore the error here, because netsh returns an error for
// deleting something that doesn't match.
// TODO(bradfitz): care? That'd involve querying it before/after to see
// whether it was necessary/worked. But the output format is localized,
// so can't rely on parsing English. Maybe need to use OLE, not netsh.exe?
d, _ := ft.runFirewall("delete", "rule", "name=Tailscale-In", "dir=in")
ft.logf("cleared Tailscale-In firewall rules in %v", d)
}
var err error
for _, cidr := range val {
ft.logf("adding Tailscale-In rule to allow %v ...", cidr)
var d time.Duration
d, err = ft.runFirewall("add", "rule", "name=Tailscale-In", "dir=in", "action=allow", "localip="+cidr, "profile=private", "enable=yes")
if err != nil {
ft.logf("error adding Tailscale-In rule to allow %v: %v", cidr, err)
break
}
ft.logf("added Tailscale-In rule to allow %v in %v", cidr, d)
}
bo.BackOff(ctx, err)
ft.mu.Lock()
ft.lastVal = val
ft.known = (err == nil)
}
}
func strsEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

View File

@@ -24,8 +24,6 @@ type commandRunner interface {
type osCommandRunner struct{}
// errCode extracts and returns the process exit code from err, or
// zero if err is nil.
func errCode(err error) int {
if err == nil {
return 0

View File

@@ -2,17 +2,19 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package logger
package wgengine
import (
"fmt"
"runtime"
"tailscale.com/types/logger"
)
// RusagePrefixLog returns a Logf func wrapping the provided logf func that adds
// a prefixed log message to each line with the current binary memory usage
// and max RSS.
func RusagePrefixLog(logf Logf) Logf {
func RusagePrefixLog(logf logger.Logf) logger.Logf {
return func(f string, argv ...interface{}) {
var m runtime.MemStats
runtime.ReadMemStats(&m)

View File

@@ -4,7 +4,7 @@
// +build !windows
package logger
package wgengine
import (
"runtime"

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package logger
package wgengine
func rusageMaxRSS() float64 {
// TODO(apenwarr): Substitute Windows equivalent of Getrusage() here.

View File

@@ -6,10 +6,8 @@ package tsdns
import (
"bytes"
"context"
"encoding/binary"
"errors"
"fmt"
"hash/crc32"
"math/rand"
"net"
@@ -18,8 +16,6 @@ import (
"time"
"inet.af/netaddr"
"tailscale.com/logtail/backoff"
"tailscale.com/net/netns"
"tailscale.com/types/logger"
)
@@ -113,7 +109,7 @@ type forwarder struct {
// conns are the UDP connections used for forwarding.
// A random one is selected for each request, regardless of the target upstream.
conns []*fwdConn
conns []*net.UDPConn
mu sync.Mutex
// upstreams are the nameserver addresses that should be used for forwarding.
@@ -131,16 +127,24 @@ func newForwarder(logf logger.Logf, responses chan Packet) *forwarder {
logf: logger.WithPrefix(logf, "forward: "),
responses: responses,
closed: make(chan struct{}),
conns: make([]*fwdConn, connCount),
conns: make([]*net.UDPConn, connCount),
txMap: make(map[txid]forwardingRecord),
}
}
func (f *forwarder) Start() error {
var err error
for i := range f.conns {
f.conns[i], err = net.ListenUDP("udp", nil)
if err != nil {
return err
}
}
f.wg.Add(connCount + 1)
for idx := range f.conns {
f.conns[idx] = newFwdConn(f.logf, idx)
go f.recv(f.conns[idx])
for idx, conn := range f.conns {
go f.recv(uint16(idx), conn)
}
go f.cleanMap()
@@ -157,10 +161,14 @@ func (f *forwarder) Close() {
close(f.closed)
for _, conn := range f.conns {
conn.close()
conn.SetDeadline(aLongTimeAgo)
}
f.wg.Wait()
for _, conn := range f.conns {
conn.Close()
}
}
func (f *forwarder) setUpstreams(upstreams []net.Addr) {
@@ -169,27 +177,31 @@ func (f *forwarder) setUpstreams(upstreams []net.Addr) {
f.mu.Unlock()
}
// send sends packet to dst. It is best effort.
func (f *forwarder) send(packet []byte, dst net.Addr) {
connIdx := rand.Intn(connCount)
conn := f.conns[connIdx]
conn.send(packet, dst)
_, err := conn.WriteTo(packet, dst)
// Do not log errors due to expired deadline.
if err != nil && !errors.Is(err, os.ErrDeadlineExceeded) {
f.logf("send: %v", err)
}
}
func (f *forwarder) recv(conn *fwdConn) {
func (f *forwarder) recv(connIdx uint16, conn *net.UDPConn) {
defer f.wg.Done()
for {
select {
case <-f.closed:
return
default:
}
out := make([]byte, maxResponseBytes)
n := conn.read(out)
if n == 0 {
continue
n, err := conn.Read(out)
if err != nil {
// Do not log errors due to expired deadline.
if !errors.Is(err, os.ErrDeadlineExceeded) {
f.logf("recv: %v", err)
}
return
}
if n < headerBytes {
f.logf("recv: packet too small (%d bytes)", n)
}
@@ -273,194 +285,3 @@ func (f *forwarder) forward(query Packet) error {
return nil
}
// A fwdConn manages a single connection used to forward DNS requests.
// Net link changes can cause a *net.UDPConn to become permanently unusable, particularly on macOS.
// fwdConn detects such situations and transparently creates new connections.
type fwdConn struct {
// logf allows a fwdConn to log.
logf logger.Logf
// wg tracks the number of outstanding conn.Read and conn.Write calls.
wg sync.WaitGroup
// change allows calls to read to block until a the network connection has been replaced.
change *sync.Cond
// mu protects fields that follow it; it is also change's Locker.
mu sync.Mutex
// closed tracks whether fwdConn has been permanently closed.
closed bool
// conn is the current active connection.
conn net.PacketConn
}
func newFwdConn(logf logger.Logf, idx int) *fwdConn {
c := new(fwdConn)
c.logf = logger.WithPrefix(logf, fmt.Sprintf("fwdConn %d: ", idx))
c.change = sync.NewCond(&c.mu)
// c.conn is created lazily in send
return c
}
// send sends packet to dst using c's connection.
// It is best effort. It is UDP, after all. Failures are logged.
func (c *fwdConn) send(packet []byte, dst net.Addr) {
var b *backoff.Backoff // lazily initialized, since it is not needed in the common case
backOff := func(err error) {
if b == nil {
b = backoff.NewBackoff("tsdns-fwdConn-send", c.logf, 30*time.Second)
}
b.BackOff(context.Background(), err)
}
for {
// Gather the current connection.
// We can't hold the lock while we call WriteTo.
c.mu.Lock()
conn := c.conn
closed := c.closed
if closed {
c.mu.Unlock()
return
}
if conn == nil {
c.reconnectLocked()
c.mu.Unlock()
continue
}
c.mu.Unlock()
c.wg.Add(1)
_, err := conn.WriteTo(packet, dst)
c.wg.Done()
if err == nil {
// Success
return
}
if errors.Is(err, os.ErrDeadlineExceeded) {
// We intentionally closed this connection.
// It has been replaced by a new connection. Try again.
continue
}
// Something else went wrong.
// We have three choices here: try again, give up, or create a new connection.
var opErr *net.OpError
if !errors.As(err, &opErr) {
// Weird. All errors from the net package should be *net.OpError. Bail.
c.logf("send: non-*net.OpErr %v (%T)", err, err)
return
}
if opErr.Temporary() || opErr.Timeout() {
// I doubt that either of these can happen (this is UDP),
// but go ahead and try again.
backOff(err)
continue
}
if networkIsDown(err) {
// Fail.
c.logf("send: network is down")
return
}
if networkIsUnreachable(err) {
// This can be caused by a link change.
// Replace the existing connection with a new one.
c.mu.Lock()
// It's possible that multiple senders discovered simultaneously
// that the network is unreachable. Avoid reconnecting multiple times:
// Only reconnect if the current connection is the one that we
// discovered to be problematic.
if c.conn == conn {
backOff(err)
c.reconnectLocked()
}
c.mu.Unlock()
// Try again with our new network connection.
continue
}
// Unrecognized error. Fail.
c.logf("send: unrecognized error: %v", err)
return
}
}
// read waits for a response from c's connection.
// It returns the number of bytes read, which may be 0
// in case of an error or a closed connection.
func (c *fwdConn) read(out []byte) int {
for {
// Gather the current connection.
// We can't hold the lock while we call ReadFrom.
c.mu.Lock()
conn := c.conn
closed := c.closed
if closed {
c.mu.Unlock()
return 0
}
if conn == nil {
// There is no current connection.
// Wait for the connection to change, then try again.
c.change.Wait()
c.mu.Unlock()
continue
}
c.mu.Unlock()
c.wg.Add(1)
n, _, err := conn.ReadFrom(out)
c.wg.Done()
if err == nil {
// Success.
return n
}
if errors.Is(err, os.ErrDeadlineExceeded) {
// We intentionally closed this connection.
// It has been replaced by a new connection. Try again.
continue
}
c.logf("read: unrecognized error: %v", err)
return 0
}
}
// reconnectLocked replaces the current connection with a new one.
// c.mu must be locked.
func (c *fwdConn) reconnectLocked() {
c.closeConnLocked()
// Make a new connection.
conn, err := netns.Listener().ListenPacket(context.Background(), "udp", "")
if err != nil {
c.logf("ListenPacket failed: %v", err)
} else {
c.conn = conn
}
// Broadcast that a new connection is available.
c.change.Broadcast()
}
// closeCurrentConn closes the current connection.
// c.mu must be locked.
func (c *fwdConn) closeConnLocked() {
if c.conn == nil {
return
}
// Unblock all readers/writers, wait for them, close the connection.
c.conn.SetDeadline(aLongTimeAgo)
c.wg.Wait()
c.conn.Close()
c.conn = nil
}
// close permanently closes c.
func (c *fwdConn) close() {
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
return
}
c.closed = true
c.closeConnLocked()
// Unblock any remaining readers.
c.change.Broadcast()
}

View File

@@ -1,25 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tsdns
import (
"errors"
"syscall"
)
// Avoid allocation when calling errors.Is below
// by converting syscall.Errno to error here.
var (
networkDown error = syscall.ENETDOWN
networkUnreachable error = syscall.ENETUNREACH
)
func networkIsDown(err error) bool {
return errors.Is(err, networkDown)
}
func networkIsUnreachable(err error) bool {
return errors.Is(err, networkUnreachable)
}

Some files were not shown because too many files have changed in this diff Show More