Compare commits
65 Commits
crawshaw/j
...
bradfitz/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63d65368db | ||
|
|
6332bc5e08 | ||
|
|
0e5f2b90a5 | ||
|
|
5041800ac6 | ||
|
|
3e4c46259d | ||
|
|
6ee219a25d | ||
|
|
7616acd118 | ||
|
|
15297a3a09 | ||
|
|
587bdc4280 | ||
|
|
a5103a4cae | ||
|
|
585a0d8997 | ||
|
|
ed5d5f920f | ||
|
|
9784cae23b | ||
|
|
12e28aa87d | ||
|
|
cab3eb995f | ||
|
|
38dda1ea9e | ||
|
|
8051ecff55 | ||
|
|
b5a3850d29 | ||
|
|
e1596d655a | ||
|
|
ce6aca13f0 | ||
|
|
070dfa0c3d | ||
|
|
efb08e4fee | ||
|
|
c8f257df00 | ||
|
|
90b7293b3b | ||
|
|
1fecf87363 | ||
|
|
2b8d2babfa | ||
|
|
e5894aba42 | ||
|
|
4d4ca2e496 | ||
|
|
c493e5804f | ||
|
|
d3701417fc | ||
|
|
c86761cfd1 | ||
|
|
8b94a769be | ||
|
|
94a68a113b | ||
|
|
01098f41d0 | ||
|
|
73cc2d8f89 | ||
|
|
5f807c389e | ||
|
|
bbb56f2303 | ||
|
|
fddbcb0c7b | ||
|
|
0d80904fc2 | ||
|
|
f0ef561049 | ||
|
|
6e8328cba5 | ||
|
|
1fd10061fd | ||
|
|
2d0ed99672 | ||
|
|
7c11f71ac5 | ||
|
|
b7e0ff598a | ||
|
|
a601a760ba | ||
|
|
8893c2ee78 | ||
|
|
fda9dc8815 | ||
|
|
5d8b88be88 | ||
|
|
ec95e901e6 | ||
|
|
3528d28ed1 | ||
|
|
56a787fff8 | ||
|
|
fb03c60c9e | ||
|
|
963b927d5b | ||
|
|
fd77268770 | ||
|
|
5bcac4eaac | ||
|
|
4cc0ed67f9 | ||
|
|
64a24e796b | ||
|
|
afb2be71de | ||
|
|
abe095f036 | ||
|
|
3bdcfa7193 | ||
|
|
f0e9dcdc0a | ||
|
|
904a91038a | ||
|
|
c41947903a | ||
|
|
815bf017fc |
@@ -28,15 +28,6 @@ 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]",
|
||||
@@ -60,7 +51,6 @@ 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)")
|
||||
}
|
||||
@@ -89,7 +79,6 @@ var upArgs struct {
|
||||
forceReauth bool
|
||||
advertiseRoutes string
|
||||
advertiseTags string
|
||||
enableDERP bool
|
||||
snat bool
|
||||
netfilterMode string
|
||||
authKey string
|
||||
@@ -216,7 +205,6 @@ 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
|
||||
if runtime.GOOS == "linux" {
|
||||
switch upArgs.netfilterMode {
|
||||
@@ -242,7 +230,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
|
||||
bc.SetPrefs(prefs)
|
||||
opts := ipn.Options{
|
||||
StateKey: globalStateKey,
|
||||
StateKey: ipn.GlobalDaemonStateKey,
|
||||
AuthKey: upArgs.authKey,
|
||||
Notify: func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
@@ -50,10 +51,10 @@ 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/stun from tailscale.com/net/netcheck+
|
||||
@@ -90,18 +91,26 @@ 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 github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/curve25519 from github.com/tailscale/wireguard-go/wgcfg+
|
||||
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/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 tailscale.com/wgengine/tsdns
|
||||
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/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
|
||||
@@ -110,27 +119,11 @@ 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+
|
||||
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/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+
|
||||
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+
|
||||
@@ -192,7 +185,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 mime/multipart+
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
net/url from crypto/x509+
|
||||
os from crypto/rand+
|
||||
os/exec from github.com/coreos/go-iptables/iptables+
|
||||
@@ -203,7 +196,6 @@ 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+
|
||||
@@ -216,4 +208,3 @@ 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+
|
||||
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
@@ -18,7 +19,6 @@ 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+
|
||||
@@ -58,7 +58,7 @@ 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/netstat from tailscale.com/ipn/ipnserver
|
||||
@@ -75,6 +75,7 @@ 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
|
||||
@@ -98,8 +99,11 @@ 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 github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/curve25519 from github.com/tailscale/wireguard-go/wgcfg+
|
||||
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/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+
|
||||
@@ -107,10 +111,15 @@ 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 tailscale.com/wgengine/tsdns
|
||||
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/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
|
||||
@@ -119,27 +128,11 @@ 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+
|
||||
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/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+
|
||||
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+
|
||||
@@ -180,7 +173,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+
|
||||
L flag from tailscale.com/net/netns
|
||||
flag from tailscale.com/cmd/tailscaled+
|
||||
fmt from compress/flate+
|
||||
hash from compress/zlib+
|
||||
hash/adler32 from compress/zlib
|
||||
@@ -203,7 +196,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 mime/multipart+
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
net/url from crypto/x509+
|
||||
os from crypto/rand+
|
||||
os/exec from github.com/coreos/go-iptables/iptables+
|
||||
@@ -214,7 +207,6 @@ 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
|
||||
@@ -231,4 +223,3 @@ 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+
|
||||
|
||||
@@ -11,6 +11,7 @@ package main // import "tailscale.com/cmd/tailscaled"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
@@ -22,10 +23,10 @@ import (
|
||||
"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/wgengine"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
@@ -71,28 +72,22 @@ func main() {
|
||||
debug.SetGCPercent(10)
|
||||
}
|
||||
|
||||
// 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")
|
||||
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")
|
||||
|
||||
err := fixconsole.FixConsoleIfNeeded()
|
||||
if err != nil {
|
||||
log.Fatalf("fixConsoleOutput: %v", err)
|
||||
}
|
||||
|
||||
getopt.Parse()
|
||||
if len(getopt.Args()) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %#v", getopt.Args()[0])
|
||||
flag.Parse()
|
||||
if flag.NArg() > 0 {
|
||||
log.Fatalf("tailscaled does not take non-flag arguments: %q", flag.Args())
|
||||
}
|
||||
|
||||
if args.statepath == "" {
|
||||
@@ -136,7 +131,7 @@ func run() error {
|
||||
|
||||
var e wgengine.Engine
|
||||
if args.fake {
|
||||
e, err = wgengine.NewFakeUserspaceEngine(logf, 0)
|
||||
e, err = wgengine.NewFakeUserspaceEngine(logf, args.port)
|
||||
} else {
|
||||
e, err = wgengine.NewUserspaceEngine(logf, args.tunname, args.port)
|
||||
}
|
||||
|
||||
@@ -45,8 +45,19 @@ import (
|
||||
)
|
||||
|
||||
type Persist struct {
|
||||
_ structs.Incomparable
|
||||
PrivateMachineKey wgcfg.PrivateKey
|
||||
_ 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"`
|
||||
|
||||
PrivateNodeKey wgcfg.PrivateKey
|
||||
OldPrivateNodeKey wgcfg.PrivateKey // needed to request key rotation
|
||||
Provider string
|
||||
@@ -61,7 +72,7 @@ func (p *Persist) Equals(p2 *Persist) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
return p.PrivateMachineKey.Equal(p2.PrivateMachineKey) &&
|
||||
return p.LegacyFrontendPrivateMachineKey.Equal(p2.LegacyFrontendPrivateMachineKey) &&
|
||||
p.PrivateNodeKey.Equal(p2.PrivateNodeKey) &&
|
||||
p.OldPrivateNodeKey.Equal(p2.OldPrivateNodeKey) &&
|
||||
p.Provider == p2.Provider &&
|
||||
@@ -70,8 +81,8 @@ func (p *Persist) Equals(p2 *Persist) bool {
|
||||
|
||||
func (p *Persist) Pretty() string {
|
||||
var mk, ok, nk wgcfg.Key
|
||||
if !p.PrivateMachineKey.IsZero() {
|
||||
mk = p.PrivateMachineKey.Public()
|
||||
if !p.LegacyFrontendPrivateMachineKey.IsZero() {
|
||||
mk = p.LegacyFrontendPrivateMachineKey.Public()
|
||||
}
|
||||
if !p.OldPrivateNodeKey.IsZero() {
|
||||
ok = p.OldPrivateNodeKey.Public()
|
||||
@@ -79,7 +90,7 @@ func (p *Persist) Pretty() string {
|
||||
if !p.PrivateNodeKey.IsZero() {
|
||||
nk = p.PrivateNodeKey.Public()
|
||||
}
|
||||
return fmt.Sprintf("Persist{m=%v, o=%v, n=%v u=%#v}",
|
||||
return fmt.Sprintf("Persist{lm=%v, o=%v, n=%v u=%#v}",
|
||||
mk.ShortString(), ok.ShortString(), nk.ShortString(),
|
||||
p.LoginName)
|
||||
}
|
||||
@@ -94,6 +105,7 @@ type Direct struct {
|
||||
keepAlive bool
|
||||
logf logger.Logf
|
||||
discoPubKey tailcfg.DiscoKey
|
||||
machinePrivKey wgcfg.PrivateKey
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
serverKey wgcfg.Key
|
||||
@@ -108,16 +120,17 @@ type Direct struct {
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
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)
|
||||
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)
|
||||
}
|
||||
|
||||
type Decompressor interface {
|
||||
@@ -130,6 +143,9 @@ 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 {
|
||||
@@ -158,6 +174,7 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
|
||||
c := &Direct{
|
||||
httpc: httpc,
|
||||
machinePrivKey: opts.MachinePrivateKey,
|
||||
serverURL: opts.ServerURL,
|
||||
timeNow: opts.TimeNow,
|
||||
logf: opts.Logf,
|
||||
@@ -249,16 +266,14 @@ func (c *Direct) TryLogout(ctx context.Context) error {
|
||||
|
||||
// TODO(crawshaw): Tell the server. This node key should be
|
||||
// immediately invalidated.
|
||||
//if c.persist.PrivateNodeKey != (wgcfg.PrivateKey{}) {
|
||||
//if !c.persist.PrivateNodeKey.IsZero() {
|
||||
//}
|
||||
c.persist = Persist{
|
||||
PrivateMachineKey: c.persist.PrivateMachineKey,
|
||||
}
|
||||
c.persist = Persist{}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Direct) TryLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags) (url string, err error) {
|
||||
c.logf("direct.TryLogin(%v, %v)", t != nil, flags)
|
||||
c.logf("direct.TryLogin(token=%v, flags=%v)", t != nil, flags)
|
||||
return c.doLoginOrRegen(ctx, t, flags, false, "")
|
||||
}
|
||||
|
||||
@@ -289,13 +304,8 @@ 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 persist.PrivateMachineKey == (wgcfg.PrivateKey{}) {
|
||||
c.logf("Generating a new machinekey.")
|
||||
mkey, err := wgcfg.NewPrivateKey()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
persist.PrivateMachineKey = mkey
|
||||
if c.machinePrivKey.IsZero() {
|
||||
return false, "", errors.New("controlclient.Direct requires a machine private key")
|
||||
}
|
||||
|
||||
if expired {
|
||||
@@ -322,7 +332,7 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
|
||||
var oldNodeKey wgcfg.Key
|
||||
if url != "" {
|
||||
} else if regen || persist.PrivateNodeKey == (wgcfg.PrivateKey{}) {
|
||||
} else if regen || persist.PrivateNodeKey.IsZero() {
|
||||
c.logf("Generating a new nodekey.")
|
||||
persist.OldPrivateNodeKey = persist.PrivateNodeKey
|
||||
key, err := wgcfg.NewPrivateKey()
|
||||
@@ -335,11 +345,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 != (wgcfg.PrivateKey{}) {
|
||||
if !persist.OldPrivateNodeKey.IsZero() {
|
||||
oldNodeKey = persist.OldPrivateNodeKey.Public()
|
||||
}
|
||||
|
||||
if tryingNewKey == (wgcfg.PrivateKey{}) {
|
||||
if tryingNewKey.IsZero() {
|
||||
log.Fatalf("tryingNewKey is empty, give up")
|
||||
}
|
||||
if backendLogID == "" {
|
||||
@@ -360,13 +370,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, &persist.PrivateMachineKey)
|
||||
bodyData, err := encode(request, &serverKey, &c.machinePrivKey)
|
||||
if err != nil {
|
||||
return regen, url, err
|
||||
}
|
||||
body := bytes.NewReader(bodyData)
|
||||
|
||||
u := fmt.Sprintf("%s/machine/%s", c.serverURL, persist.PrivateMachineKey.Public().HexString())
|
||||
u := fmt.Sprintf("%s/machine/%s", c.serverURL, c.machinePrivKey.Public().HexString())
|
||||
req, err := http.NewRequest("POST", u, body)
|
||||
if err != nil {
|
||||
return regen, url, err
|
||||
@@ -377,11 +387,20 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
if err != nil {
|
||||
return regen, url, fmt.Errorf("register request: %v", err)
|
||||
}
|
||||
c.logf("RegisterReq: returned.")
|
||||
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)))
|
||||
}
|
||||
resp := tailcfg.RegisterResponse{}
|
||||
if err := decode(res, &resp, &serverKey, &persist.PrivateMachineKey); err != nil {
|
||||
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)
|
||||
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 {
|
||||
@@ -493,29 +512,29 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
}
|
||||
|
||||
request := tailcfg.MapRequest{
|
||||
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,
|
||||
Version: 4,
|
||||
IncludeIPv6: true,
|
||||
DeltaPeers: true,
|
||||
KeepAlive: c.keepAlive,
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
DiscoKey: c.discoPubKey,
|
||||
Endpoints: ep,
|
||||
Stream: allowStream,
|
||||
Hostinfo: hostinfo,
|
||||
}
|
||||
if c.newDecompressor != nil {
|
||||
request.Compress = "zstd"
|
||||
}
|
||||
|
||||
bodyData, err := encode(request, &serverKey, &persist.PrivateMachineKey)
|
||||
bodyData, err := encode(request, &serverKey, &c.machinePrivKey)
|
||||
if err != nil {
|
||||
vlogf("netmap: encode: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
machinePubKey := tailcfg.MachineKey(c.machinePrivKey.Public())
|
||||
t0 := time.Now()
|
||||
u := fmt.Sprintf("%s/machine/%s/map", serverURL, persist.PrivateMachineKey.Public().HexString())
|
||||
u := fmt.Sprintf("%s/machine/%s/map", serverURL, machinePubKey.HexString())
|
||||
req, err := http.NewRequest("POST", u, bytes.NewReader(bodyData))
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -533,7 +552,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: %s",
|
||||
return fmt.Errorf("initial fetch failed %d: %.200s",
|
||||
res.StatusCode, strings.TrimSpace(string(msg)))
|
||||
}
|
||||
defer res.Body.Close()
|
||||
@@ -649,6 +668,7 @@ 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,
|
||||
@@ -657,7 +677,6 @@ 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),
|
||||
@@ -721,11 +740,10 @@ 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, &mkey)
|
||||
decrypted, err := decryptMsg(msg, &serverKey, &c.machinePrivKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -777,7 +795,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")
|
||||
return nil, fmt.Errorf("cannot decrypt response (len %d + nonce %d = %d)", len(msg), len(nonce), len(msg)+len(nonce))
|
||||
}
|
||||
return decrypted, nil
|
||||
}
|
||||
@@ -843,25 +861,20 @@ func wgIPToNetaddr(ips []wgcfg.IP) (ret []netaddr.IP) {
|
||||
var Debug = initDebug()
|
||||
|
||||
type debug struct {
|
||||
NetMap bool
|
||||
ProxyDNS bool
|
||||
OnlyDisco bool
|
||||
Disco bool
|
||||
ForceDisco bool // ask control server to not filter out our disco key
|
||||
NetMap bool
|
||||
ProxyDNS bool
|
||||
OnlyDisco bool
|
||||
Disco bool
|
||||
}
|
||||
|
||||
func initDebug() debug {
|
||||
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"),
|
||||
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"),
|
||||
}
|
||||
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 {
|
||||
|
||||
@@ -26,8 +26,11 @@ func init() {
|
||||
func osVersionLinux() string {
|
||||
dist := distro.Get()
|
||||
propFile := "/etc/os-release"
|
||||
if dist == distro.Synology {
|
||||
switch dist {
|
||||
case distro.Synology:
|
||||
propFile = "/etc.defaults/VERSION"
|
||||
case distro.OpenWrt:
|
||||
propFile = "/etc/openwrt_release"
|
||||
}
|
||||
|
||||
m := map[string]string{}
|
||||
@@ -36,7 +39,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
|
||||
})
|
||||
@@ -78,8 +81,11 @@ func osVersionLinux() string {
|
||||
return fmt.Sprintf("%s%s", v, attr)
|
||||
}
|
||||
}
|
||||
if dist == distro.Synology {
|
||||
switch dist {
|
||||
case 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)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ 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
|
||||
@@ -49,7 +50,6 @@ 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
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
func TestPersistEqual(t *testing.T) {
|
||||
persistHandles := []string{"PrivateMachineKey", "PrivateNodeKey", "OldPrivateNodeKey", "Provider", "LoginName"}
|
||||
persistHandles := []string{"LegacyFrontendPrivateMachineKey", "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{PrivateMachineKey: k1},
|
||||
&Persist{PrivateMachineKey: newPrivate()},
|
||||
&Persist{LegacyFrontendPrivateMachineKey: k1},
|
||||
&Persist{LegacyFrontendPrivateMachineKey: newPrivate()},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Persist{PrivateMachineKey: k1},
|
||||
&Persist{PrivateMachineKey: k1},
|
||||
&Persist{LegacyFrontendPrivateMachineKey: k1},
|
||||
&Persist{LegacyFrontendPrivateMachineKey: k1},
|
||||
true,
|
||||
},
|
||||
|
||||
|
||||
13
go.mod
13
go.mod
@@ -9,6 +9,7 @@ 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
|
||||
@@ -21,19 +22,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-20200914232109-e09ee10c1824
|
||||
github.com/tailscale/depaware v0.0.0-20201003033024-5d95aab075be
|
||||
github.com/tailscale/winipcfg-go v0.0.0-20200916205758-decb9ee8e170
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200902185615-1997cf6f9fe4
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201008164108-2c83f43a9859
|
||||
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-20200429183012-4b2356b1ed79
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
|
||||
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-20191216052735-49a3e744a425
|
||||
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d
|
||||
honnef.co/go/tools v0.0.1-2020.1.4
|
||||
inet.af/netaddr v0.0.0-20200810144936-56928fe48a98
|
||||
rsc.io/goversion v1.2.0
|
||||
|
||||
37
go.sum
37
go.sum
@@ -30,6 +30,8 @@ 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=
|
||||
@@ -85,24 +87,21 @@ 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-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/depaware v0.0.0-20201002180857-4fbc64c5c030 h1:KvsY/hGDJ7euXQtoyNgNnXxVyjhWUmxzYWdxUHGMnyM=
|
||||
github.com/tailscale/depaware v0.0.0-20201002180857-4fbc64c5c030/go.mod h1:jissDaJNHiyV2tFdr3QyNEfsZrax/i2yQiSO+CljThI=
|
||||
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/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/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/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=
|
||||
@@ -111,6 +110,7 @@ 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=
|
||||
@@ -119,8 +119,12 @@ golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8U
|
||||
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/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=
|
||||
@@ -128,11 +132,14 @@ 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-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=
|
||||
@@ -140,6 +147,8 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/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/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/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=
|
||||
@@ -148,6 +157,7 @@ golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
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-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=
|
||||
@@ -162,14 +172,21 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
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 h1:07IVqnnzaip3TGyl/cy32V5YP3FguWG4BybYDTBNpm0=
|
||||
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=
|
||||
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=
|
||||
|
||||
@@ -81,14 +81,12 @@ type Notify struct {
|
||||
// shared by several consecutive users. Ideally we would just use the
|
||||
// username of the connected frontend as the StateKey.
|
||||
//
|
||||
// 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).
|
||||
// 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 always the the empty string
|
||||
// * on Linux/etc, it's always "_daemon" (ipn.GlobalDaemonStateKey)
|
||||
type StateKey string
|
||||
|
||||
type Options struct {
|
||||
@@ -97,7 +95,8 @@ 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.
|
||||
// don't persist changes in the backend, except for the machine key
|
||||
// for migration purposes.
|
||||
// - StateKey!="" && Prefs==nil: load the given backend-side
|
||||
// state and use/update that.
|
||||
// - StateKey!="" && Prefs!=nil: like the previous case, but do
|
||||
|
||||
@@ -7,8 +7,6 @@ package ipnserver_test
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -25,11 +23,7 @@ func TestRunMultipleAccepts(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
td, err := ioutil.TempDir("", "TestRunMultipleAccepts")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(td)
|
||||
td := t.TempDir()
|
||||
socketPath := filepath.Join(td, "tailscale.sock")
|
||||
|
||||
logf := func(format string, args ...interface{}) {
|
||||
|
||||
180
ipn/local.go
180
ipn/local.go
@@ -5,9 +5,11 @@
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -61,12 +63,13 @@ type LocalBackend struct {
|
||||
filterHash string
|
||||
|
||||
// The mutex protects the following elements.
|
||||
mu sync.Mutex
|
||||
notify func(Notify)
|
||||
c *controlclient.Client
|
||||
stateKey StateKey
|
||||
prefs *Prefs
|
||||
state State
|
||||
mu sync.Mutex
|
||||
notify func(Notify)
|
||||
c *controlclient.Client
|
||||
stateKey StateKey
|
||||
prefs *Prefs
|
||||
machinePrivKey wgcfg.PrivateKey
|
||||
state State
|
||||
// hostinfo is mutated in-place while mu is held.
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
// netMap is not mutated in-place once set.
|
||||
@@ -76,6 +79,7 @@ type LocalBackend struct {
|
||||
blocked bool
|
||||
authURL string
|
||||
interact int
|
||||
prevIfState *interfaces.State
|
||||
|
||||
// statusLock must be held before calling statusChanged.Wait() or
|
||||
// statusChanged.Broadcast().
|
||||
@@ -116,15 +120,31 @@ 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) {
|
||||
// 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.
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown halts the backend and all its sub-components. The backend
|
||||
@@ -272,17 +292,10 @@ 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)
|
||||
}
|
||||
|
||||
disableDERP := prefs != nil && prefs.DisableDERP
|
||||
if disableDERP {
|
||||
b.e.SetDERPMap(nil)
|
||||
} else {
|
||||
b.e.SetDERPMap(st.NetMap.DERPMap)
|
||||
}
|
||||
b.e.SetDERPMap(st.NetMap.DERPMap)
|
||||
|
||||
b.send(Notify{NetMap: st.NetMap})
|
||||
}
|
||||
@@ -388,6 +401,7 @@ func (b *LocalBackend) Start(opts Options) error {
|
||||
b.notify = opts.Notify
|
||||
b.netMap = nil
|
||||
persist := b.prefs.Persist
|
||||
machinePrivKey := b.machinePrivKey
|
||||
b.mu.Unlock()
|
||||
|
||||
b.updateFilter(nil, nil)
|
||||
@@ -403,15 +417,16 @@ func (b *LocalBackend) Start(opts Options) error {
|
||||
persist = &controlclient.Persist{}
|
||||
}
|
||||
cli, err := controlclient.New(controlclient.Options{
|
||||
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,
|
||||
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,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -637,6 +652,63 @@ func (b *LocalBackend) popBrowserAuthNow() {
|
||||
}
|
||||
}
|
||||
|
||||
// initMachineKeyLocked is called to initialize b.machinePrivKey.
|
||||
//
|
||||
// b.prefs must already be initialized.
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) initMachineKeyLocked() error {
|
||||
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() {
|
||||
b.logf("using frontend-provided legacy machine key")
|
||||
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
|
||||
}
|
||||
|
||||
// loadStateLocked sets b.prefs and b.stateKey based on a complex
|
||||
// combination of key, prefs, and legacyPath. b.mu must be held when
|
||||
// calling.
|
||||
@@ -646,9 +718,16 @@ func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath st
|
||||
}
|
||||
|
||||
if key == "" {
|
||||
// Frontend fully owns the state, we just need to obey it.
|
||||
// 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")
|
||||
b.prefs = prefs.Clone()
|
||||
if err := b.initMachineKeyLocked(); err != nil {
|
||||
return fmt.Errorf("initMachineKeyLocked: %w", err)
|
||||
}
|
||||
b.stateKey = ""
|
||||
return nil
|
||||
}
|
||||
@@ -669,7 +748,9 @@ func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath st
|
||||
if legacyPath != "" {
|
||||
b.prefs, err = LoadPrefs(legacyPath)
|
||||
if err != nil {
|
||||
b.logf("Failed to load legacy prefs: %v", err)
|
||||
if !os.IsNotExist(err) {
|
||||
b.logf("Failed to load legacy prefs: %v", err)
|
||||
}
|
||||
b.prefs = NewPrefs()
|
||||
} else {
|
||||
b.logf("Imported state from relaynode for %q", key)
|
||||
@@ -678,6 +759,9 @@ func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath st
|
||||
b.prefs = NewPrefs()
|
||||
b.logf("Created empty state for %q", key)
|
||||
}
|
||||
if err := b.initMachineKeyLocked(); err != nil {
|
||||
return fmt.Errorf("initMachineKeyLocked: %w", err)
|
||||
}
|
||||
b.stateKey = key
|
||||
return nil
|
||||
}
|
||||
@@ -688,6 +772,9 @@ func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath st
|
||||
return fmt.Errorf("PrefsFromBytes: %v", err)
|
||||
}
|
||||
b.stateKey = key
|
||||
if err := b.initMachineKeyLocked(); err != nil {
|
||||
return fmt.Errorf("initMachineKeyLocked: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -866,11 +953,7 @@ func (b *LocalBackend) SetPrefs(new *Prefs) {
|
||||
|
||||
b.updateFilter(netMap, new)
|
||||
|
||||
turnDERPOff := new.DisableDERP && !old.DisableDERP
|
||||
turnDERPOn := !new.DisableDERP && old.DisableDERP
|
||||
if turnDERPOff {
|
||||
b.e.SetDERPMap(nil)
|
||||
} else if turnDERPOn && netMap != nil {
|
||||
if netMap != nil {
|
||||
b.e.SetDERPMap(netMap.DERPMap)
|
||||
}
|
||||
|
||||
@@ -933,6 +1016,7 @@ func (b *LocalBackend) authReconfig() {
|
||||
blocked := b.blocked
|
||||
uc := b.prefs
|
||||
nm := b.netMap
|
||||
hasPAC := b.prevIfState.HasPAC()
|
||||
b.mu.Unlock()
|
||||
|
||||
if blocked {
|
||||
@@ -957,6 +1041,18 @@ func (b *LocalBackend) authReconfig() {
|
||||
if uc.AllowSingleHosts {
|
||||
flags |= controlclient.AllowSingleHosts
|
||||
}
|
||||
if hasPAC {
|
||||
// TODO(bradfitz): make this policy configurable per
|
||||
// domain, flesh out all the edge cases where subnet
|
||||
// routes might shadow corp HTTP proxies, DNS servers,
|
||||
// domain controllers, etc. For now we just want
|
||||
// Tailscale to stay enabled while laptops roam
|
||||
// between corp & non-corp networks.
|
||||
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 {
|
||||
@@ -1106,6 +1202,7 @@ 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 {
|
||||
@@ -1118,7 +1215,7 @@ func (b *LocalBackend) enterState(newState State) {
|
||||
}
|
||||
|
||||
if bc != nil {
|
||||
bc.SetPaused(newState == Stopped)
|
||||
bc.SetPaused(newState == Stopped || !networkUp)
|
||||
}
|
||||
|
||||
switch newState {
|
||||
@@ -1298,13 +1395,14 @@ func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) {
|
||||
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 {
|
||||
if prefs == nil || machinePrivKey.IsZero() {
|
||||
return
|
||||
}
|
||||
|
||||
mk := prefs.Persist.PrivateMachineKey.Public()
|
||||
mk := machinePrivKey.Public()
|
||||
nk := prefs.Persist.PrivateNodeKey.Public()
|
||||
return tailcfg.MachineKey(mk), tailcfg.NodeKey(nk)
|
||||
}
|
||||
|
||||
12
ipn/prefs.go
12
ipn/prefs.go
@@ -67,9 +67,6 @@ type Prefs struct {
|
||||
// users narrow it down a bit.
|
||||
NotepadURLs bool
|
||||
|
||||
// DisableDERP prevents DERP from being used.
|
||||
DisableDERP bool
|
||||
|
||||
// The following block of options only have an effect on Linux.
|
||||
|
||||
// AdvertiseRoutes specifies CIDR prefixes to advertise into the
|
||||
@@ -109,9 +106,9 @@ func (p *Prefs) Pretty() string {
|
||||
} else {
|
||||
pp = "Persist=nil"
|
||||
}
|
||||
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}",
|
||||
return fmt.Sprintf("Prefs{ra=%v mesh=%v dns=%v want=%v notepad=%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)
|
||||
p.NotepadURLs, p.ShieldsUp, p.AdvertiseRoutes, !p.NoSNAT, p.NetfilterMode, pp)
|
||||
}
|
||||
|
||||
func (p *Prefs) ToBytes() []byte {
|
||||
@@ -137,7 +134,6 @@ 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 &&
|
||||
@@ -228,11 +224,11 @@ func (p *Prefs) Clone() *Prefs {
|
||||
func LoadPrefs(filename string) (*Prefs, error) {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("loading prefs from %q: %v", filename, err)
|
||||
return nil, fmt.Errorf("LoadPrefs open: %w", err) // err includes path
|
||||
}
|
||||
p, err := PrefsFromBytes(data, false)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding prefs in %q: %v", filename, err)
|
||||
return nil, fmt.Errorf("LoadPrefs(%q) decode: %w", filename, err)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
@@ -24,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", "DisableDERP", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"}
|
||||
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "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)
|
||||
|
||||
20
ipn/store.go
20
ipn/store.go
@@ -7,6 +7,7 @@ package ipn
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -19,6 +20,21 @@ 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")
|
||||
)
|
||||
|
||||
// StateStore persists state, and produces it back on request.
|
||||
type StateStore interface {
|
||||
// ReadState returns the bytes associated with ID. Returns (nil,
|
||||
@@ -34,6 +50,8 @@ 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()
|
||||
@@ -67,6 +85,8 @@ 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)
|
||||
|
||||
@@ -356,7 +356,7 @@ func New(collection string) *Policy {
|
||||
newc.PrivateID = logtail.PrivateID{}
|
||||
newc.Collection = collection
|
||||
}
|
||||
if newc.PrivateID == (logtail.PrivateID{}) {
|
||||
if newc.PrivateID.IsZero() {
|
||||
newc.PrivateID, err = logtail.NewPrivateID()
|
||||
if err != nil {
|
||||
log.Fatalf("logpolicy: NewPrivateID() should never fail")
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
@@ -56,19 +55,8 @@ 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) {
|
||||
td, filePrefix := genFilePrefix(t)
|
||||
defer os.RemoveAll(td)
|
||||
|
||||
filePrefix := t.TempDir()
|
||||
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
|
||||
|
||||
f.readEOF(t)
|
||||
@@ -90,8 +78,7 @@ func TestQueue(t *testing.T) {
|
||||
|
||||
func TestRecover(t *testing.T) {
|
||||
t.Run("empty", func(t *testing.T) {
|
||||
td, filePrefix := genFilePrefix(t)
|
||||
defer os.RemoveAll(td)
|
||||
filePrefix := t.TempDir()
|
||||
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
|
||||
f.write(t, "hello")
|
||||
f.read(t, "hello")
|
||||
@@ -104,8 +91,7 @@ func TestRecover(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("cur", func(t *testing.T) {
|
||||
td, filePrefix := genFilePrefix(t)
|
||||
defer os.RemoveAll(td)
|
||||
filePrefix := t.TempDir()
|
||||
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
|
||||
f.write(t, "hello")
|
||||
f.close(t)
|
||||
@@ -123,8 +109,7 @@ func TestRecover(t *testing.T) {
|
||||
filch_test.go:129: r.ReadLine()="hello", want "world"
|
||||
*/
|
||||
|
||||
td, filePrefix := genFilePrefix(t)
|
||||
defer os.RemoveAll(td)
|
||||
filePrefix := t.TempDir()
|
||||
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: false})
|
||||
f.write(t, "hello")
|
||||
f.read(t, "hello")
|
||||
@@ -155,8 +140,7 @@ func TestFilchStderr(t *testing.T) {
|
||||
stderrFD = 2
|
||||
}()
|
||||
|
||||
td, filePrefix := genFilePrefix(t)
|
||||
defer os.RemoveAll(td)
|
||||
filePrefix := t.TempDir()
|
||||
f := newFilchTest(t, filePrefix, Options{ReplaceStderr: true})
|
||||
f.write(t, "hello")
|
||||
if _, err := fmt.Fprintf(pipeW, "filch\n"); err != nil {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
@@ -160,10 +161,11 @@ type State struct {
|
||||
InterfaceUp map[string]bool
|
||||
|
||||
// HaveV6Global is whether this machine has an IPv6 global address
|
||||
// on some interface.
|
||||
// on some non-Tailscale interface that's up.
|
||||
HaveV6Global bool
|
||||
|
||||
// HaveV4 is whether the machine has some non-localhost IPv4 address.
|
||||
// HaveV4 is whether the machine has some non-localhost,
|
||||
// non-link-local IPv4 address on a non-Tailscale interface that's up.
|
||||
HaveV4 bool
|
||||
|
||||
// IsExpensive is whether the current network interface is
|
||||
@@ -173,30 +175,104 @@ 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 name == "Tailscale" || // as it is on Windows
|
||||
strings.HasPrefix(name, "tailscale") { // TODO: use --tun flag value, etc; see TODO in method doc
|
||||
if isTailscaleInterfaceName(name) {
|
||||
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.
|
||||
@@ -206,21 +282,29 @@ 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] = ni.IsUp()
|
||||
s.HaveV6Global = s.HaveV6Global || isGlobalV6(ip)
|
||||
s.HaveV4 = s.HaveV4 || (ip.Is4() && !ip.IsLoopback())
|
||||
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()
|
||||
}
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.DefaultRouteInterface, _ = DefaultRouteInterface()
|
||||
|
||||
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 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()
|
||||
}
|
||||
}
|
||||
|
||||
return s, nil
|
||||
@@ -312,3 +396,15 @@ 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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// +build !linux,!windows
|
||||
|
||||
package interfaces
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ 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 {
|
||||
@@ -46,6 +47,9 @@ 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) {
|
||||
|
||||
@@ -5,11 +5,15 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/tailscale/winipcfg-go"
|
||||
"go4.org/mem"
|
||||
"golang.org/x/sys/windows"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tsconst"
|
||||
"tailscale.com/util/lineread"
|
||||
@@ -17,6 +21,7 @@ import (
|
||||
|
||||
func init() {
|
||||
likelyHomeRouterIP = likelyHomeRouterIPWindows
|
||||
getPAC = getPACWindows
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -77,18 +82,111 @@ 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[uint64]uint32, error) {
|
||||
ifs, err := winipcfg.GetInterfaces()
|
||||
mtus := map[uint64]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[uint64]*winipcfg.Interface, error) {
|
||||
ifs, err := winipcfg.GetInterfacesEx(&winipcfg.GetAdapterAddressesFlags{
|
||||
GAA_FLAG_INCLUDE_ALL_INTERFACES: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := map[uint64]uint32{}
|
||||
ret := map[uint64]*winipcfg.Interface{}
|
||||
for _, iface := range ifs {
|
||||
if iface.Description == tsconst.WintunInterfaceDesc {
|
||||
continue
|
||||
}
|
||||
ret[iface.Luid] = iface.Mtu
|
||||
ret[iface.Luid] = iface
|
||||
}
|
||||
|
||||
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.Interface, error) {
|
||||
ifs, err := NonTailscaleInterfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routes, err := winipcfg.GetRoutes(family)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bestMetric := ^uint32(0)
|
||||
var bestIface *winipcfg.Interface
|
||||
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(winipcfg.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)))
|
||||
return windows.UTF16PtrToString(res)
|
||||
}
|
||||
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 ""
|
||||
}
|
||||
|
||||
17
net/interfaces/interfaces_windows_test.go
Normal file
17
net/interfaces/interfaces_windows_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,17 @@ import (
|
||||
"tailscale.com/net/interfaces"
|
||||
)
|
||||
|
||||
func interfaceIndex(iface *winipcfg.Interface) 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.Index
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -28,21 +39,21 @@ func control(network, address string, c syscall.RawConn) error {
|
||||
}
|
||||
|
||||
if canV4 {
|
||||
if4, err := getDefaultInterface(winipcfg.AF_INET)
|
||||
iface, err := interfaces.GetWindowsDefault(winipcfg.AF_INET)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bindSocket4(c, if4); err != nil {
|
||||
if err := bindSocket4(c, interfaceIndex(iface)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if canV6 {
|
||||
if6, err := getDefaultInterface(winipcfg.AF_INET6)
|
||||
iface, err := interfaces.GetWindowsDefault(winipcfg.AF_INET6)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bindSocket6(c, if6); err != nil {
|
||||
if err := bindSocket6(c, interfaceIndex(iface)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -50,49 +61,27 @@ func control(network, address string, c syscall.RawConn) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
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 v4 the interface index must be passed as a big-endian
|
||||
// integer, regardless of platform endianness.
|
||||
index := nativeToBigEndian(ifidx)
|
||||
// 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)
|
||||
var controlErr error
|
||||
err := c.Control(func(fd uintptr) {
|
||||
controlErr = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, sockoptBoundInterface, int(index))
|
||||
controlErr = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, sockoptBoundInterface, int(indexAsAddr))
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -10,14 +10,45 @@ 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
|
||||
|
||||
@@ -71,11 +71,19 @@ 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
|
||||
const (
|
||||
ERROR_WINHTTP_AUTODETECTION_FAILED = 12180
|
||||
ERROR_WINHTTP_UNABLE_TO_DOWNLOAD_SCRIPT = 12167
|
||||
)
|
||||
if err == syscall.Errno(ERROR_WINHTTP_AUTODETECTION_FAILED) {
|
||||
setNoProxyUntil(10 * time.Second)
|
||||
return nil, nil
|
||||
}
|
||||
log.Printf("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
|
||||
}
|
||||
return nil, err
|
||||
case <-ctx.Done():
|
||||
cachedProxy.Lock()
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
package tailcfg
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig -output=tailcfg_clone.go
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse -output=tailcfg_clone.go
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -27,14 +27,34 @@ 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.
|
||||
@@ -114,7 +134,6 @@ type UserProfile struct {
|
||||
LoginName string // "alice@smith.com"; for display purposes only (provider is not listed)
|
||||
DisplayName string // "Alice Smith"
|
||||
ProfilePicURL string
|
||||
Roles []RoleID
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
@@ -452,11 +471,19 @@ type MapRequest struct {
|
||||
Stream bool // if true, multiple MapResponse objects are returned
|
||||
Hostinfo *Hostinfo
|
||||
|
||||
// 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"`
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// PortRange represents a range of UDP or TCP port numbers.
|
||||
@@ -546,7 +573,6 @@ type MapResponse struct {
|
||||
Domain string
|
||||
PacketFilter []FilterRule
|
||||
UserProfiles []UserProfile
|
||||
Roles []Role
|
||||
// TODO: Groups []Group
|
||||
// TODO: Capabilities []Capability
|
||||
|
||||
@@ -586,6 +612,7 @@ type Debug struct {
|
||||
|
||||
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 {
|
||||
@@ -615,6 +642,9 @@ 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) }
|
||||
|
||||
@@ -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; DO NOT EDIT.
|
||||
// Code generated by tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse; 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
|
||||
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
|
||||
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
|
||||
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
|
||||
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
|
||||
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
|
||||
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
|
||||
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
|
||||
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
|
||||
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
|
||||
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
|
||||
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
|
||||
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
|
||||
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
|
||||
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
|
||||
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
|
||||
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
|
||||
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse
|
||||
var _DNSConfigNeedsRegeneration = DNSConfig(struct {
|
||||
Nameservers []netaddr.IP
|
||||
Domains []string
|
||||
@@ -248,9 +248,31 @@ 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.
|
||||
// where T is one of User,Node,Hostinfo,NetInfo,Group,Role,Capability,Login,DNSConfig,RegisterResponse.
|
||||
func Clone(dst, src interface{}) bool {
|
||||
switch src := src.(type) {
|
||||
case *User:
|
||||
@@ -334,6 +356,15 @@ 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
|
||||
}
|
||||
|
||||
@@ -7,17 +7,12 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLookPathUnixEmptyPath(t *testing.T) {
|
||||
tmp, err := ioutil.TempDir("", "TestLookPathUnixEmptyPath")
|
||||
if err != nil {
|
||||
t.Fatal("TempDir failed: ", err)
|
||||
}
|
||||
defer os.RemoveAll(tmp)
|
||||
tmp := t.TempDir()
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Fatal("Getwd failed: ", err)
|
||||
|
||||
@@ -40,6 +40,12 @@ 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
|
||||
|
||||
|
||||
@@ -58,6 +58,9 @@ func (fn JSONHandlerFunc) ServeHTTPReturn(w http.ResponseWriter, r *http.Request
|
||||
// the client in this handler. We don't want the wrapping
|
||||
// ReturnHandler to do it too.
|
||||
err = werr.Err
|
||||
if werr.Msg != "" {
|
||||
err = fmt.Errorf("%s: %w", werr.Msg, err)
|
||||
}
|
||||
} else {
|
||||
resp = &response{
|
||||
Status: "error",
|
||||
|
||||
@@ -236,8 +236,13 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
case hErrOK:
|
||||
// Handler asked us to send an error. Do so, if we haven't
|
||||
// already sent a response.
|
||||
msg.Err = hErr.Msg
|
||||
if hErr.Err != nil {
|
||||
msg.Err = hErr.Err.Error()
|
||||
if msg.Err == "" {
|
||||
msg.Err = hErr.Err.Error()
|
||||
} else {
|
||||
msg.Err = msg.Err + ": " + hErr.Err.Error()
|
||||
}
|
||||
}
|
||||
if lw.code != 0 {
|
||||
h.logf("[unexpected] handler returned HTTPError %v, but already sent a response with code %d", hErr, lw.code)
|
||||
|
||||
@@ -122,7 +122,7 @@ func TestStdHandler(t *testing.T) {
|
||||
Host: "example.com",
|
||||
Method: "GET",
|
||||
RequestURI: "/foo",
|
||||
Err: testErr.Error(),
|
||||
Err: "not found: " + testErr.Error(),
|
||||
Code: 404,
|
||||
},
|
||||
},
|
||||
@@ -139,6 +139,7 @@ func TestStdHandler(t *testing.T) {
|
||||
Host: "example.com",
|
||||
Method: "GET",
|
||||
RequestURI: "/foo",
|
||||
Err: "not found",
|
||||
Code: 404,
|
||||
},
|
||||
},
|
||||
@@ -189,7 +190,7 @@ func TestStdHandler(t *testing.T) {
|
||||
Host: "example.com",
|
||||
Method: "GET",
|
||||
RequestURI: "/foo",
|
||||
Err: testErr.Error(),
|
||||
Err: "not found: " + testErr.Error(),
|
||||
Code: 200,
|
||||
},
|
||||
},
|
||||
|
||||
41
types/flagtype/flagtype.go
Normal file
41
types/flagtype/flagtype.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// 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 { 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
|
||||
}
|
||||
65
util/uniq/slice.go
Normal file
65
util/uniq/slice.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
88
util/uniq/slice_test.go
Normal file
88
util/uniq/slice_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
// 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] })
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ const (
|
||||
Debian = Distro("debian")
|
||||
Arch = Distro("arch")
|
||||
Synology = Distro("synology")
|
||||
OpenWrt = Distro("openwrt")
|
||||
)
|
||||
|
||||
// Get returns the current distro, or the empty string if unknown.
|
||||
@@ -36,5 +37,8 @@ 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 ""
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
// Package version provides the version that the binary was built at.
|
||||
package version
|
||||
|
||||
const LONG = "date.20200820"
|
||||
const LONG = "date.20200921"
|
||||
const SHORT = LONG
|
||||
|
||||
@@ -5,7 +5,13 @@ read -r short <short.txt
|
||||
# get it into "major.minor.patch" format
|
||||
ver=$(echo "$ver" | sed -e 's/-/./')
|
||||
|
||||
# winres is the MAJOR,MINOR,BUILD,REVISION 4-tuple used to identify
|
||||
# the version of Windows binaries. We always set REVISION to 0, which
|
||||
# seems to be how you map SemVer.
|
||||
winres=$(echo "$short,0" | sed -e 's/\./,/g')
|
||||
|
||||
(
|
||||
printf '#define TAILSCALE_VERSION_LONG "%s"\n' "$long"
|
||||
printf '#define TAILSCALE_VERSION_SHORT "%s"\n' "$short"
|
||||
printf '#define TAILSCALE_VERSION_WIN_RES %s\n' "$winres"
|
||||
) >$3
|
||||
|
||||
@@ -367,6 +367,15 @@ 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.IsLinkLocalUnicast() {
|
||||
f.logRateLimit(rf, q, dir, Drop, "link-local-unicast")
|
||||
return Drop
|
||||
}
|
||||
|
||||
switch q.IPProto {
|
||||
case packet.Unknown:
|
||||
// Unknown packets are dangerous; always drop them.
|
||||
@@ -409,6 +418,9 @@ func omitDropLogging(p *packet.ParsedPacket, dir direction) bool {
|
||||
if ipProto == packet.IGMP {
|
||||
return true
|
||||
}
|
||||
if p.DstIP.IsMulticast() || p.DstIP.IsLinkLocalUnicast() {
|
||||
return true
|
||||
}
|
||||
case 6:
|
||||
if len(b) < 40 {
|
||||
return false
|
||||
|
||||
@@ -379,6 +379,24 @@ 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 {
|
||||
|
||||
@@ -208,6 +208,11 @@ 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
|
||||
@@ -345,6 +350,7 @@ 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
|
||||
}
|
||||
|
||||
@@ -454,7 +460,7 @@ func (c *Conn) updateNetInfo(ctx context.Context) (*netcheck.Report, error) {
|
||||
dm := c.derpMap
|
||||
c.mu.Unlock()
|
||||
|
||||
if dm == nil {
|
||||
if dm == nil || c.networkDown() {
|
||||
return new(netcheck.Report), nil
|
||||
}
|
||||
|
||||
@@ -969,8 +975,15 @@ 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:
|
||||
@@ -1111,6 +1124,10 @@ 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 {
|
||||
@@ -1304,15 +1321,19 @@ func (c *Conn) runDerpReader(ctx context.Context, derpFakeAddr netaddr.IPPort, d
|
||||
|
||||
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
|
||||
@@ -1691,7 +1712,9 @@ func (c *Conn) sendDiscoMessage(dst netaddr.IPPort, dstKey tailcfg.NodeKey, dstD
|
||||
} else if err == nil {
|
||||
// Can't send. (e.g. no IPv6 locally)
|
||||
} else {
|
||||
c.logf("magicsock: disco: failed to send %T to %v: %v", m, dst, err)
|
||||
if !c.networkDown() {
|
||||
c.logf("magicsock: disco: failed to send %T to %v: %v", m, dst, err)
|
||||
}
|
||||
}
|
||||
return sent, err
|
||||
}
|
||||
@@ -1956,6 +1979,21 @@ 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
|
||||
@@ -2282,6 +2320,10 @@ 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 {
|
||||
|
||||
@@ -118,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 // tuntap device to send/receive packets
|
||||
tun *tuntest.ChannelTUN // TUN 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
|
||||
}
|
||||
|
||||
@@ -39,6 +39,14 @@ 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
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"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"
|
||||
@@ -217,7 +218,13 @@ func setPrivateNetwork(ifcGUID *windows.GUID) (bool, error) {
|
||||
func configureInterface(cfg *Config, tun *tun.NativeTun) error {
|
||||
const mtu = 0
|
||||
guid := tun.GUID()
|
||||
iface, err := winipcfg.InterfaceFromGUID(&guid)
|
||||
iface, err := winipcfg.InterfaceFromGUIDEx(&guid, &winipcfg.GetAdapterAddressesFlags{
|
||||
// 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:
|
||||
GAA_FLAG_INCLUDE_ALL_INTERFACES: true,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -242,7 +249,6 @@ 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))
|
||||
@@ -257,6 +263,7 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
|
||||
}
|
||||
}
|
||||
|
||||
var routes []winipcfg.RouteData
|
||||
foundDefault4 := false
|
||||
foundDefault6 := false
|
||||
for _, route := range cfg.Routes {
|
||||
@@ -301,7 +308,7 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
|
||||
routes = append(routes, r)
|
||||
}
|
||||
|
||||
err = iface.SyncAddresses(addresses)
|
||||
err = syncAddresses(iface, addresses)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -321,7 +328,7 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
|
||||
}
|
||||
|
||||
var errAcc error
|
||||
err = iface.SyncRoutes(deduplicatedRoutes)
|
||||
err = syncRoutes(iface, deduplicatedRoutes)
|
||||
if err != nil && errAcc == nil {
|
||||
log.Printf("setroutes: %v", err)
|
||||
errAcc = err
|
||||
@@ -406,3 +413,220 @@ 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
|
||||
}
|
||||
|
||||
// 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.Interface, want []*net.IPNet) error {
|
||||
var erracc error
|
||||
|
||||
got := ifc.UnicastIPNets
|
||||
add, del := deltaNets(got, want)
|
||||
del = excludeIPv6LinkLocal(del)
|
||||
for _, a := range del {
|
||||
err := ifc.DeleteAddress(&a.IP)
|
||||
if err != nil {
|
||||
erracc = err
|
||||
}
|
||||
}
|
||||
|
||||
err := ifc.AddAddresses(add)
|
||||
if err != nil {
|
||||
erracc = err
|
||||
}
|
||||
|
||||
ifc.UnicastIPNets = make([]*net.IPNet, len(want))
|
||||
copy(ifc.UnicastIPNets, want)
|
||||
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
|
||||
}
|
||||
|
||||
// syncRoutes incrementally sets multiples routes on an interface.
|
||||
// This avoids a full ifc.FlushRoutes call.
|
||||
func syncRoutes(ifc *winipcfg.Interface, want []*winipcfg.RouteData) error {
|
||||
routes, err := ifc.GetRoutes(windows.AF_INET)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
got := make([]*winipcfg.RouteData, 0, len(routes))
|
||||
for _, r := range routes {
|
||||
v, err := r.ToRouteData()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
got = append(got, v)
|
||||
}
|
||||
|
||||
add, del := deltaRouteData(got, want)
|
||||
|
||||
var errs []error
|
||||
for _, a := range del {
|
||||
err := ifc.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.AddRoute(a)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("adding route %v: %w", &a.Destination, err))
|
||||
}
|
||||
}
|
||||
|
||||
return multierror.New(errs)
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
winipcfg "github.com/tailscale/winipcfg-go"
|
||||
@@ -95,3 +97,144 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func newUserspaceRouter(logf logger.Logf, tunname string, dev *device.Device, tuntap tun.Device, netChanged func()) Router {
|
||||
return NewFakeRouter(logf, tunname, dev, tuntap, netChanged)
|
||||
func newUserspaceRouter(logf logger.Logf, tunname string, dev *device.Device, tunDev tun.Device, netChanged func()) Router {
|
||||
return NewFakeRouter(logf, tunname, dev, tunDev, netChanged)
|
||||
}
|
||||
|
||||
func cleanup(logf logger.Logf, interfaceName string) {
|
||||
|
||||
@@ -5,11 +5,16 @@
|
||||
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"
|
||||
@@ -79,16 +84,21 @@ 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
|
||||
}
|
||||
|
||||
@@ -103,12 +113,24 @@ func newUserspaceRouter(logf logger.Logf, _ *device.Device, tunDev tun.Device) (
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newUserspaceRouterAdvanced(logf, tunname, ipt4, osCommandRunner{})
|
||||
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)
|
||||
}
|
||||
|
||||
func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netfilter netfilterRunner, cmd commandRunner) (Router, error) {
|
||||
_, err := exec.Command("ip", "rule").Output()
|
||||
ipRuleAvailable := (err == nil)
|
||||
func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netfilter4, netfilter6 netfilterRunner, cmd commandRunner, supportsV6, supportsV6NAT bool) (Router, error) {
|
||||
ipRuleAvailable := (cmd.run("ip", "rule") == nil)
|
||||
|
||||
mconfig := dns.ManagerConfig{
|
||||
Logf: logf,
|
||||
@@ -116,13 +138,18 @@ func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netfilter netf
|
||||
}
|
||||
|
||||
return &linuxRouter{
|
||||
logf: logf,
|
||||
logf: logf,
|
||||
tunname: tunname,
|
||||
netfilterMode: NetfilterOff,
|
||||
|
||||
ipRuleAvailable: ipRuleAvailable,
|
||||
tunname: tunname,
|
||||
netfilterMode: NetfilterOff,
|
||||
ipt4: netfilter,
|
||||
cmd: cmd,
|
||||
dns: dns.NewManager(mconfig),
|
||||
v6Available: supportsV6,
|
||||
v6NATAvailable: supportsV6NAT,
|
||||
|
||||
ipt4: netfilter4,
|
||||
ipt6: netfilter6,
|
||||
cmd: cmd,
|
||||
dns: dns.NewManager(mconfig),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -145,7 +172,7 @@ func (r *linuxRouter) Up() error {
|
||||
|
||||
func (r *linuxRouter) Close() error {
|
||||
if err := r.dns.Down(); err != nil {
|
||||
return fmt.Errorf("dns down: %v", err)
|
||||
return fmt.Errorf("dns down: %w", err)
|
||||
}
|
||||
if err := r.downInterface(); err != nil {
|
||||
return err
|
||||
@@ -165,23 +192,28 @@ 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 {
|
||||
return err
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
newRoutes, err := cidrDiff("route", r.routes, cfg.Routes, r.addRoute, r.delRoute, r.logf)
|
||||
if err != nil {
|
||||
return err
|
||||
errs = append(errs, err)
|
||||
}
|
||||
r.routes = newRoutes
|
||||
|
||||
newAddrs, err := cidrDiff("addr", r.addrs, cfg.LocalAddrs, r.addAddress, r.delAddress, r.logf)
|
||||
if err != nil {
|
||||
return err
|
||||
errs = append(errs, err)
|
||||
}
|
||||
r.addrs = newAddrs
|
||||
|
||||
@@ -190,20 +222,16 @@ func (r *linuxRouter) Set(cfg *Config) error {
|
||||
// state already correct, nothing to do.
|
||||
case cfg.SNATSubnetRoutes:
|
||||
if err := r.addSNATRule(); err != nil {
|
||||
return err
|
||||
errs = append(errs, err)
|
||||
}
|
||||
default:
|
||||
if err := r.delSNATRule(); err != nil {
|
||||
return err
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
r.snatSubnetRoutes = cfg.SNATSubnetRoutes
|
||||
|
||||
if err := r.dns.Set(cfg.DNS); err != nil {
|
||||
return fmt.Errorf("dns set: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
return multierror.New(errs)
|
||||
}
|
||||
|
||||
// setNetfilterMode switches the router to the given netfilter
|
||||
@@ -416,6 +444,13 @@ 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.
|
||||
@@ -432,58 +467,60 @@ func (r *linuxRouter) addIPRules() error {
|
||||
|
||||
rg := newRunGroup(nil, r.cmd)
|
||||
|
||||
// NOTE(apenwarr): We leave spaces between each pref number.
|
||||
// This is so the sysadmin can override by inserting rules in
|
||||
// between if they want.
|
||||
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): 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", "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).
|
||||
// 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).
|
||||
}
|
||||
|
||||
return rg.ErrAcc
|
||||
}
|
||||
@@ -503,73 +540,105 @@ func (r *linuxRouter) delIPRules() error {
|
||||
// unknown rules during deletion.
|
||||
rg := newRunGroup([]int{2, 254}, r.cmd)
|
||||
|
||||
// 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.
|
||||
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.
|
||||
|
||||
// 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 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 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(table, chain string) error {
|
||||
err := r.ipt4.ClearChain(table, chain)
|
||||
create := func(ipt netfilterRunner, table, chain string) error {
|
||||
err := ipt.ClearChain(table, chain)
|
||||
if errCode(err) == 1 {
|
||||
// nonexistent chain. let's create it!
|
||||
return r.ipt4.NewChain(table, chain)
|
||||
return ipt.NewChain(table, chain)
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting up %s/%s: %w", table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := create("filter", "ts-input"); err != 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 {
|
||||
return err
|
||||
}
|
||||
if err := create("filter", "ts-forward"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := create("nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
if r.v6NATAvailable {
|
||||
if err := create(r.ipt6, "nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// addNetfilterBase adds with some basic processing rules to be supplemented
|
||||
// by later calls to other helpers.
|
||||
// addNetfilterBase adds 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.
|
||||
@@ -578,11 +647,11 @@ func (r *linuxRouter) addNetfilterBase() 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 filter/ts-input: %w", args, err)
|
||||
return fmt.Errorf("adding %v in v4/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 filter/ts-input: %w", args, err)
|
||||
return fmt.Errorf("adding %v in v4/filter/ts-input: %w", args, err)
|
||||
}
|
||||
|
||||
// Forward all traffic from the Tailscale interface, and drop
|
||||
@@ -598,19 +667,43 @@ func (r *linuxRouter) addNetfilterBase() 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 filter/ts-forward: %w", args, err)
|
||||
return fmt.Errorf("adding %v in v4/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 filter/ts-forward: %w", args, err)
|
||||
return fmt.Errorf("adding %v in v4/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 filter/ts-forward: %w", args, err)
|
||||
return fmt.Errorf("adding %v in v4/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 filter/ts-forward: %w", args, err)
|
||||
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 nil
|
||||
@@ -618,8 +711,8 @@ func (r *linuxRouter) addNetfilterBase() error {
|
||||
|
||||
// delNetfilterChains removes the custom Tailscale chains from netfilter.
|
||||
func (r *linuxRouter) delNetfilterChains() error {
|
||||
del := func(table, chain string) error {
|
||||
if err := r.ipt4.ClearChain(table, chain); err != nil {
|
||||
del := func(ipt netfilterRunner, table, chain string) error {
|
||||
if err := ipt.ClearChain(table, chain); err != nil {
|
||||
if errCode(err) == 1 {
|
||||
// nonexistent chain. That's fine, since it's
|
||||
// the desired state anyway.
|
||||
@@ -627,7 +720,7 @@ func (r *linuxRouter) delNetfilterChains() error {
|
||||
}
|
||||
return fmt.Errorf("flushing %s/%s: %w", table, chain, err)
|
||||
}
|
||||
if err := r.ipt4.DeleteChain(table, chain); err != nil {
|
||||
if err := ipt.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)
|
||||
@@ -635,14 +728,21 @@ func (r *linuxRouter) delNetfilterChains() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := del("filter", "ts-input"); err != 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 {
|
||||
return err
|
||||
}
|
||||
if err := del("filter", "ts-forward"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := del("nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
if r.v6NATAvailable {
|
||||
if err := del(r.ipt6, "nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -651,8 +751,8 @@ func (r *linuxRouter) delNetfilterChains() error {
|
||||
// delNetfilterBase empties but does not remove custom Tailscale chains from
|
||||
// netfilter.
|
||||
func (r *linuxRouter) delNetfilterBase() error {
|
||||
del := func(table, chain string) error {
|
||||
if err := r.ipt4.ClearChain(table, chain); err != nil {
|
||||
del := func(ipt netfilterRunner, table, chain string) error {
|
||||
if err := ipt.ClearChain(table, chain); err != nil {
|
||||
if errCode(err) == 1 {
|
||||
// nonexistent chain. That's fine, since it's
|
||||
// the desired state anyway.
|
||||
@@ -663,14 +763,21 @@ func (r *linuxRouter) delNetfilterBase() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := del("filter", "ts-input"); err != 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 {
|
||||
return err
|
||||
}
|
||||
if err := del("filter", "ts-forward"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := del("nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
if r.v6NATAvailable {
|
||||
if err := del(r.ipt6, "nat", "ts-postrouting"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -680,31 +787,38 @@ func (r *linuxRouter) delNetfilterBase() error {
|
||||
// the relevant main netfilter chains. The tailscale chains must
|
||||
// already exist.
|
||||
func (r *linuxRouter) addNetfilterHooks() error {
|
||||
divert := func(table, chain string) error {
|
||||
divert := func(ipt netfilterRunner, table, chain string) error {
|
||||
tsChain := tsChain(chain)
|
||||
|
||||
args := []string{"-j", tsChain}
|
||||
exists, err := r.ipt4.Exists(table, chain, args...)
|
||||
exists, err := ipt.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 := r.ipt4.Insert(table, chain, 1, args...); err != nil {
|
||||
if err := ipt.Insert(table, chain, 1, args...); err != nil {
|
||||
return fmt.Errorf("adding %v in %s/%s: %w", args, table, chain, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := divert("filter", "INPUT"); err != 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 {
|
||||
return err
|
||||
}
|
||||
if err := divert("filter", "FORWARD"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := divert("nat", "POSTROUTING"); err != nil {
|
||||
return err
|
||||
if r.v6NATAvailable {
|
||||
if err := divert(r.ipt6, "nat", "POSTROUTING"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -712,10 +826,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(table, chain string) error {
|
||||
del := func(ipt netfilterRunner, table, chain string) error {
|
||||
tsChain := tsChain(chain)
|
||||
args := []string{"-j", tsChain}
|
||||
if err := r.ipt4.Delete(table, chain, args...); err != nil {
|
||||
if err := ipt.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
|
||||
@@ -727,14 +841,21 @@ func (r *linuxRouter) delNetfilterHooks() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := del("filter", "INPUT"); err != 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 {
|
||||
return err
|
||||
}
|
||||
if err := del("filter", "FORWARD"); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := del("nat", "POSTROUTING"); err != nil {
|
||||
return err
|
||||
if r.v6NATAvailable {
|
||||
if err := del(r.ipt6, "nat", "POSTROUTING"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -748,7 +869,12 @@ 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 nat/ts-postrouting: %w", args, err)
|
||||
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 nil
|
||||
}
|
||||
@@ -762,7 +888,12 @@ 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 nat/ts-postrouting: %w", args, err)
|
||||
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 nil
|
||||
}
|
||||
@@ -850,3 +981,47 @@ 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
|
||||
}
|
||||
|
||||
// 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"))
|
||||
}
|
||||
|
||||
@@ -34,10 +34,14 @@ func mustCIDRs(ss ...string) []netaddr.IPPrefix {
|
||||
|
||||
func TestRouterStates(t *testing.T) {
|
||||
basic := `
|
||||
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
|
||||
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
|
||||
`
|
||||
states := []struct {
|
||||
name string
|
||||
@@ -104,17 +108,24 @@ 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 +
|
||||
`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
|
||||
`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
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -129,16 +140,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 +
|
||||
`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
|
||||
`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
|
||||
`,
|
||||
},
|
||||
|
||||
@@ -156,16 +173,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 +
|
||||
`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
|
||||
`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
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -180,16 +203,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 +
|
||||
`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
|
||||
`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
|
||||
`,
|
||||
},
|
||||
|
||||
@@ -205,13 +234,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 +
|
||||
`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
|
||||
`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
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -226,22 +258,28 @@ 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 +
|
||||
`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
|
||||
`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
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
fake := NewFakeOS(t)
|
||||
router, err := newUserspaceRouterAdvanced(t.Logf, "tailscale0", fake, fake)
|
||||
router, err := newUserspaceRouterAdvanced(t.Logf, "tailscale0", fake.netfilter4, fake.netfilter6, fake, true, true)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create router: %v", err)
|
||||
}
|
||||
@@ -275,21 +313,15 @@ nat/POSTROUTING -j ts-postrouting
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
type fakeNetfilter struct {
|
||||
t *testing.T
|
||||
n map[string][]string
|
||||
}
|
||||
|
||||
func NewFakeOS(t *testing.T) *fakeOS {
|
||||
return &fakeOS{
|
||||
func newNetfilter(t *testing.T) *fakeNetfilter {
|
||||
return &fakeNetfilter{
|
||||
t: t,
|
||||
netfilter: map[string][]string{
|
||||
n: map[string][]string{
|
||||
"filter/INPUT": nil,
|
||||
"filter/OUTPUT": nil,
|
||||
"filter/FORWARD": nil,
|
||||
@@ -300,6 +332,118 @@ func NewFakeOS(t *testing.T) *fakeOS {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -323,120 +467,30 @@ func (o *fakeOS) String() string {
|
||||
}
|
||||
|
||||
var chains []string
|
||||
for chain := range o.netfilter {
|
||||
for chain := range o.netfilter4.n {
|
||||
chains = append(chains, chain)
|
||||
}
|
||||
sort.Strings(chains)
|
||||
for _, chain := range chains {
|
||||
for _, rule := range o.netfilter[chain] {
|
||||
fmt.Fprintf(&b, "%s %s\n", chain, rule)
|
||||
for _, rule := range o.netfilter4.n[chain] {
|
||||
fmt.Fprintf(&b, "v4/%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, " "))
|
||||
@@ -446,7 +500,20 @@ 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] {
|
||||
|
||||
@@ -6,10 +6,10 @@ package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
winipcfg "github.com/tailscale/winipcfg-go"
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
@@ -27,7 +27,8 @@ type winRouter struct {
|
||||
dns *dns.Manager
|
||||
|
||||
mu sync.Mutex
|
||||
firewallRuleIP string // the IP rule exists for, or "" if rule doesn't exist
|
||||
firewallRuleIP string // the IP rule exists for, or "" when rule is deleted
|
||||
didRemove bool
|
||||
}
|
||||
|
||||
func newUserspaceRouter(logf logger.Logf, wgdev *device.Device, tundev tun.Device) (Router, error) {
|
||||
@@ -39,7 +40,7 @@ func newUserspaceRouter(logf logger.Logf, wgdev *device.Device, tundev tun.Devic
|
||||
nativeTun := tundev.(*tun.NativeTun)
|
||||
guid := nativeTun.GUID().String()
|
||||
mconfig := dns.ManagerConfig{
|
||||
Logf: logf,
|
||||
Logf: logger.WithPrefix(logf, "dns: "),
|
||||
InterfaceName: guid,
|
||||
}
|
||||
|
||||
@@ -56,10 +57,13 @@ func (r *winRouter) Up() error {
|
||||
r.removeFirewallAcceptRule()
|
||||
|
||||
var err error
|
||||
t0 := time.Now()
|
||||
r.routeChangeCallback, err = monitorDefaultRoutes(r.nativeTun)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err != nil {
|
||||
log.Fatalf("MonitorDefaultRoutes: %v", err)
|
||||
return fmt.Errorf("monitorDefaultRoutes, after %v: %v", d, err)
|
||||
}
|
||||
r.logf("monitorDefaultRoutes done after %v", d)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -69,14 +73,24 @@ func (r *winRouter) Up() error {
|
||||
//
|
||||
// So callers should ignore its error for now.
|
||||
func (r *winRouter) removeFirewallAcceptRule() error {
|
||||
t0 := time.Now()
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.firewallRuleIP == "" && r.didRemove {
|
||||
// Already done.
|
||||
return nil
|
||||
}
|
||||
r.firewallRuleIP = ""
|
||||
r.didRemove = true
|
||||
|
||||
cmd := exec.Command("netsh", "advfirewall", "firewall", "delete", "rule", "name=Tailscale-In", "dir=in")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
return cmd.Run()
|
||||
err := cmd.Run()
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
r.logf("after %v, removed firewall rule (wasPresent=%v)", d, err == nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// addFirewallAcceptRule adds a firewall rule to allow all incoming
|
||||
|
||||
@@ -24,6 +24,8 @@ 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
|
||||
|
||||
@@ -6,8 +6,10 @@ package tsdns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"math/rand"
|
||||
"net"
|
||||
@@ -16,6 +18,8 @@ import (
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -109,7 +113,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 []*net.UDPConn
|
||||
conns []*fwdConn
|
||||
|
||||
mu sync.Mutex
|
||||
// upstreams are the nameserver addresses that should be used for forwarding.
|
||||
@@ -127,24 +131,16 @@ func newForwarder(logf logger.Logf, responses chan Packet) *forwarder {
|
||||
logf: logger.WithPrefix(logf, "forward: "),
|
||||
responses: responses,
|
||||
closed: make(chan struct{}),
|
||||
conns: make([]*net.UDPConn, connCount),
|
||||
conns: make([]*fwdConn, 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, conn := range f.conns {
|
||||
go f.recv(uint16(idx), conn)
|
||||
for idx := range f.conns {
|
||||
f.conns[idx] = newFwdConn(f.logf, idx)
|
||||
go f.recv(f.conns[idx])
|
||||
}
|
||||
go f.cleanMap()
|
||||
|
||||
@@ -161,14 +157,10 @@ func (f *forwarder) Close() {
|
||||
close(f.closed)
|
||||
|
||||
for _, conn := range f.conns {
|
||||
conn.SetDeadline(aLongTimeAgo)
|
||||
conn.close()
|
||||
}
|
||||
|
||||
f.wg.Wait()
|
||||
|
||||
for _, conn := range f.conns {
|
||||
conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (f *forwarder) setUpstreams(upstreams []net.Addr) {
|
||||
@@ -177,31 +169,27 @@ 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]
|
||||
_, 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)
|
||||
}
|
||||
conn.send(packet, dst)
|
||||
}
|
||||
|
||||
func (f *forwarder) recv(connIdx uint16, conn *net.UDPConn) {
|
||||
func (f *forwarder) recv(conn *fwdConn) {
|
||||
defer f.wg.Done()
|
||||
|
||||
for {
|
||||
out := make([]byte, maxResponseBytes)
|
||||
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)
|
||||
}
|
||||
select {
|
||||
case <-f.closed:
|
||||
return
|
||||
default:
|
||||
}
|
||||
out := make([]byte, maxResponseBytes)
|
||||
n := conn.read(out)
|
||||
if n == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if n < headerBytes {
|
||||
f.logf("recv: packet too small (%d bytes)", n)
|
||||
}
|
||||
@@ -285,3 +273,194 @@ 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()
|
||||
}
|
||||
|
||||
25
wgengine/tsdns/neterr_darwin.go
Normal file
25
wgengine/tsdns/neterr_darwin.go
Normal file
@@ -0,0 +1,25 @@
|
||||
// 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)
|
||||
}
|
||||
10
wgengine/tsdns/neterr_other.go
Normal file
10
wgengine/tsdns/neterr_other.go
Normal file
@@ -0,0 +1,10 @@
|
||||
// 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 !darwin,!windows
|
||||
|
||||
package tsdns
|
||||
|
||||
func networkIsDown(err error) bool { return false }
|
||||
func networkIsUnreachable(err error) bool { return false }
|
||||
29
wgengine/tsdns/neterr_windows.go
Normal file
29
wgengine/tsdns/neterr_windows.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// 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 (
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func networkIsDown(err error) bool {
|
||||
if oe, ok := err.(*net.OpError); ok && oe.Op == "write" {
|
||||
if se, ok := oe.Err.(*os.SyscallError); ok {
|
||||
if se.Syscall == "wsasendto" && se.Err == windows.WSAENETUNREACH {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func networkIsUnreachable(err error) bool {
|
||||
// TODO(bradfitz,josharian): something here? what is the
|
||||
// difference between down and unreachable? Add comments.
|
||||
return false
|
||||
}
|
||||
@@ -299,7 +299,7 @@ type response struct {
|
||||
}
|
||||
|
||||
// parseQuery parses the query in given packet into a response struct.
|
||||
func (r *Resolver) parseQuery(query []byte, resp *response) error {
|
||||
func parseQuery(query []byte, resp *response) error {
|
||||
var parser dns.Parser
|
||||
var err error
|
||||
|
||||
@@ -423,6 +423,35 @@ const (
|
||||
rdnsv6Suffix = ".ip6.arpa."
|
||||
)
|
||||
|
||||
// hasRDNSBonjourPrefix reports whether name has a Bonjour Service Prefix..
|
||||
//
|
||||
// https://tools.ietf.org/html/rfc6763 lists
|
||||
// "five special RR names" for Bonjour service discovery:
|
||||
//
|
||||
// b._dns-sd._udp.<domain>.
|
||||
// db._dns-sd._udp.<domain>.
|
||||
// r._dns-sd._udp.<domain>.
|
||||
// dr._dns-sd._udp.<domain>.
|
||||
// lb._dns-sd._udp.<domain>.
|
||||
func hasRDNSBonjourPrefix(s string) bool {
|
||||
// Even the shortest name containing a Bonjour prefix is long,
|
||||
// so check length (cheap) and bail early if possible.
|
||||
if len(s) < len("*._dns-sd._udp.0.0.0.0.in-addr.arpa.") {
|
||||
return false
|
||||
}
|
||||
dot := strings.IndexByte(s, '.')
|
||||
if dot == -1 {
|
||||
return false // shouldn't happen
|
||||
}
|
||||
switch s[:dot] {
|
||||
case "b", "db", "r", "dr", "lb":
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
return strings.HasPrefix(s[dot:], "._dns-sd._udp.")
|
||||
}
|
||||
|
||||
// rawNameToLower converts a raw DNS name to a string, lowercasing it.
|
||||
func rawNameToLower(name []byte) string {
|
||||
var sb strings.Builder
|
||||
@@ -502,9 +531,12 @@ func rdnsNameToIPv6(name string) (ip netaddr.IP, ok bool) {
|
||||
// respondReverse returns a DNS response to a PTR query.
|
||||
// It is assumed that resp.Question is populated by respond before this is called.
|
||||
func (r *Resolver) respondReverse(query []byte, name string, resp *response) ([]byte, error) {
|
||||
if hasRDNSBonjourPrefix(name) {
|
||||
return nil, errNotOurName
|
||||
}
|
||||
|
||||
var ip netaddr.IP
|
||||
var ok bool
|
||||
var err error
|
||||
switch {
|
||||
case strings.HasSuffix(name, rdnsv4Suffix):
|
||||
ip, ok = rdnsNameToIPv4(name)
|
||||
@@ -521,6 +553,7 @@ func (r *Resolver) respondReverse(query []byte, name string, resp *response) ([]
|
||||
return nil, errNotOurName
|
||||
}
|
||||
|
||||
var err error
|
||||
resp.Name, resp.Header.RCode, err = r.ResolveReverse(ip)
|
||||
if err != nil {
|
||||
r.logf("resolving rdns: %v", ip, err)
|
||||
@@ -540,7 +573,7 @@ func (r *Resolver) respond(query []byte) ([]byte, error) {
|
||||
// ParseQuery is sufficiently fast to run on every DNS packet.
|
||||
// This is considerably simpler than extracting the name by hand
|
||||
// to shave off microseconds in case of delegation.
|
||||
err := r.parseQuery(query, resp)
|
||||
err := parseQuery(query, resp)
|
||||
// We will not return this error: it is the sender's fault.
|
||||
if err != nil {
|
||||
r.logf("parsing query: %v", err)
|
||||
@@ -551,7 +584,7 @@ func (r *Resolver) respond(query []byte) ([]byte, error) {
|
||||
name := rawNameToLower(rawName)
|
||||
|
||||
// Always try to handle reverse lookups; delegate inside when not found.
|
||||
// This way, queries for exitent nodes do not leak,
|
||||
// This way, queries for existent nodes do not leak,
|
||||
// but we behave gracefully if non-Tailscale nodes exist in CGNATRange.
|
||||
if resp.Question.Type == dns.TypePTR {
|
||||
return r.respondReverse(query, name, resp)
|
||||
|
||||
@@ -684,6 +684,29 @@ func TestAllocs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrimRDNSBonjourPrefix(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want bool
|
||||
}{
|
||||
{"b._dns-sd._udp.0.10.20.172.in-addr.arpa.", true},
|
||||
{"db._dns-sd._udp.0.10.20.172.in-addr.arpa.", true},
|
||||
{"r._dns-sd._udp.0.10.20.172.in-addr.arpa.", true},
|
||||
{"dr._dns-sd._udp.0.10.20.172.in-addr.arpa.", true},
|
||||
{"lb._dns-sd._udp.0.10.20.172.in-addr.arpa.", true},
|
||||
{"qq._dns-sd._udp.0.10.20.172.in-addr.arpa.", false},
|
||||
{"0.10.20.172.in-addr.arpa.", false},
|
||||
{"i-have-no-dot", false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
got := hasRDNSBonjourPrefix(test.in)
|
||||
if got != test.want {
|
||||
t.Errorf("trimRDNSBonjourPrefix(%q) = %v, want %v", test.in, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFull(b *testing.B) {
|
||||
dnsHandleFunc("test.site.", resolveToIP(testipv4, testipv6, "dns.test.site."))
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
)
|
||||
|
||||
type fakeTUN struct {
|
||||
datachan chan []byte
|
||||
evchan chan tun.Event
|
||||
closechan chan struct{}
|
||||
}
|
||||
@@ -22,7 +21,6 @@ type fakeTUN struct {
|
||||
// It primarily exists for testing.
|
||||
func NewFakeTUN() tun.Device {
|
||||
return &fakeTUN{
|
||||
datachan: make(chan []byte),
|
||||
evchan: make(chan tun.Event),
|
||||
closechan: make(chan struct{}),
|
||||
}
|
||||
@@ -39,22 +37,17 @@ func (t *fakeTUN) Close() error {
|
||||
}
|
||||
|
||||
func (t *fakeTUN) Read(out []byte, offset int) (int, error) {
|
||||
select {
|
||||
case <-t.closechan:
|
||||
return 0, io.EOF
|
||||
case b := <-t.datachan:
|
||||
copy(out[offset:offset+len(b)], b)
|
||||
return len(b), nil
|
||||
}
|
||||
<-t.closechan
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func (t *fakeTUN) Write(b []byte, n int) (int, error) {
|
||||
select {
|
||||
case <-t.closechan:
|
||||
return 0, ErrClosed
|
||||
case t.datachan <- b[n:]:
|
||||
return len(b), nil
|
||||
default:
|
||||
}
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func (t *fakeTUN) Flush() error { return nil }
|
||||
|
||||
@@ -63,8 +63,9 @@ type TUN struct {
|
||||
// tdev is the underlying TUN device.
|
||||
tdev tun.Device
|
||||
|
||||
_ [4]byte // force 64-bit alignment of following field on 32-bit
|
||||
lastActivityAtomic int64 // unix seconds of last send or receive
|
||||
closeOnce sync.Once
|
||||
|
||||
lastActivityAtomic int64 // unix seconds of last send or receive
|
||||
|
||||
destIPActivity atomic.Value // of map[packet.IP]func()
|
||||
|
||||
@@ -140,15 +141,14 @@ func (t *TUN) SetDestIPActivityFuncs(m map[packet.IP]func()) {
|
||||
}
|
||||
|
||||
func (t *TUN) Close() error {
|
||||
select {
|
||||
case <-t.closed:
|
||||
// continue
|
||||
default:
|
||||
var err error
|
||||
t.closeOnce.Do(func() {
|
||||
// Other channels need not be closed: poll will exit gracefully after this.
|
||||
close(t.closed)
|
||||
}
|
||||
|
||||
return t.tdev.Close()
|
||||
err = t.tdev.Close()
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *TUN) Events() chan tun.Event {
|
||||
@@ -376,7 +376,7 @@ func (t *TUN) InjectInboundDirect(buf []byte, offset int) error {
|
||||
}
|
||||
|
||||
// InjectInboundCopy takes a packet without leading space,
|
||||
// reallocates it to conform to the InjectInbondDirect interface
|
||||
// reallocates it to conform to the InjectInboundDirect interface
|
||||
// and calls InjectInboundDirect on it. Injecting a nil packet is a no-op.
|
||||
func (t *TUN) InjectInboundCopy(packet []byte) error {
|
||||
// We duplicate this check from InjectInboundDirect here
|
||||
|
||||
@@ -274,39 +274,13 @@ func TestAllocs(t *testing.T) {
|
||||
ftun, tun := newFakeTUN(t.Logf, false)
|
||||
defer tun.Close()
|
||||
|
||||
go func() {
|
||||
var buf []byte
|
||||
for {
|
||||
select {
|
||||
case <-tun.closed:
|
||||
return
|
||||
case buf = <-ftun.datachan:
|
||||
// continue
|
||||
}
|
||||
|
||||
select {
|
||||
case <-tun.closed:
|
||||
return
|
||||
case ftun.datachan <- buf:
|
||||
// continue
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
buf := []byte{0x00}
|
||||
allocs := testing.AllocsPerRun(100, func() {
|
||||
_, err := tun.Write(buf, 0)
|
||||
_, err := ftun.Write(buf, 0)
|
||||
if err != nil {
|
||||
t.Errorf("write: error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = tun.Read(buf, 0)
|
||||
if err != nil {
|
||||
t.Errorf("read: error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
if allocs > 0 {
|
||||
@@ -318,45 +292,9 @@ func BenchmarkWrite(b *testing.B) {
|
||||
ftun, tun := newFakeTUN(b.Logf, true)
|
||||
defer tun.Close()
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-tun.closed:
|
||||
return
|
||||
case <-ftun.datachan:
|
||||
// continue
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
packet := udp(0x05060708, 0x01020304, 89, 89)
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := tun.Write(packet, 0)
|
||||
if err != nil {
|
||||
b.Errorf("err = %v; want nil", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRead(b *testing.B) {
|
||||
ftun, tun := newFakeTUN(b.Logf, true)
|
||||
defer tun.Close()
|
||||
|
||||
packet := udp(0x05060708, 0x01020304, 89, 89)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-tun.closed:
|
||||
return
|
||||
case ftun.datachan <- packet:
|
||||
// continue
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var buf [128]byte
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := tun.Read(buf[:], 0)
|
||||
_, err := ftun.Write(packet, 0)
|
||||
if err != nil {
|
||||
b.Errorf("err = %v; want nil", err)
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -47,7 +48,7 @@ import (
|
||||
"tailscale.com/wgengine/tstun"
|
||||
)
|
||||
|
||||
// minimalMTU is the MTU we set on tailscale's tuntap
|
||||
// minimalMTU is the MTU we set on tailscale's TUN
|
||||
// interface. wireguard-go defaults to 1420 bytes, which only works if
|
||||
// the "outer" MTU is 1500 bytes. This breaks on DSL connections
|
||||
// (typically 1492 MTU) and on GCE (1460 MTU?!).
|
||||
@@ -140,17 +141,8 @@ type EngineConfig struct {
|
||||
Fake bool
|
||||
}
|
||||
|
||||
type Loggify struct {
|
||||
f logger.Logf
|
||||
}
|
||||
|
||||
func (l *Loggify) Write(b []byte) (int, error) {
|
||||
l.f(string(b))
|
||||
return len(b), nil
|
||||
}
|
||||
|
||||
func NewFakeUserspaceEngine(logf logger.Logf, listenPort uint16) (Engine, error) {
|
||||
logf("Starting userspace wireguard engine (FAKE tuntap device).")
|
||||
logf("Starting userspace wireguard engine (with fake TUN device)")
|
||||
conf := EngineConfig{
|
||||
Logf: logf,
|
||||
TUN: tstun.NewFakeTUN(),
|
||||
@@ -224,7 +216,10 @@ func newUserspaceEngineAdvanced(conf EngineConfig) (_ Engine, reterr error) {
|
||||
}
|
||||
e.tundev.PreFilterOut = e.handleLocalPackets
|
||||
|
||||
mon, err := monitor.New(logf, func() { e.LinkChange(false) })
|
||||
mon, err := monitor.New(logf, func() {
|
||||
e.LinkChange(false)
|
||||
tshttpproxy.InvalidateCache()
|
||||
})
|
||||
if err != nil {
|
||||
e.tundev.Close()
|
||||
return nil, err
|
||||
@@ -251,10 +246,11 @@ func newUserspaceEngineAdvanced(conf EngineConfig) (_ Engine, reterr error) {
|
||||
e.tundev.Close()
|
||||
return nil, fmt.Errorf("wgengine: %v", err)
|
||||
}
|
||||
e.magicConn.SetNetworkUp(e.linkState.AnyInterfaceUp())
|
||||
|
||||
// flags==0 because logf is already nested in another logger.
|
||||
// The outer one can display the preferred log prefixes, etc.
|
||||
dlog := log.New(&Loggify{logf}, "", 0)
|
||||
dlog := logger.StdLogger(logf)
|
||||
logger := device.Logger{
|
||||
Debug: dlog,
|
||||
Info: dlog,
|
||||
@@ -305,6 +301,7 @@ func newUserspaceEngineAdvanced(conf EngineConfig) (_ Engine, reterr error) {
|
||||
}
|
||||
|
||||
// wgdev takes ownership of tundev, will close it when closed.
|
||||
e.logf("Creating wireguard device...")
|
||||
e.wgdev = device.NewDevice(e.tundev, opts)
|
||||
defer func() {
|
||||
if reterr != nil {
|
||||
@@ -314,6 +311,7 @@ func newUserspaceEngineAdvanced(conf EngineConfig) (_ Engine, reterr error) {
|
||||
|
||||
// Pass the underlying tun.(*NativeDevice) to the router:
|
||||
// routers do not Read or Write, but do access native interfaces.
|
||||
e.logf("Creating router...")
|
||||
e.router, err = conf.RouterGen(logf, e.wgdev, e.tundev.Unwrap())
|
||||
if err != nil {
|
||||
e.magicConn.Close()
|
||||
@@ -340,7 +338,9 @@ func newUserspaceEngineAdvanced(conf EngineConfig) (_ Engine, reterr error) {
|
||||
}
|
||||
}()
|
||||
|
||||
e.logf("Bringing wireguard device up...")
|
||||
e.wgdev.Up()
|
||||
e.logf("Bringing router up...")
|
||||
if err := e.router.Up(); err != nil {
|
||||
e.magicConn.Close()
|
||||
e.wgdev.Close()
|
||||
@@ -348,17 +348,24 @@ func newUserspaceEngineAdvanced(conf EngineConfig) (_ Engine, reterr error) {
|
||||
}
|
||||
// TODO(danderson): we should delete this. It's pointless to apply
|
||||
// a no-op settings here.
|
||||
// TODO(bradfitz): counter-point: it tests the router implementation early
|
||||
// to see if any part of it might fail.
|
||||
e.logf("Clearing router settings...")
|
||||
if err := e.router.Set(nil); err != nil {
|
||||
e.magicConn.Close()
|
||||
e.wgdev.Close()
|
||||
return nil, err
|
||||
}
|
||||
e.logf("Starting link monitor...")
|
||||
e.linkMon.Start()
|
||||
e.logf("Starting magicsock...")
|
||||
e.magicConn.Start()
|
||||
|
||||
e.logf("Starting resolver...")
|
||||
e.resolver.Start()
|
||||
go e.pollResolver()
|
||||
|
||||
e.logf("Engine created.")
|
||||
return e, nil
|
||||
}
|
||||
|
||||
@@ -367,10 +374,15 @@ func echoRespondToAll(p *packet.ParsedPacket, t *tstun.TUN) filter.Response {
|
||||
if p.IsEchoRequest() {
|
||||
header := p.ICMPHeader()
|
||||
header.ToResponse()
|
||||
packet := packet.Generate(&header, p.Payload())
|
||||
t.InjectOutbound(packet)
|
||||
// We already handled it, stop.
|
||||
return filter.Drop
|
||||
outp := packet.Generate(&header, p.Payload())
|
||||
t.InjectOutbound(outp)
|
||||
// We already responded to it, but it's not an error.
|
||||
// Proceed with regular delivery. (Since this code is only
|
||||
// used in fake mode, regular delivery just means throwing
|
||||
// it away. If this ever gets run in non-fake mode, you'll
|
||||
// get double responses to pings, which is an indicator you
|
||||
// shouldn't be doing that I guess.)
|
||||
return filter.Accept
|
||||
}
|
||||
return filter.Accept
|
||||
}
|
||||
@@ -1098,6 +1110,7 @@ func (e *userspaceEngine) Close() {
|
||||
e.linkMon.Close()
|
||||
e.router.Close()
|
||||
e.wgdev.Close()
|
||||
e.tundev.Close()
|
||||
|
||||
// Shut down pingers after tundev is closed (by e.wgdev.Close) so the
|
||||
// synchronous close does not get stuck on InjectOutbound.
|
||||
@@ -1132,12 +1145,17 @@ func (e *userspaceEngine) LinkChange(isExpensive bool) {
|
||||
cur.IsExpensive = isExpensive
|
||||
needRebind, linkChangeCallback := e.setLinkState(cur)
|
||||
|
||||
if needRebind {
|
||||
e.logf("LinkChange: major, rebinding. New state: %+v", cur)
|
||||
up := cur.AnyInterfaceUp()
|
||||
if !up {
|
||||
e.logf("LinkChange: all links down; pausing: %v", cur)
|
||||
} else if needRebind {
|
||||
e.logf("LinkChange: major, rebinding. New state: %v", cur)
|
||||
} else {
|
||||
e.logf("LinkChange: minor")
|
||||
}
|
||||
|
||||
e.magicConn.SetNetworkUp(up)
|
||||
|
||||
why := "link-change-minor"
|
||||
if needRebind {
|
||||
why = "link-change-major"
|
||||
@@ -1153,6 +1171,9 @@ func (e *userspaceEngine) SetLinkChangeCallback(cb func(major bool, newState *in
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
e.linkChangeCallback = cb
|
||||
if e.linkState != nil {
|
||||
go cb(false, e.linkState)
|
||||
}
|
||||
}
|
||||
|
||||
func getLinkState() (*interfaces.State, error) {
|
||||
@@ -1264,5 +1285,16 @@ func diagnoseLinuxTUNFailure(logf logger.Logf) {
|
||||
if !bytes.Contains(findOut, kernel) {
|
||||
logf("kernel/drivers/net/tun.ko found on disk, but not for current kernel; are you in middle of a system update and haven't rebooted? found: %s", findOut)
|
||||
}
|
||||
case distro.OpenWrt:
|
||||
out, err := exec.Command("opkg", "list-installed").CombinedOutput()
|
||||
if err != nil {
|
||||
logf("error querying OpenWrt installed packages: %s", out)
|
||||
return
|
||||
}
|
||||
for _, pkg := range []string{"kmod-tun", "ca-bundle"} {
|
||||
if !bytes.Contains(out, []byte(pkg+" - ")) {
|
||||
logf("Missing required package %s; run: opkg install %s", pkg, pkg)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,9 @@ type Engine interface {
|
||||
|
||||
// SetLinkChangeCallback sets the function to call when the
|
||||
// link state changes.
|
||||
// The provided function is run in a new goroutine once upon
|
||||
// initial call (if the engine has a known link state) and
|
||||
// upon any change.
|
||||
SetLinkChangeCallback(func(major bool, newState *interfaces.State))
|
||||
|
||||
// DiscoPublicKey gets the public key used for path discovery
|
||||
|
||||
Reference in New Issue
Block a user