Compare commits
205 Commits
crawshaw/j
...
netstat-un
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6ce1f7f817 | ||
|
|
d9e2edb5ae | ||
|
|
3c508a58cc | ||
|
|
51c8fd1dfc | ||
|
|
ff50ddf1ee | ||
|
|
fc8bc76e58 | ||
|
|
7a01cd27ca | ||
|
|
45d96788b5 | ||
|
|
000347d4cf | ||
|
|
b0526e8284 | ||
|
|
efad55cf86 | ||
|
|
cccdd81441 | ||
|
|
2eb474dd8d | ||
|
|
ce45f4f3ff | ||
|
|
3fdae12f0c | ||
|
|
47380ebcfb | ||
|
|
5062131aad | ||
|
|
2d604b3791 | ||
|
|
04ff3c91ee | ||
|
|
fac2b30eff | ||
|
|
a664aac877 | ||
|
|
a2d78b4d3e | ||
|
|
97e82c6cc0 | ||
|
|
19b0cfe89e | ||
|
|
258b680bc5 | ||
|
|
563d43b2a5 | ||
|
|
b246810377 | ||
|
|
c03543dbe2 | ||
|
|
0050070493 | ||
|
|
f99f6608ff | ||
|
|
a38e28da07 | ||
|
|
c2cc3acbaf | ||
|
|
d7ee3096dd | ||
|
|
9ef39af2f2 | ||
|
|
22bf48f37c | ||
|
|
55b1221db2 | ||
|
|
89894c6930 | ||
|
|
d192bd0f86 | ||
|
|
d21956436a | ||
|
|
450cfedeba | ||
|
|
e7ac9a4b90 | ||
|
|
6e52633c53 | ||
|
|
093431f5dd | ||
|
|
c48253e63b | ||
|
|
7a54910990 | ||
|
|
76d99cf01a | ||
|
|
b950bd60bf | ||
|
|
a8589636a8 | ||
|
|
b3634f020d | ||
|
|
7988f75b87 | ||
|
|
427bf2134f | ||
|
|
19df6a2ee2 | ||
|
|
ebd96bf4a9 | ||
|
|
e9bca0c00b | ||
|
|
b1de2020d7 | ||
|
|
b4e19b95ed | ||
|
|
8f30fa67aa | ||
|
|
3aa68cd397 | ||
|
|
119101962c | ||
|
|
bda53897b5 | ||
|
|
782e07c0ae | ||
|
|
4f4e84236a | ||
|
|
6bcb466096 | ||
|
|
696e160cfc | ||
|
|
946c1edb42 | ||
|
|
fb9f80cd61 | ||
|
|
ed17f5ddae | ||
|
|
39bbb86b09 | ||
|
|
28f6552646 | ||
|
|
1036f51a56 | ||
|
|
07b6ffd55c | ||
|
|
de5da37a22 | ||
|
|
65bad9a8bd | ||
|
|
20a357b386 | ||
|
|
437142daa5 | ||
|
|
710b105f38 | ||
|
|
f3aa08de76 | ||
|
|
cc3259f8d9 | ||
|
|
01ee638cca | ||
|
|
037daad47a | ||
|
|
3b46655dbb | ||
|
|
e98f2c57d6 | ||
|
|
eab6e9ea4e | ||
|
|
68ddf134d7 | ||
|
|
7e1a146e6c | ||
|
|
2b819ab38c | ||
|
|
8b904b1493 | ||
|
|
ff7ddd9d20 | ||
|
|
420838f90e | ||
|
|
508f5c3ae0 | ||
|
|
38bde61b3d | ||
|
|
c64718e9a0 | ||
|
|
09721fede8 | ||
|
|
54e6c3a290 | ||
|
|
a1ccaa9658 | ||
|
|
4a92fc9dc5 | ||
|
|
7ac91c15bd | ||
|
|
fd2a30cd32 | ||
|
|
cd07437ade | ||
|
|
d6ad41dcea | ||
|
|
e72f480d22 | ||
|
|
a3f17b8108 | ||
|
|
999bc93a4d | ||
|
|
66d196326f | ||
|
|
5b1d03f016 | ||
|
|
f33da73a82 | ||
|
|
311899709b | ||
|
|
3d34128171 | ||
|
|
4f55ebf2d9 | ||
|
|
c44e244276 | ||
|
|
9957c45995 | ||
|
|
3909c82f3d | ||
|
|
6b1d2a5630 | ||
|
|
691f1d5c1d | ||
|
|
62d941dc26 | ||
|
|
ac866054c7 | ||
|
|
22024a38c3 | ||
|
|
7c8ca28c74 | ||
|
|
6cc6e251a9 | ||
|
|
86c271caba | ||
|
|
ff0cf6340a | ||
|
|
5c35c35e7f | ||
|
|
c6dbd24f67 | ||
|
|
7a2a3955d3 | ||
|
|
a6c34bdc28 | ||
|
|
0e3048d8e0 | ||
|
|
82f2fdc194 | ||
|
|
1fd9958e9d | ||
|
|
1819f6f8c8 | ||
|
|
105a820622 | ||
|
|
551e1e99e9 | ||
|
|
746f03669c | ||
|
|
2076a50862 | ||
|
|
371f1a9502 | ||
|
|
f2ce64f0c6 | ||
|
|
515866d7c6 | ||
|
|
d027cd81df | ||
|
|
638127530b | ||
|
|
400e89367c | ||
|
|
22c462bd91 | ||
|
|
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 |
52
.github/workflows/windows.yml
vendored
Normal file
52
.github/workflows/windows.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: Windows
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: windows-latest
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.15.x
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Restore Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Test
|
||||
run: go test ./...
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"attachments": [{
|
||||
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
|
||||
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
|
||||
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
|
||||
"color": "danger"
|
||||
}]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
if: failure() && github.event_name == 'push'
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
# Binaries for programs and plugins
|
||||
*~
|
||||
*.tmp
|
||||
*.exe
|
||||
*.dll
|
||||
*.so
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
# $ docker exec tailscaled tailscale status
|
||||
|
||||
|
||||
FROM golang:1.14-alpine AS build-env
|
||||
FROM golang:1.15-alpine AS build-env
|
||||
|
||||
WORKDIR /go/src/tailscale
|
||||
|
||||
|
||||
12
README.md
12
README.md
@@ -30,6 +30,18 @@ wrappers that are not open source.
|
||||
go install tailscale.com/cmd/tailscale{,d}
|
||||
```
|
||||
|
||||
If you're packaging Tailscale for distribution, use `build_dist.sh`
|
||||
instead, to burn commit IDs and version info into the binaries:
|
||||
|
||||
```
|
||||
./build_dist.sh tailscale.com/cmd/tailscale
|
||||
./build_dist.sh tailscale.com/cmd/tailscaled
|
||||
```
|
||||
|
||||
If your distro has conventions that preclude the use of
|
||||
`build_dist.sh`, please do the equivalent of what it does in your
|
||||
distro's way, so that bug reports contain useful version information.
|
||||
|
||||
We only guarantee to support the latest Go release and any Go beta or
|
||||
release candidate builds (currently Go 1.15) in module mode. It might
|
||||
work in earlier Go versions or in GOPATH mode, but we're making no
|
||||
|
||||
1
VERSION.txt
Normal file
1
VERSION.txt
Normal file
@@ -0,0 +1 @@
|
||||
1.3.0
|
||||
16
build_dist.sh
Executable file
16
build_dist.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env sh
|
||||
#
|
||||
# Runs `go build` with flags configured for binary distribution. All
|
||||
# it does differently from `go build` is burn git commit and version
|
||||
# information into the binaries, so that we can track down user
|
||||
# issues.
|
||||
#
|
||||
# If you're packaging Tailscale for a distro, please consider using
|
||||
# this script, or executing equivalent commands in your
|
||||
# distro-specific build system.
|
||||
|
||||
set -eu
|
||||
|
||||
eval $(./version/version.sh)
|
||||
|
||||
exec go build -tags xversion -ldflags "-X tailscale.com/version.Long=${VERSION_LONG} -X tailscale.com/version.Short=${VERSION_SHORT} -X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}" "$@"
|
||||
@@ -33,6 +33,7 @@ var (
|
||||
flagTypes = flag.String("type", "", "comma-separated list of types; required")
|
||||
flagOutput = flag.String("output", "", "output file; required")
|
||||
flagBuildTags = flag.String("tags", "", "compiler build tags to apply")
|
||||
flagCloneFunc = flag.Bool("clonefunc", false, "add a top-level Clone func")
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -98,25 +99,27 @@ func main() {
|
||||
w := func(format string, args ...interface{}) {
|
||||
fmt.Fprintf(buf, format+"\n", args...)
|
||||
}
|
||||
w("// Clone duplicates src into dst and reports whether it succeeded.")
|
||||
w("// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,")
|
||||
w("// where T is one of %s.", *flagTypes)
|
||||
w("func Clone(dst, src interface{}) bool {")
|
||||
w(" switch src := src.(type) {")
|
||||
for _, typeName := range typeNames {
|
||||
w(" case *%s:", typeName)
|
||||
w(" switch dst := dst.(type) {")
|
||||
w(" case *%s:", typeName)
|
||||
w(" *dst = *src.Clone()")
|
||||
w(" return true")
|
||||
w(" case **%s:", typeName)
|
||||
w(" *dst = src.Clone()")
|
||||
w(" return true")
|
||||
w(" }")
|
||||
if *flagCloneFunc {
|
||||
w("// Clone duplicates src into dst and reports whether it succeeded.")
|
||||
w("// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,")
|
||||
w("// where T is one of %s.", *flagTypes)
|
||||
w("func Clone(dst, src interface{}) bool {")
|
||||
w(" switch src := src.(type) {")
|
||||
for _, typeName := range typeNames {
|
||||
w(" case *%s:", typeName)
|
||||
w(" switch dst := dst.(type) {")
|
||||
w(" case *%s:", typeName)
|
||||
w(" *dst = *src.Clone()")
|
||||
w(" return true")
|
||||
w(" case **%s:", typeName)
|
||||
w(" *dst = src.Clone()")
|
||||
w(" return true")
|
||||
w(" }")
|
||||
}
|
||||
w(" }")
|
||||
w(" return false")
|
||||
w("}")
|
||||
}
|
||||
w(" }")
|
||||
w(" return false")
|
||||
w("}")
|
||||
|
||||
contents := new(bytes.Buffer)
|
||||
fmt.Fprintf(contents, header, *flagTypes, pkg.Name)
|
||||
|
||||
@@ -63,7 +63,7 @@ func loadConfig() config {
|
||||
}
|
||||
b, err := ioutil.ReadFile(*configPath)
|
||||
switch {
|
||||
case os.IsNotExist(err):
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
return writeNewConfig()
|
||||
case err != nil:
|
||||
log.Fatal(err)
|
||||
@@ -233,7 +233,7 @@ func debugHandler(s *derp.Server) http.Handler {
|
||||
f("<li><b>Hostname:</b> %v</li>\n", html.EscapeString(*hostname))
|
||||
f("<li><b>Uptime:</b> %v</li>\n", tsweb.Uptime())
|
||||
f("<li><b>Mesh Key:</b> %v</li>\n", s.HasMeshKey())
|
||||
f("<li><b>Version:</b> %v</li>\n", html.EscapeString(version.LONG))
|
||||
f("<li><b>Version:</b> %v</li>\n", html.EscapeString(version.Long))
|
||||
|
||||
f(`<li><a href="/debug/vars">/debug/vars</a> (Go)</li>
|
||||
<li><a href="/debug/varz">/debug/varz</a> (Prometheus)</li>
|
||||
|
||||
@@ -56,7 +56,6 @@ func runDown(ctx context.Context, args []string) error {
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Printf("Notify: %#v", n)
|
||||
})
|
||||
|
||||
bc.RequestStatus()
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/derp/derpmap"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netcheck"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -44,9 +43,7 @@ var netcheckArgs struct {
|
||||
}
|
||||
|
||||
func runNetcheck(ctx context.Context, args []string) error {
|
||||
c := &netcheck.Client{
|
||||
DNSCache: dnscache.Get(),
|
||||
}
|
||||
c := &netcheck.Client{}
|
||||
if netcheckArgs.verbose {
|
||||
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
|
||||
c.Verbose = true
|
||||
|
||||
@@ -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,8 +205,9 @@ func runUp(ctx context.Context, args []string) error {
|
||||
prefs.AdvertiseRoutes = routes
|
||||
prefs.AdvertiseTags = tags
|
||||
prefs.NoSNAT = !upArgs.snat
|
||||
prefs.DisableDERP = !upArgs.enableDERP
|
||||
prefs.Hostname = upArgs.hostname
|
||||
prefs.ForceDaemon = (runtime.GOOS == "windows")
|
||||
|
||||
if runtime.GOOS == "linux" {
|
||||
switch upArgs.netfilterMode {
|
||||
case "on":
|
||||
@@ -241,8 +231,9 @@ func runUp(ctx context.Context, args []string) error {
|
||||
startLoginInteractive := func() { loginOnce.Do(func() { bc.StartLoginInteractive() }) }
|
||||
|
||||
bc.SetPrefs(prefs)
|
||||
|
||||
opts := ipn.Options{
|
||||
StateKey: globalStateKey,
|
||||
StateKey: ipn.GlobalDaemonStateKey,
|
||||
AuthKey: upArgs.authKey,
|
||||
Notify: func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
@@ -270,6 +261,22 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// On Windows, we still run in mostly the "legacy" way that
|
||||
// predated the server's StateStore. That is, we send an empty
|
||||
// StateKey and send the prefs directly. Although the Windows
|
||||
// supports server mode, though, the transition to StateStore
|
||||
// is only half complete. Only server mode uses it, and the
|
||||
// Windows service (~tailscaled) is the one that computes the
|
||||
// StateKey based on the connection idenity. So for now, just
|
||||
// do as the Windows GUI's always done:
|
||||
if runtime.GOOS == "windows" {
|
||||
// The Windows service will set this as needed based
|
||||
// on our connection's identity.
|
||||
opts.StateKey = ""
|
||||
opts.Prefs = prefs
|
||||
}
|
||||
|
||||
// We still have to Start right now because it's the only way to
|
||||
// set up notifications and whatnot. This causes a bunch of churn
|
||||
// every time the CLI touches anything.
|
||||
|
||||
@@ -36,10 +36,11 @@ func runVersion(ctx context.Context, args []string) error {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
if !versionArgs.daemon {
|
||||
fmt.Println(version.LONG)
|
||||
fmt.Println(version.String())
|
||||
return nil
|
||||
}
|
||||
fmt.Printf("Client: %s\n", version.LONG)
|
||||
|
||||
fmt.Printf("Client: %s\n", version.String())
|
||||
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
@@ -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
|
||||
@@ -15,7 +16,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli
|
||||
github.com/peterbourgon/ff/v2/ffcli from tailscale.com/cmd/tailscale/cli
|
||||
W 💣 github.com/tailscale/winipcfg-go from tailscale.com/net/interfaces+
|
||||
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
|
||||
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
|
||||
@@ -30,12 +30,13 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/iphlpapi from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/namespaceapi from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/nci from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/registry from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/registry from github.com/tailscale/wireguard-go/tun/wintun+
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/setupapi from github.com/tailscale/wireguard-go/tun/wintun
|
||||
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/conn+
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
|
||||
💣 go4.org/mem from tailscale.com/control/controlclient+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
@@ -50,12 +51,13 @@ 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/dnscache from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli+
|
||||
💣 tailscale.com/net/netns from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/netns from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/packet from tailscale.com/wgengine+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
@@ -72,8 +74,9 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/opt from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/strbuilder from tailscale.com/wgengine/packet
|
||||
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
W tailscale.com/util/endian from tailscale.com/net/netns
|
||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
|
||||
@@ -81,7 +84,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/wgengine
|
||||
💣 tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/wgengine/packet from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/router from tailscale.com/cmd/tailscale/cli+
|
||||
💣 tailscale.com/wgengine/router/dns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
|
||||
@@ -90,18 +92,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 +120,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 +186,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 +197,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 +209,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,8 +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+
|
||||
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
|
||||
@@ -34,11 +33,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/iphlpapi from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/namespaceapi from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/nci from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/registry from github.com/tailscale/wireguard-go/tun/wintun
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/registry from github.com/tailscale/wireguard-go/tun/wintun+
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun/setupapi from github.com/tailscale/wireguard-go/tun/wintun
|
||||
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/conn+
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
💣 go4.org/mem from tailscale.com/control/controlclient+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr from tailscale.com/control/controlclient+
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
@@ -51,17 +51,19 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/ipn+
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn
|
||||
tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
||||
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/logtail from tailscale.com/logpolicy
|
||||
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
|
||||
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/dnscache from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/ipn+
|
||||
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
|
||||
💣 tailscale.com/net/netns from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/netns from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/net/packet from tailscale.com/wgengine+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
@@ -75,21 +77,23 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
DW tailscale.com/tempfork/osexec from tailscale.com/portlist
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/types/empty from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/types/key from tailscale.com/derp+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/opt from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/strbuilder from tailscale.com/wgengine/packet
|
||||
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
W tailscale.com/util/endian from tailscale.com/net/netns+
|
||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/version from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
tailscale.com/version from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/version/distro from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/wgengine/monitor from tailscale.com/wgengine
|
||||
tailscale.com/wgengine/packet from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/wgengine/router/dns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
|
||||
@@ -98,8 +102,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 +114,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 +131,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 +176,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 +199,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 +210,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 +226,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,8 @@ package main // import "tailscale.com/cmd/tailscaled"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
@@ -18,15 +20,17 @@ import (
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/apenwarr/fixconsole"
|
||||
"github.com/pborman/getopt/v2"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/types/flagtype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
"tailscale.com/wgengine/router"
|
||||
@@ -71,28 +75,29 @@ 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")
|
||||
printVersion := false
|
||||
flag.BoolVar(&args.cleanup, "cleanup", false, "clean up system state and exit")
|
||||
flag.BoolVar(&args.fake, "fake", false, "use userspace fake tunnel+routing instead of kernel TUN interface")
|
||||
flag.StringVar(&args.debug, "debug", "", "listen address ([ip]:port) of optional debug server")
|
||||
flag.StringVar(&args.tunname, "tun", defaultTunName(), "tunnel interface name")
|
||||
flag.Var(flagtype.PortValue(&args.port, magicsock.DefaultPort), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
||||
flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "path of state file")
|
||||
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
|
||||
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
|
||||
|
||||
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 printVersion {
|
||||
fmt.Println(version.String())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if args.statepath == "" {
|
||||
@@ -120,7 +125,10 @@ func run() error {
|
||||
pol.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
logf := wgengine.RusagePrefixLog(log.Printf)
|
||||
var logf logger.Logf = log.Printf
|
||||
if v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_MEMORY")); v {
|
||||
logf = logger.RusagePrefixLog(logf)
|
||||
}
|
||||
logf = logger.RateLimitedFn(logf, 5*time.Second, 5, 100)
|
||||
|
||||
if args.cleanup {
|
||||
@@ -136,7 +144,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)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ After=network-pre.target
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/etc/default/tailscaled
|
||||
ExecStartPre=/usr/sbin/tailscaled --cleanup
|
||||
ExecStart=/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port $PORT $FLAGS
|
||||
ExecStopPost=/usr/sbin/tailscaled --cleanup
|
||||
|
||||
|
||||
@@ -288,7 +288,7 @@ func (c *Client) authRoutine() {
|
||||
// don't send status updates for context errors,
|
||||
// since context cancelation is always on purpose.
|
||||
if ctx.Err() == nil {
|
||||
c.sendStatus("authRoutine1", err, "", nil)
|
||||
c.sendStatus("authRoutine-report", err, "", nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,7 +353,7 @@ func (c *Client) authRoutine() {
|
||||
c.synced = false
|
||||
c.mu.Unlock()
|
||||
|
||||
c.sendStatus("authRoutine2", nil, "", nil)
|
||||
c.sendStatus("authRoutine-wantout", nil, "", nil)
|
||||
bo.BackOff(ctx, nil)
|
||||
} else { // ie. goal.wantLoggedIn
|
||||
c.mu.Lock()
|
||||
@@ -394,7 +394,7 @@ func (c *Client) authRoutine() {
|
||||
c.synced = false
|
||||
c.mu.Unlock()
|
||||
|
||||
c.sendStatus("authRoutine3", err, url, nil)
|
||||
c.sendStatus("authRoutine-url", err, url, nil)
|
||||
bo.BackOff(ctx, err)
|
||||
continue
|
||||
}
|
||||
@@ -406,7 +406,7 @@ func (c *Client) authRoutine() {
|
||||
c.state = StateAuthenticated
|
||||
c.mu.Unlock()
|
||||
|
||||
c.sendStatus("authRoutine4", nil, "", nil)
|
||||
c.sendStatus("authRoutine-success", nil, "", nil)
|
||||
c.cancelMapSafely()
|
||||
bo.BackOff(ctx, nil)
|
||||
}
|
||||
@@ -528,7 +528,7 @@ func (c *Client) mapRoutine() {
|
||||
|
||||
c.logf("mapRoutine: netmap received: %s", state)
|
||||
if stillAuthed {
|
||||
c.sendStatus("mapRoutine2", nil, "", nm)
|
||||
c.sendStatus("mapRoutine-got-netmap", nil, "", nm)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -20,6 +21,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
@@ -34,6 +36,7 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/log/logheap"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
@@ -45,8 +48,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 +75,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 +84,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,9 +93,14 @@ 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}",
|
||||
mk.ShortString(), ok.ShortString(), nk.ShortString(),
|
||||
p.LoginName)
|
||||
ss := func(k wgcfg.Key) string {
|
||||
if k.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return k.ShortString()
|
||||
}
|
||||
return fmt.Sprintf("Persist{lm=%v, o=%v, n=%v u=%#v}",
|
||||
ss(mk), ss(ok), ss(nk), p.LoginName)
|
||||
}
|
||||
|
||||
// Direct is the client that connects to a tailcontrol server for a node.
|
||||
@@ -94,6 +113,8 @@ type Direct struct {
|
||||
keepAlive bool
|
||||
logf logger.Logf
|
||||
discoPubKey tailcfg.DiscoKey
|
||||
machinePrivKey wgcfg.PrivateKey
|
||||
debugFlags []string
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
serverKey wgcfg.Key
|
||||
@@ -102,22 +123,25 @@ type Direct struct {
|
||||
tryingNewKey wgcfg.PrivateKey
|
||||
expiry *time.Time
|
||||
// hostinfo is mutated in-place while mu is held.
|
||||
hostinfo *tailcfg.Hostinfo // always non-nil
|
||||
endpoints []string
|
||||
localPort uint16 // or zero to mean auto
|
||||
hostinfo *tailcfg.Hostinfo // always non-nil
|
||||
endpoints []string
|
||||
everEndpoints bool // whether we've ever had non-empty endpoints
|
||||
localPort uint16 // or zero to mean auto
|
||||
}
|
||||
|
||||
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)
|
||||
DebugFlags []string // debug settings to send to control
|
||||
}
|
||||
|
||||
type Decompressor interface {
|
||||
@@ -130,6 +154,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 {
|
||||
@@ -146,11 +173,15 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
|
||||
httpc := opts.HTTPTestClient
|
||||
if httpc == nil {
|
||||
dnsCache := &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward, // use default cache's forwarder
|
||||
UseLastGood: true,
|
||||
}
|
||||
dialer := netns.NewDialer()
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
tr.DialContext = dialer.DialContext
|
||||
tr.DialContext = dnscache.Dialer(dialer.DialContext, dnsCache)
|
||||
tr.ForceAttemptHTTP2 = true
|
||||
tr.TLSClientConfig = tlsdial.Config(serverURL.Host, tr.TLSClientConfig)
|
||||
httpc = &http.Client{Transport: tr}
|
||||
@@ -158,6 +189,7 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
|
||||
c := &Direct{
|
||||
httpc: httpc,
|
||||
machinePrivKey: opts.MachinePrivateKey,
|
||||
serverURL: opts.ServerURL,
|
||||
timeNow: opts.TimeNow,
|
||||
logf: opts.Logf,
|
||||
@@ -166,6 +198,7 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
persist: opts.Persist,
|
||||
authKey: opts.AuthKey,
|
||||
discoPubKey: opts.DiscoPublicKey,
|
||||
debugFlags: opts.DebugFlags,
|
||||
}
|
||||
if opts.Hostinfo == nil {
|
||||
c.SetHostinfo(NewHostinfo())
|
||||
@@ -184,7 +217,7 @@ func NewHostinfo() *tailcfg.Hostinfo {
|
||||
osv = osVersion()
|
||||
}
|
||||
return &tailcfg.Hostinfo{
|
||||
IPNVersion: version.LONG,
|
||||
IPNVersion: version.Long,
|
||||
Hostname: hostname,
|
||||
OS: version.OS(),
|
||||
OSVersion: osv,
|
||||
@@ -205,6 +238,8 @@ func (c *Direct) SetHostinfo(hi *tailcfg.Hostinfo) bool {
|
||||
return false
|
||||
}
|
||||
c.hostinfo = hi.Clone()
|
||||
j, _ := json.Marshal(c.hostinfo)
|
||||
c.logf("HostInfo: %s", j)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -249,16 +284,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 +322,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 +350,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 +363,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 +388,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 +405,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 {
|
||||
@@ -457,6 +494,9 @@ func (c *Direct) newEndpoints(localPort uint16, endpoints []string) (changed boo
|
||||
c.logf("client.newEndpoints(%v, %v)", localPort, endpoints)
|
||||
c.localPort = localPort
|
||||
c.endpoints = append(c.endpoints[:0], endpoints...)
|
||||
if len(endpoints) > 0 {
|
||||
c.everEndpoints = true
|
||||
}
|
||||
return true // changed
|
||||
}
|
||||
|
||||
@@ -469,6 +509,13 @@ func (c *Direct) SetEndpoints(localPort uint16, endpoints []string) (changed boo
|
||||
return c.newEndpoints(localPort, endpoints)
|
||||
}
|
||||
|
||||
func inTest() bool { return flag.Lookup("test.v") != nil }
|
||||
|
||||
// PollNetMap makes a /map request to download the network map, calling cb with
|
||||
// each new netmap.
|
||||
//
|
||||
// maxPolls is how many network maps to download; common values are 1
|
||||
// or -1 (to keep a long-poll query open to the server).
|
||||
func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkMap)) error {
|
||||
c.mu.Lock()
|
||||
persist := c.persist
|
||||
@@ -478,6 +525,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
backendLogID := hostinfo.BackendLogID
|
||||
localPort := c.localPort
|
||||
ep := append([]string(nil), c.endpoints...)
|
||||
everEndpoints := c.everEndpoints
|
||||
c.mu.Unlock()
|
||||
|
||||
if backendLogID == "" {
|
||||
@@ -485,7 +533,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
}
|
||||
|
||||
allowStream := maxPolls != 1
|
||||
c.logf("PollNetMap: stream=%v :%v %v", maxPolls, localPort, ep)
|
||||
c.logf("PollNetMap: stream=%v :%v ep=%v", allowStream, localPort, ep)
|
||||
|
||||
vlogf := logger.Discard
|
||||
if Debug.NetMap {
|
||||
@@ -493,36 +541,51 @@ 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: 5,
|
||||
KeepAlive: c.keepAlive,
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
DiscoKey: c.discoPubKey,
|
||||
Endpoints: ep,
|
||||
Stream: allowStream,
|
||||
Hostinfo: hostinfo,
|
||||
DebugFlags: c.debugFlags,
|
||||
}
|
||||
if hostinfo != nil && ipForwardingBroken(hostinfo.RoutableIPs) {
|
||||
old := request.DebugFlags
|
||||
request.DebugFlags = append(old[:len(old):len(old)], "warn-ip-forwarding-off")
|
||||
}
|
||||
if c.newDecompressor != nil {
|
||||
request.Compress = "zstd"
|
||||
}
|
||||
// On initial startup before we know our endpoints, set the ReadOnly flag
|
||||
// to tell the control server not to distribute out our (empty) endpoints to peers.
|
||||
// Presumably we'll learn our endpoints in a half second and do another post
|
||||
// with useful results. The first POST just gets us the DERP map which we
|
||||
// need to do the STUN queries to discover our endpoints.
|
||||
// TODO(bradfitz): we skip this optimization in tests, though,
|
||||
// because the e2e tests are currently hyperspecific about the
|
||||
// ordering of things. The e2e tests need love.
|
||||
if len(ep) == 0 && !everEndpoints && !inTest() {
|
||||
request.ReadOnly = true
|
||||
}
|
||||
|
||||
bodyData, err := encode(request, &serverKey, &persist.PrivateMachineKey)
|
||||
bodyData, err := encode(request, &serverKey, &c.machinePrivKey)
|
||||
if err != nil {
|
||||
vlogf("netmap: encode: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
machinePubKey := tailcfg.MachineKey(c.machinePrivKey.Public())
|
||||
t0 := time.Now()
|
||||
u := fmt.Sprintf("%s/machine/%s/map", serverURL, persist.PrivateMachineKey.Public().HexString())
|
||||
req, err := http.NewRequest("POST", u, bytes.NewReader(bodyData))
|
||||
u := fmt.Sprintf("%s/machine/%s/map", serverURL, machinePubKey.HexString())
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", u, bytes.NewReader(bodyData))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
res, err := c.httpc.Do(req)
|
||||
if err != nil {
|
||||
@@ -533,7 +596,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()
|
||||
@@ -572,6 +635,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
}()
|
||||
|
||||
var lastDERPMap *tailcfg.DERPMap
|
||||
var lastUserProfile = map[tailcfg.UserID]tailcfg.UserProfile{}
|
||||
|
||||
// If allowStream, then the server will use an HTTP long poll to
|
||||
// return incremental results. There is always one response right
|
||||
@@ -621,6 +685,9 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
|
||||
undeltaPeers(&resp, previousPeers)
|
||||
previousPeers = cloneNodes(resp.Peers) // defensive/lazy clone, since this escapes to who knows where
|
||||
for _, up := range resp.UserProfiles {
|
||||
lastUserProfile[up.ID] = up
|
||||
}
|
||||
|
||||
if resp.DERPMap != nil {
|
||||
vlogf("netmap: new map contains DERP map")
|
||||
@@ -649,6 +716,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,15 +725,24 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
User: resp.Node.User,
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
|
||||
Domain: resp.Domain,
|
||||
Roles: resp.Roles,
|
||||
DNS: resp.DNSConfig,
|
||||
Hostinfo: resp.Node.Hostinfo,
|
||||
PacketFilter: c.parsePacketFilter(resp.PacketFilter),
|
||||
DERPMap: lastDERPMap,
|
||||
Debug: resp.Debug,
|
||||
}
|
||||
for _, profile := range resp.UserProfiles {
|
||||
nm.UserProfiles[profile.ID] = profile
|
||||
addUserProfile := func(userID tailcfg.UserID) {
|
||||
if _, dup := nm.UserProfiles[userID]; dup {
|
||||
// Already populated it from a previous peer.
|
||||
return
|
||||
}
|
||||
if up, ok := lastUserProfile[userID]; ok {
|
||||
nm.UserProfiles[userID] = up
|
||||
}
|
||||
}
|
||||
addUserProfile(nm.User)
|
||||
for _, peer := range resp.Peers {
|
||||
addUserProfile(peer.User)
|
||||
}
|
||||
if resp.Node.MachineAuthorized {
|
||||
nm.MachineStatus = tailcfg.MachineAuthorized
|
||||
@@ -717,15 +794,16 @@ func decode(res *http.Response, v interface{}, serverKey *wgcfg.Key, mkey *wgcfg
|
||||
return decodeMsg(msg, v, serverKey, mkey)
|
||||
}
|
||||
|
||||
var dumpMapResponse, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_MAPRESPONSE"))
|
||||
var debugMap, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_MAP"))
|
||||
|
||||
var jsonEscapedZero = []byte(`\u0000`)
|
||||
|
||||
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
|
||||
}
|
||||
@@ -743,11 +821,15 @@ func (c *Direct) decodeMsg(msg []byte, v interface{}) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if dumpMapResponse {
|
||||
if debugMap {
|
||||
var buf bytes.Buffer
|
||||
json.Indent(&buf, b, "", " ")
|
||||
log.Printf("MapResponse: %s", buf.Bytes())
|
||||
}
|
||||
|
||||
if bytes.Contains(b, jsonEscapedZero) {
|
||||
log.Printf("[unexpected] zero byte in controlclient.Direct.decodeMsg into %T: %q", v, b)
|
||||
}
|
||||
if err := json.Unmarshal(b, v); err != nil {
|
||||
return fmt.Errorf("response: %v", err)
|
||||
}
|
||||
@@ -760,6 +842,9 @@ func decodeMsg(msg []byte, v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.Priv
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bytes.Contains(decrypted, jsonEscapedZero) {
|
||||
log.Printf("[unexpected] zero byte in controlclient decodeMsg into %T: %q", v, decrypted)
|
||||
}
|
||||
if err := json.Unmarshal(decrypted, v); err != nil {
|
||||
return fmt.Errorf("response: %v", err)
|
||||
}
|
||||
@@ -777,7 +862,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
|
||||
}
|
||||
@@ -787,8 +872,7 @@ func encode(v interface{}, serverKey *wgcfg.Key, mkey *wgcfg.PrivateKey) ([]byte
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
const debugMapRequests = false
|
||||
if debugMapRequests {
|
||||
if debugMap {
|
||||
if _, ok := v.(tailcfg.MapRequest); ok {
|
||||
log.Printf("MapRequest: %s", b)
|
||||
}
|
||||
@@ -843,25 +927,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 {
|
||||
@@ -996,3 +1075,34 @@ func TrimWGConfig() opt.Bool {
|
||||
v, _ := controlTrimWGConfig.Load().(opt.Bool)
|
||||
return v
|
||||
}
|
||||
|
||||
// ipForwardingBroken reports whether the system's IP forwarding is disabled
|
||||
// and will definitely not work for the routes provided.
|
||||
//
|
||||
// It should not return false positives.
|
||||
func ipForwardingBroken(routes []wgcfg.CIDR) bool {
|
||||
if len(routes) == 0 {
|
||||
// Nothing to route, so no need to warn.
|
||||
return false
|
||||
}
|
||||
if runtime.GOOS != "linux" {
|
||||
// We only do subnet routing on Linux for now.
|
||||
// It might work on darwin/macOS when building from source, so
|
||||
// don't return true for other OSes. We can OS-based warnings
|
||||
// already in the admin panel.
|
||||
return false
|
||||
}
|
||||
out, err := ioutil.ReadFile("/proc/sys/net/ipv4/ip_forward")
|
||||
if err != nil {
|
||||
// Try another way.
|
||||
out, err = exec.Command("sysctl", "-n", "net.ipv4.ip_forward").Output()
|
||||
}
|
||||
if err != nil {
|
||||
// Oh well, we tried. This is just for debugging.
|
||||
// We don't want false positives.
|
||||
// TODO: maybe we want a different warning for inability to check?
|
||||
return false
|
||||
}
|
||||
return strings.TrimSpace(string(out)) == "0"
|
||||
// TODO: also check IPv6 if 'routes' contains any IPv6 routes
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
// Parse a backward-compatible FilterRule used by control's wire format,
|
||||
// producing the most current filter.Matches format.
|
||||
func (c *Direct) parsePacketFilter(pf []tailcfg.FilterRule) filter.Matches {
|
||||
// Parse a backward-compatible FilterRule used by control's wire
|
||||
// format, producing the most current filter format.
|
||||
func (c *Direct) parsePacketFilter(pf []tailcfg.FilterRule) []filter.Match {
|
||||
mm, err := filter.MatchesFromFilterRules(pf)
|
||||
if err != nil {
|
||||
c.logf("parsePacketFilter: %s\n", err)
|
||||
|
||||
@@ -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,10 +30,11 @@ 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
|
||||
PacketFilter filter.Matches
|
||||
PacketFilter []filter.Match
|
||||
|
||||
// DERPMap is the last DERP server map received. It's reused
|
||||
// between updates and should not be modified.
|
||||
@@ -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
|
||||
}
|
||||
@@ -75,6 +75,15 @@ func (nm *NetworkMap) Concise() string {
|
||||
func (nm *NetworkMap) printConciseHeader(buf *strings.Builder) {
|
||||
fmt.Fprintf(buf, "netmap: self: %v auth=%v",
|
||||
nm.NodeKey.ShortString(), nm.MachineStatus)
|
||||
login := nm.UserProfiles[nm.User].LoginName
|
||||
if login == "" {
|
||||
if nm.User.IsZero() {
|
||||
login = "?"
|
||||
} else {
|
||||
login = fmt.Sprint(nm.User)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(buf, " u=%s", login)
|
||||
if nm.LocalPort != 0 {
|
||||
fmt.Fprintf(buf, " port=%v", nm.LocalPort)
|
||||
}
|
||||
@@ -92,6 +101,7 @@ func (a *NetworkMap) equalConciseHeader(b *NetworkMap) bool {
|
||||
if a.NodeKey != b.NodeKey ||
|
||||
a.MachineStatus != b.MachineStatus ||
|
||||
a.LocalPort != b.LocalPort ||
|
||||
a.User != b.User ||
|
||||
len(a.Addresses) != len(b.Addresses) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestNetworkMapConcise(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "netmap: self: [AQEBA] auth=machine-unknown []\n [AgICA] D2 : 192.168.0.100:12 192.168.0.100:12354\n [AwMDA] D4 : 10.2.0.100:12 10.1.0.100:12345\n",
|
||||
want: "netmap: self: [AQEBA] auth=machine-unknown u=? []\n [AgICA] D2 : 192.168.0.100:12 192.168.0.100:12354\n [AwMDA] D4 : 10.2.0.100:12 10.1.0.100:12345\n",
|
||||
},
|
||||
{
|
||||
name: "debug_non_nil",
|
||||
@@ -59,7 +59,7 @@ func TestNetworkMapConcise(t *testing.T) {
|
||||
NodeKey: testNodeKey(1),
|
||||
Debug: &tailcfg.Debug{},
|
||||
},
|
||||
want: "netmap: self: [AQEBA] auth=machine-unknown debug={} []\n",
|
||||
want: "netmap: self: [AQEBA] auth=machine-unknown u=? debug={} []\n",
|
||||
},
|
||||
{
|
||||
name: "debug_values",
|
||||
@@ -67,7 +67,7 @@ func TestNetworkMapConcise(t *testing.T) {
|
||||
NodeKey: testNodeKey(1),
|
||||
Debug: &tailcfg.Debug{LogHeapPprof: true},
|
||||
},
|
||||
want: "netmap: self: [AQEBA] auth=machine-unknown debug={\"LogHeapPprof\":true} []\n",
|
||||
want: "netmap: self: [AQEBA] auth=machine-unknown u=? debug={\"LogHeapPprof\":true} []\n",
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -135,7 +135,7 @@ func TestConciseDiffFrom(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "-netmap: self: [AQEBA] auth=machine-unknown []\n+netmap: self: [AgICA] auth=machine-unknown []\n",
|
||||
want: "-netmap: self: [AQEBA] auth=machine-unknown u=? []\n+netmap: self: [AgICA] auth=machine-unknown u=? []\n",
|
||||
},
|
||||
{
|
||||
name: "peer_add",
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
|
||||
@@ -1291,7 +1291,7 @@ func (s *Server) ExpVar() expvar.Var {
|
||||
m.Set("multiforwarder_deleted", &s.multiForwarderDeleted)
|
||||
m.Set("packet_forwarder_delete_other_value", &s.removePktForwardOther)
|
||||
var expvarVersion expvar.String
|
||||
expvarVersion.Set(version.LONG)
|
||||
expvarVersion.Set(version.Long)
|
||||
m.Set("version", &expvarVersion)
|
||||
return m
|
||||
}
|
||||
|
||||
17
go.mod
17
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/winipcfg-go v0.0.0-20200916205758-decb9ee8e170
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200902185615-1997cf6f9fe4
|
||||
github.com/tailscale/depaware v0.0.0-20201003033024-5d95aab075be
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201021041318-a6168fd06b3f
|
||||
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-20201112155050-0c6587e931a9
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
|
||||
golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
|
||||
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd
|
||||
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
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8
|
||||
honnef.co/go/tools v0.0.1-2020.1.4
|
||||
inet.af/netaddr v0.0.0-20200810144936-56928fe48a98
|
||||
rsc.io/goversion v1.2.0
|
||||
|
||||
109
go.sum
109
go.sum
@@ -1,13 +1,14 @@
|
||||
cloud.google.com/go v0.0.0-20170206221025-ce650573d812/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/GoogleCloudPlatform/cloudsql-proxy v0.0.0-20190129172621-c8b1d7a94ddf/go.mod h1:aJ4qN3TfrelA6NZ6AXsXRfmEVaYin3EDbSPJrKS8OXo=
|
||||
github.com/Masterminds/semver/v3 v3.0.3 h1:znjIyLfpXEDQjOIEWh+ehwpTU14UzUPub3c3sm36u14=
|
||||
github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI=
|
||||
github.com/aclements/go-gg v0.0.0-20170118225347-6dbb4e4fefb0/go.mod h1:55qNq4vcpkIuHowELi5C8e+1yUHtoLoOUR9QU5j7Tes=
|
||||
github.com/aclements/go-moremath v0.0.0-20161014184102-0ff62e0875ff/go.mod h1:idZL3yvz4kzx1dsBOAC+oYv6L92P1oFEhUXUB1A/lwQ=
|
||||
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 h1:P5U+E4x5OkVEKQDklVPmzs71WM56RTTRqV4OrDC//Y4=
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro=
|
||||
@@ -30,21 +31,31 @@ 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/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
|
||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/gonum/blas v0.0.0-20181208220705-f22b278b28ac/go.mod h1:P32wAyui1PQ58Oce/KYkOqQv8cVw1zAapXOl+dRFGbc=
|
||||
github.com/gonum/floats v0.0.0-20181209220543-c233463c7e82/go.mod h1:PxC8OnwL11+aosOB5+iEPoV3picfs8tUpkVd0pDo+Kg=
|
||||
github.com/gonum/internal v0.0.0-20181124074243-f884aa714029/go.mod h1:Pu4dmpkhSyOzRwuXkOgAvijx4o+4YMUJJo9OvPYMkks=
|
||||
github.com/gonum/lapack v0.0.0-20181123203213-e4cdc5a0bff9/go.mod h1:XA3DeT6rxh2EAE789SSiSJNqxPaC0aE9J8NTOI0Jo/A=
|
||||
github.com/gonum/matrix v0.0.0-20181209220409-c518dec07be9/go.mod h1:0EXg4mc1CNP0HCqCz+K4ts155PXIlUywf0wqN+GfPZw=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0 h1:BW6OvS3kpT5UEPbCZ+KyX/OB4Ks9/MNMhWjqPPkZxsE=
|
||||
github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg=
|
||||
github.com/googleapis/gax-go v0.0.0-20161107002406-da06d194a00e/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/goreleaser/nfpm v1.1.10 h1:0nwzKUJTcygNxTzVKq2Dh9wpVP1W2biUH6SNKmoxR3w=
|
||||
github.com/goreleaser/nfpm v1.1.10/go.mod h1:oOcoGRVwvKIODz57NUfiRwFWGfn00NXdgnn6MrYtO5k=
|
||||
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
|
||||
@@ -61,6 +72,11 @@ github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lxn/walk v0.0.0-20191128110447-55ccb3a9f5c1/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/walk v0.0.0-20201110160827-18ea5e372cdb/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20191128105842-2da648fda5b4/go.mod h1:ouWl4wViUNh8tPSIwxTVMuS014WakR1hqvBc2I0bMoA=
|
||||
github.com/lxn/win v0.0.0-20201111105847-2a20daff6a55/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/mattn/go-sqlite3 v0.0.0-20161215041557-2d44decb4941/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mattn/go-zglob v0.0.1 h1:xsEx/XUoVlI6yXjqBK062zYhRTZltCNmYPx6v+8DNaY=
|
||||
github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
|
||||
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
|
||||
@@ -85,24 +101,19 @@ 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/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/depaware v0.0.0-20201003033024-5d95aab075be h1:ZKe3kVGbu/goUVxXcaCPbQ4b0STQ5NsCpG90CG6mw/c=
|
||||
github.com/tailscale/depaware v0.0.0-20201003033024-5d95aab075be/go.mod h1:jissDaJNHiyV2tFdr3QyNEfsZrax/i2yQiSO+CljThI=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200921221757-11a958a67bdd h1:yEWpro9EdxGgkt24NInVnONIJxRLURH5c37Ki5+06EE=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200921221757-11a958a67bdd/go.mod h1:WXq+IkSOJGIgfF1XW+4z4oW+LX/TXzU9DcKlT5EZLi4=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201008164108-2c83f43a9859 h1:Z7bXXCYRg/8sjSyKTk0V8Yso/gQjNvPb10DBemKuz+A=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201008164108-2c83f43a9859/go.mod h1:WXq+IkSOJGIgfF1XW+4z4oW+LX/TXzU9DcKlT5EZLi4=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201021041318-a6168fd06b3f h1:KMx58dbn2YCutzOvjNHgmvbwQH7nGE8H+J42Nenjl/M=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201021041318-a6168fd06b3f/go.mod h1:WXq+IkSOJGIgfF1XW+4z4oW+LX/TXzU9DcKlT5EZLi4=
|
||||
github.com/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,16 +122,25 @@ github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
|
||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go4.org/mem v0.0.0-20200706164138-185c595c3ecc h1:paujszgN6SpsO/UsXC7xax3gQAKz/XQKCYZLQdU34Tw=
|
||||
go4.org/mem v0.0.0-20200706164138-185c595c3ecc/go.mod h1:NEYvpHWemiG/E5UWfaN5QAIGZeT1sa0Z2UNk6oeMb/k=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88=
|
||||
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9 h1:umElSU9WZirRdgu2yFHY0ayQkEnKiOC1TtM3fWXFnoU=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/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,50 +148,79 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 h1:WQ8q63x+f/zpC8Ac1s9wLElVoHhm32p6tudrU72n1QA=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/oauth2 v0.0.0-20170207211851-4464e7848382/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
||||
golang.org/x/perf v0.0.0-20200918155509-d949658356f9 h1:yVBHF5pcQLKR9B+y+dOJ6y68nqJBDWaZ9DhB1Ohg0qE=
|
||||
golang.org/x/perf v0.0.0-20200918155509-d949658356f9/go.mod h1:FrqOtQDO3iMDVUtw5nNTDFpR1HUCGh00M3kj2wiSzLQ=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-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=
|
||||
golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 h1:5B6i6EAiSYyejWfvc5Rc9BbI3rzIsrrXfAQBWnYfn+w=
|
||||
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d h1:QQrM/CCYEzTs91GZylDCQjGHudbPTxF/1fvXdVh5lMo=
|
||||
golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd h1:5CtCZbICpIOFdgO940moixOPjc0178IU44m4EjOO5IY=
|
||||
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d h1:/iIZNFGxc/a7C3yWjGcnboV+Tkc7mxr+p6fDztwoxuM=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20201001230009-b5b87423c93b/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d h1:vWQvJ/Z0Lu+9/8oQ/pAYXNzbc7CMnBl+tULGVHOy3oE=
|
||||
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20200715051853-507f148e1c42 h1:SrR1hmxGKKarHEEDvaHxatwnqE3uT+7jvMcin6SHOkw=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20200715051853-507f148e1c42/go.mod h1:GJvYs5O24/ASlwPiRklVnjMx2xQzrOic0DuU6GvYJL4=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20201111175144-60b3766b89b9 h1:qowcZ56hhpeoESmWzI4Exhx4Y78TpCyXUJur4/c0CoE=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20201111175144-60b3766b89b9/go.mod h1:LMeNfjlcPZTrBC1juwgbQyA4Zy2XVcsrdO/fIJxwyuA=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201004085714-dd60d0447f81 h1:cT2oWlz8v9g7bjFZclT362akxJJfGv9d7ccKu6GQUbA=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201004085714-dd60d0447f81/go.mod h1:GaK5zcgr5XE98WaRzIDilumDBp5/yP8j2kG/LCDnvAM=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8 h1:nlXPqGA98n+qcq1pwZ28KjM5EsFQvamKS00A+VUeVjs=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8/go.mod h1:psva4yDnAHLuh7lUzOK7J7bLYxNFfo0iKWz+mi9gzkA=
|
||||
google.golang.org/api v0.0.0-20170206182103-3d017632ea10/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
|
||||
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=
|
||||
google.golang.org/grpc v0.0.0-20170208002647-2a6bf6142e96/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
|
||||
@@ -21,6 +21,7 @@ type State int
|
||||
|
||||
const (
|
||||
NoState = State(iota)
|
||||
InUseOtherUser
|
||||
NeedsLogin
|
||||
NeedsMachineAuth
|
||||
Stopped
|
||||
@@ -33,8 +34,14 @@ const (
|
||||
const GoogleIDTokenType = "ts_android_google_login"
|
||||
|
||||
func (s State) String() string {
|
||||
return [...]string{"NoState", "NeedsLogin", "NeedsMachineAuth",
|
||||
"Stopped", "Starting", "Running"}[s]
|
||||
return [...]string{
|
||||
"NoState",
|
||||
"InUseOtherUser",
|
||||
"NeedsLogin",
|
||||
"NeedsMachineAuth",
|
||||
"Stopped",
|
||||
"Starting",
|
||||
"Running"}[s]
|
||||
}
|
||||
|
||||
// EngineStatus contains WireGuard engine stats.
|
||||
@@ -53,7 +60,7 @@ type EngineStatus struct {
|
||||
type Notify struct {
|
||||
_ structs.Incomparable
|
||||
Version string // version number of IPN backend
|
||||
ErrMessage *string // critical error message, if any
|
||||
ErrMessage *string // critical error message, if any; for InUseOtherUser, the details
|
||||
LoginFinished *empty.Message // event: non-nil when login process succeeded
|
||||
State *State // current IPN state has changed
|
||||
Prefs *Prefs // preferences were changed
|
||||
@@ -81,14 +88,14 @@ 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 the empty string (in client mode) or, via
|
||||
// LocalBackend.userID, a string like "user-$USER_ID" (used in
|
||||
// server mode).
|
||||
// * on Linux/etc, it's always "_daemon" (ipn.GlobalDaemonStateKey)
|
||||
type StateKey string
|
||||
|
||||
type Options struct {
|
||||
@@ -97,7 +104,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,9 +7,10 @@ package ipnserver
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -18,6 +19,7 @@ import (
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -25,6 +27,7 @@ import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/log/filelogger"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/netstat"
|
||||
"tailscale.com/safesocket"
|
||||
@@ -68,6 +71,12 @@ type Options struct {
|
||||
// its existing state, and accepts new frontend connections. If
|
||||
// false, the server dumps its state and becomes idle.
|
||||
//
|
||||
// This is effectively whether the platform is in "server
|
||||
// mode" by default. On Linux, it's true; on Windows, it's
|
||||
// false. But on some platforms (currently only Windows), the
|
||||
// "server mode" can be overridden at runtime with a change in
|
||||
// Prefs.ForceDaemon/WantRunning.
|
||||
//
|
||||
// To support CLI connections (notably, "tailscale status"),
|
||||
// the actual definition of "disconnect" is when the
|
||||
// connection count transitions from 1 to 0.
|
||||
@@ -81,30 +90,183 @@ type Options struct {
|
||||
// server is an IPN backend and its set of 0 or more active connections
|
||||
// talking to an IPN backend.
|
||||
type server struct {
|
||||
resetOnZero bool // call bs.Reset on transition from 1->0 connections
|
||||
b *ipn.LocalBackend
|
||||
logf logger.Logf
|
||||
// resetOnZero is whether to call bs.Reset on transition from
|
||||
// 1->0 connections. That is, this is whether the backend is
|
||||
// being run in "client mode" that requires an active GUI
|
||||
// connection (such as on Windows by default). Even if this
|
||||
// is true, the ForceDaemon pref can override this.
|
||||
resetOnZero bool
|
||||
|
||||
bsMu sync.Mutex // lock order: bsMu, then mu
|
||||
bs *ipn.BackendServer
|
||||
|
||||
mu sync.Mutex
|
||||
clients map[net.Conn]bool
|
||||
mu sync.Mutex
|
||||
serverModeUser *user.User // or nil if not in server mode
|
||||
lastUserID string // tracks last userid; on change, Reset state for paranoia
|
||||
allClients map[net.Conn]connIdentity // HTTP or IPN
|
||||
clients map[net.Conn]bool // subset of allClients; only IPN protocol
|
||||
disconnectSub map[chan<- struct{}]struct{} // keys are subscribers of disconnects
|
||||
}
|
||||
|
||||
// connIdentity represents the owner of a localhost TCP connection.
|
||||
type connIdentity struct {
|
||||
Unknown bool
|
||||
Pid int
|
||||
UserID string
|
||||
User *user.User
|
||||
}
|
||||
|
||||
// getConnIdentity returns the localhost TCP connection's identity information
|
||||
// (pid, userid, user). If it's not Windows (for now), it returns a nil error
|
||||
// and a ConnIdentity with Unknown set true. It's only an error if we expected
|
||||
// to be able to map it and couldn't.
|
||||
func (s *server) getConnIdentity(c net.Conn) (ci connIdentity, err error) {
|
||||
if runtime.GOOS != "windows" { // for now; TODO: expand to other OSes
|
||||
return connIdentity{Unknown: true}, nil
|
||||
}
|
||||
la, err := netaddr.ParseIPPort(c.LocalAddr().String())
|
||||
if err != nil {
|
||||
return ci, fmt.Errorf("parsing local address: %w", err)
|
||||
}
|
||||
ra, err := netaddr.ParseIPPort(c.RemoteAddr().String())
|
||||
if err != nil {
|
||||
return ci, fmt.Errorf("parsing local remote: %w", err)
|
||||
}
|
||||
if !la.IP.IsLoopback() || !ra.IP.IsLoopback() {
|
||||
return ci, errors.New("non-loopback connection")
|
||||
}
|
||||
tab, err := netstat.Get()
|
||||
if err != nil {
|
||||
return ci, fmt.Errorf("failed to get local connection table: %w", err)
|
||||
}
|
||||
pid := peerPid(tab.Entries, la, ra)
|
||||
if pid == 0 {
|
||||
return ci, errors.New("no local process found matching localhost connection")
|
||||
}
|
||||
ci.Pid = pid
|
||||
uid, err := pidowner.OwnerOfPID(pid)
|
||||
if err != nil {
|
||||
var hint string
|
||||
if runtime.GOOS == "windows" {
|
||||
hint = " (WSL?)"
|
||||
}
|
||||
return ci, fmt.Errorf("failed to map connection's pid to a user%s: %w", hint, err)
|
||||
}
|
||||
ci.UserID = uid
|
||||
u, err := s.lookupUserFromID(uid)
|
||||
if err != nil {
|
||||
return ci, fmt.Errorf("failed to look up user from userid: %w", err)
|
||||
}
|
||||
ci.User = u
|
||||
return ci, nil
|
||||
}
|
||||
|
||||
func (s *server) lookupUserFromID(uid string) (*user.User, error) {
|
||||
u, err := user.LookupId(uid)
|
||||
if err != nil && runtime.GOOS == "windows" && errors.Is(err, syscall.Errno(0x534)) {
|
||||
s.logf("[warning] issue 869: os/user.LookupId failed; ignoring")
|
||||
// Work around https://github.com/tailscale/tailscale/issues/869 for
|
||||
// now. We don't strictly need the username. It's just a nice-to-have.
|
||||
// So make up a *user.User if their machine is broken in this way.
|
||||
return &user.User{
|
||||
Uid: uid,
|
||||
Username: "unknown-user-" + uid,
|
||||
Name: "unknown user " + uid,
|
||||
}, nil
|
||||
}
|
||||
return u, err
|
||||
}
|
||||
|
||||
// blockWhileInUse blocks while until either a Read from conn fails
|
||||
// (i.e. it's closed) or until the server is able to accept ci as a
|
||||
// user.
|
||||
func (s *server) blockWhileInUse(conn io.Reader, ci connIdentity) {
|
||||
s.logf("blocking client while server in use; connIdentity=%v", ci)
|
||||
connDone := make(chan struct{})
|
||||
go func() {
|
||||
io.Copy(ioutil.Discard, conn)
|
||||
close(connDone)
|
||||
}()
|
||||
ch := make(chan struct{}, 1)
|
||||
s.registerDisconnectSub(ch, true)
|
||||
defer s.registerDisconnectSub(ch, false)
|
||||
for {
|
||||
select {
|
||||
case <-connDone:
|
||||
s.logf("blocked client Read completed; connIdentity=%v", ci)
|
||||
return
|
||||
case <-ch:
|
||||
s.mu.Lock()
|
||||
err := s.checkConnIdentityLocked(ci)
|
||||
s.mu.Unlock()
|
||||
if err == nil {
|
||||
s.logf("unblocking client, server is free; connIdentity=%v", ci)
|
||||
// Server is now available again for a new user.
|
||||
// TODO(bradfitz): keep this connection alive. But for
|
||||
// now just return and have our caller close the connection
|
||||
// (which unblocks the io.Copy goroutine we started above)
|
||||
// and then the client (e.g. Windows) will reconnect and
|
||||
// discover that it works.
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
br := bufio.NewReader(c)
|
||||
|
||||
// First see if it's an HTTP request.
|
||||
br := bufio.NewReader(c)
|
||||
c.SetReadDeadline(time.Now().Add(time.Second))
|
||||
peek, _ := br.Peek(4)
|
||||
c.SetReadDeadline(time.Time{})
|
||||
if string(peek) == "GET " {
|
||||
http.Serve(&oneConnListener{altReaderNetConn{br, c}}, localhostHandler(c))
|
||||
isHTTPReq := string(peek) == "GET "
|
||||
|
||||
ci, err := s.addConn(c, isHTTPReq)
|
||||
if err != nil {
|
||||
if isHTTPReq {
|
||||
fmt.Fprintf(c, "HTTP/1.0 500 Nope\r\nContent-Type: text/plain\r\nX-Content-Type-Options: nosniff\r\n\r\n%s\n", err.Error())
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
|
||||
bs := ipn.NewBackendServer(logf, nil, serverToClient)
|
||||
_, occupied := err.(inUseOtherUserError)
|
||||
if occupied {
|
||||
bs.SendInUseOtherUserErrorMessage(err.Error())
|
||||
s.blockWhileInUse(c, ci)
|
||||
} else {
|
||||
bs.SendErrorMessage(err.Error())
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Tell the LocalBackend about the identity we're now running as.
|
||||
s.b.SetCurrentUserID(ci.UserID)
|
||||
|
||||
if isHTTPReq {
|
||||
httpServer := http.Server{
|
||||
// Localhost connections are cheap; so only do
|
||||
// keep-alives for a short period of time, as these
|
||||
// active connections lock the server into only serving
|
||||
// that user. If the user has this page open, we don't
|
||||
// want another switching user to be locked out for
|
||||
// minutes. 5 seconds is enough to let browser hit
|
||||
// favicon.ico and such.
|
||||
IdleTimeout: 5 * time.Second,
|
||||
ErrorLog: logger.StdLogger(logf),
|
||||
Handler: s.localhostHandler(ci),
|
||||
}
|
||||
httpServer.Serve(&oneConnListener{&protoSwitchConn{s: s, br: br, Conn: c}})
|
||||
return
|
||||
}
|
||||
|
||||
s.addConn(c)
|
||||
logf("incoming control connection")
|
||||
defer s.removeAndCloseConn(c)
|
||||
logf("incoming control connection")
|
||||
|
||||
for ctx.Err() == nil {
|
||||
msg, err := ipn.ReadMsg(br)
|
||||
if err != nil {
|
||||
@@ -125,25 +287,126 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) addConn(c net.Conn) {
|
||||
// inUseOtherUserError is the error type for when the server is in use
|
||||
// by a different local user.
|
||||
type inUseOtherUserError struct{ error }
|
||||
|
||||
func (e inUseOtherUserError) Unwrap() error { return e.error }
|
||||
|
||||
// checkConnIdentityLocked checks whether the provided identity is
|
||||
// allowed to connect to the server.
|
||||
//
|
||||
// The returned error, when non-nil, will be of type inUseOtherUserError.
|
||||
//
|
||||
// s.mu must be held.
|
||||
func (s *server) checkConnIdentityLocked(ci connIdentity) error {
|
||||
// If clients are already connected, verify they're the same user.
|
||||
// This mostly matters on Windows at the moment.
|
||||
if len(s.allClients) > 0 {
|
||||
var active connIdentity
|
||||
for _, active = range s.allClients {
|
||||
break
|
||||
}
|
||||
if ci.UserID != active.UserID {
|
||||
//lint:ignore ST1005 we want to capitalize Tailscale here
|
||||
return inUseOtherUserError{fmt.Errorf("Tailscale already in use by %s, pid %d", active.User.Username, active.Pid)}
|
||||
}
|
||||
}
|
||||
if su := s.serverModeUser; su != nil && ci.UserID != su.Uid {
|
||||
//lint:ignore ST1005 we want to capitalize Tailscale here
|
||||
return inUseOtherUserError{fmt.Errorf("Tailscale already in use by %s", su.Username)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// registerDisconnectSub adds ch as a subscribe to connection disconnect
|
||||
// events. If add is false, the subscriber is removed.
|
||||
func (s *server) registerDisconnectSub(ch chan<- struct{}, add bool) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if add {
|
||||
if s.disconnectSub == nil {
|
||||
s.disconnectSub = make(map[chan<- struct{}]struct{})
|
||||
}
|
||||
s.disconnectSub[ch] = struct{}{}
|
||||
} else {
|
||||
delete(s.disconnectSub, ch)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// addConn adds c to the server's list of clients.
|
||||
//
|
||||
// If the returned error is of type inUseOtherUserError then the
|
||||
// returned connIdentity is also valid.
|
||||
func (s *server) addConn(c net.Conn, isHTTP bool) (ci connIdentity, err error) {
|
||||
ci, err = s.getConnIdentity(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// If the connected user changes, reset the backend server state to make
|
||||
// sure node keys don't leak between users.
|
||||
var doReset bool
|
||||
defer func() {
|
||||
if doReset {
|
||||
s.logf("identity changed; resetting server")
|
||||
s.bsMu.Lock()
|
||||
s.bs.Reset()
|
||||
s.bsMu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if s.clients == nil {
|
||||
s.clients = map[net.Conn]bool{}
|
||||
}
|
||||
s.clients[c] = true
|
||||
if s.allClients == nil {
|
||||
s.allClients = map[net.Conn]connIdentity{}
|
||||
}
|
||||
|
||||
if err := s.checkConnIdentityLocked(ci); err != nil {
|
||||
return ci, err
|
||||
}
|
||||
|
||||
if !isHTTP {
|
||||
s.clients[c] = true
|
||||
}
|
||||
s.allClients[c] = ci
|
||||
|
||||
if s.lastUserID != ci.UserID {
|
||||
if s.lastUserID != "" {
|
||||
doReset = true
|
||||
}
|
||||
s.lastUserID = ci.UserID
|
||||
}
|
||||
return ci, nil
|
||||
}
|
||||
|
||||
func (s *server) removeAndCloseConn(c net.Conn) {
|
||||
s.mu.Lock()
|
||||
delete(s.clients, c)
|
||||
remain := len(s.clients)
|
||||
delete(s.allClients, c)
|
||||
remain := len(s.allClients)
|
||||
for sub := range s.disconnectSub {
|
||||
select {
|
||||
case sub <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
if remain == 0 && s.resetOnZero {
|
||||
s.bsMu.Lock()
|
||||
s.bs.Reset()
|
||||
s.bsMu.Unlock()
|
||||
if s.b.InServerMode() {
|
||||
s.logf("client disconnected; staying alive in server mode")
|
||||
} else {
|
||||
s.logf("client disconnected; stopping server")
|
||||
s.bsMu.Lock()
|
||||
s.bs.Reset()
|
||||
s.bsMu.Unlock()
|
||||
}
|
||||
}
|
||||
c.Close()
|
||||
}
|
||||
@@ -158,9 +421,48 @@ func (s *server) stopAll() {
|
||||
s.clients = nil
|
||||
}
|
||||
|
||||
// setServerModeUserLocked is called when we're in server mode but our s.serverModeUser is nil.
|
||||
//
|
||||
// s.mu must be held
|
||||
func (s *server) setServerModeUserLocked() {
|
||||
var ci connIdentity
|
||||
var ok bool
|
||||
for _, ci = range s.allClients {
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
if !ok {
|
||||
s.logf("ipnserver: [unexpected] now in server mode, but no connected client")
|
||||
return
|
||||
}
|
||||
if ci.Unknown {
|
||||
return
|
||||
}
|
||||
if ci.User != nil {
|
||||
s.logf("ipnserver: now in server mode; user=%v", ci.User.Username)
|
||||
s.serverModeUser = ci.User
|
||||
} else {
|
||||
s.logf("ipnserver: [unexpected] now in server mode, but nil User")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) writeToClients(b []byte) {
|
||||
inServerMode := s.b.InServerMode()
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if inServerMode {
|
||||
if s.serverModeUser == nil {
|
||||
s.setServerModeUserLocked()
|
||||
}
|
||||
} else {
|
||||
if s.serverModeUser != nil {
|
||||
s.logf("ipnserver: no longer in server mode")
|
||||
s.serverModeUser = nil
|
||||
}
|
||||
}
|
||||
|
||||
for c := range s.clients {
|
||||
ipn.WriteMsg(c, b)
|
||||
}
|
||||
@@ -178,6 +480,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
}
|
||||
|
||||
server := &server{
|
||||
logf: logf,
|
||||
resetOnZero: !opts.SurviveDisconnects,
|
||||
}
|
||||
|
||||
@@ -194,12 +497,11 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
logf("Listening on %v", listen.Addr())
|
||||
|
||||
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
|
||||
|
||||
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
|
||||
|
||||
eng, err := getEngine()
|
||||
if err != nil {
|
||||
logf("Initial getEngine call: %v", err)
|
||||
logf("ipnserver: initial getEngine call: %v", err)
|
||||
for i := 1; ctx.Err() == nil; i++ {
|
||||
c, err := listen.Accept()
|
||||
if err != nil {
|
||||
@@ -207,14 +509,14 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
bo.BackOff(ctx, err)
|
||||
continue
|
||||
}
|
||||
logf("%d: trying getEngine again...", i)
|
||||
logf("ipnserver: try%d: trying getEngine again...", i)
|
||||
eng, err = getEngine()
|
||||
if err == nil {
|
||||
logf("%d: GetEngine worked; exiting failure loop", i)
|
||||
unservedConn = c
|
||||
break
|
||||
}
|
||||
logf("%d: getEngine failed again: %v", i, err)
|
||||
logf("ipnserver%d: getEngine failed again: %v", i, err)
|
||||
errMsg := err.Error()
|
||||
go func() {
|
||||
defer c.Close()
|
||||
@@ -235,6 +537,24 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
if err != nil {
|
||||
return fmt.Errorf("ipn.NewFileStore(%q): %v", opts.StatePath, err)
|
||||
}
|
||||
if opts.AutostartStateKey == "" {
|
||||
autoStartKey, err := store.ReadState(ipn.ServerModeStartKey)
|
||||
if err != nil && err != ipn.ErrStateNotExist {
|
||||
return fmt.Errorf("calling ReadState on %s: %w", opts.StatePath, err)
|
||||
}
|
||||
key := string(autoStartKey)
|
||||
if strings.HasPrefix(key, "user-") {
|
||||
uid := strings.TrimPrefix(key, "user-")
|
||||
u, err := server.lookupUserFromID(uid)
|
||||
if err != nil {
|
||||
logf("ipnserver: found server mode auto-start key %q; failed to load user: %v", key, err)
|
||||
} else {
|
||||
logf("ipnserver: found server mode auto-start key %q (user %s)", key, u.Username)
|
||||
server.serverModeUser = u
|
||||
}
|
||||
opts.AutostartStateKey = ipn.StateKey(key)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
store = &ipn.MemoryStore{}
|
||||
}
|
||||
@@ -250,18 +570,16 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
|
||||
if opts.DebugMux != nil {
|
||||
opts.DebugMux.HandleFunc("/debug/ipn", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
st := b.Status()
|
||||
// TODO(bradfitz): add LogID and opts to st?
|
||||
st.WriteHTML(w)
|
||||
serveHTMLStatus(w, b)
|
||||
})
|
||||
}
|
||||
|
||||
server.b = b
|
||||
server.bs = ipn.NewBackendServer(logf, b, server.writeToClients)
|
||||
|
||||
if opts.AutostartStateKey != "" {
|
||||
server.bs.GotCommand(&ipn.Command{
|
||||
Version: version.LONG,
|
||||
Version: version.Long,
|
||||
Start: &ipn.StartArgs{
|
||||
Opts: ipn.Options{
|
||||
StateKey: opts.AutostartStateKey,
|
||||
@@ -292,6 +610,11 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
// BabysitProc runs the current executable as a child process with the
|
||||
// provided args, capturing its output, writing it to files, and
|
||||
// restarting the process on any crashes.
|
||||
//
|
||||
// It's only currently (2020-10-29) used on Windows.
|
||||
func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
|
||||
executable, err := os.Executable()
|
||||
@@ -299,6 +622,14 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
panic("cannot determine executable: " + err.Error())
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
if len(args) != 2 && args[0] != "/subproc" {
|
||||
panic(fmt.Sprintf("unexpected arguments %q", args))
|
||||
}
|
||||
logID := args[1]
|
||||
logf = filelogger.New("tailscale-service", logID, logf)
|
||||
}
|
||||
|
||||
var proc struct {
|
||||
mu sync.Mutex
|
||||
p *os.Process
|
||||
@@ -436,54 +767,40 @@ func (l *oneConnListener) Addr() net.Addr { return dummyAddr("unused-address") }
|
||||
func (a dummyAddr) Network() string { return string(a) }
|
||||
func (a dummyAddr) String() string { return string(a) }
|
||||
|
||||
type altReaderNetConn struct {
|
||||
r io.Reader
|
||||
// protoSwitchConn is a net.Conn that's we want to speak HTTP to but
|
||||
// it's already had a few bytes read from it to determine that it's
|
||||
// HTTP. So we Read from its bufio.Reader. On Close, we we tell the
|
||||
// server it's closed, so the server can account the who's connected.
|
||||
type protoSwitchConn struct {
|
||||
s *server
|
||||
net.Conn
|
||||
br *bufio.Reader
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func (a altReaderNetConn) Read(p []byte) (int, error) { return a.r.Read(p) }
|
||||
func (psc *protoSwitchConn) Read(p []byte) (int, error) { return psc.br.Read(p) }
|
||||
func (psc *protoSwitchConn) Close() error {
|
||||
psc.closeOnce.Do(func() { psc.s.removeAndCloseConn(psc.Conn) })
|
||||
return nil
|
||||
}
|
||||
|
||||
func localhostHandler(c net.Conn) http.Handler {
|
||||
la, lerr := netaddr.ParseIPPort(c.LocalAddr().String())
|
||||
ra, rerr := netaddr.ParseIPPort(c.RemoteAddr().String())
|
||||
func (s *server) localhostHandler(ci connIdentity) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintf(w, "<html><body><h1>tailscale</h1>\n")
|
||||
if lerr != nil || rerr != nil {
|
||||
io.WriteString(w, "failed to parse remote address")
|
||||
if ci.Unknown {
|
||||
io.WriteString(w, "<html><title>Tailscale</title><body><h1>Tailscale</h1>This is the local Tailscale daemon.")
|
||||
return
|
||||
}
|
||||
if !la.IP.IsLoopback() || !ra.IP.IsLoopback() {
|
||||
io.WriteString(w, "not loopback")
|
||||
return
|
||||
}
|
||||
tab, err := netstat.Get()
|
||||
if err == netstat.ErrNotImplemented {
|
||||
io.WriteString(w, "status page not available on "+runtime.GOOS)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
io.WriteString(w, "failed to get netstat table")
|
||||
return
|
||||
}
|
||||
pid := peerPid(tab.Entries, la, ra)
|
||||
if pid == 0 {
|
||||
io.WriteString(w, "peer pid not found")
|
||||
return
|
||||
}
|
||||
uid, err := pidowner.OwnerOfPID(pid)
|
||||
if err != nil {
|
||||
io.WriteString(w, "owner of peer pid not found")
|
||||
return
|
||||
}
|
||||
u, err := user.LookupId(uid)
|
||||
if err != nil {
|
||||
io.WriteString(w, "User lookup failed")
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "Hello, %s", html.EscapeString(u.Username))
|
||||
serveHTMLStatus(w, s.b)
|
||||
})
|
||||
}
|
||||
|
||||
func serveHTMLStatus(w http.ResponseWriter, b *ipn.LocalBackend) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
st := b.Status()
|
||||
// TODO(bradfitz): add LogID and opts to st?
|
||||
st.WriteHTML(w)
|
||||
}
|
||||
|
||||
func peerPid(entries []netstat.Entry, la, ra netaddr.IPPort) int {
|
||||
for _, e := range entries {
|
||||
if e.Local == ra && e.Remote == la {
|
||||
|
||||
@@ -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{}) {
|
||||
|
||||
516
ipn/local.go
516
ipn/local.go
@@ -5,9 +5,12 @@
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -34,6 +37,15 @@ import (
|
||||
"tailscale.com/wgengine/tsdns"
|
||||
)
|
||||
|
||||
var controlDebugFlags = getControlDebugFlags()
|
||||
|
||||
func getControlDebugFlags() []string {
|
||||
if e := os.Getenv("TS_DEBUG_CONTROL_FLAGS"); e != "" {
|
||||
return strings.Split(e, ",")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LocalBackend is the glue between the major pieces of the Tailscale
|
||||
// network software: the cloud control plane (via controlclient), the
|
||||
// network data plane (via wgengine), and the user-facing UIs and CLIs
|
||||
@@ -50,32 +62,39 @@ type LocalBackend struct {
|
||||
ctxCancel context.CancelFunc // cancels ctx
|
||||
logf logger.Logf // general logging
|
||||
keyLogf logger.Logf // for printing list of peers on change
|
||||
statsLogf logger.Logf // for printing peers stats on change
|
||||
e wgengine.Engine
|
||||
store StateStore
|
||||
backendLogID string
|
||||
portpoll *portlist.Poller // may be nil
|
||||
portpollOnce sync.Once
|
||||
serverURL string // tailcontrol URL
|
||||
portpollOnce sync.Once // guards starting readPoller
|
||||
gotPortPollRes chan struct{} // closed upon first readPoller result
|
||||
serverURL string // tailcontrol URL
|
||||
newDecompressor func() (controlclient.Decompressor, error)
|
||||
|
||||
filterHash string
|
||||
|
||||
// The mutex protects the following elements.
|
||||
mu sync.Mutex
|
||||
notify func(Notify)
|
||||
c *controlclient.Client
|
||||
stateKey StateKey
|
||||
prefs *Prefs
|
||||
state State
|
||||
mu sync.Mutex
|
||||
notify func(Notify)
|
||||
c *controlclient.Client
|
||||
stateKey StateKey // computed in part from user-provided value
|
||||
userID string // current controlling user ID (for Windows, primarily)
|
||||
prefs *Prefs
|
||||
inServerMode bool
|
||||
machinePrivKey wgcfg.PrivateKey
|
||||
state State
|
||||
// hostinfo is mutated in-place while mu is held.
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
// netMap is not mutated in-place once set.
|
||||
netMap *controlclient.NetworkMap
|
||||
activeLogin string // last logged LoginName from netMap
|
||||
engineStatus EngineStatus
|
||||
endpoints []string
|
||||
blocked bool
|
||||
authURL string
|
||||
interact int
|
||||
interact bool
|
||||
prevIfState *interfaces.State
|
||||
|
||||
// statusLock must be held before calling statusChanged.Wait() or
|
||||
// statusChanged.Broadcast().
|
||||
@@ -100,15 +119,17 @@ func NewLocalBackend(logf logger.Logf, logid string, store StateStore, e wgengin
|
||||
}
|
||||
|
||||
b := &LocalBackend{
|
||||
ctx: ctx,
|
||||
ctxCancel: cancel,
|
||||
logf: logf,
|
||||
keyLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
|
||||
e: e,
|
||||
store: store,
|
||||
backendLogID: logid,
|
||||
state: NoState,
|
||||
portpoll: portpoll,
|
||||
ctx: ctx,
|
||||
ctxCancel: cancel,
|
||||
logf: logf,
|
||||
keyLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
|
||||
statsLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
|
||||
e: e,
|
||||
store: store,
|
||||
backendLogID: logid,
|
||||
state: NoState,
|
||||
portpoll: portpoll,
|
||||
gotPortPollRes: make(chan struct{}),
|
||||
}
|
||||
e.SetLinkChangeCallback(b.linkChange)
|
||||
b.statusChanged = sync.NewCond(&b.statusLock)
|
||||
@@ -116,15 +137,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
|
||||
@@ -232,8 +269,14 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
b.prefs.Persist = st.Persist.Clone()
|
||||
}
|
||||
}
|
||||
if temporarilySetMachineKeyInPersist() && b.prefs.Persist != nil &&
|
||||
b.prefs.Persist.LegacyFrontendPrivateMachineKey.IsZero() {
|
||||
b.prefs.Persist.LegacyFrontendPrivateMachineKey = b.machinePrivKey
|
||||
prefsChanged = true
|
||||
}
|
||||
if st.NetMap != nil {
|
||||
b.netMap = st.NetMap
|
||||
b.setNetMapLocked(st.NetMap)
|
||||
|
||||
}
|
||||
if st.URL != "" {
|
||||
b.authURL = st.URL
|
||||
@@ -272,23 +315,16 @@ 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})
|
||||
}
|
||||
if st.URL != "" {
|
||||
b.logf("Received auth URL: %.20v...", st.URL)
|
||||
if interact > 0 {
|
||||
if interact {
|
||||
b.popBrowserAuthNow()
|
||||
}
|
||||
}
|
||||
@@ -310,9 +346,8 @@ func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
es := b.parseWgStatus(s)
|
||||
|
||||
b.mu.Lock()
|
||||
es := b.parseWgStatusLocked(s)
|
||||
c := b.c
|
||||
b.engineStatus = es
|
||||
b.endpoints = append([]string{}, s.LocalAddrs...)
|
||||
@@ -380,18 +415,44 @@ func (b *LocalBackend) Start(opts Options) error {
|
||||
return fmt.Errorf("loading requested state: %v", err)
|
||||
}
|
||||
|
||||
b.inServerMode = b.prefs.ForceDaemon
|
||||
b.serverURL = b.prefs.ControlURL
|
||||
hostinfo.RoutableIPs = append(hostinfo.RoutableIPs, b.prefs.AdvertiseRoutes...)
|
||||
hostinfo.RequestTags = append(hostinfo.RequestTags, b.prefs.AdvertiseTags...)
|
||||
if b.inServerMode || runtime.GOOS == "windows" {
|
||||
b.logf("Start: serverMode=%v", b.inServerMode)
|
||||
}
|
||||
applyPrefsToHostinfo(hostinfo, b.prefs)
|
||||
|
||||
b.notify = opts.Notify
|
||||
b.netMap = nil
|
||||
b.setNetMapLocked(nil)
|
||||
persist := b.prefs.Persist
|
||||
machinePrivKey := b.machinePrivKey
|
||||
b.mu.Unlock()
|
||||
|
||||
b.updateFilter(nil, nil)
|
||||
|
||||
if b.portpoll != nil {
|
||||
b.portpollOnce.Do(func() {
|
||||
go b.portpoll.Run(b.ctx)
|
||||
go b.readPoller()
|
||||
|
||||
// Give the poller a second to get results to
|
||||
// prevent it from restarting our map poll
|
||||
// HTTP request (via doSetHostinfoFilterServices >
|
||||
// cli.SetHostinfo). In practice this is very quick.
|
||||
t0 := time.Now()
|
||||
timer := time.NewTimer(time.Second)
|
||||
select {
|
||||
case <-b.gotPortPollRes:
|
||||
b.logf("got initial portlist info in %v", time.Since(t0).Round(time.Millisecond))
|
||||
timer.Stop()
|
||||
case <-timer.C:
|
||||
b.logf("timeout waiting for initial portlist")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var discoPublic tailcfg.DiscoKey
|
||||
if controlclient.Debug.Disco {
|
||||
discoPublic = b.e.DiscoPublicKey()
|
||||
@@ -403,29 +464,22 @@ 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,
|
||||
DebugFlags: controlDebugFlags,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// At this point, we have finished using hostinfo without synchronization,
|
||||
// so it is safe to start readPoller which concurrently writes to it.
|
||||
if b.portpoll != nil {
|
||||
b.portpollOnce.Do(func() {
|
||||
go b.portpoll.Run(b.ctx)
|
||||
go b.readPoller()
|
||||
})
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
b.c = cli
|
||||
endpoints := b.endpoints
|
||||
@@ -441,6 +495,12 @@ func (b *LocalBackend) Start(opts Options) error {
|
||||
|
||||
b.mu.Lock()
|
||||
prefs := b.prefs.Clone()
|
||||
|
||||
if temporarilySetMachineKeyInPersist() && prefs.Persist != nil &&
|
||||
prefs.Persist.LegacyFrontendPrivateMachineKey.IsZero() {
|
||||
prefs.Persist.LegacyFrontendPrivateMachineKey = b.machinePrivKey
|
||||
}
|
||||
|
||||
b.mu.Unlock()
|
||||
|
||||
blid := b.backendLogID
|
||||
@@ -463,7 +523,7 @@ func (b *LocalBackend) updateFilter(netMap *controlclient.NetworkMap, prefs *Pre
|
||||
var (
|
||||
haveNetmap = netMap != nil
|
||||
addrs []wgcfg.CIDR
|
||||
packetFilter filter.Matches
|
||||
packetFilter []filter.Match
|
||||
advRoutes []wgcfg.CIDR
|
||||
shieldsUp = prefs == nil || prefs.ShieldsUp // Be conservative when not ready
|
||||
)
|
||||
@@ -486,12 +546,12 @@ func (b *LocalBackend) updateFilter(netMap *controlclient.NetworkMap, prefs *Pre
|
||||
return
|
||||
}
|
||||
|
||||
localNets := wgCIDRsToFilter(netMap.Addresses, advRoutes)
|
||||
localNets := wgCIDRsToNetaddr(netMap.Addresses, advRoutes)
|
||||
|
||||
if shieldsUp {
|
||||
b.logf("netmap packet filter: (shields up)")
|
||||
var prevFilter *filter.Filter // don't reuse old filter state
|
||||
b.e.SetFilter(filter.New(filter.Matches{}, localNets, prevFilter, b.logf))
|
||||
b.e.SetFilter(filter.New(nil, localNets, prevFilter, b.logf))
|
||||
} else {
|
||||
b.logf("netmap packet filter: %v", packetFilter)
|
||||
b.e.SetFilter(filter.New(packetFilter, localNets, b.e.GetFilter(), b.logf))
|
||||
@@ -574,6 +634,7 @@ func (b *LocalBackend) updateDNSMap(netMap *controlclient.NetworkMap) {
|
||||
// readPoller is a goroutine that receives service lists from
|
||||
// b.portpoll and propagates them into the controlclient's HostInfo.
|
||||
func (b *LocalBackend) readPoller() {
|
||||
n := 0
|
||||
for {
|
||||
ports, ok := <-b.portpoll.C
|
||||
if !ok {
|
||||
@@ -600,6 +661,11 @@ func (b *LocalBackend) readPoller() {
|
||||
b.mu.Unlock()
|
||||
|
||||
b.doSetHostinfoFilterServices(hi)
|
||||
|
||||
n++
|
||||
if n == 1 {
|
||||
close(b.gotPortPollRes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -611,7 +677,7 @@ func (b *LocalBackend) send(n Notify) {
|
||||
b.mu.Unlock()
|
||||
|
||||
if notify != nil {
|
||||
n.Version = version.LONG
|
||||
n.Version = version.Long
|
||||
notify(n)
|
||||
} else {
|
||||
b.logf("nil notify callback; dropping %+v", n)
|
||||
@@ -623,7 +689,7 @@ func (b *LocalBackend) send(n Notify) {
|
||||
func (b *LocalBackend) popBrowserAuthNow() {
|
||||
b.mu.Lock()
|
||||
url := b.authURL
|
||||
b.interact = 0
|
||||
b.interact = false
|
||||
b.authURL = ""
|
||||
b.mu.Unlock()
|
||||
|
||||
@@ -637,48 +703,171 @@ func (b *LocalBackend) popBrowserAuthNow() {
|
||||
}
|
||||
}
|
||||
|
||||
// initMachineKeyLocked is called to initialize b.machinePrivKey.
|
||||
//
|
||||
// b.prefs must already be initialized.
|
||||
// b.stateKey should be set too, but just for nicer log messages.
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) initMachineKeyLocked() (err error) {
|
||||
if temporarilySetMachineKeyInPersist() {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if b.prefs != nil && b.prefs.Persist != nil {
|
||||
b.prefs.Persist.LegacyFrontendPrivateMachineKey = b.machinePrivKey
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if !b.machinePrivKey.IsZero() {
|
||||
// Already set.
|
||||
return nil
|
||||
}
|
||||
|
||||
var legacyMachineKey wgcfg.PrivateKey
|
||||
if b.prefs.Persist != nil {
|
||||
legacyMachineKey = b.prefs.Persist.LegacyFrontendPrivateMachineKey
|
||||
}
|
||||
|
||||
keyText, err := b.store.ReadState(MachineKeyStateKey)
|
||||
if err == nil {
|
||||
if err := b.machinePrivKey.UnmarshalText(keyText); err != nil {
|
||||
return fmt.Errorf("invalid key in %s key of %v: %w", MachineKeyStateKey, b.store, err)
|
||||
}
|
||||
if b.machinePrivKey.IsZero() {
|
||||
return fmt.Errorf("invalid zero key stored in %v key of %v", MachineKeyStateKey, b.store)
|
||||
}
|
||||
if !legacyMachineKey.IsZero() && !bytes.Equal(legacyMachineKey[:], b.machinePrivKey[:]) {
|
||||
b.logf("frontend-provided legacy machine key ignored; used value from server state")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err != ErrStateNotExist {
|
||||
return fmt.Errorf("error reading %v key of %v: %w", MachineKeyStateKey, b.store, err)
|
||||
}
|
||||
|
||||
// If we didn't find one already on disk and the prefs already
|
||||
// have a legacy machine key, use that. Otherwise generate a
|
||||
// new one.
|
||||
if !legacyMachineKey.IsZero() {
|
||||
if b.stateKey == "" {
|
||||
b.logf("using frontend-provided legacy machine key")
|
||||
} else {
|
||||
b.logf("using legacy machine key from state key %q", b.stateKey)
|
||||
}
|
||||
b.machinePrivKey = legacyMachineKey
|
||||
} else {
|
||||
b.logf("generating new machine key")
|
||||
var err error
|
||||
b.machinePrivKey, err = wgcfg.NewPrivateKey()
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing new machine key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
keyText, _ = b.machinePrivKey.MarshalText()
|
||||
if err := b.store.WriteState(MachineKeyStateKey, keyText); err != nil {
|
||||
b.logf("error writing machine key to store: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
b.logf("machine key written to store")
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeServerModeStartState stores the ServerModeStartKey value based on the current
|
||||
// user and prefs. If userID is blank or prefs is blank, no work is done.
|
||||
//
|
||||
// b.mu may either be held or not.
|
||||
func (b *LocalBackend) writeServerModeStartState(userID string, prefs *Prefs) {
|
||||
if userID == "" || prefs == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if prefs.ForceDaemon {
|
||||
stateKey := StateKey("user-" + userID)
|
||||
if err := b.store.WriteState(ServerModeStartKey, []byte(stateKey)); err != nil {
|
||||
b.logf("WriteState error: %v", err)
|
||||
}
|
||||
// It's important we do this here too, even if it looks
|
||||
// redundant with the one in the 'if stateKey != ""'
|
||||
// check block above. That one won't fire in the case
|
||||
// where the Windows client started up in client mode.
|
||||
// This happens when we transition into server mode:
|
||||
if err := b.store.WriteState(stateKey, prefs.ToBytes()); err != nil {
|
||||
b.logf("WriteState error: %v", err)
|
||||
}
|
||||
} else {
|
||||
if err := b.store.WriteState(ServerModeStartKey, nil); err != nil {
|
||||
b.logf("WriteState error: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// loadStateLocked sets b.prefs and b.stateKey based on a complex
|
||||
// combination of key, prefs, and legacyPath. b.mu must be held when
|
||||
// calling.
|
||||
func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath string) error {
|
||||
func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath string) (err error) {
|
||||
if prefs == nil && key == "" {
|
||||
panic("state key and prefs are both unset")
|
||||
}
|
||||
|
||||
// Optimistically set stateKey (for initMachineKeyLocked's
|
||||
// logging), but revert it if we return an error so a later SetPrefs
|
||||
// call can't pick it up if it's bogus.
|
||||
b.stateKey = key
|
||||
defer func() {
|
||||
if err != nil {
|
||||
b.stateKey = ""
|
||||
}
|
||||
}()
|
||||
|
||||
if key == "" {
|
||||
// Frontend fully owns the state, we just need to obey it.
|
||||
b.logf("Using frontend prefs")
|
||||
// Frontend owns the state, we just need to obey it.
|
||||
//
|
||||
// If the frontend (e.g. on Windows) supplied the
|
||||
// optional/legacy machine key then it's used as the
|
||||
// value instead of making up a new one.
|
||||
b.logf("using frontend prefs: %s", prefs.Pretty())
|
||||
b.prefs = prefs.Clone()
|
||||
b.stateKey = ""
|
||||
if err := b.initMachineKeyLocked(); err != nil {
|
||||
return fmt.Errorf("initMachineKeyLocked: %w", err)
|
||||
}
|
||||
b.writeServerModeStartState(b.userID, b.prefs)
|
||||
return nil
|
||||
}
|
||||
|
||||
if prefs != nil {
|
||||
// Backend owns the state, but frontend is trying to migrate
|
||||
// state into the backend.
|
||||
b.logf("Importing frontend prefs into backend store")
|
||||
b.logf("importing frontend prefs into backend store; frontend prefs: %s", prefs.Pretty())
|
||||
if err := b.store.WriteState(key, prefs.ToBytes()); err != nil {
|
||||
return fmt.Errorf("store.WriteState: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
b.logf("Using backend prefs")
|
||||
b.logf("using backend prefs")
|
||||
bs, err := b.store.ReadState(key)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrStateNotExist) {
|
||||
if legacyPath != "" {
|
||||
b.prefs, err = LoadPrefs(legacyPath)
|
||||
if err != nil {
|
||||
b.logf("Failed to load legacy prefs: %v", err)
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
b.logf("failed to load legacy prefs: %v", err)
|
||||
}
|
||||
b.prefs = NewPrefs()
|
||||
} else {
|
||||
b.logf("Imported state from relaynode for %q", key)
|
||||
b.logf("imported prefs from relaynode for %q: %v", key, b.prefs.Pretty())
|
||||
}
|
||||
} else {
|
||||
b.prefs = NewPrefs()
|
||||
b.logf("Created empty state for %q", key)
|
||||
b.logf("created empty state for %q: %s", key, b.prefs.Pretty())
|
||||
}
|
||||
if err := b.initMachineKeyLocked(); err != nil {
|
||||
return fmt.Errorf("initMachineKeyLocked: %w", err)
|
||||
}
|
||||
b.stateKey = key
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("store.ReadState(%q): %v", key, err)
|
||||
@@ -687,7 +876,10 @@ func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath st
|
||||
if err != nil {
|
||||
return fmt.Errorf("PrefsFromBytes: %v", err)
|
||||
}
|
||||
b.stateKey = key
|
||||
b.logf("backend prefs for %q: %s", key, b.prefs.Pretty())
|
||||
if err := b.initMachineKeyLocked(); err != nil {
|
||||
return fmt.Errorf("initMachineKeyLocked: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -699,6 +891,12 @@ func (b *LocalBackend) State() State {
|
||||
return b.state
|
||||
}
|
||||
|
||||
func (b *LocalBackend) InServerMode() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.inServerMode
|
||||
}
|
||||
|
||||
// getEngineStatus returns a copy of b.engineStatus.
|
||||
//
|
||||
// TODO(bradfitz): remove this and use Status() throughout.
|
||||
@@ -726,7 +924,7 @@ func (b *LocalBackend) Login(token *oauth2.Token) {
|
||||
func (b *LocalBackend) StartLoginInteractive() {
|
||||
b.mu.Lock()
|
||||
b.assertClientLocked()
|
||||
b.interact++
|
||||
b.interact = true
|
||||
url := b.authURL
|
||||
c := b.c
|
||||
b.mu.Unlock()
|
||||
@@ -757,7 +955,7 @@ func (b *LocalBackend) FakeExpireAfter(x time.Duration) {
|
||||
if e.IsZero() || time.Until(e) > x {
|
||||
mapCopy.Expiry = time.Now().Add(x)
|
||||
}
|
||||
b.netMap = &mapCopy
|
||||
b.setNetMapLocked(&mapCopy)
|
||||
b.send(Notify{NetMap: b.netMap})
|
||||
}
|
||||
|
||||
@@ -772,30 +970,33 @@ func (b *LocalBackend) Ping(ipStr string) {
|
||||
})
|
||||
}
|
||||
|
||||
func (b *LocalBackend) parseWgStatus(s *wgengine.Status) (ret EngineStatus) {
|
||||
var (
|
||||
peerStats []string
|
||||
peerKeys []string
|
||||
)
|
||||
// parseWgStatusLocked returns an EngineStatus based on s.
|
||||
//
|
||||
// b.mu must be held; mostly because the caller is about to anyway, and doing so
|
||||
// gives us slightly better guarantees about the two peers stats lines not
|
||||
// being intermixed if there are concurrent calls to our caller.
|
||||
func (b *LocalBackend) parseWgStatusLocked(s *wgengine.Status) (ret EngineStatus) {
|
||||
var peerStats, peerKeys strings.Builder
|
||||
|
||||
ret.LiveDERPs = s.DERPs
|
||||
ret.LivePeers = map[tailcfg.NodeKey]wgengine.PeerStatus{}
|
||||
for _, p := range s.Peers {
|
||||
if !p.LastHandshake.IsZero() {
|
||||
peerStats = append(peerStats, fmt.Sprintf("%d/%d", p.RxBytes, p.TxBytes))
|
||||
fmt.Fprintf(&peerStats, "%d/%d ", p.RxBytes, p.TxBytes)
|
||||
fmt.Fprintf(&peerKeys, "%s ", p.NodeKey.ShortString())
|
||||
|
||||
ret.NumLive++
|
||||
ret.LivePeers[p.NodeKey] = p
|
||||
|
||||
peerKeys = append(peerKeys, p.NodeKey.ShortString())
|
||||
}
|
||||
ret.RBytes += p.RxBytes
|
||||
ret.WBytes += p.TxBytes
|
||||
}
|
||||
if len(peerStats) > 0 {
|
||||
// [GRINDER STATS LINE] - please don't remove (used for log parsing)
|
||||
b.keyLogf("peer keys: %s", strings.Join(peerKeys, " "))
|
||||
// [GRINDER STATS LINE] - please don't remove (used for log parsing)
|
||||
b.logf("v%v peers: %v", version.LONG, strings.Join(peerStats, " "))
|
||||
|
||||
// [GRINDER STATS LINES] - please don't remove (used for log parsing)
|
||||
if peerStats.Len() > 0 {
|
||||
b.keyLogf("peer keys: %s", strings.TrimSpace(peerKeys.String()))
|
||||
b.statsLogf("v%v peers: %v", version.Long, strings.TrimSpace(peerStats.String()))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -812,6 +1013,12 @@ func (b *LocalBackend) shieldsAreUp() bool {
|
||||
return b.prefs.ShieldsUp
|
||||
}
|
||||
|
||||
func (b *LocalBackend) SetCurrentUserID(uid string) {
|
||||
b.mu.Lock()
|
||||
b.userID = uid
|
||||
b.mu.Unlock()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) SetWantRunning(wantRunning bool) {
|
||||
b.mu.Lock()
|
||||
new := b.prefs.Clone()
|
||||
@@ -826,8 +1033,8 @@ func (b *LocalBackend) SetWantRunning(wantRunning bool) {
|
||||
|
||||
// SetPrefs saves new user preferences and propagates them throughout
|
||||
// the system. Implements Backend.
|
||||
func (b *LocalBackend) SetPrefs(new *Prefs) {
|
||||
if new == nil {
|
||||
func (b *LocalBackend) SetPrefs(newp *Prefs) {
|
||||
if newp == nil {
|
||||
panic("SetPrefs got nil prefs")
|
||||
}
|
||||
|
||||
@@ -836,51 +1043,64 @@ func (b *LocalBackend) SetPrefs(new *Prefs) {
|
||||
netMap := b.netMap
|
||||
stateKey := b.stateKey
|
||||
|
||||
old := b.prefs
|
||||
new.Persist = old.Persist // caller isn't allowed to override this
|
||||
b.prefs = new
|
||||
oldp := b.prefs
|
||||
newp.Persist = oldp.Persist // caller isn't allowed to override this
|
||||
b.prefs = newp
|
||||
b.inServerMode = newp.ForceDaemon
|
||||
// We do this to avoid holding the lock while doing everything else.
|
||||
new = b.prefs.Clone()
|
||||
newp = b.prefs.Clone()
|
||||
|
||||
oldHi := b.hostinfo
|
||||
newHi := oldHi.Clone()
|
||||
newHi.RoutableIPs = append([]wgcfg.CIDR(nil), b.prefs.AdvertiseRoutes...)
|
||||
applyPrefsToHostinfo(newHi, new)
|
||||
applyPrefsToHostinfo(newHi, newp)
|
||||
b.hostinfo = newHi
|
||||
hostInfoChanged := !oldHi.Equal(newHi)
|
||||
userID := b.userID
|
||||
|
||||
b.mu.Unlock()
|
||||
|
||||
if stateKey != "" {
|
||||
if err := b.store.WriteState(stateKey, new.ToBytes()); err != nil {
|
||||
if err := b.store.WriteState(stateKey, newp.ToBytes()); err != nil {
|
||||
b.logf("Failed to save new controlclient state: %v", err)
|
||||
}
|
||||
}
|
||||
b.writeServerModeStartState(userID, newp)
|
||||
|
||||
// [GRINDER STATS LINE] - please don't remove (used for log parsing)
|
||||
b.logf("SetPrefs: %v", new.Pretty())
|
||||
b.logf("SetPrefs: %v", newp.Pretty())
|
||||
if netMap != nil {
|
||||
if login := netMap.UserProfiles[netMap.User].LoginName; login != "" {
|
||||
if newp.Persist == nil {
|
||||
b.logf("active login: %s", login)
|
||||
} else if newp.Persist.LoginName != login {
|
||||
// Corp issue 461: sometimes the wrong prefs are
|
||||
// logged; the frontend isn't always getting
|
||||
// notified (to update its prefs/persist) on
|
||||
// account switch. Log this while we figure it
|
||||
// out.
|
||||
b.logf("active login: %s ([unexpected] corp#461, not %s)", newp.Persist.LoginName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if old.ShieldsUp != new.ShieldsUp || hostInfoChanged {
|
||||
if oldp.ShieldsUp != newp.ShieldsUp || hostInfoChanged {
|
||||
b.doSetHostinfoFilterServices(newHi)
|
||||
}
|
||||
|
||||
b.updateFilter(netMap, new)
|
||||
b.updateFilter(netMap, newp)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
if old.WantRunning != new.WantRunning {
|
||||
if oldp.WantRunning != newp.WantRunning {
|
||||
b.stateMachine()
|
||||
} else {
|
||||
b.authReconfig()
|
||||
}
|
||||
|
||||
b.send(Notify{Prefs: new})
|
||||
b.send(Notify{Prefs: newp})
|
||||
}
|
||||
|
||||
// doSetHostinfoFilterServices calls SetHostinfo on the controlclient,
|
||||
@@ -933,6 +1153,8 @@ func (b *LocalBackend) authReconfig() {
|
||||
blocked := b.blocked
|
||||
uc := b.prefs
|
||||
nm := b.netMap
|
||||
hasPAC := b.prevIfState.HasPAC()
|
||||
disableSubnetsIfPAC := nm != nil && nm.Debug != nil && nm.Debug.DisableSubnetsIfPAC.EqualBool(true)
|
||||
b.mu.Unlock()
|
||||
|
||||
if blocked {
|
||||
@@ -957,6 +1179,12 @@ func (b *LocalBackend) authReconfig() {
|
||||
if uc.AllowSingleHosts {
|
||||
flags |= controlclient.AllowSingleHosts
|
||||
}
|
||||
if hasPAC && disableSubnetsIfPAC {
|
||||
if flags&controlclient.AllowSubnetRoutes != 0 {
|
||||
b.logf("authReconfig: have PAC; disabling subnet routes")
|
||||
flags &^= controlclient.AllowSubnetRoutes
|
||||
}
|
||||
}
|
||||
|
||||
cfg, err := nm.WGCfg(b.logf, flags)
|
||||
if err != nil {
|
||||
@@ -1033,14 +1261,14 @@ func routerConfig(cfg *wgcfg.Config, prefs *Prefs) *router.Config {
|
||||
}
|
||||
|
||||
rs := &router.Config{
|
||||
LocalAddrs: wgCIDRToNetaddr(addrs),
|
||||
SubnetRoutes: wgCIDRToNetaddr(prefs.AdvertiseRoutes),
|
||||
LocalAddrs: wgCIDRsToNetaddr(addrs),
|
||||
SubnetRoutes: wgCIDRsToNetaddr(prefs.AdvertiseRoutes),
|
||||
SNATSubnetRoutes: !prefs.NoSNAT,
|
||||
NetfilterMode: prefs.NetfilterMode,
|
||||
}
|
||||
|
||||
for _, peer := range cfg.Peers {
|
||||
rs.Routes = append(rs.Routes, wgCIDRToNetaddr(peer.AllowedIPs)...)
|
||||
rs.Routes = append(rs.Routes, wgCIDRsToNetaddr(peer.AllowedIPs)...)
|
||||
}
|
||||
|
||||
rs.Routes = append(rs.Routes, netaddr.IPPrefix{
|
||||
@@ -1051,35 +1279,20 @@ func routerConfig(cfg *wgcfg.Config, prefs *Prefs) *router.Config {
|
||||
return rs
|
||||
}
|
||||
|
||||
// wgCIDRsToFilter converts lists of wgcfg.CIDR into a single list of
|
||||
// filter.Net.
|
||||
func wgCIDRsToFilter(cidrLists ...[]wgcfg.CIDR) (ret []filter.Net) {
|
||||
func wgCIDRsToNetaddr(cidrLists ...[]wgcfg.CIDR) (ret []netaddr.IPPrefix) {
|
||||
for _, cidrs := range cidrLists {
|
||||
for _, cidr := range cidrs {
|
||||
if !cidr.IP.Is4() {
|
||||
continue
|
||||
ncidr, ok := netaddr.FromStdIPNet(cidr.IPNet())
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("conversion of %s from wgcfg to netaddr IPNet failed", cidr))
|
||||
}
|
||||
ret = append(ret, filter.Net{
|
||||
IP: filter.NewIP(cidr.IP.IP()),
|
||||
Mask: filter.Netmask(int(cidr.Mask)),
|
||||
})
|
||||
ncidr.IP = ncidr.IP.Unmap()
|
||||
ret = append(ret, ncidr)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func wgCIDRToNetaddr(cidrs []wgcfg.CIDR) (ret []netaddr.IPPrefix) {
|
||||
for _, cidr := range cidrs {
|
||||
ncidr, ok := netaddr.FromStdIPNet(cidr.IPNet())
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("conversion of %s from wgcfg to netaddr IPNet failed", cidr))
|
||||
}
|
||||
ncidr.IP = ncidr.IP.Unmap()
|
||||
ret = append(ret, ncidr)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *Prefs) {
|
||||
if h := prefs.Hostname; h != "" {
|
||||
hi.Hostname = h
|
||||
@@ -1106,6 +1319,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 +1332,7 @@ func (b *LocalBackend) enterState(newState State) {
|
||||
}
|
||||
|
||||
if bc != nil {
|
||||
bc.SetPaused(newState == Stopped)
|
||||
bc.SetPaused(newState == Stopped || !networkUp)
|
||||
}
|
||||
|
||||
switch newState {
|
||||
@@ -1246,7 +1460,7 @@ func (b *LocalBackend) requestEngineStatusAndWait() {
|
||||
func (b *LocalBackend) Logout() {
|
||||
b.mu.Lock()
|
||||
c := b.c
|
||||
b.netMap = nil
|
||||
b.setNetMapLocked(nil)
|
||||
b.mu.Unlock()
|
||||
|
||||
if c == nil {
|
||||
@@ -1263,7 +1477,7 @@ func (b *LocalBackend) Logout() {
|
||||
c.Logout()
|
||||
|
||||
b.mu.Lock()
|
||||
b.netMap = nil
|
||||
b.setNetMapLocked(nil)
|
||||
b.mu.Unlock()
|
||||
|
||||
b.stateMachine()
|
||||
@@ -1292,19 +1506,53 @@ func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) {
|
||||
c.SetNetInfo(ni)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) setNetMapLocked(nm *controlclient.NetworkMap) {
|
||||
var login string
|
||||
if nm != nil {
|
||||
login = nm.UserProfiles[nm.User].LoginName
|
||||
if login == "" {
|
||||
login = "<missing-profile>"
|
||||
}
|
||||
}
|
||||
b.netMap = nm
|
||||
if login != b.activeLogin {
|
||||
b.logf("active login: %v", login)
|
||||
b.activeLogin = login
|
||||
}
|
||||
}
|
||||
|
||||
// TestOnlyPublicKeys returns the current machine and node public
|
||||
// keys. Used in tests only to facilitate automated node authorization
|
||||
// in the test harness.
|
||||
func (b *LocalBackend) TestOnlyPublicKeys() (machineKey tailcfg.MachineKey, nodeKey tailcfg.NodeKey) {
|
||||
b.mu.Lock()
|
||||
prefs := b.prefs
|
||||
machinePrivKey := b.machinePrivKey
|
||||
b.mu.Unlock()
|
||||
|
||||
if prefs == nil {
|
||||
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)
|
||||
}
|
||||
|
||||
// temporarilySetMachineKeyInPersist reports whether we should set
|
||||
// the machine key in Prefs.Persist.LegacyFrontendPrivateMachineKey
|
||||
// for the frontend to write out to its preferences for use later.
|
||||
//
|
||||
// TODO: remove this in Tailscale 1.3.x (so it effectively always
|
||||
// returns false). It just exists so users can downgrade from 1.2.x to
|
||||
// 1.0.x. But eventually we want to stop sending the machine key to
|
||||
// clients. We can't do that until 1.0.x is no longer supported.
|
||||
func temporarilySetMachineKeyInPersist() bool {
|
||||
//lint:ignore S1008 for comments
|
||||
switch runtime.GOOS {
|
||||
case "darwin", "ios", "android":
|
||||
// iOS, macOS, Android users can't downgrade anyway.
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -83,7 +83,9 @@ func TestLocalLogLines(t *testing.T) {
|
||||
}},
|
||||
LocalAddrs: []string{"idk an address"},
|
||||
}
|
||||
lb.parseWgStatus(status)
|
||||
lb.mu.Lock()
|
||||
lb.parseWgStatusLocked(status)
|
||||
lb.mu.Unlock()
|
||||
|
||||
t.Run("after_peers", testWantRemain())
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -19,6 +20,8 @@ import (
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var jsonEscapedZero = []byte(`\u0000`)
|
||||
|
||||
type NoArgs struct{}
|
||||
|
||||
type StartArgs struct {
|
||||
@@ -80,11 +83,14 @@ func NewBackendServer(logf logger.Logf, b Backend, sendNotifyMsg func(b []byte))
|
||||
}
|
||||
|
||||
func (bs *BackendServer) send(n Notify) {
|
||||
n.Version = version.LONG
|
||||
n.Version = version.Long
|
||||
b, err := json.Marshal(n)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed json.Marshal(notify): %v\n%#v", err, n)
|
||||
}
|
||||
if bytes.Contains(b, jsonEscapedZero) {
|
||||
log.Printf("[unexpected] zero byte in BackendServer.send notify message: %q", b)
|
||||
}
|
||||
bs.sendNotifyMsg(b)
|
||||
}
|
||||
|
||||
@@ -92,6 +98,17 @@ func (bs *BackendServer) SendErrorMessage(msg string) {
|
||||
bs.send(Notify{ErrMessage: &msg})
|
||||
}
|
||||
|
||||
// SendInUseOtherUserErrorMessage sends a Notify message to the client that
|
||||
// both sets the state to 'InUseOtherUser' and sets the associated reason
|
||||
// to msg.
|
||||
func (bs *BackendServer) SendInUseOtherUserErrorMessage(msg string) {
|
||||
inUse := InUseOtherUser
|
||||
bs.send(Notify{
|
||||
State: &inUse,
|
||||
ErrMessage: &msg,
|
||||
})
|
||||
}
|
||||
|
||||
// GotCommandMsg parses the incoming message b as a JSON Command and
|
||||
// calls GotCommand with it.
|
||||
func (bs *BackendServer) GotCommandMsg(b []byte) error {
|
||||
@@ -106,14 +123,14 @@ func (bs *BackendServer) GotCommandMsg(b []byte) error {
|
||||
}
|
||||
|
||||
func (bs *BackendServer) GotFakeCommand(cmd *Command) error {
|
||||
cmd.Version = version.LONG
|
||||
cmd.Version = version.Long
|
||||
return bs.GotCommand(cmd)
|
||||
}
|
||||
|
||||
func (bs *BackendServer) GotCommand(cmd *Command) error {
|
||||
if cmd.Version != version.LONG && !cmd.AllowVersionSkew {
|
||||
if cmd.Version != version.Long && !cmd.AllowVersionSkew {
|
||||
vs := fmt.Sprintf("GotCommand: Version mismatch! frontend=%#v backend=%#v",
|
||||
cmd.Version, version.LONG)
|
||||
cmd.Version, version.Long)
|
||||
bs.logf("%s", vs)
|
||||
// ignore the command, but send a message back to the
|
||||
// caller so it can realize the version mismatch too.
|
||||
@@ -193,13 +210,16 @@ func (bc *BackendClient) GotNotifyMsg(b []byte) {
|
||||
// not interesting
|
||||
return
|
||||
}
|
||||
if bytes.Contains(b, jsonEscapedZero) {
|
||||
log.Printf("[unexpected] zero byte in BackendClient.GotNotifyMsg message: %q", b)
|
||||
}
|
||||
n := Notify{}
|
||||
if err := json.Unmarshal(b, &n); err != nil {
|
||||
log.Fatalf("BackendClient.Notify: cannot decode message (length=%d)\n%#v", len(b), string(b))
|
||||
}
|
||||
if n.Version != version.LONG && !bc.AllowVersionSkew {
|
||||
if n.Version != version.Long && !bc.AllowVersionSkew {
|
||||
vs := fmt.Sprintf("GotNotify: Version mismatch! frontend=%#v backend=%#v",
|
||||
version.LONG, n.Version)
|
||||
version.Long, n.Version)
|
||||
bc.logf("%s", vs)
|
||||
// delete anything in the notification except the version,
|
||||
// to prevent incorrect operation.
|
||||
@@ -214,11 +234,14 @@ func (bc *BackendClient) GotNotifyMsg(b []byte) {
|
||||
}
|
||||
|
||||
func (bc *BackendClient) send(cmd Command) {
|
||||
cmd.Version = version.LONG
|
||||
cmd.Version = version.Long
|
||||
b, err := json.Marshal(cmd)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed json.Marshal(cmd): %v\n%#v\n", err, cmd)
|
||||
}
|
||||
if bytes.Contains(b, jsonEscapedZero) {
|
||||
log.Printf("[unexpected] zero byte in BackendClient.send command: %q", b)
|
||||
}
|
||||
bc.sendCommandMsg(b)
|
||||
}
|
||||
|
||||
|
||||
94
ipn/prefs.go
94
ipn/prefs.go
@@ -11,6 +11,8 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/atomicfile"
|
||||
@@ -18,44 +20,57 @@ import (
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=Prefs -output=prefs_clone.go
|
||||
|
||||
// Prefs are the user modifiable settings of the Tailscale node agent.
|
||||
type Prefs struct {
|
||||
// ControlURL is the URL of the control server to use.
|
||||
ControlURL string
|
||||
|
||||
// RouteAll specifies whether to accept subnet and default routes
|
||||
// advertised by other nodes on the Tailscale network.
|
||||
RouteAll bool
|
||||
|
||||
// AllowSingleHosts specifies whether to install routes for each
|
||||
// node IP on the tailscale network, in addition to a route for
|
||||
// the whole network.
|
||||
// This corresponds to the "tailscale up --host-routes" value,
|
||||
// which defaults to true.
|
||||
//
|
||||
// TODO(danderson): why do we have this? It dumps a lot of stuff
|
||||
// into the routing table, and a single network route _should_ be
|
||||
// all that we need. But when I turn this off in my tailscaled,
|
||||
// packets stop flowing. What's up with that?
|
||||
AllowSingleHosts bool
|
||||
|
||||
// CorpDNS specifies whether to install the Tailscale network's
|
||||
// DNS configuration, if it exists.
|
||||
CorpDNS bool
|
||||
|
||||
// WantRunning indicates whether networking should be active on
|
||||
// this node.
|
||||
WantRunning bool
|
||||
|
||||
// ShieldsUp indicates whether to block all incoming connections,
|
||||
// regardless of the control-provided packet filter. If false, we
|
||||
// use the packet filter as provided. If true, we block incoming
|
||||
// connections.
|
||||
ShieldsUp bool
|
||||
|
||||
// AdvertiseTags specifies groups that this node wants to join, for
|
||||
// purposes of ACL enforcement. These can be referenced from the ACL
|
||||
// security policy. Note that advertising a tag doesn't guarantee that
|
||||
// the control server will allow you to take on the rights for that
|
||||
// tag.
|
||||
AdvertiseTags []string
|
||||
|
||||
// Hostname is the hostname to use for identifying the node. If
|
||||
// not set, os.Hostname is used.
|
||||
Hostname string
|
||||
|
||||
// OSVersion overrides tailcfg.Hostinfo's OSVersion.
|
||||
OSVersion string
|
||||
|
||||
// DeviceModel overrides tailcfg.Hostinfo's DeviceModel.
|
||||
DeviceModel string
|
||||
|
||||
@@ -67,8 +82,17 @@ type Prefs struct {
|
||||
// users narrow it down a bit.
|
||||
NotepadURLs bool
|
||||
|
||||
// DisableDERP prevents DERP from being used.
|
||||
DisableDERP bool
|
||||
// ForceDaemon specifies whether a platform that normally
|
||||
// operates in "client mode" (that is, requires an active user
|
||||
// logged in with the GUI app running) should keep running after the
|
||||
// GUI ends and/or the user logs out.
|
||||
//
|
||||
// The only current applicable platform is Windows. This
|
||||
// forced Windows to go into "server mode" where Tailscale is
|
||||
// running even with no users logged in. This might also be
|
||||
// used for macOS in the future. This setting has no effect
|
||||
// for Linux/etc, which always operate in daemon mode.
|
||||
ForceDaemon bool `json:"ForceDaemon,omitempty"`
|
||||
|
||||
// The following block of options only have an effect on Linux.
|
||||
|
||||
@@ -76,6 +100,7 @@ type Prefs struct {
|
||||
// Tailscale network as reachable through the current
|
||||
// node.
|
||||
AdvertiseRoutes []wgcfg.CIDR
|
||||
|
||||
// NoSNAT specifies whether to source NAT traffic going to
|
||||
// destinations in AdvertiseRoutes. The default is to apply source
|
||||
// NAT, which makes the traffic appear to come from the router
|
||||
@@ -87,6 +112,7 @@ type Prefs struct {
|
||||
//
|
||||
// Linux-only.
|
||||
NoSNAT bool
|
||||
|
||||
// NetfilterMode specifies how much to manage netfilter rules for
|
||||
// Tailscale, if at all.
|
||||
NetfilterMode router.NetfilterMode
|
||||
@@ -102,16 +128,46 @@ type Prefs struct {
|
||||
// IsEmpty reports whether p is nil or pointing to a Prefs zero value.
|
||||
func (p *Prefs) IsEmpty() bool { return p == nil || p.Equals(&Prefs{}) }
|
||||
|
||||
func (p *Prefs) Pretty() string {
|
||||
var pp string
|
||||
if p.Persist != nil {
|
||||
pp = p.Persist.Pretty()
|
||||
} else {
|
||||
pp = "Persist=nil"
|
||||
func (p *Prefs) Pretty() string { return p.pretty(runtime.GOOS) }
|
||||
func (p *Prefs) pretty(goos string) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Prefs{")
|
||||
fmt.Fprintf(&sb, "ra=%v ", p.RouteAll)
|
||||
if !p.AllowSingleHosts {
|
||||
sb.WriteString("mesh=false ")
|
||||
}
|
||||
return fmt.Sprintf("Prefs{ra=%v mesh=%v dns=%v want=%v notepad=%v derp=%v shields=%v routes=%v snat=%v nf=%v %v}",
|
||||
p.RouteAll, p.AllowSingleHosts, p.CorpDNS, p.WantRunning,
|
||||
p.NotepadURLs, !p.DisableDERP, p.ShieldsUp, p.AdvertiseRoutes, !p.NoSNAT, p.NetfilterMode, pp)
|
||||
fmt.Fprintf(&sb, "dns=%v want=%v ", p.CorpDNS, p.WantRunning)
|
||||
if p.ForceDaemon {
|
||||
sb.WriteString("server=true ")
|
||||
}
|
||||
if p.NotepadURLs {
|
||||
sb.WriteString("notepad=true ")
|
||||
}
|
||||
if p.ShieldsUp {
|
||||
sb.WriteString("shields=true ")
|
||||
}
|
||||
if len(p.AdvertiseRoutes) > 0 || goos == "linux" {
|
||||
fmt.Fprintf(&sb, "routes=%v ", p.AdvertiseRoutes)
|
||||
}
|
||||
if len(p.AdvertiseRoutes) > 0 || p.NoSNAT {
|
||||
fmt.Fprintf(&sb, "snat=%v ", !p.NoSNAT)
|
||||
}
|
||||
if len(p.AdvertiseTags) > 0 {
|
||||
fmt.Fprintf(&sb, "tags=%s ", strings.Join(p.AdvertiseTags, ","))
|
||||
}
|
||||
if goos == "linux" {
|
||||
fmt.Fprintf(&sb, "nf=%v ", p.NetfilterMode)
|
||||
}
|
||||
if p.ControlURL != "" && p.ControlURL != "https://login.tailscale.com" {
|
||||
fmt.Fprintf(&sb, "url=%q ", p.ControlURL)
|
||||
}
|
||||
if p.Persist != nil {
|
||||
sb.WriteString(p.Persist.Pretty())
|
||||
} else {
|
||||
sb.WriteString("Persist=nil")
|
||||
}
|
||||
sb.WriteString("}")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (p *Prefs) ToBytes() []byte {
|
||||
@@ -137,13 +193,13 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
||||
p.CorpDNS == p2.CorpDNS &&
|
||||
p.WantRunning == p2.WantRunning &&
|
||||
p.NotepadURLs == p2.NotepadURLs &&
|
||||
p.DisableDERP == p2.DisableDERP &&
|
||||
p.ShieldsUp == p2.ShieldsUp &&
|
||||
p.NoSNAT == p2.NoSNAT &&
|
||||
p.NetfilterMode == p2.NetfilterMode &&
|
||||
p.Hostname == p2.Hostname &&
|
||||
p.OSVersion == p2.OSVersion &&
|
||||
p.DeviceModel == p2.DeviceModel &&
|
||||
p.ForceDaemon == p2.ForceDaemon &&
|
||||
compareIPNets(p.AdvertiseRoutes, p2.AdvertiseRoutes) &&
|
||||
compareStrings(p.AdvertiseTags, p2.AdvertiseTags) &&
|
||||
p.Persist.Equals(p2.Persist)
|
||||
@@ -213,26 +269,16 @@ func PrefsFromBytes(b []byte, enforceDefaults bool) (*Prefs, error) {
|
||||
return p, err
|
||||
}
|
||||
|
||||
// Clone returns a deep copy of p.
|
||||
func (p *Prefs) Clone() *Prefs {
|
||||
// TODO: write a faster/non-Fatal-y Clone implementation?
|
||||
p2, err := PrefsFromBytes(p.ToBytes(), false)
|
||||
if err != nil {
|
||||
log.Fatalf("Prefs was uncopyable: %v\n", err)
|
||||
}
|
||||
return p2
|
||||
}
|
||||
|
||||
// LoadPrefs loads a legacy relaynode config file into Prefs
|
||||
// with sensible migration defaults set.
|
||||
func LoadPrefs(filename string) (*Prefs, error) {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("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
|
||||
}
|
||||
|
||||
51
ipn/prefs_clone.go
Normal file
51
ipn/prefs_clone.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner -type Prefs; DO NOT EDIT.
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
// Clone makes a deep copy of Prefs.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Prefs) Clone() *Prefs {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Prefs)
|
||||
*dst = *src
|
||||
dst.AdvertiseTags = append(src.AdvertiseTags[:0:0], src.AdvertiseTags...)
|
||||
dst.AdvertiseRoutes = append(src.AdvertiseRoutes[:0:0], src.AdvertiseRoutes...)
|
||||
if dst.Persist != nil {
|
||||
dst.Persist = new(controlclient.Persist)
|
||||
*dst.Persist = *src.Persist
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with command:
|
||||
// tailscale.com/cmd/cloner -type Prefs
|
||||
var _PrefsNeedsRegeneration = Prefs(struct {
|
||||
ControlURL string
|
||||
RouteAll bool
|
||||
AllowSingleHosts bool
|
||||
CorpDNS bool
|
||||
WantRunning bool
|
||||
ShieldsUp bool
|
||||
AdvertiseTags []string
|
||||
Hostname string
|
||||
OSVersion string
|
||||
DeviceModel string
|
||||
NotepadURLs bool
|
||||
ForceDaemon bool
|
||||
AdvertiseRoutes []wgcfg.CIDR
|
||||
NoSNAT bool
|
||||
NetfilterMode router.NetfilterMode
|
||||
Persist *controlclient.Persist
|
||||
}{})
|
||||
@@ -5,8 +5,12 @@
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/control/controlclient"
|
||||
@@ -24,7 +28,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", "ForceDaemon", "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)
|
||||
@@ -278,3 +282,92 @@ func TestPrefsPersist(t *testing.T) {
|
||||
}
|
||||
checkPrefs(t, p)
|
||||
}
|
||||
|
||||
func TestPrefsPretty(t *testing.T) {
|
||||
tests := []struct {
|
||||
p Prefs
|
||||
os string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
Prefs{},
|
||||
"linux",
|
||||
"Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist=nil}",
|
||||
},
|
||||
{
|
||||
Prefs{},
|
||||
"windows",
|
||||
"Prefs{ra=false mesh=false dns=false want=false Persist=nil}",
|
||||
},
|
||||
{
|
||||
Prefs{ShieldsUp: true},
|
||||
"windows",
|
||||
"Prefs{ra=false mesh=false dns=false want=false shields=true Persist=nil}",
|
||||
},
|
||||
{
|
||||
Prefs{AllowSingleHosts: true},
|
||||
"windows",
|
||||
"Prefs{ra=false dns=false want=false Persist=nil}",
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
NotepadURLs: true,
|
||||
AllowSingleHosts: true,
|
||||
},
|
||||
"windows",
|
||||
"Prefs{ra=false dns=false want=false notepad=true Persist=nil}",
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
AllowSingleHosts: true,
|
||||
WantRunning: true,
|
||||
ForceDaemon: true, // server mode
|
||||
},
|
||||
"windows",
|
||||
"Prefs{ra=false dns=false want=true server=true Persist=nil}",
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
AllowSingleHosts: true,
|
||||
WantRunning: true,
|
||||
ControlURL: "http://localhost:1234",
|
||||
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
||||
},
|
||||
"darwin",
|
||||
`Prefs{ra=false dns=false want=true tags=tag:foo,tag:bar url="http://localhost:1234" Persist=nil}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
Persist: &controlclient.Persist{},
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist{lm=, o=, n= u=""}}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
Persist: &controlclient.Persist{
|
||||
PrivateNodeKey: wgcfg.PrivateKey{1: 1},
|
||||
},
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist{lm=, o=, n=[B1VKl] u=""}}`,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
got := tt.p.pretty(tt.os)
|
||||
if got != tt.want {
|
||||
t.Errorf("%d. wrong String:\n got: %s\nwant: %s\n", i, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPrefsNotExist(t *testing.T) {
|
||||
bogusFile := fmt.Sprintf("/tmp/not-exist-%d", time.Now().UnixNano())
|
||||
|
||||
p, err := LoadPrefs(bogusFile)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
// expected.
|
||||
return
|
||||
}
|
||||
t.Fatalf("unexpected prefs=%#v, err=%v", p, err)
|
||||
}
|
||||
|
||||
42
ipn/store.go
42
ipn/store.go
@@ -5,9 +5,12 @@
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
@@ -19,6 +22,30 @@ import (
|
||||
// requested state ID doesn't exist.
|
||||
var ErrStateNotExist = errors.New("no state with given ID")
|
||||
|
||||
const (
|
||||
// MachineKeyStateKey is the key under which we store the machine key,
|
||||
// in its wgcfg.PrivateKey.MarshalText representation.
|
||||
MachineKeyStateKey = StateKey("_machinekey")
|
||||
|
||||
// GlobalDaemonStateKey is the ipn.StateKey that tailscaled
|
||||
// loads on startup.
|
||||
//
|
||||
// We have to support multiple state keys for other OSes (Windows in
|
||||
// particular), but right now Unix daemons run with a single
|
||||
// node-global state. To keep open the option of having per-user state
|
||||
// later, the global state key doesn't look like a username.
|
||||
GlobalDaemonStateKey = StateKey("_daemon")
|
||||
|
||||
// ServerModeStartKey's value, if non-empty, is the value of a
|
||||
// StateKey containing the prefs to start with which to start the
|
||||
// server.
|
||||
//
|
||||
// For example, the value might be "user-1234", meaning the
|
||||
// the server should start with the Prefs JSON loaded from
|
||||
// StateKey "user-1234".
|
||||
ServerModeStartKey = StateKey("server-mode-start-key")
|
||||
)
|
||||
|
||||
// StateStore persists state, and produces it back on request.
|
||||
type StateStore interface {
|
||||
// ReadState returns the bytes associated with ID. Returns (nil,
|
||||
@@ -34,6 +61,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,9 +96,19 @@ type FileStore struct {
|
||||
cache map[StateKey][]byte
|
||||
}
|
||||
|
||||
func (s *FileStore) String() string { return fmt.Sprintf("FileStore(%q)", s.path) }
|
||||
|
||||
// NewFileStore returns a new file store that persists to path.
|
||||
func NewFileStore(path string) (*FileStore, error) {
|
||||
bs, err := ioutil.ReadFile(path)
|
||||
|
||||
// Treat an empty file as a missing file.
|
||||
// (https://github.com/tailscale/tailscale/issues/895#issuecomment-723255589)
|
||||
if err == nil && len(bs) == 0 {
|
||||
log.Printf("ipn.NewFileStore(%q): file empty; treating it like a missing file [warning]", path)
|
||||
err = os.ErrNotExist
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// Write out an initial file, to verify that we can write
|
||||
@@ -112,6 +151,9 @@ func (s *FileStore) ReadState(id StateKey) ([]byte, error) {
|
||||
func (s *FileStore) WriteState(id StateKey, bs []byte) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if bytes.Equal(s.cache[id], bs) {
|
||||
return nil
|
||||
}
|
||||
s.cache[id] = append([]byte(nil), bs...)
|
||||
bs, err := json.MarshalIndent(s.cache, "", " ")
|
||||
if err != nil {
|
||||
|
||||
199
log/filelogger/log.go
Normal file
199
log/filelogger/log.go
Normal file
@@ -0,0 +1,199 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package filelogger provides localdisk log writing & rotation, primarily for Windows
|
||||
// clients. (We get this for free on other platforms.)
|
||||
package filelogger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
maxSize = 100 << 20
|
||||
maxFiles = 50
|
||||
)
|
||||
|
||||
// New returns a logf wrapper that appends to local disk log
|
||||
// files on Windows, rotating old log files as needed to stay under
|
||||
// file count & byte limits.
|
||||
func New(fileBasePrefix, logID string, logf logger.Logf) logger.Logf {
|
||||
if runtime.GOOS != "windows" {
|
||||
panic("not yet supported on any platform except Windows")
|
||||
}
|
||||
if logf == nil {
|
||||
panic("nil logf")
|
||||
}
|
||||
dir := filepath.Join(os.Getenv("LocalAppData"), "Tailscale", "Logs")
|
||||
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
log.Printf("failed to create local log directory; not writing logs to disk: %v", err)
|
||||
return logf
|
||||
}
|
||||
logf("local disk logdir: %v", dir)
|
||||
lfw := &logFileWriter{
|
||||
fileBasePrefix: fileBasePrefix,
|
||||
logID: logID,
|
||||
dir: dir,
|
||||
wrappedLogf: logf,
|
||||
}
|
||||
return lfw.Logf
|
||||
}
|
||||
|
||||
// logFileWriter is the state for the log writer & rotator.
|
||||
type logFileWriter struct {
|
||||
dir string // e.g. `C:\Users\FooBarUser\AppData\Local\Tailscale\Logs`
|
||||
logID string // hex logID
|
||||
fileBasePrefix string // e.g. "tailscale-service" or "tailscale-gui"
|
||||
wrappedLogf logger.Logf // underlying logger to send to
|
||||
|
||||
mu sync.Mutex // guards following
|
||||
buf bytes.Buffer // scratch buffer to avoid allocs
|
||||
fday civilDay // day that f was opened; zero means no file yet open
|
||||
f *os.File // file currently opened for append
|
||||
}
|
||||
|
||||
// civilDay is a year, month, and day in the local timezone.
|
||||
// It's a comparable value type.
|
||||
type civilDay struct {
|
||||
year int
|
||||
month time.Month
|
||||
day int
|
||||
}
|
||||
|
||||
func dayOf(t time.Time) civilDay {
|
||||
return civilDay{t.Year(), t.Month(), t.Day()}
|
||||
}
|
||||
|
||||
func (w *logFileWriter) Logf(format string, a ...interface{}) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
w.buf.Reset()
|
||||
fmt.Fprintf(&w.buf, format, a...)
|
||||
if w.buf.Len() == 0 {
|
||||
return
|
||||
}
|
||||
out := w.buf.Bytes()
|
||||
w.wrappedLogf("%s", out)
|
||||
|
||||
// Make sure there's a final newline before we write to the log file.
|
||||
if out[len(out)-1] != '\n' {
|
||||
w.buf.WriteByte('\n')
|
||||
out = w.buf.Bytes()
|
||||
}
|
||||
|
||||
w.appendToFileLocked(out)
|
||||
}
|
||||
|
||||
// out should end in a newline.
|
||||
// w.mu must be held.
|
||||
func (w *logFileWriter) appendToFileLocked(out []byte) {
|
||||
now := time.Now()
|
||||
day := dayOf(now)
|
||||
if w.fday != day {
|
||||
w.startNewFileLocked()
|
||||
}
|
||||
if w.f != nil {
|
||||
// RFC3339Nano but with a fixed number (3) of nanosecond digits:
|
||||
const formatPre = "2006-01-02T15:04:05"
|
||||
const formatPost = "Z07:00"
|
||||
fmt.Fprintf(w.f, "%s.%03d%s: %s",
|
||||
now.Format(formatPre),
|
||||
now.Nanosecond()/int(time.Millisecond/time.Nanosecond),
|
||||
now.Format(formatPost),
|
||||
out)
|
||||
}
|
||||
}
|
||||
|
||||
// startNewFileLocked opens a new log file for writing
|
||||
// and also cleans up any old files.
|
||||
//
|
||||
// w.mu must be held.
|
||||
func (w *logFileWriter) startNewFileLocked() {
|
||||
var oldName string
|
||||
if w.f != nil {
|
||||
oldName = filepath.Base(w.f.Name())
|
||||
w.f.Close()
|
||||
w.f = nil
|
||||
w.fday = civilDay{}
|
||||
}
|
||||
w.cleanLocked()
|
||||
|
||||
now := time.Now()
|
||||
day := dayOf(now)
|
||||
name := filepath.Join(w.dir, fmt.Sprintf("%s-%04d%02d%02dT%02d%02d%02d-%d.txt",
|
||||
w.fileBasePrefix,
|
||||
day.year,
|
||||
day.month,
|
||||
day.day,
|
||||
now.Hour(),
|
||||
now.Minute(),
|
||||
now.Second(),
|
||||
now.Unix()))
|
||||
var err error
|
||||
w.f, err = os.Create(name)
|
||||
if err != nil {
|
||||
w.wrappedLogf("failed to create log file: %v", err)
|
||||
return
|
||||
}
|
||||
if oldName != "" {
|
||||
fmt.Fprintf(w.f, "(logID %q; continued from log file %s)\n", w.logID, oldName)
|
||||
} else {
|
||||
fmt.Fprintf(w.f, "(logID %q)\n", w.logID)
|
||||
}
|
||||
w.fday = day
|
||||
}
|
||||
|
||||
// cleanLocked cleans up old log files.
|
||||
//
|
||||
// w.mu must be held.
|
||||
func (w *logFileWriter) cleanLocked() {
|
||||
fis, _ := ioutil.ReadDir(w.dir)
|
||||
prefix := w.fileBasePrefix + "-"
|
||||
fileSize := map[string]int64{}
|
||||
var files []string
|
||||
var sumSize int64
|
||||
for _, fi := range fis {
|
||||
baseName := filepath.Base(fi.Name())
|
||||
if !strings.HasPrefix(baseName, prefix) {
|
||||
continue
|
||||
}
|
||||
size := fi.Size()
|
||||
fileSize[baseName] = size
|
||||
sumSize += size
|
||||
files = append(files, baseName)
|
||||
}
|
||||
if sumSize > maxSize {
|
||||
w.wrappedLogf("cleaning log files; sum byte count %d > %d", sumSize, maxSize)
|
||||
}
|
||||
if len(files) > maxFiles {
|
||||
w.wrappedLogf("cleaning log files; number of files %d > %d", len(files), maxFiles)
|
||||
}
|
||||
for (sumSize > maxSize || len(files) > maxFiles) && len(files) > 0 {
|
||||
target := files[0]
|
||||
files = files[1:]
|
||||
|
||||
targetSize := fileSize[target]
|
||||
targetFull := filepath.Join(w.dir, target)
|
||||
err := os.Remove(targetFull)
|
||||
if err != nil {
|
||||
w.wrappedLogf("error cleaning log file: %v", err)
|
||||
} else {
|
||||
sumSize -= targetSize
|
||||
w.wrappedLogf("cleaned log file %s (size %d); new bytes=%v, files=%v", targetFull, targetSize, sumSize, len(files))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import (
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/racebuild"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -315,6 +316,9 @@ func New(collection string) *Policy {
|
||||
} else {
|
||||
lflags = log.LstdFlags
|
||||
}
|
||||
if v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_LOG_TIME")); v {
|
||||
lflags = log.LstdFlags | log.Lmicroseconds
|
||||
}
|
||||
if runningUnderSystemd() {
|
||||
// If journalctl is going to prepend its own timestamp
|
||||
// anyway, no need to add one.
|
||||
@@ -356,7 +360,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")
|
||||
@@ -392,8 +396,8 @@ func New(collection string) *Policy {
|
||||
log.SetOutput(lw)
|
||||
|
||||
log.Printf("Program starting: v%v, Go %v: %#v",
|
||||
version.LONG,
|
||||
strings.TrimPrefix(runtime.Version(), "go"),
|
||||
version.Long,
|
||||
goVersion(),
|
||||
os.Args)
|
||||
log.Printf("LogID: %v", newc.PublicID)
|
||||
if filchErr != nil {
|
||||
@@ -476,3 +480,11 @@ func newLogtailTransport(host string) *http.Transport {
|
||||
|
||||
return tr
|
||||
}
|
||||
|
||||
func goVersion() string {
|
||||
v := strings.TrimPrefix(runtime.Version(), "go")
|
||||
if racebuild.On {
|
||||
return v + "-race"
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
@@ -56,19 +56,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 +79,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 +92,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 +110,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")
|
||||
@@ -143,6 +129,14 @@ func TestRecover(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFilchStderr(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
// TODO(bradfitz): this is broken on Windows but not
|
||||
// fully sure why. Investigate. But notably, the
|
||||
// stderrFD variable (defined in filch.go) and set
|
||||
// below is only ever read in filch_unix.go. So just
|
||||
// skip this for test for now.
|
||||
t.Skip("test broken on Windows")
|
||||
}
|
||||
pipeR, pipeW, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -155,8 +149,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 {
|
||||
|
||||
@@ -9,8 +9,11 @@ package dnscache
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -25,7 +28,7 @@ func preferGoResolver() bool {
|
||||
// There does not appear to be a local resolver running
|
||||
// on iOS, and NetworkExtension is good at isolating DNS.
|
||||
// So do not use the Go resolver on macOS/iOS.
|
||||
if runtime.GOOS == "darwin" {
|
||||
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -42,8 +45,6 @@ func preferGoResolver() bool {
|
||||
// Get returns a caching Resolver singleton.
|
||||
func Get() *Resolver { return single }
|
||||
|
||||
const fixedTTL = 10 * time.Minute
|
||||
|
||||
// Resolver is a minimal DNS caching resolver.
|
||||
//
|
||||
// The TTL is always fixed for now. It's not intended for general use.
|
||||
@@ -54,6 +55,15 @@ type Resolver struct {
|
||||
// If nil, net.DefaultResolver is used.
|
||||
Forward *net.Resolver
|
||||
|
||||
// TTL is how long to keep entries cached
|
||||
//
|
||||
// If zero, a default (currently 10 minutes) is used.
|
||||
TTL time.Duration
|
||||
|
||||
// UseLastGood controls whether a cached entry older than TTL is used
|
||||
// if a refresh fails.
|
||||
UseLastGood bool
|
||||
|
||||
sf singleflight.Group
|
||||
|
||||
mu sync.Mutex
|
||||
@@ -72,16 +82,31 @@ func (r *Resolver) fwd() *net.Resolver {
|
||||
return net.DefaultResolver
|
||||
}
|
||||
|
||||
func (r *Resolver) ttl() time.Duration {
|
||||
if r.TTL > 0 {
|
||||
return r.TTL
|
||||
}
|
||||
return 10 * time.Minute
|
||||
}
|
||||
|
||||
var debug, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_DNS_CACHE"))
|
||||
|
||||
// LookupIP returns the first IPv4 address found, otherwise the first IPv6 address.
|
||||
func (r *Resolver) LookupIP(ctx context.Context, host string) (net.IP, error) {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
return ip4, nil
|
||||
}
|
||||
if debug {
|
||||
log.Printf("dnscache: %q is an IP", host)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
if ip, ok := r.lookupIPCache(host); ok {
|
||||
if debug {
|
||||
log.Printf("dnscache: %q = %v (cached)", host, ip)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
@@ -95,10 +120,24 @@ func (r *Resolver) LookupIP(ctx context.Context, host string) (net.IP, error) {
|
||||
select {
|
||||
case res := <-ch:
|
||||
if res.Err != nil {
|
||||
if r.UseLastGood {
|
||||
if ip, ok := r.lookupIPCacheExpired(host); ok {
|
||||
if debug {
|
||||
log.Printf("dnscache: %q using %v after error", host, ip)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
if debug {
|
||||
log.Printf("dnscache: error resolving %q: %v", host, res.Err)
|
||||
}
|
||||
return nil, res.Err
|
||||
}
|
||||
return res.Val.(net.IP), nil
|
||||
case <-ctx.Done():
|
||||
if debug {
|
||||
log.Printf("dnscache: context done while resolving %q: %v", host, ctx.Err())
|
||||
}
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
@@ -112,12 +151,41 @@ func (r *Resolver) lookupIPCache(host string) (ip net.IP, ok bool) {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupIPCacheExpired(host string) (ip net.IP, ok bool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if ent, ok := r.ipCache[host]; ok {
|
||||
return ent.ip, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupTimeoutForHost(host string) time.Duration {
|
||||
if r.UseLastGood {
|
||||
if _, ok := r.lookupIPCacheExpired(host); ok {
|
||||
// If we have some previous good value for this host,
|
||||
// don't give this DNS lookup much time. If we're in a
|
||||
// situation where the user's DNS server is unreachable
|
||||
// (e.g. their corp DNS server is behind a subnet router
|
||||
// that can't come up due to Tailscale needing to
|
||||
// connect to itself), then we want to fail fast and let
|
||||
// our caller (who set UseLastGood) fall back to using
|
||||
// the last-known-good IP address.
|
||||
return 3 * time.Second
|
||||
}
|
||||
}
|
||||
return 10 * time.Second
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupIP(host string) (net.IP, error) {
|
||||
if ip, ok := r.lookupIPCache(host); ok {
|
||||
if debug {
|
||||
log.Printf("dnscache: %q found in cache as %v", host, ip)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), r.lookupTimeoutForHost(host))
|
||||
defer cancel()
|
||||
ips, err := r.fwd().LookupIPAddr(ctx, host)
|
||||
if err != nil {
|
||||
@@ -129,19 +197,26 @@ func (r *Resolver) lookupIP(host string) (net.IP, error) {
|
||||
|
||||
for _, ipa := range ips {
|
||||
if ip4 := ipa.IP.To4(); ip4 != nil {
|
||||
return r.addIPCache(host, ip4, fixedTTL), nil
|
||||
return r.addIPCache(host, ip4, r.ttl()), nil
|
||||
}
|
||||
}
|
||||
return r.addIPCache(host, ips[0].IP, fixedTTL), nil
|
||||
return r.addIPCache(host, ips[0].IP, r.ttl()), nil
|
||||
}
|
||||
|
||||
func (r *Resolver) addIPCache(host string, ip net.IP, d time.Duration) net.IP {
|
||||
if isPrivateIP(ip) {
|
||||
// Don't cache obviously wrong entries from captive portals.
|
||||
// TODO: use DoH or DoT for the forwarding resolver?
|
||||
if debug {
|
||||
log.Printf("dnscache: %q resolved to private IP %v; using but not caching", host, ip)
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
if debug {
|
||||
log.Printf("dnscache: %q resolved to IP %v; caching", host, ip)
|
||||
}
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.ipCache == nil {
|
||||
@@ -168,3 +243,26 @@ var (
|
||||
private2 = mustCIDR("172.16.0.0/12")
|
||||
private3 = mustCIDR("192.168.0.0/16")
|
||||
)
|
||||
|
||||
type DialContextFunc func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
|
||||
// Dialer returns a wrapped DialContext func that uses the provided dnsCache.
|
||||
func Dialer(fwd DialContextFunc, dnsCache *Resolver) DialContextFunc {
|
||||
return func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
// Bogus. But just let the real dialer return an error rather than
|
||||
// inventing a similar one.
|
||||
return fwd(ctx, network, address)
|
||||
}
|
||||
ip, err := dnsCache.LookupIP(ctx, host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve %q: %w", host, err)
|
||||
}
|
||||
dst := net.JoinHostPort(ip.String(), port)
|
||||
if debug {
|
||||
log.Printf("dnscache: dialing %s, %s for %s", network, dst, address)
|
||||
}
|
||||
return fwd(ctx, network, dst)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,16 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/tailscale/winipcfg-go"
|
||||
"go4.org/mem"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tsconst"
|
||||
"tailscale.com/util/lineread"
|
||||
@@ -17,6 +22,7 @@ import (
|
||||
|
||||
func init() {
|
||||
likelyHomeRouterIP = likelyHomeRouterIPWindows
|
||||
getPAC = getPACWindows
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -76,19 +82,115 @@ 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()
|
||||
func NonTailscaleMTUs() (map[winipcfg.LUID]uint32, error) {
|
||||
mtus := map[winipcfg.LUID]uint32{}
|
||||
ifs, err := NonTailscaleInterfaces()
|
||||
for luid, iface := range ifs {
|
||||
mtus[luid] = iface.MTU
|
||||
}
|
||||
return mtus, err
|
||||
}
|
||||
|
||||
// NonTailscaleInterfaces returns a map of interface LUID to interface
|
||||
// for all interfaces except Tailscale tunnels.
|
||||
func NonTailscaleInterfaces() (map[winipcfg.LUID]*winipcfg.IPAdapterAddresses, error) {
|
||||
ifs, err := winipcfg.GetAdaptersAddresses(windows.AF_UNSPEC, winipcfg.GAAFlagIncludeAllInterfaces)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := map[uint64]uint32{}
|
||||
ret := map[winipcfg.LUID]*winipcfg.IPAdapterAddresses{}
|
||||
for _, iface := range ifs {
|
||||
if iface.Description == tsconst.WintunInterfaceDesc {
|
||||
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.IPAdapterAddresses, error) {
|
||||
ifs, err := NonTailscaleInterfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
routes, err := winipcfg.GetIPForwardTable2(family)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
bestMetric := ^uint32(0)
|
||||
var bestIface *winipcfg.IPAdapterAddresses
|
||||
for _, route := range routes {
|
||||
iface := ifs[route.InterfaceLUID]
|
||||
if route.DestinationPrefix.PrefixLength != 0 || iface == nil {
|
||||
continue
|
||||
}
|
||||
if iface.OperStatus == winipcfg.IfOperStatusUp && route.Metric < bestMetric {
|
||||
bestMetric = route.Metric
|
||||
bestIface = iface
|
||||
}
|
||||
}
|
||||
|
||||
return bestIface, nil
|
||||
}
|
||||
|
||||
func DefaultRouteInterface() (string, error) {
|
||||
iface, err := GetWindowsDefault(windows.AF_INET)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if iface == nil {
|
||||
return "(none)", nil
|
||||
}
|
||||
return fmt.Sprintf("%s (%s)", iface.FriendlyName(), iface.Description()), nil
|
||||
}
|
||||
|
||||
var (
|
||||
winHTTP = windows.NewLazySystemDLL("winhttp.dll")
|
||||
detectAutoProxyConfigURL = winHTTP.NewProc("WinHttpDetectAutoProxyConfigUrl")
|
||||
|
||||
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
globalFree = kernel32.NewProc("GlobalFree")
|
||||
)
|
||||
|
||||
const (
|
||||
winHTTP_AUTO_DETECT_TYPE_DHCP = 0x00000001
|
||||
winHTTP_AUTO_DETECT_TYPE_DNS_A = 0x00000002
|
||||
)
|
||||
|
||||
func getPACWindows() string {
|
||||
var res *uint16
|
||||
r, _, e := detectAutoProxyConfigURL.Call(
|
||||
winHTTP_AUTO_DETECT_TYPE_DHCP|winHTTP_AUTO_DETECT_TYPE_DNS_A,
|
||||
uintptr(unsafe.Pointer(&res)),
|
||||
)
|
||||
if r == 1 {
|
||||
if res == nil {
|
||||
log.Printf("getPACWindows: unexpected success with nil result")
|
||||
return ""
|
||||
}
|
||||
defer globalFree.Call(uintptr(unsafe.Pointer(res)))
|
||||
s := windows.UTF16PtrToString(res)
|
||||
if _, err := url.Parse(s); err != nil {
|
||||
log.Printf("getPACWindows: invalid URL %q from winhttp; ignoring", s)
|
||||
return ""
|
||||
}
|
||||
return s
|
||||
}
|
||||
const (
|
||||
ERROR_WINHTTP_AUTODETECTION_FAILED = 12180
|
||||
)
|
||||
if e == syscall.Errno(ERROR_WINHTTP_AUTODETECTION_FAILED) {
|
||||
// Common case on networks without advertised PAC.
|
||||
return ""
|
||||
}
|
||||
log.Printf("getPACWindows: %T=%v", e, e) // syscall.Errno=0x....
|
||||
return ""
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,6 @@ import (
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/stun"
|
||||
@@ -134,10 +133,6 @@ func cloneDurationMap(m map[int]time.Duration) map[int]time.Duration {
|
||||
|
||||
// Client generates a netcheck Report.
|
||||
type Client struct {
|
||||
// DNSCache optionally specifies a DNSCache to use.
|
||||
// If nil, a DNS cache is not used.
|
||||
DNSCache *dnscache.Resolver
|
||||
|
||||
// Verbose enables verbose logging.
|
||||
Verbose bool
|
||||
|
||||
@@ -156,6 +151,15 @@ type Client struct {
|
||||
// GetSTUNConn6 is like GetSTUNConn4, but for IPv6.
|
||||
GetSTUNConn6 func() STUNConn
|
||||
|
||||
// SkipExternalNetwork controls whether the client should not try
|
||||
// to reach things other than localhost. This is set to true
|
||||
// in tests to avoid probing the local LAN's router, etc.
|
||||
SkipExternalNetwork bool
|
||||
|
||||
// UDPBindAddr, if non-empty, is the address to listen on for UDP.
|
||||
// It defaults to ":0".
|
||||
UDPBindAddr string
|
||||
|
||||
mu sync.Mutex // guards following
|
||||
nextFull bool // do a full region scan, even if last != nil
|
||||
prev map[time.Time]*Report // some previous reports
|
||||
@@ -772,6 +776,13 @@ func newReport() *Report {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) udpBindAddr() string {
|
||||
if v := c.UDPBindAddr; v != "" {
|
||||
return v
|
||||
}
|
||||
return ":0"
|
||||
}
|
||||
|
||||
// GetReport gets a report.
|
||||
//
|
||||
// It may not be called concurrently with itself.
|
||||
@@ -831,8 +842,10 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
||||
}
|
||||
defer rs.pc4Hair.Close()
|
||||
|
||||
rs.waitPortMap.Add(1)
|
||||
go rs.probePortMapServices()
|
||||
if !c.SkipExternalNetwork {
|
||||
rs.waitPortMap.Add(1)
|
||||
go rs.probePortMapServices()
|
||||
}
|
||||
|
||||
// At least the Apple Airport Extreme doesn't allow hairpin
|
||||
// sends from a private socket until it's seen traffic from
|
||||
@@ -850,7 +863,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
||||
if f := c.GetSTUNConn4; f != nil {
|
||||
rs.pc4 = f()
|
||||
} else {
|
||||
u4, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
|
||||
u4, err := netns.Listener().ListenPacket(ctx, "udp4", c.udpBindAddr())
|
||||
if err != nil {
|
||||
c.logf("udp4: %v", err)
|
||||
return nil, err
|
||||
@@ -863,7 +876,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
||||
if f := c.GetSTUNConn6; f != nil {
|
||||
rs.pc6 = f()
|
||||
} else {
|
||||
u6, err := netns.Listener().ListenPacket(ctx, "udp6", ":0")
|
||||
u6, err := netns.Listener().ListenPacket(ctx, "udp6", c.udpBindAddr())
|
||||
if err != nil {
|
||||
c.logf("udp6: %v", err)
|
||||
} else {
|
||||
@@ -902,8 +915,10 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
||||
|
||||
rs.waitHairCheck(ctx)
|
||||
c.vlogf("hairCheck done")
|
||||
rs.waitPortMap.Wait()
|
||||
c.vlogf("portMap done")
|
||||
if !c.SkipExternalNetwork {
|
||||
rs.waitPortMap.Wait()
|
||||
c.vlogf("portMap done")
|
||||
}
|
||||
rs.stopTimers()
|
||||
|
||||
// Try HTTPS latency check if all STUN probes failed due to UDP presumably being blocked.
|
||||
|
||||
@@ -50,7 +50,8 @@ func TestBasic(t *testing.T) {
|
||||
defer cleanup()
|
||||
|
||||
c := &Client{
|
||||
Logf: t.Logf,
|
||||
Logf: t.Logf,
|
||||
UDPBindAddr: "127.0.0.1:0",
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
|
||||
|
||||
@@ -5,18 +5,37 @@
|
||||
package netns
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"math/bits"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"github.com/tailscale/winipcfg-go"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/util/endian"
|
||||
)
|
||||
|
||||
func interfaceIndex(iface *winipcfg.IPAdapterAddresses) uint32 {
|
||||
if iface == nil {
|
||||
// The zero ifidx means "unspecified". If we end up passing zero
|
||||
// to bindSocket*(), it unsets the binding and lets the socket
|
||||
// behave as normal again, which is what we want if there's no
|
||||
// default route we can use.
|
||||
return 0
|
||||
}
|
||||
return iface.IfIndex
|
||||
}
|
||||
|
||||
// control binds c to the Windows interface that holds a default
|
||||
// route, and is not the Tailscale WinTun interface.
|
||||
func control(network, address string, c syscall.RawConn) error {
|
||||
if strings.HasPrefix(address, "127.") {
|
||||
// Don't bind to an interface for localhost connections,
|
||||
// otherwise we get:
|
||||
// connectex: The requested address is not valid in its context
|
||||
// (The derphttp tests were failing)
|
||||
return nil
|
||||
}
|
||||
canV4, canV6 := false, false
|
||||
switch network {
|
||||
case "tcp", "udp":
|
||||
@@ -28,21 +47,21 @@ func control(network, address string, c syscall.RawConn) error {
|
||||
}
|
||||
|
||||
if canV4 {
|
||||
if4, err := getDefaultInterface(winipcfg.AF_INET)
|
||||
iface, err := interfaces.GetWindowsDefault(windows.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(windows.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 +69,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
|
||||
@@ -117,7 +114,8 @@ func bindSocket6(c syscall.RawConn, ifidx uint32) error {
|
||||
// representation, suitable for passing to Windows APIs that require a
|
||||
// mangled uint32.
|
||||
func nativeToBigEndian(i uint32) uint32 {
|
||||
var b [4]byte
|
||||
binary.BigEndian.PutUint32(b[:], i)
|
||||
return *(*uint32)(unsafe.Pointer(&b[0]))
|
||||
if endian.Big {
|
||||
return i
|
||||
}
|
||||
return bits.ReverseBytes32(i)
|
||||
}
|
||||
|
||||
@@ -6,14 +6,16 @@
|
||||
package netstat
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/bits"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/util/endian"
|
||||
)
|
||||
|
||||
// See https://docs.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getextendedtcptable
|
||||
@@ -61,13 +63,13 @@ func get() (*Table, error) {
|
||||
}
|
||||
|
||||
func (t *Table) addEntries(fam int) error {
|
||||
var size uint32
|
||||
size := new(uint32)
|
||||
var addr unsafe.Pointer
|
||||
var buf []byte
|
||||
for {
|
||||
err, _, _ := getTCPTable.Call(
|
||||
uintptr(addr),
|
||||
uintptr(unsafe.Pointer(&size)),
|
||||
uintptr(unsafe.Pointer(size)),
|
||||
1, // sorted
|
||||
uintptr(fam),
|
||||
tcpTableOwnerPidAll,
|
||||
@@ -78,19 +80,23 @@ func (t *Table) addEntries(fam int) error {
|
||||
}
|
||||
if err == uintptr(syscall.ERROR_INSUFFICIENT_BUFFER) {
|
||||
const maxSize = 10 << 20
|
||||
if size > maxSize || size < 4 {
|
||||
return fmt.Errorf("unreasonable kernel-reported size %d", size)
|
||||
if *size > maxSize || *size < 4 {
|
||||
return fmt.Errorf("unreasonable kernel-reported size %d", *size)
|
||||
}
|
||||
buf = make([]byte, size)
|
||||
buf = make([]byte, *size)
|
||||
addr = unsafe.Pointer(&buf[0])
|
||||
continue
|
||||
}
|
||||
runtime.KeepAlive(size)
|
||||
runtime.KeepAlive(addr)
|
||||
return syscall.Errno(err)
|
||||
}
|
||||
if len(buf) < int(size) {
|
||||
if len(buf) < int(*size) {
|
||||
runtime.KeepAlive(size)
|
||||
runtime.KeepAlive(addr)
|
||||
return errors.New("unexpected size growth from system call")
|
||||
}
|
||||
buf = buf[:size]
|
||||
buf = buf[:*size]
|
||||
|
||||
numEntries := *(*uint32)(unsafe.Pointer(&buf[0]))
|
||||
buf = buf[4:]
|
||||
@@ -127,6 +133,8 @@ func (t *Table) addEntries(fam int) error {
|
||||
}
|
||||
buf = buf[recSize:]
|
||||
}
|
||||
runtime.KeepAlive(addr)
|
||||
runtime.KeepAlive(size)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -153,9 +161,11 @@ func state(v uint32) string {
|
||||
}
|
||||
|
||||
func ipport4(addr uint32, port uint16) netaddr.IPPort {
|
||||
a4 := (*[4]byte)(unsafe.Pointer(&addr))
|
||||
if !endian.Big {
|
||||
addr = bits.ReverseBytes32(addr)
|
||||
}
|
||||
return netaddr.IPPort{
|
||||
IP: netaddr.IPv4(a4[0], a4[1], a4[2], a4[3]),
|
||||
IP: netaddr.IPv4(byte(addr>>24), byte(addr>>16), byte(addr>>8), byte(addr)),
|
||||
Port: port,
|
||||
}
|
||||
}
|
||||
@@ -173,6 +183,8 @@ func ipport6(addr [16]byte, scope uint32, port uint16) netaddr.IPPort {
|
||||
}
|
||||
|
||||
func port(v *uint32) uint16 {
|
||||
p := (*[4]byte)(unsafe.Pointer(v))
|
||||
return binary.BigEndian.Uint16(p[:2])
|
||||
if !endian.Big {
|
||||
return uint16(bits.ReverseBytes32(*v) >> 16)
|
||||
}
|
||||
return uint16(*v >> 16)
|
||||
}
|
||||
|
||||
16
net/packet/doc.go
Normal file
16
net/packet/doc.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// 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 packet contains packet parsing and marshaling utilities.
|
||||
//
|
||||
// Parsed provides allocation-free minimal packet header decoding, for
|
||||
// use in packet filtering. The other types in the package are for
|
||||
// constructing and marshaling packets into []bytes.
|
||||
//
|
||||
// To support allocation-free parsing, this package defines IPv4 and
|
||||
// IPv6 address types. You should prefer to use netaddr's types,
|
||||
// except where you absolutely need allocation-free IP handling
|
||||
// (i.e. in the tunnel datapath) and are willing to implement all
|
||||
// codepaths and data structures twice, once per IP family.
|
||||
package packet
|
||||
@@ -16,27 +16,34 @@ const tcpHeaderLength = 20
|
||||
const maxPacketLength = math.MaxUint16
|
||||
|
||||
var (
|
||||
// errSmallBuffer is returned when Marshal receives a buffer
|
||||
// too small to contain the header to marshal.
|
||||
errSmallBuffer = errors.New("buffer too small")
|
||||
// errLargePacket is returned when Marshal receives a payload
|
||||
// larger than the maximum representable size in header
|
||||
// fields.
|
||||
errLargePacket = errors.New("packet too large")
|
||||
)
|
||||
|
||||
// Header is a packet header capable of marshaling itself into a byte buffer.
|
||||
// Header is a packet header capable of marshaling itself into a byte
|
||||
// buffer.
|
||||
type Header interface {
|
||||
// Len returns the length of the header after marshaling.
|
||||
// Len returns the length of the marshaled packet.
|
||||
Len() int
|
||||
// Marshal serializes the header into buf in wire format.
|
||||
// It clobbers the header region, which is the first h.Length() bytes of buf.
|
||||
// It explicitly initializes every byte of the header region,
|
||||
// so pre-zeroing it on reuse is not required. It does not allocate memory.
|
||||
// It fails if and only if len(buf) < Length().
|
||||
// Marshal serializes the header into buf, which must be at
|
||||
// least Len() bytes long. Implementations of Marshal assume
|
||||
// that bytes after the first Len() are payload bytes for the
|
||||
// purpose of computing length and checksum fields. Marshal
|
||||
// implementations must not allocate memory.
|
||||
Marshal(buf []byte) error
|
||||
// ToResponse transforms the header into one for a response packet.
|
||||
// For instance, this swaps the source and destination IPs.
|
||||
ToResponse()
|
||||
}
|
||||
|
||||
// Generate generates a new packet with the given header and payload.
|
||||
// Unlike Header.Marshal, this does allocate memory.
|
||||
// Generate generates a new packet with the given Header and
|
||||
// payload. This function allocates memory, see Header.Marshal for an
|
||||
// allocation-free option.
|
||||
func Generate(h Header, payload []byte) []byte {
|
||||
hlen := h.Len()
|
||||
buf := make([]byte, hlen+len(payload))
|
||||
90
net/packet/icmp4.go
Normal file
90
net/packet/icmp4.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// 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 packet
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
// icmp4HeaderLength is the size of the ICMPv4 packet header, not
|
||||
// including the outer IP layer or the variable "response data"
|
||||
// trailer.
|
||||
const icmp4HeaderLength = 4
|
||||
|
||||
// ICMP4Type is an ICMPv4 type, as specified in
|
||||
// https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml
|
||||
type ICMP4Type uint8
|
||||
|
||||
const (
|
||||
ICMP4EchoReply ICMP4Type = 0x00
|
||||
ICMP4EchoRequest ICMP4Type = 0x08
|
||||
ICMP4Unreachable ICMP4Type = 0x03
|
||||
ICMP4TimeExceeded ICMP4Type = 0x0b
|
||||
)
|
||||
|
||||
func (t ICMP4Type) String() string {
|
||||
switch t {
|
||||
case ICMP4EchoReply:
|
||||
return "EchoReply"
|
||||
case ICMP4EchoRequest:
|
||||
return "EchoRequest"
|
||||
case ICMP4Unreachable:
|
||||
return "Unreachable"
|
||||
case ICMP4TimeExceeded:
|
||||
return "TimeExceeded"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// ICMP4Code is an ICMPv4 code, as specified in
|
||||
// https://www.iana.org/assignments/icmp-parameters/icmp-parameters.xhtml
|
||||
type ICMP4Code uint8
|
||||
|
||||
const (
|
||||
ICMP4NoCode ICMP4Code = 0
|
||||
)
|
||||
|
||||
// ICMP4Header is an IPv4+ICMPv4 header.
|
||||
type ICMP4Header struct {
|
||||
IP4Header
|
||||
Type ICMP4Type
|
||||
Code ICMP4Code
|
||||
}
|
||||
|
||||
// Len implements Header.
|
||||
func (h ICMP4Header) Len() int {
|
||||
return h.IP4Header.Len() + icmp4HeaderLength
|
||||
}
|
||||
|
||||
// Marshal implements Header.
|
||||
func (h ICMP4Header) Marshal(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
// The caller does not need to set this.
|
||||
h.IPProto = ICMPv4
|
||||
|
||||
buf[20] = uint8(h.Type)
|
||||
buf[21] = uint8(h.Code)
|
||||
|
||||
h.IP4Header.Marshal(buf)
|
||||
|
||||
binary.BigEndian.PutUint16(buf[22:24], ip4Checksum(buf))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToResponse implements Header. TODO: it doesn't implement it
|
||||
// correctly, instead it statically generates an ICMP Echo Reply
|
||||
// packet.
|
||||
func (h *ICMP4Header) ToResponse() {
|
||||
// TODO: this doesn't implement ToResponse correctly, as it
|
||||
// assumes the ICMP request type.
|
||||
h.Type = ICMP4EchoReply
|
||||
h.Code = ICMP4NoCode
|
||||
h.IP4Header.ToResponse()
|
||||
}
|
||||
44
net/packet/icmp6.go
Normal file
44
net/packet/icmp6.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// 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 packet
|
||||
|
||||
// icmp6HeaderLength is the size of the ICMPv6 packet header, not
|
||||
// including the outer IP layer or the variable "response data"
|
||||
// trailer.
|
||||
const icmp6HeaderLength = 4
|
||||
|
||||
// ICMP6Type is an ICMPv6 type, as specified in
|
||||
// https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml
|
||||
type ICMP6Type uint8
|
||||
|
||||
const (
|
||||
ICMP6Unreachable ICMP6Type = 1
|
||||
ICMP6TimeExceeded ICMP6Type = 3
|
||||
ICMP6EchoRequest ICMP6Type = 128
|
||||
ICMP6EchoReply ICMP6Type = 129
|
||||
)
|
||||
|
||||
func (t ICMP6Type) String() string {
|
||||
switch t {
|
||||
case ICMP6Unreachable:
|
||||
return "Unreachable"
|
||||
case ICMP6TimeExceeded:
|
||||
return "TimeExceeded"
|
||||
case ICMP6EchoRequest:
|
||||
return "EchoRequest"
|
||||
case ICMP6EchoReply:
|
||||
return "EchoReply"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// ICMP6Code is an ICMPv6 code, as specified in
|
||||
// https://www.iana.org/assignments/icmpv6-parameters/icmpv6-parameters.xhtml
|
||||
type ICMP6Code uint8
|
||||
|
||||
const (
|
||||
ICMP6NoCode ICMP6Code = 0
|
||||
)
|
||||
53
net/packet/ip.go
Normal file
53
net/packet/ip.go
Normal file
@@ -0,0 +1,53 @@
|
||||
// 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 packet
|
||||
|
||||
// IPProto is an IP subprotocol as defined by the IANA protocol
|
||||
// numbers list
|
||||
// (https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml),
|
||||
// or the special values Unknown or Fragment.
|
||||
type IPProto uint8
|
||||
|
||||
const (
|
||||
// Unknown represents an unknown or unsupported protocol; it's
|
||||
// deliberately the zero value. Strictly speaking the zero
|
||||
// value is IPv6 hop-by-hop extensions, but we don't support
|
||||
// those, so this is still technically correct.
|
||||
Unknown IPProto = 0x00
|
||||
|
||||
// Values from the IANA registry.
|
||||
ICMPv4 IPProto = 0x01
|
||||
IGMP IPProto = 0x02
|
||||
ICMPv6 IPProto = 0x3a
|
||||
TCP IPProto = 0x06
|
||||
UDP IPProto = 0x11
|
||||
|
||||
// Fragment represents any non-first IP fragment, for which we
|
||||
// don't have the sub-protocol header (and therefore can't
|
||||
// figure out what the sub-protocol is).
|
||||
//
|
||||
// 0xFF is reserved in the IANA registry, so we steal it for
|
||||
// internal use.
|
||||
Fragment IPProto = 0xFF
|
||||
)
|
||||
|
||||
func (p IPProto) String() string {
|
||||
switch p {
|
||||
case Fragment:
|
||||
return "Frag"
|
||||
case ICMPv4:
|
||||
return "ICMPv4"
|
||||
case IGMP:
|
||||
return "IGMP"
|
||||
case ICMPv6:
|
||||
return "ICMPv6"
|
||||
case UDP:
|
||||
return "UDP"
|
||||
case TCP:
|
||||
return "TCP"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
144
net/packet/ip4.go
Normal file
144
net/packet/ip4.go
Normal file
@@ -0,0 +1,144 @@
|
||||
// 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 packet
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// IP4 is an IPv4 address.
|
||||
type IP4 uint32
|
||||
|
||||
// IPFromNetaddr converts a netaddr.IP to an IP4. Panics if !ip.Is4.
|
||||
func IP4FromNetaddr(ip netaddr.IP) IP4 {
|
||||
ipbytes := ip.As4()
|
||||
return IP4(binary.BigEndian.Uint32(ipbytes[:]))
|
||||
}
|
||||
|
||||
// Netaddr converts ip to a netaddr.IP.
|
||||
func (ip IP4) Netaddr() netaddr.IP {
|
||||
return netaddr.IPv4(byte(ip>>24), byte(ip>>16), byte(ip>>8), byte(ip))
|
||||
}
|
||||
|
||||
func (ip IP4) String() string {
|
||||
return fmt.Sprintf("%d.%d.%d.%d", byte(ip>>24), byte(ip>>16), byte(ip>>8), byte(ip))
|
||||
}
|
||||
|
||||
// IsMulticast returns whether ip is a multicast address.
|
||||
func (ip IP4) IsMulticast() bool {
|
||||
return byte(ip>>24)&0xf0 == 0xe0
|
||||
}
|
||||
|
||||
// IsLinkLocalUnicast returns whether ip is a link-local unicast
|
||||
// address.
|
||||
func (ip IP4) IsLinkLocalUnicast() bool {
|
||||
return byte(ip>>24) == 169 && byte(ip>>16) == 254
|
||||
}
|
||||
|
||||
// IsMostLinkLocalUnicast returns whether ip is a link-local unicast
|
||||
// address other than the magical "169.254.169.254" address used by
|
||||
// GCP DNS.
|
||||
func (ip IP4) IsMostLinkLocalUnicast() bool {
|
||||
return ip.IsLinkLocalUnicast() && ip != 0xA9FEA9FE
|
||||
}
|
||||
|
||||
// ip4HeaderLength is the length of an IPv4 header with no IP options.
|
||||
const ip4HeaderLength = 20
|
||||
|
||||
// IP4Header represents an IPv4 packet header.
|
||||
type IP4Header struct {
|
||||
IPProto IPProto
|
||||
IPID uint16
|
||||
SrcIP IP4
|
||||
DstIP IP4
|
||||
}
|
||||
|
||||
// Len implements Header.
|
||||
func (h IP4Header) Len() int {
|
||||
return ip4HeaderLength
|
||||
}
|
||||
|
||||
// Marshal implements Header.
|
||||
func (h IP4Header) Marshal(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
|
||||
buf[0] = 0x40 | (byte(h.Len() >> 2)) // IPv4 + IHL
|
||||
buf[1] = 0x00 // DSCP + ECN
|
||||
binary.BigEndian.PutUint16(buf[2:4], uint16(len(buf))) // Total length
|
||||
binary.BigEndian.PutUint16(buf[4:6], h.IPID) // ID
|
||||
binary.BigEndian.PutUint16(buf[6:8], 0) // Flags + fragment offset
|
||||
buf[8] = 64 // TTL
|
||||
buf[9] = uint8(h.IPProto) // Inner protocol
|
||||
// Blank checksum. This is necessary even though we overwrite
|
||||
// it later, because the checksum computation runs over these
|
||||
// bytes and expects them to be zero.
|
||||
binary.BigEndian.PutUint16(buf[10:12], 0)
|
||||
binary.BigEndian.PutUint32(buf[12:16], uint32(h.SrcIP)) // Src
|
||||
binary.BigEndian.PutUint32(buf[16:20], uint32(h.DstIP)) // Dst
|
||||
|
||||
binary.BigEndian.PutUint16(buf[10:12], ip4Checksum(buf[0:20])) // Checksum
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToResponse implements Header.
|
||||
func (h *IP4Header) ToResponse() {
|
||||
h.SrcIP, h.DstIP = h.DstIP, h.SrcIP
|
||||
// Flip the bits in the IPID. If incoming IPIDs are distinct, so are these.
|
||||
h.IPID = ^h.IPID
|
||||
}
|
||||
|
||||
// ip4Checksum computes an IPv4 checksum, as specified in
|
||||
// https://tools.ietf.org/html/rfc1071
|
||||
func ip4Checksum(b []byte) uint16 {
|
||||
var ac uint32
|
||||
i := 0
|
||||
n := len(b)
|
||||
for n >= 2 {
|
||||
ac += uint32(binary.BigEndian.Uint16(b[i : i+2]))
|
||||
n -= 2
|
||||
i += 2
|
||||
}
|
||||
if n == 1 {
|
||||
ac += uint32(b[i]) << 8
|
||||
}
|
||||
for (ac >> 16) > 0 {
|
||||
ac = (ac >> 16) + (ac & 0xffff)
|
||||
}
|
||||
return uint16(^ac)
|
||||
}
|
||||
|
||||
// ip4PseudoHeaderOffset is the number of bytes by which the IPv4 UDP
|
||||
// pseudo-header is smaller than the real IPv4 header.
|
||||
const ip4PseudoHeaderOffset = 8
|
||||
|
||||
// marshalPseudo serializes h into buf in the "pseudo-header" form
|
||||
// required when calculating UDP checksums. The pseudo-header starts
|
||||
// at buf[ip4PseudoHeaderOffset] so as to abut the following UDP
|
||||
// header, while leaving enough space in buf for a full IPv4 header.
|
||||
func (h IP4Header) marshalPseudo(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
|
||||
length := len(buf) - h.Len()
|
||||
binary.BigEndian.PutUint32(buf[8:12], uint32(h.SrcIP))
|
||||
binary.BigEndian.PutUint32(buf[12:16], uint32(h.DstIP))
|
||||
buf[16] = 0x0
|
||||
buf[17] = uint8(h.IPProto)
|
||||
binary.BigEndian.PutUint16(buf[18:20], uint16(length))
|
||||
return nil
|
||||
}
|
||||
113
net/packet/ip6.go
Normal file
113
net/packet/ip6.go
Normal file
@@ -0,0 +1,113 @@
|
||||
// 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 packet
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// IP6 is an IPv6 address.
|
||||
type IP6 struct {
|
||||
Hi, Lo uint64
|
||||
}
|
||||
|
||||
// IP6FromNetaddr converts a netaddr.IP to an IP6. Panics if !ip.Is6.
|
||||
func IP6FromNetaddr(ip netaddr.IP) IP6 {
|
||||
if !ip.Is6() {
|
||||
panic(fmt.Sprintf("IP6FromNetaddr called with non-v6 addr %q", ip))
|
||||
}
|
||||
b := ip.As16()
|
||||
return IP6{binary.BigEndian.Uint64(b[:8]), binary.BigEndian.Uint64(b[8:])}
|
||||
}
|
||||
|
||||
// Netaddr converts ip to a netaddr.IP.
|
||||
func (ip IP6) Netaddr() netaddr.IP {
|
||||
var b [16]byte
|
||||
binary.BigEndian.PutUint64(b[:8], ip.Hi)
|
||||
binary.BigEndian.PutUint64(b[8:], ip.Lo)
|
||||
return netaddr.IPFrom16(b)
|
||||
}
|
||||
|
||||
func (ip IP6) String() string {
|
||||
return ip.Netaddr().String()
|
||||
}
|
||||
|
||||
func (ip IP6) IsMulticast() bool {
|
||||
return (ip.Hi >> 56) == 0xFF
|
||||
}
|
||||
|
||||
func (ip IP6) IsLinkLocalUnicast() bool {
|
||||
return (ip.Hi >> 48) == 0xFE80
|
||||
}
|
||||
|
||||
// ip6HeaderLength is the length of an IPv6 header with no IP options.
|
||||
const ip6HeaderLength = 40
|
||||
|
||||
// IP6Header represents an IPv6 packet header.
|
||||
type IP6Header struct {
|
||||
IPProto IPProto
|
||||
IPID uint32 // only lower 20 bits used
|
||||
SrcIP IP6
|
||||
DstIP IP6
|
||||
}
|
||||
|
||||
// Len implements Header.
|
||||
func (h IP6Header) Len() int {
|
||||
return ip6HeaderLength
|
||||
}
|
||||
|
||||
// Marshal implements Header.
|
||||
func (h IP6Header) Marshal(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint32(buf[:4], h.IPID&0x000FFFFF)
|
||||
buf[0] = 0x60
|
||||
binary.BigEndian.PutUint16(buf[4:6], uint16(len(buf)-ip6HeaderLength)) // Total length
|
||||
buf[6] = uint8(h.IPProto) // Inner protocol
|
||||
buf[7] = 64 // TTL
|
||||
binary.BigEndian.PutUint64(buf[8:16], h.SrcIP.Hi)
|
||||
binary.BigEndian.PutUint64(buf[16:24], h.SrcIP.Lo)
|
||||
binary.BigEndian.PutUint64(buf[24:32], h.DstIP.Hi)
|
||||
binary.BigEndian.PutUint64(buf[32:40], h.DstIP.Lo)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToResponse implements Header.
|
||||
func (h *IP6Header) ToResponse() {
|
||||
h.SrcIP, h.DstIP = h.DstIP, h.SrcIP
|
||||
// Flip the bits in the IPID. If incoming IPIDs are distinct, so are these.
|
||||
h.IPID = (^h.IPID) & 0x000FFFFF
|
||||
}
|
||||
|
||||
// marshalPseudo serializes h into buf in the "pseudo-header" form
|
||||
// required when calculating UDP checksums.
|
||||
func (h IP6Header) marshalPseudo(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
|
||||
binary.BigEndian.PutUint64(buf[:8], h.SrcIP.Hi)
|
||||
binary.BigEndian.PutUint64(buf[8:16], h.SrcIP.Lo)
|
||||
binary.BigEndian.PutUint64(buf[16:24], h.DstIP.Hi)
|
||||
binary.BigEndian.PutUint64(buf[24:32], h.DstIP.Lo)
|
||||
binary.BigEndian.PutUint32(buf[32:36], uint32(len(buf)-h.Len()))
|
||||
buf[36] = 0
|
||||
buf[37] = 0
|
||||
buf[38] = 0
|
||||
buf[39] = 17 // NextProto
|
||||
return nil
|
||||
}
|
||||
434
net/packet/packet.go
Normal file
434
net/packet/packet.go
Normal file
@@ -0,0 +1,434 @@
|
||||
// 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 packet
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/types/strbuilder"
|
||||
)
|
||||
|
||||
// RFC1858: prevent overlapping fragment attacks.
|
||||
const minFrag = 60 + 20 // max IPv4 header + basic TCP header
|
||||
|
||||
const (
|
||||
TCPSyn = 0x02
|
||||
TCPAck = 0x10
|
||||
TCPSynAck = TCPSyn | TCPAck
|
||||
)
|
||||
|
||||
// Parsed is a minimal decoding of a packet suitable for use in filters.
|
||||
type Parsed struct {
|
||||
// b is the byte buffer that this decodes.
|
||||
b []byte
|
||||
// subofs is the offset of IP subprotocol.
|
||||
subofs int
|
||||
// dataofs is the offset of IP subprotocol payload.
|
||||
dataofs int
|
||||
// length is the total length of the packet.
|
||||
// This is not the same as len(b) because b can have trailing zeros.
|
||||
length int
|
||||
|
||||
// IPVersion is the IP protocol version of the packet (4 or
|
||||
// 6), or 0 if the packet doesn't look like IPv4 or IPv6.
|
||||
IPVersion uint8
|
||||
// IPProto is the IP subprotocol (UDP, TCP, etc.). Valid iff IPVersion != 0.
|
||||
IPProto IPProto
|
||||
// SrcIP4 is the IPv4 source address. Valid iff IPVersion == 4.
|
||||
SrcIP4 IP4
|
||||
// DstIP4 is the IPv4 destination address. Valid iff IPVersion == 4.
|
||||
DstIP4 IP4
|
||||
// SrcIP6 is the IPv6 source address. Valid iff IPVersion == 6.
|
||||
SrcIP6 IP6
|
||||
// DstIP6 is the IPv6 destination address. Valid iff IPVersion == 6.
|
||||
DstIP6 IP6
|
||||
// SrcPort is the TCP/UDP source port. Valid iff IPProto == TCP || IPProto == UDP.
|
||||
SrcPort uint16
|
||||
// DstPort is the TCP/UDP source port. Valid iff IPProto == TCP || IPProto == UDP.
|
||||
DstPort uint16
|
||||
// TCPFlags is the packet's TCP flag bigs. Valid iff IPProto == TCP.
|
||||
TCPFlags uint8
|
||||
}
|
||||
|
||||
func (p *Parsed) String() string {
|
||||
switch p.IPVersion {
|
||||
case 4:
|
||||
sb := strbuilder.Get()
|
||||
sb.WriteString(p.IPProto.String())
|
||||
sb.WriteByte('{')
|
||||
writeIP4Port(sb, p.SrcIP4, p.SrcPort)
|
||||
sb.WriteString(" > ")
|
||||
writeIP4Port(sb, p.DstIP4, p.DstPort)
|
||||
sb.WriteByte('}')
|
||||
return sb.String()
|
||||
case 6:
|
||||
sb := strbuilder.Get()
|
||||
sb.WriteString(p.IPProto.String())
|
||||
sb.WriteByte('{')
|
||||
writeIP6Port(sb, p.SrcIP6, p.SrcPort)
|
||||
sb.WriteString(" > ")
|
||||
writeIP6Port(sb, p.DstIP6, p.DstPort)
|
||||
sb.WriteByte('}')
|
||||
return sb.String()
|
||||
default:
|
||||
return "Unknown{???}"
|
||||
}
|
||||
}
|
||||
|
||||
func writeIP4Port(sb *strbuilder.Builder, ip IP4, port uint16) {
|
||||
sb.WriteUint(uint64(byte(ip >> 24)))
|
||||
sb.WriteByte('.')
|
||||
sb.WriteUint(uint64(byte(ip >> 16)))
|
||||
sb.WriteByte('.')
|
||||
sb.WriteUint(uint64(byte(ip >> 8)))
|
||||
sb.WriteByte('.')
|
||||
sb.WriteUint(uint64(byte(ip)))
|
||||
sb.WriteByte(':')
|
||||
sb.WriteUint(uint64(port))
|
||||
}
|
||||
|
||||
func writeIP6Port(sb *strbuilder.Builder, ip IP6, port uint16) {
|
||||
sb.WriteByte('[')
|
||||
sb.WriteString(ip.Netaddr().String()) // TODO: faster?
|
||||
sb.WriteString("]:")
|
||||
sb.WriteUint(uint64(port))
|
||||
}
|
||||
|
||||
// Decode extracts data from the packet in b into q.
|
||||
// It performs extremely simple packet decoding for basic IPv4 packet types.
|
||||
// It extracts only the subprotocol id, IP addresses, and (if any) ports,
|
||||
// and shouldn't need any memory allocation.
|
||||
func (q *Parsed) Decode(b []byte) {
|
||||
q.b = b
|
||||
|
||||
if len(b) < 1 {
|
||||
q.IPVersion = 0
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
|
||||
q.IPVersion = b[0] >> 4
|
||||
switch q.IPVersion {
|
||||
case 4:
|
||||
q.decode4(b)
|
||||
case 6:
|
||||
q.decode6(b)
|
||||
default:
|
||||
q.IPVersion = 0
|
||||
q.IPProto = Unknown
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Parsed) decode4(b []byte) {
|
||||
if len(b) < ip4HeaderLength {
|
||||
q.IPVersion = 0
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
|
||||
// Check that it's IPv4.
|
||||
q.IPProto = IPProto(b[9])
|
||||
q.length = int(binary.BigEndian.Uint16(b[2:4]))
|
||||
if len(b) < q.length {
|
||||
// Packet was cut off before full IPv4 length.
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
|
||||
// If it's valid IPv4, then the IP addresses are valid
|
||||
q.SrcIP4 = IP4(binary.BigEndian.Uint32(b[12:16]))
|
||||
q.DstIP4 = IP4(binary.BigEndian.Uint32(b[16:20]))
|
||||
|
||||
q.subofs = int((b[0] & 0x0F) << 2)
|
||||
if q.subofs > q.length {
|
||||
// next-proto starts beyond end of packet.
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
sub := b[q.subofs:]
|
||||
sub = sub[:len(sub):len(sub)] // help the compiler do bounds check elimination
|
||||
|
||||
// We don't care much about IP fragmentation, except insofar as it's
|
||||
// used for firewall bypass attacks. The trick is make the first
|
||||
// fragment of a TCP or UDP packet so short that it doesn't fit
|
||||
// the TCP or UDP header, so we can't read the port, in hope that
|
||||
// it'll sneak past. Then subsequent fragments fill it in, but we're
|
||||
// missing the first part of the header, so we can't read that either.
|
||||
//
|
||||
// A "perfectly correct" implementation would have to reassemble
|
||||
// fragments before deciding what to do. But the truth is there's
|
||||
// zero reason to send such a short first fragment, so we can treat
|
||||
// it as Unknown. We can also treat any subsequent fragment that starts
|
||||
// at such a low offset as Unknown.
|
||||
fragFlags := binary.BigEndian.Uint16(b[6:8])
|
||||
moreFrags := (fragFlags & 0x20) != 0
|
||||
fragOfs := fragFlags & 0x1FFF
|
||||
if fragOfs == 0 {
|
||||
// This is the first fragment
|
||||
if moreFrags && len(sub) < minFrag {
|
||||
// Suspiciously short first fragment, dump it.
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
// otherwise, this is either non-fragmented (the usual case)
|
||||
// or a big enough initial fragment that we can read the
|
||||
// whole subprotocol header.
|
||||
switch q.IPProto {
|
||||
case ICMPv4:
|
||||
if len(sub) < icmp4HeaderLength {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
q.SrcPort = 0
|
||||
q.DstPort = 0
|
||||
q.dataofs = q.subofs + icmp4HeaderLength
|
||||
return
|
||||
case IGMP:
|
||||
// Keep IPProto, but don't parse anything else
|
||||
// out.
|
||||
return
|
||||
case TCP:
|
||||
if len(sub) < tcpHeaderLength {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
q.SrcPort = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.DstPort = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.TCPFlags = sub[13] & 0x3F
|
||||
headerLength := (sub[12] & 0xF0) >> 2
|
||||
q.dataofs = q.subofs + int(headerLength)
|
||||
return
|
||||
case UDP:
|
||||
if len(sub) < udpHeaderLength {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
q.SrcPort = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.DstPort = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.dataofs = q.subofs + udpHeaderLength
|
||||
return
|
||||
default:
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// This is a fragment other than the first one.
|
||||
if fragOfs < minFrag {
|
||||
// First frag was suspiciously short, so we can't
|
||||
// trust the followup either.
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
// otherwise, we have to permit the fragment to slide through.
|
||||
// Second and later fragments don't have sub-headers.
|
||||
// Ideally, we would drop fragments that we can't identify,
|
||||
// but that would require statefulness. Anyway, receivers'
|
||||
// kernels know to drop fragments where the initial fragment
|
||||
// doesn't arrive.
|
||||
q.IPProto = Fragment
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Parsed) decode6(b []byte) {
|
||||
if len(b) < ip6HeaderLength {
|
||||
q.IPVersion = 0
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
|
||||
q.IPProto = IPProto(b[6])
|
||||
q.length = int(binary.BigEndian.Uint16(b[4:6])) + ip6HeaderLength
|
||||
if len(b) < q.length {
|
||||
// Packet was cut off before the full IPv6 length.
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
|
||||
q.SrcIP6.Hi = binary.BigEndian.Uint64(b[8:16])
|
||||
q.SrcIP6.Lo = binary.BigEndian.Uint64(b[16:24])
|
||||
q.DstIP6.Hi = binary.BigEndian.Uint64(b[24:32])
|
||||
q.DstIP6.Lo = binary.BigEndian.Uint64(b[32:40])
|
||||
|
||||
// We don't support any IPv6 extension headers. Don't try to
|
||||
// be clever. Therefore, the IP subprotocol always starts at
|
||||
// byte 40.
|
||||
//
|
||||
// Note that this means we don't support fragmentation in
|
||||
// IPv6. This is fine, because IPv6 strongly mandates that you
|
||||
// should not fragment, which makes fragmentation on the open
|
||||
// internet extremely uncommon.
|
||||
//
|
||||
// This also means we don't support IPSec headers (AH/ESP), or
|
||||
// IPv6 jumbo frames. Those will get marked Unknown and
|
||||
// dropped.
|
||||
q.subofs = 40
|
||||
sub := b[q.subofs:]
|
||||
sub = sub[:len(sub):len(sub)] // help the compiler do bounds check elimination
|
||||
|
||||
switch q.IPProto {
|
||||
case ICMPv6:
|
||||
if len(sub) < icmp6HeaderLength {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
q.SrcPort = 0
|
||||
q.DstPort = 0
|
||||
q.dataofs = q.subofs + icmp6HeaderLength
|
||||
case TCP:
|
||||
if len(sub) < tcpHeaderLength {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
q.SrcPort = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.DstPort = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.TCPFlags = sub[13] & 0x3F
|
||||
headerLength := (sub[12] & 0xF0) >> 2
|
||||
q.dataofs = q.subofs + int(headerLength)
|
||||
return
|
||||
case UDP:
|
||||
if len(sub) < udpHeaderLength {
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
q.SrcPort = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.DstPort = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.dataofs = q.subofs + udpHeaderLength
|
||||
default:
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Parsed) IP4Header() IP4Header {
|
||||
if q.IPVersion != 4 {
|
||||
panic("IP4Header called on non-IPv4 Parsed")
|
||||
}
|
||||
ipid := binary.BigEndian.Uint16(q.b[4:6])
|
||||
return IP4Header{
|
||||
IPID: ipid,
|
||||
IPProto: q.IPProto,
|
||||
SrcIP: q.SrcIP4,
|
||||
DstIP: q.DstIP4,
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Parsed) ICMP4Header() ICMP4Header {
|
||||
if q.IPVersion != 4 {
|
||||
panic("IP4Header called on non-IPv4 Parsed")
|
||||
}
|
||||
return ICMP4Header{
|
||||
IP4Header: q.IP4Header(),
|
||||
Type: ICMP4Type(q.b[q.subofs+0]),
|
||||
Code: ICMP4Code(q.b[q.subofs+1]),
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Parsed) UDP4Header() UDP4Header {
|
||||
if q.IPVersion != 4 {
|
||||
panic("IP4Header called on non-IPv4 Parsed")
|
||||
}
|
||||
return UDP4Header{
|
||||
IP4Header: q.IP4Header(),
|
||||
SrcPort: q.SrcPort,
|
||||
DstPort: q.DstPort,
|
||||
}
|
||||
}
|
||||
|
||||
// Buffer returns the entire packet buffer.
|
||||
// This is a read-only view; that is, q retains the ownership of the buffer.
|
||||
func (q *Parsed) Buffer() []byte {
|
||||
return q.b
|
||||
}
|
||||
|
||||
// Payload returns the payload of the IP subprotocol section.
|
||||
// This is a read-only view; that is, q retains the ownership of the buffer.
|
||||
func (q *Parsed) Payload() []byte {
|
||||
return q.b[q.dataofs:q.length]
|
||||
}
|
||||
|
||||
// IsTCPSyn reports whether q is a TCP SYN packet
|
||||
// (i.e. the first packet in a new connection).
|
||||
func (q *Parsed) IsTCPSyn() bool {
|
||||
return (q.TCPFlags & TCPSynAck) == TCPSyn
|
||||
}
|
||||
|
||||
// IsError reports whether q is an ICMP "Error" packet.
|
||||
func (q *Parsed) IsError() bool {
|
||||
switch q.IPProto {
|
||||
case ICMPv4:
|
||||
if len(q.b) < q.subofs+8 {
|
||||
return false
|
||||
}
|
||||
t := ICMP4Type(q.b[q.subofs])
|
||||
return t == ICMP4Unreachable || t == ICMP4TimeExceeded
|
||||
case ICMPv6:
|
||||
if len(q.b) < q.subofs+8 {
|
||||
return false
|
||||
}
|
||||
t := ICMP6Type(q.b[q.subofs])
|
||||
return t == ICMP6Unreachable || t == ICMP6TimeExceeded
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsEchoRequest reports whether q is an ICMP Echo Request.
|
||||
func (q *Parsed) IsEchoRequest() bool {
|
||||
switch q.IPProto {
|
||||
case ICMPv4:
|
||||
return len(q.b) >= q.subofs+8 && ICMP4Type(q.b[q.subofs]) == ICMP4EchoRequest && ICMP4Code(q.b[q.subofs+1]) == ICMP4NoCode
|
||||
case ICMPv6:
|
||||
return len(q.b) >= q.subofs+8 && ICMP6Type(q.b[q.subofs]) == ICMP6EchoRequest && ICMP6Code(q.b[q.subofs+1]) == ICMP6NoCode
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsEchoRequest reports whether q is an IPv4 ICMP Echo Response.
|
||||
func (q *Parsed) IsEchoResponse() bool {
|
||||
switch q.IPProto {
|
||||
case ICMPv4:
|
||||
return len(q.b) >= q.subofs+8 && ICMP4Type(q.b[q.subofs]) == ICMP4EchoReply && ICMP4Code(q.b[q.subofs+1]) == ICMP4NoCode
|
||||
case ICMPv6:
|
||||
return len(q.b) >= q.subofs+8 && ICMP6Type(q.b[q.subofs]) == ICMP6EchoReply && ICMP6Code(q.b[q.subofs+1]) == ICMP6NoCode
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func Hexdump(b []byte) string {
|
||||
out := new(strings.Builder)
|
||||
for i := 0; i < len(b); i += 16 {
|
||||
if i > 0 {
|
||||
fmt.Fprintf(out, "\n")
|
||||
}
|
||||
fmt.Fprintf(out, " %04x ", i)
|
||||
j := 0
|
||||
for ; j < 16 && i+j < len(b); j++ {
|
||||
if j == 8 {
|
||||
fmt.Fprintf(out, " ")
|
||||
}
|
||||
fmt.Fprintf(out, "%02x ", b[i+j])
|
||||
}
|
||||
for ; j < 16; j++ {
|
||||
if j == 8 {
|
||||
fmt.Fprintf(out, " ")
|
||||
}
|
||||
fmt.Fprintf(out, " ")
|
||||
}
|
||||
fmt.Fprintf(out, " ")
|
||||
for j = 0; j < 16 && i+j < len(b); j++ {
|
||||
if b[i+j] >= 32 && b[i+j] < 128 {
|
||||
fmt.Fprintf(out, "%c", b[i+j])
|
||||
} else {
|
||||
fmt.Fprintf(out, ".")
|
||||
}
|
||||
}
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
502
net/packet/packet_test.go
Normal file
502
net/packet/packet_test.go
Normal file
@@ -0,0 +1,502 @@
|
||||
// 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 packet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func mustIP4(s string) IP4 {
|
||||
ip, err := netaddr.ParseIP(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return IP4FromNetaddr(ip)
|
||||
}
|
||||
|
||||
func mustIP6(s string) IP6 {
|
||||
ip, err := netaddr.ParseIP(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return IP6FromNetaddr(ip)
|
||||
}
|
||||
|
||||
func TestIP4String(t *testing.T) {
|
||||
const str = "1.2.3.4"
|
||||
ip := mustIP4(str)
|
||||
|
||||
var got string
|
||||
allocs := testing.AllocsPerRun(1000, func() {
|
||||
got = ip.String()
|
||||
})
|
||||
|
||||
if got != str {
|
||||
t.Errorf("got %q; want %q", got, str)
|
||||
}
|
||||
if allocs != 1 {
|
||||
t.Errorf("allocs = %v; want 1", allocs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIP6String(t *testing.T) {
|
||||
const str = "2607:f8b0:400a:809::200e"
|
||||
ip := mustIP6(str)
|
||||
|
||||
var got string
|
||||
allocs := testing.AllocsPerRun(1000, func() {
|
||||
got = ip.String()
|
||||
})
|
||||
|
||||
if got != str {
|
||||
t.Errorf("got %q; want %q", got, str)
|
||||
}
|
||||
if allocs != 2 {
|
||||
t.Errorf("allocs = %v; want 1", allocs)
|
||||
}
|
||||
}
|
||||
|
||||
var icmp4RequestBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x45, 0x00, 0x00, 0x27, 0xde, 0xad, 0x00, 0x00, 0x40, 0x01, 0x8c, 0x15,
|
||||
// source ip
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// destination ip
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// ICMP header
|
||||
0x08, 0x00, 0x7d, 0x22,
|
||||
// "request_payload"
|
||||
0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
|
||||
}
|
||||
|
||||
var icmp4RequestDecode = Parsed{
|
||||
b: icmp4RequestBuffer,
|
||||
subofs: 20,
|
||||
dataofs: 24,
|
||||
length: len(icmp4RequestBuffer),
|
||||
|
||||
IPVersion: 4,
|
||||
IPProto: ICMPv4,
|
||||
SrcIP4: mustIP4("1.2.3.4"),
|
||||
DstIP4: mustIP4("5.6.7.8"),
|
||||
SrcPort: 0,
|
||||
DstPort: 0,
|
||||
}
|
||||
|
||||
var icmp4ReplyBuffer = []byte{
|
||||
0x45, 0x00, 0x00, 0x25, 0x21, 0x52, 0x00, 0x00, 0x40, 0x01, 0x49, 0x73,
|
||||
// source ip
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// destination ip
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// ICMP header
|
||||
0x00, 0x00, 0xe6, 0x9e,
|
||||
// "reply_payload"
|
||||
0x72, 0x65, 0x70, 0x6c, 0x79, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
|
||||
}
|
||||
|
||||
var icmp4ReplyDecode = Parsed{
|
||||
b: icmp4ReplyBuffer,
|
||||
subofs: 20,
|
||||
dataofs: 24,
|
||||
length: len(icmp4ReplyBuffer),
|
||||
|
||||
IPVersion: 4,
|
||||
IPProto: ICMPv4,
|
||||
SrcIP4: mustIP4("1.2.3.4"),
|
||||
DstIP4: mustIP4("5.6.7.8"),
|
||||
SrcPort: 0,
|
||||
DstPort: 0,
|
||||
}
|
||||
|
||||
// ICMPv6 Router Solicitation
|
||||
var icmp6PacketBuffer = []byte{
|
||||
0x60, 0x00, 0x00, 0x00, 0x00, 0x08, 0x3a, 0xff,
|
||||
0xfe, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xfb, 0x57, 0x1d, 0xea, 0x9c, 0x39, 0x8f, 0xb7,
|
||||
0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02,
|
||||
0x85, 0x00, 0x38, 0x04, 0x00, 0x00, 0x00, 0x00,
|
||||
}
|
||||
|
||||
var icmp6PacketDecode = Parsed{
|
||||
b: icmp6PacketBuffer,
|
||||
subofs: 40,
|
||||
dataofs: 44,
|
||||
length: len(icmp6PacketBuffer),
|
||||
IPVersion: 6,
|
||||
IPProto: ICMPv6,
|
||||
SrcIP6: mustIP6("fe80::fb57:1dea:9c39:8fb7"),
|
||||
DstIP6: mustIP6("ff02::2"),
|
||||
}
|
||||
|
||||
// This is a malformed IPv4 packet.
|
||||
// Namely, the string "tcp_payload" follows the first byte of the IPv4 header.
|
||||
var unknownPacketBuffer = []byte{
|
||||
0x45, 0x74, 0x63, 0x70, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
|
||||
}
|
||||
|
||||
var unknownPacketDecode = Parsed{
|
||||
b: unknownPacketBuffer,
|
||||
IPVersion: 0,
|
||||
IPProto: Unknown,
|
||||
}
|
||||
|
||||
var tcp4PacketBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x45, 0x00, 0x00, 0x37, 0xde, 0xad, 0x00, 0x00, 0x40, 0x06, 0x49, 0x5f,
|
||||
// source ip
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// destination ip
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// TCP header with SYN, ACK set
|
||||
0x00, 0x7b, 0x02, 0x37, 0x00, 0x00, 0x12, 0x34, 0x00, 0x00, 0x00, 0x00,
|
||||
0x50, 0x12, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// "request_payload"
|
||||
0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
|
||||
}
|
||||
|
||||
var tcp4PacketDecode = Parsed{
|
||||
b: tcp4PacketBuffer,
|
||||
subofs: 20,
|
||||
dataofs: 40,
|
||||
length: len(tcp4PacketBuffer),
|
||||
|
||||
IPVersion: 4,
|
||||
IPProto: TCP,
|
||||
SrcIP4: mustIP4("1.2.3.4"),
|
||||
DstIP4: mustIP4("5.6.7.8"),
|
||||
SrcPort: 123,
|
||||
DstPort: 567,
|
||||
TCPFlags: TCPSynAck,
|
||||
}
|
||||
|
||||
var tcp6RequestBuffer = []byte{
|
||||
// IPv6 header up to hop limit
|
||||
0x60, 0x06, 0xef, 0xcc, 0x00, 0x28, 0x06, 0x40,
|
||||
// Src addr
|
||||
0x20, 0x01, 0x05, 0x59, 0xbc, 0x13, 0x54, 0x00, 0x17, 0x49, 0x46, 0x28, 0x39, 0x34, 0x0e, 0x1b,
|
||||
// Dst addr
|
||||
0x26, 0x07, 0xf8, 0xb0, 0x40, 0x0a, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0e,
|
||||
// TCP SYN segment, no payload
|
||||
0xa4, 0x60, 0x00, 0x50, 0xf3, 0x82, 0xa1, 0x25, 0x00, 0x00, 0x00, 0x00, 0xa0, 0x02, 0xfd, 0x20,
|
||||
0xb1, 0xc6, 0x00, 0x00, 0x02, 0x04, 0x05, 0xa0, 0x04, 0x02, 0x08, 0x0a, 0xca, 0x76, 0xa6, 0x8e,
|
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x03, 0x07,
|
||||
}
|
||||
|
||||
var tcp6RequestDecode = Parsed{
|
||||
b: tcp6RequestBuffer,
|
||||
subofs: 40,
|
||||
dataofs: len(tcp6RequestBuffer),
|
||||
length: len(tcp6RequestBuffer),
|
||||
|
||||
IPVersion: 6,
|
||||
IPProto: TCP,
|
||||
SrcIP6: mustIP6("2001:559:bc13:5400:1749:4628:3934:e1b"),
|
||||
DstIP6: mustIP6("2607:f8b0:400a:809::200e"),
|
||||
SrcPort: 42080,
|
||||
DstPort: 80,
|
||||
TCPFlags: TCPSyn,
|
||||
}
|
||||
|
||||
var udp4RequestBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x45, 0x00, 0x00, 0x2b, 0xde, 0xad, 0x00, 0x00, 0x40, 0x11, 0x8c, 0x01,
|
||||
// source ip
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// destination ip
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// UDP header
|
||||
0x00, 0x7b, 0x02, 0x37, 0x00, 0x17, 0x72, 0x1d,
|
||||
// "request_payload"
|
||||
0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
|
||||
}
|
||||
|
||||
var udp4RequestDecode = Parsed{
|
||||
b: udp4RequestBuffer,
|
||||
subofs: 20,
|
||||
dataofs: 28,
|
||||
length: len(udp4RequestBuffer),
|
||||
|
||||
IPVersion: 4,
|
||||
IPProto: UDP,
|
||||
SrcIP4: mustIP4("1.2.3.4"),
|
||||
DstIP4: mustIP4("5.6.7.8"),
|
||||
SrcPort: 123,
|
||||
DstPort: 567,
|
||||
}
|
||||
|
||||
var invalid4RequestBuffer = []byte{
|
||||
// IP header up to checksum. IHL field points beyond end of packet.
|
||||
0x4a, 0x00, 0x00, 0x14, 0xde, 0xad, 0x00, 0x00, 0x40, 0x11, 0x8c, 0x01,
|
||||
// source ip
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// destination ip
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
}
|
||||
|
||||
// Regression check for the IHL field pointing beyond the end of the
|
||||
// packet.
|
||||
var invalid4RequestDecode = Parsed{
|
||||
b: invalid4RequestBuffer,
|
||||
subofs: 40,
|
||||
length: len(invalid4RequestBuffer),
|
||||
|
||||
IPVersion: 4,
|
||||
IPProto: Unknown,
|
||||
SrcIP4: mustIP4("1.2.3.4"),
|
||||
DstIP4: mustIP4("5.6.7.8"),
|
||||
}
|
||||
|
||||
var udp6RequestBuffer = []byte{
|
||||
// IPv6 header up to hop limit
|
||||
0x60, 0x0e, 0xc9, 0x67, 0x00, 0x29, 0x11, 0x40,
|
||||
// Src addr
|
||||
0x20, 0x01, 0x05, 0x59, 0xbc, 0x13, 0x54, 0x00, 0x17, 0x49, 0x46, 0x28, 0x39, 0x34, 0x0e, 0x1b,
|
||||
// Dst addr
|
||||
0x26, 0x07, 0xf8, 0xb0, 0x40, 0x0a, 0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x20, 0x0e,
|
||||
// UDP header
|
||||
0xd4, 0x04, 0x01, 0xbb, 0x00, 0x29, 0x96, 0x84,
|
||||
// Payload
|
||||
0x5c, 0x06, 0xae, 0x85, 0x02, 0xf5, 0xdb, 0x90, 0xe0, 0xe0, 0x93, 0xed, 0x9a, 0xd9, 0x92, 0x69, 0xbe, 0x36, 0x8a, 0x7d, 0xd7, 0xce, 0xd0, 0x8a, 0xf2, 0x51, 0x95, 0xff, 0xb6, 0x92, 0x70, 0x10, 0xd7,
|
||||
}
|
||||
|
||||
var udp6RequestDecode = Parsed{
|
||||
b: udp6RequestBuffer,
|
||||
subofs: 40,
|
||||
dataofs: 48,
|
||||
length: len(udp6RequestBuffer),
|
||||
|
||||
IPVersion: 6,
|
||||
IPProto: UDP,
|
||||
SrcIP6: mustIP6("2001:559:bc13:5400:1749:4628:3934:e1b"),
|
||||
DstIP6: mustIP6("2607:f8b0:400a:809::200e"),
|
||||
SrcPort: 54276,
|
||||
DstPort: 443,
|
||||
}
|
||||
|
||||
var udp4ReplyBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x45, 0x00, 0x00, 0x29, 0x21, 0x52, 0x00, 0x00, 0x40, 0x11, 0x49, 0x5f,
|
||||
// source ip
|
||||
0x05, 0x06, 0x07, 0x08,
|
||||
// destination ip
|
||||
0x01, 0x02, 0x03, 0x04,
|
||||
// UDP header
|
||||
0x02, 0x37, 0x00, 0x7b, 0x00, 0x15, 0xd3, 0x9d,
|
||||
// "reply_payload"
|
||||
0x72, 0x65, 0x70, 0x6c, 0x79, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
|
||||
}
|
||||
|
||||
var udp4ReplyDecode = Parsed{
|
||||
b: udp4ReplyBuffer,
|
||||
subofs: 20,
|
||||
dataofs: 28,
|
||||
length: len(udp4ReplyBuffer),
|
||||
|
||||
IPProto: UDP,
|
||||
SrcIP4: mustIP4("1.2.3.4"),
|
||||
DstIP4: mustIP4("5.6.7.8"),
|
||||
SrcPort: 567,
|
||||
DstPort: 123,
|
||||
}
|
||||
|
||||
var igmpPacketBuffer = []byte{
|
||||
// IP header up to checksum
|
||||
0x46, 0xc0, 0x00, 0x20, 0x00, 0x00, 0x40, 0x00, 0x01, 0x02, 0x41, 0x22,
|
||||
// source IP
|
||||
0xc0, 0xa8, 0x01, 0x52,
|
||||
// destination IP
|
||||
0xe0, 0x00, 0x00, 0xfb,
|
||||
// IGMP Membership Report
|
||||
0x94, 0x04, 0x00, 0x00, 0x16, 0x00, 0x09, 0x04, 0xe0, 0x00, 0x00, 0xfb,
|
||||
//0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
}
|
||||
|
||||
var igmpPacketDecode = Parsed{
|
||||
b: igmpPacketBuffer,
|
||||
subofs: 24,
|
||||
length: len(igmpPacketBuffer),
|
||||
|
||||
IPVersion: 4,
|
||||
IPProto: IGMP,
|
||||
SrcIP4: mustIP4("192.168.1.82"),
|
||||
DstIP4: mustIP4("224.0.0.251"),
|
||||
}
|
||||
|
||||
func TestParsed(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
qdecode Parsed
|
||||
want string
|
||||
}{
|
||||
{"tcp4", tcp4PacketDecode, "TCP{1.2.3.4:123 > 5.6.7.8:567}"},
|
||||
{"tcp6", tcp6RequestDecode, "TCP{[2001:559:bc13:5400:1749:4628:3934:e1b]:42080 > [2607:f8b0:400a:809::200e]:80}"},
|
||||
{"udp4", udp4RequestDecode, "UDP{1.2.3.4:123 > 5.6.7.8:567}"},
|
||||
{"udp6", udp6RequestDecode, "UDP{[2001:559:bc13:5400:1749:4628:3934:e1b]:54276 > [2607:f8b0:400a:809::200e]:443}"},
|
||||
{"icmp4", icmp4RequestDecode, "ICMPv4{1.2.3.4:0 > 5.6.7.8:0}"},
|
||||
{"icmp6", icmp6PacketDecode, "ICMPv6{[fe80::fb57:1dea:9c39:8fb7]:0 > [ff02::2]:0}"},
|
||||
{"igmp", igmpPacketDecode, "IGMP{192.168.1.82:0 > 224.0.0.251:0}"},
|
||||
{"unknown", unknownPacketDecode, "Unknown{???}"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.qdecode.String()
|
||||
if got != tt.want {
|
||||
t.Errorf("got %q; want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
var sink string
|
||||
allocs := testing.AllocsPerRun(1000, func() {
|
||||
sink = tests[0].qdecode.String()
|
||||
})
|
||||
_ = sink
|
||||
if allocs != 1 {
|
||||
t.Errorf("allocs = %v; want 1", allocs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
buf []byte
|
||||
want Parsed
|
||||
}{
|
||||
{"icmp4", icmp4RequestBuffer, icmp4RequestDecode},
|
||||
{"icmp6", icmp6PacketBuffer, icmp6PacketDecode},
|
||||
{"tcp4", tcp4PacketBuffer, tcp4PacketDecode},
|
||||
{"tcp6", tcp6RequestBuffer, tcp6RequestDecode},
|
||||
{"udp4", udp4RequestBuffer, udp4RequestDecode},
|
||||
{"udp6", udp6RequestBuffer, udp6RequestDecode},
|
||||
{"igmp", igmpPacketBuffer, igmpPacketDecode},
|
||||
{"unknown", unknownPacketBuffer, unknownPacketDecode},
|
||||
{"invalid4", invalid4RequestBuffer, invalid4RequestDecode},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var got Parsed
|
||||
got.Decode(tt.buf)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("mismatch\n got: %#v\nwant: %#v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
allocs := testing.AllocsPerRun(1000, func() {
|
||||
var got Parsed
|
||||
got.Decode(tests[0].buf)
|
||||
})
|
||||
if allocs != 0 {
|
||||
t.Errorf("allocs = %v; want 0", allocs)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkDecode(b *testing.B) {
|
||||
benches := []struct {
|
||||
name string
|
||||
buf []byte
|
||||
}{
|
||||
{"tcp4", tcp4PacketBuffer},
|
||||
{"tcp6", tcp6RequestBuffer},
|
||||
{"udp4", udp4RequestBuffer},
|
||||
{"udp6", udp6RequestBuffer},
|
||||
{"icmp4", icmp4RequestBuffer},
|
||||
{"icmp6", icmp6PacketBuffer},
|
||||
{"igmp", igmpPacketBuffer},
|
||||
{"unknown", unknownPacketBuffer},
|
||||
}
|
||||
|
||||
for _, bench := range benches {
|
||||
b.Run(bench.name, func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
var p Parsed
|
||||
p.Decode(bench.buf)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalRequest(t *testing.T) {
|
||||
// Too small to hold our packets, but only barely.
|
||||
var small [20]byte
|
||||
var large [64]byte
|
||||
|
||||
icmpHeader := icmp4RequestDecode.ICMP4Header()
|
||||
udpHeader := udp4RequestDecode.UDP4Header()
|
||||
tests := []struct {
|
||||
name string
|
||||
header Header
|
||||
want []byte
|
||||
}{
|
||||
{"icmp", &icmpHeader, icmp4RequestBuffer},
|
||||
{"udp", &udpHeader, udp4RequestBuffer},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.header.Marshal(small[:])
|
||||
if err != errSmallBuffer {
|
||||
t.Errorf("got err: nil; want: %s", errSmallBuffer)
|
||||
}
|
||||
|
||||
dataOffset := tt.header.Len()
|
||||
dataLength := copy(large[dataOffset:], []byte("request_payload"))
|
||||
end := dataOffset + dataLength
|
||||
err = tt.header.Marshal(large[:end])
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("got err: %s; want nil", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(large[:end], tt.want) {
|
||||
t.Errorf("got %x; want %x", large[:end], tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalResponse(t *testing.T) {
|
||||
var buf [64]byte
|
||||
|
||||
icmpHeader := icmp4RequestDecode.ICMP4Header()
|
||||
udpHeader := udp4RequestDecode.UDP4Header()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
header Header
|
||||
want []byte
|
||||
}{
|
||||
{"icmp", &icmpHeader, icmp4ReplyBuffer},
|
||||
{"udp", &udpHeader, udp4ReplyBuffer},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.header.ToResponse()
|
||||
|
||||
dataOffset := tt.header.Len()
|
||||
dataLength := copy(buf[dataOffset:], []byte("reply_payload"))
|
||||
end := dataOffset + dataLength
|
||||
err := tt.header.Marshal(buf[:end])
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("got err: %s; want nil", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(buf[:end], tt.want) {
|
||||
t.Errorf("got %x; want %x", buf[:end], tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
55
net/packet/udp4.go
Normal file
55
net/packet/udp4.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// 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 packet
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
// udpHeaderLength is the size of the UDP packet header, not including
|
||||
// the outer IP header.
|
||||
const udpHeaderLength = 8
|
||||
|
||||
// UDP4Header is an IPv4+UDP header.
|
||||
type UDP4Header struct {
|
||||
IP4Header
|
||||
SrcPort uint16
|
||||
DstPort uint16
|
||||
}
|
||||
|
||||
// Len implements Header.
|
||||
func (h UDP4Header) Len() int {
|
||||
return h.IP4Header.Len() + udpHeaderLength
|
||||
}
|
||||
|
||||
// Marshal implements Header.
|
||||
func (h UDP4Header) Marshal(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
// The caller does not need to set this.
|
||||
h.IPProto = UDP
|
||||
|
||||
length := len(buf) - h.IP4Header.Len()
|
||||
binary.BigEndian.PutUint16(buf[20:22], h.SrcPort)
|
||||
binary.BigEndian.PutUint16(buf[22:24], h.DstPort)
|
||||
binary.BigEndian.PutUint16(buf[24:26], uint16(length))
|
||||
binary.BigEndian.PutUint16(buf[26:28], 0) // blank checksum
|
||||
|
||||
// UDP checksum with IP pseudo header.
|
||||
h.IP4Header.marshalPseudo(buf)
|
||||
binary.BigEndian.PutUint16(buf[26:28], ip4Checksum(buf[ip4PseudoHeaderOffset:]))
|
||||
|
||||
h.IP4Header.Marshal(buf)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToResponse implements Header.
|
||||
func (h *UDP4Header) ToResponse() {
|
||||
h.SrcPort, h.DstPort = h.DstPort, h.SrcPort
|
||||
h.IP4Header.ToResponse()
|
||||
}
|
||||
51
net/packet/udp6.go
Normal file
51
net/packet/udp6.go
Normal file
@@ -0,0 +1,51 @@
|
||||
// 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 packet
|
||||
|
||||
import "encoding/binary"
|
||||
|
||||
// UDP6Header is an IPv6+UDP header.
|
||||
type UDP6Header struct {
|
||||
IP6Header
|
||||
SrcPort uint16
|
||||
DstPort uint16
|
||||
}
|
||||
|
||||
// Len implements Header.
|
||||
func (h UDP6Header) Len() int {
|
||||
return h.IP6Header.Len() + udpHeaderLength
|
||||
}
|
||||
|
||||
// Marshal implements Header.
|
||||
func (h UDP6Header) Marshal(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
// The caller does not need to set this.
|
||||
h.IPProto = UDP
|
||||
|
||||
length := len(buf) - h.IP6Header.Len()
|
||||
binary.BigEndian.PutUint16(buf[40:42], h.SrcPort)
|
||||
binary.BigEndian.PutUint16(buf[42:44], h.DstPort)
|
||||
binary.BigEndian.PutUint16(buf[44:46], uint16(length))
|
||||
binary.BigEndian.PutUint16(buf[46:48], 0) // blank checksum
|
||||
|
||||
// UDP checksum with IP pseudo header.
|
||||
h.IP6Header.marshalPseudo(buf)
|
||||
binary.BigEndian.PutUint16(buf[46:48], ip4Checksum(buf[:]))
|
||||
|
||||
h.IP6Header.Marshal(buf)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToResponse implements Header.
|
||||
func (h *UDP6Header) ToResponse() {
|
||||
h.SrcPort, h.DstPort = h.DstPort, h.SrcPort
|
||||
h.IP6Header.ToResponse()
|
||||
}
|
||||
@@ -30,7 +30,11 @@ func CGNATRange() netaddr.IPPrefix {
|
||||
return cgnatRange.v
|
||||
}
|
||||
|
||||
var cgnatRange oncePrefix
|
||||
var (
|
||||
cgnatRange oncePrefix
|
||||
ulaRange oncePrefix
|
||||
ula4To6Range oncePrefix
|
||||
)
|
||||
|
||||
// TailscaleServiceIP returns the listen address of services
|
||||
// provided by Tailscale itself such as the Magic DNS proxy.
|
||||
@@ -44,7 +48,40 @@ var serviceIP onceIP
|
||||
// IsTailscaleIP reports whether ip is an IP address in a range that
|
||||
// Tailscale assigns from.
|
||||
func IsTailscaleIP(ip netaddr.IP) bool {
|
||||
return CGNATRange().Contains(ip) && !ChromeOSVMRange().Contains(ip)
|
||||
if ip.Is4() {
|
||||
return CGNATRange().Contains(ip) && !ChromeOSVMRange().Contains(ip)
|
||||
}
|
||||
return TailscaleULARange().Contains(ip)
|
||||
}
|
||||
|
||||
// TailscaleULARange returns the IPv6 Unique Local Address range that
|
||||
// is the superset range that Tailscale assigns out of.
|
||||
func TailscaleULARange() netaddr.IPPrefix {
|
||||
ulaRange.Do(func() { mustPrefix(&ulaRange.v, "fd7a:115c:a1e0::/48") })
|
||||
return ulaRange.v
|
||||
}
|
||||
|
||||
// Tailscale4To6Range returns the subset of TailscaleULARange used for
|
||||
// auto-translated Tailscale ipv4 addresses.
|
||||
func Tailscale4To6Range() netaddr.IPPrefix {
|
||||
// This IP range has no significance, beyond being a subset of
|
||||
// TailscaleULARange. The bits from /48 to /104 were picked at
|
||||
// random.
|
||||
ula4To6Range.Do(func() { mustPrefix(&ula4To6Range.v, "fd7a:115c:a1e0:ab12:4843:cd96:6200::/104") })
|
||||
return ula4To6Range.v
|
||||
}
|
||||
|
||||
// Tailscale4To6 returns a Tailscale IPv6 address that maps 1:1 to the
|
||||
// given Tailscale IPv4 address. Returns a zero IP if ipv4 isn't a
|
||||
// Tailscale IPv4 address.
|
||||
func Tailscale4To6(ipv4 netaddr.IP) netaddr.IP {
|
||||
if !ipv4.Is4() || !IsTailscaleIP(ipv4) {
|
||||
return netaddr.IP{}
|
||||
}
|
||||
ret := Tailscale4To6Range().IP.As16()
|
||||
v4 := ipv4.As4()
|
||||
copy(ret[13:], v4[1:])
|
||||
return netaddr.IPFrom16(ret)
|
||||
}
|
||||
|
||||
func mustPrefix(v *netaddr.IPPrefix, prefix string) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
|
||||
"github.com/alexbrainman/sspi/negotiate"
|
||||
"golang.org/x/sys/windows"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -38,6 +39,13 @@ var cachedProxy struct {
|
||||
val *url.URL
|
||||
}
|
||||
|
||||
// proxyErrorf is a rate-limited logger specifically for errors asking
|
||||
// WinHTTP for the proxy information. We don't want to log about
|
||||
// errors often, otherwise the log message itself will generate a new
|
||||
// HTTP request which ultimately will call back into us to log again,
|
||||
// forever. So for errors, we only log a bit.
|
||||
var proxyErrorf = logger.RateLimitedFn(log.Printf, 10*time.Minute, 2 /* burst*/, 10 /* maxCache */)
|
||||
|
||||
func proxyFromWinHTTPOrCache(req *http.Request) (*url.URL, error) {
|
||||
if req.URL == nil {
|
||||
return nil, nil
|
||||
@@ -71,16 +79,31 @@ 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
|
||||
}
|
||||
if err == windows.ERROR_INVALID_PARAMETER {
|
||||
// Seen on Windows 8.1. (https://github.com/tailscale/tailscale/issues/879)
|
||||
// TODO(bradfitz): figure this out.
|
||||
setNoProxyUntil(time.Hour)
|
||||
proxyErrorf("tshttpproxy: winhttp: GetProxyForURL(%q): ERROR_INVALID_PARAMETER [unexpected]", urlStr)
|
||||
return nil, nil
|
||||
}
|
||||
proxyErrorf("tshttpproxy: winhttp: GetProxyForURL(%q): %v/%#v", urlStr, err, err)
|
||||
if err == syscall.Errno(ERROR_WINHTTP_UNABLE_TO_DOWNLOAD_SCRIPT) {
|
||||
setNoProxyUntil(10 * time.Second)
|
||||
return nil, nil
|
||||
}
|
||||
log.Printf("tshttpproxy: winhttp: GetProxyForURL(%q): %v/%#v", urlStr, err, err)
|
||||
return nil, err
|
||||
case <-ctx.Done():
|
||||
cachedProxy.Lock()
|
||||
defer cachedProxy.Unlock()
|
||||
log.Printf("tshttpproxy: winhttp: GetProxyForURL(%q): timeout; using cached proxy %v", urlStr, cachedProxy.val)
|
||||
proxyErrorf("tshttpproxy: winhttp: GetProxyForURL(%q): timeout; using cached proxy %v", urlStr, cachedProxy.val)
|
||||
return cachedProxy.val, nil
|
||||
}
|
||||
}
|
||||
@@ -88,7 +111,7 @@ func proxyFromWinHTTPOrCache(req *http.Request) (*url.URL, error) {
|
||||
func proxyFromWinHTTP(ctx context.Context, urlStr string) (proxy *url.URL, err error) {
|
||||
whi, err := winHTTPOpen()
|
||||
if err != nil {
|
||||
log.Printf("winhttp: Open: %v", err)
|
||||
proxyErrorf("winhttp: Open: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
defer whi.Close()
|
||||
|
||||
@@ -7,7 +7,6 @@ package portlist
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"tailscale.com/version"
|
||||
@@ -34,7 +33,7 @@ type Poller struct {
|
||||
// NewPoller returns a new portlist Poller. It returns an error
|
||||
// if the portlist couldn't be obtained.
|
||||
func NewPoller() (*Poller, error) {
|
||||
if runtime.GOOS == "darwin" && version.IsMobile() {
|
||||
if version.OS() == "iOS" {
|
||||
return nil, errors.New("not available on iOS")
|
||||
}
|
||||
p := &Poller{
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
exec "tailscale.com/tempfork/osexec"
|
||||
)
|
||||
|
||||
@@ -19,6 +20,9 @@ func listPorts() (List, error) {
|
||||
}
|
||||
|
||||
func addProcesses(pl []Port) ([]Port, error) {
|
||||
if t := windows.GetCurrentProcessToken(); !t.IsElevated() {
|
||||
return listPortsNetstat("-na")
|
||||
}
|
||||
return listPortsNetstat("-nab")
|
||||
}
|
||||
|
||||
|
||||
@@ -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 --clonefunc=true --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,7 @@ type UserProfile struct {
|
||||
LoginName string // "alice@smith.com"; for display purposes only (provider is not listed)
|
||||
DisplayName string // "Alice Smith"
|
||||
ProfilePicURL string
|
||||
Roles []RoleID
|
||||
Roles []RoleID // deprecated; clients should not rely on Roles
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
@@ -198,7 +218,7 @@ func isAlpha(b byte) bool {
|
||||
return (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z')
|
||||
}
|
||||
|
||||
// CheckTag valids whether a given string can be used as an ACL tag.
|
||||
// CheckTag validates tag for use as an ACL tag.
|
||||
// For now we allow only ascii alphanumeric tags, and they need to start
|
||||
// with a letter. No unicode shenanigans allowed, and we reserve punctuation
|
||||
// marks other than '-' for a possible future URI scheme.
|
||||
@@ -228,6 +248,19 @@ func CheckTag(tag string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckRequestTags checks that all of h.RequestTags are valid.
|
||||
func (h *Hostinfo) CheckRequestTags() error {
|
||||
if h == nil {
|
||||
return nil
|
||||
}
|
||||
for _, tag := range h.RequestTags {
|
||||
if err := CheckTag(tag); err != nil {
|
||||
return fmt.Errorf("tag(%#v): %w", tag, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ServiceProto string
|
||||
|
||||
const (
|
||||
@@ -239,7 +272,7 @@ type Service struct {
|
||||
_ structs.Incomparable
|
||||
Proto ServiceProto // TCP or UDP
|
||||
Port uint16 // port number service is listening on
|
||||
Description string // text description of service
|
||||
Description string `json:",omitempty"` // text description of service
|
||||
// TODO(apenwarr): allow advertising services on subnet IPs?
|
||||
// TODO(apenwarr): add "tags" here for each service?
|
||||
|
||||
@@ -255,13 +288,13 @@ type Hostinfo struct {
|
||||
// TODO(crawshaw): mark all these fields ",omitempty" when all the
|
||||
// iOS apps are updated with the latest swift version of this struct.
|
||||
IPNVersion string // version of this code
|
||||
FrontendLogID string // logtail ID of frontend instance
|
||||
BackendLogID string // logtail ID of backend instance
|
||||
FrontendLogID string `json:",omitempty"` // logtail ID of frontend instance
|
||||
BackendLogID string `json:",omitempty"` // logtail ID of backend instance
|
||||
OS string // operating system the client runs on (a version.OS value)
|
||||
OSVersion string // operating system version, with optional distro prefix ("Debian 10.4", "Windows 10 Pro 10.0.19041")
|
||||
DeviceModel string // mobile phone model ("Pixel 3a", "iPhone 11 Pro")
|
||||
OSVersion string `json:",omitempty"` // operating system version, with optional distro prefix ("Debian 10.4", "Windows 10 Pro 10.0.19041")
|
||||
DeviceModel string `json:",omitempty"` // mobile phone model ("Pixel 3a", "iPhone 11 Pro")
|
||||
Hostname string // name of the host the client runs on
|
||||
GoArch string // the host's GOARCH value (of the running binary)
|
||||
GoArch string `json:",omitempty"` // the host's GOARCH value (of the running binary)
|
||||
RoutableIPs []wgcfg.CIDR `json:",omitempty"` // set of IP ranges this client can route
|
||||
RequestTags []string `json:",omitempty"` // set of ACL tags this node wants to claim
|
||||
Services []Service `json:",omitempty"` // services advertised by this machine
|
||||
@@ -308,7 +341,7 @@ type NetInfo struct {
|
||||
PreferredDERP int
|
||||
|
||||
// LinkType is the current link type, if known.
|
||||
LinkType string // "wired", "wifi", "mobile" (LTE, 4G, 3G, etc)
|
||||
LinkType string `json:",omitempty"` // "wired", "wifi", "mobile" (LTE, 4G, 3G, etc)
|
||||
|
||||
// DERPLatency is the fastest recent time to reach various
|
||||
// DERP STUN servers, in seconds. The map key is the
|
||||
@@ -441,22 +474,51 @@ type RegisterResponse struct {
|
||||
// using the local machine key, and sent to:
|
||||
// https://login.tailscale.com/machine/<mkey hex>/map
|
||||
type MapRequest struct {
|
||||
Version int // current version is 4
|
||||
// Version is incremented whenever the client code changes enough that
|
||||
// we want to signal to the control server that we're capable of something
|
||||
// different.
|
||||
//
|
||||
// History of versions:
|
||||
// 3: implicit compression, keep-alives
|
||||
// 4: opt-in keep-alives via KeepAlive field, opt-in compression via Compress
|
||||
// 5: 2020-10-19, implies IncludeIPv6, DeltaPeers/DeltaUserProfiles, supports MagicDNS
|
||||
Version int
|
||||
Compress string // "zstd" or "" (no compression)
|
||||
KeepAlive bool // whether server should send keep-alives back to us
|
||||
NodeKey NodeKey
|
||||
DiscoKey DiscoKey
|
||||
Endpoints []string // caller's endpoints (IPv4 or IPv6)
|
||||
IncludeIPv6 bool // include IPv6 endpoints in returned Node Endpoints
|
||||
DeltaPeers bool // whether the 2nd+ network map in response should be deltas, using PeersChanged, PeersRemoved
|
||||
IncludeIPv6 bool `json:",omitempty"` // include IPv6 endpoints in returned Node Endpoints (for Version 4 clients)
|
||||
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"`
|
||||
|
||||
// DebugFlags is a list of strings specifying debugging and
|
||||
// development features to enable in handling this map
|
||||
// request. The values are deliberately unspecified, as they get
|
||||
// added and removed all the time during development, and offer no
|
||||
// compatibility promise. To roll out semantic changes, bump
|
||||
// Version instead.
|
||||
//
|
||||
// Current DebugFlags values are:
|
||||
// * "warn-ip-forwarding-off": client is trying to be a subnet
|
||||
// router but their IP forwarding is broken.
|
||||
// * "v6-overlay": IPv6 development flag to have control send
|
||||
// v6 node addrs
|
||||
DebugFlags []string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// PortRange represents a range of UDP or TCP port numbers.
|
||||
@@ -470,23 +532,42 @@ var PortRangeAny = PortRange{0, 65535}
|
||||
// NetPortRange represents a single subnet:portrange.
|
||||
type NetPortRange struct {
|
||||
_ structs.Incomparable
|
||||
IP string
|
||||
Bits *int // backward compatibility: if missing, means "all" bits
|
||||
IP string // "*" means all
|
||||
Bits *int // backward compatibility: if missing, means "all" bits
|
||||
Ports PortRange
|
||||
}
|
||||
|
||||
// FilterRule represents one rule in a packet filter.
|
||||
//
|
||||
// A rule is logically a set of source CIDRs to match (described by
|
||||
// SrcIPs and SrcBits), and a set of destination targets that are then
|
||||
// allowed if a source IP is mathces of those CIDRs.
|
||||
type FilterRule struct {
|
||||
SrcIPs []string
|
||||
SrcBits []int
|
||||
// SrcIPs are the source IPs/networks to match.
|
||||
// The special value "*" means to match all.
|
||||
SrcIPs []string
|
||||
|
||||
// SrcBits values correspond to the SrcIPs above.
|
||||
//
|
||||
// If present at the same index, it changes the SrcIP above to
|
||||
// be a network with /n CIDR bits. If the slice is nil or
|
||||
// insufficiently long, the default value (for an IPv4
|
||||
// address) for a position is 32, as if the SrcIPs above were
|
||||
// a /32 mask. For a "*" SrcIPs value, the corresponding
|
||||
// SrcBits value is ignored.
|
||||
// TODO: for IPv6, clarify default bits length.
|
||||
SrcBits []int
|
||||
|
||||
// DstPorts are the port ranges to allow once a source IP
|
||||
// matches (is in the CIDR described by SrcIPs & SrcBits).
|
||||
DstPorts []NetPortRange
|
||||
}
|
||||
|
||||
var FilterAllowAll = []FilterRule{
|
||||
FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"*"},
|
||||
SrcBits: nil,
|
||||
DstPorts: []NetPortRange{NetPortRange{
|
||||
DstPorts: []NetPortRange{{
|
||||
IP: "*",
|
||||
Bits: nil,
|
||||
Ports: PortRange{0, 65535},
|
||||
@@ -545,8 +626,8 @@ type MapResponse struct {
|
||||
// ACLs
|
||||
Domain string
|
||||
PacketFilter []FilterRule
|
||||
UserProfiles []UserProfile
|
||||
Roles []Role
|
||||
UserProfiles []UserProfile // as of 1.1.541: may be new or updated user profiles only
|
||||
Roles []Role // deprecated; clients should not rely on Roles
|
||||
// TODO: Groups []Group
|
||||
// TODO: Capabilities []Capability
|
||||
|
||||
@@ -582,10 +663,15 @@ type Debug struct {
|
||||
// TrimWGConfig controls whether Tailscale does lazy, on-demand
|
||||
// wireguard configuration of peers.
|
||||
TrimWGConfig opt.Bool `json:",omitempty"`
|
||||
|
||||
// DisableSubnetsIfPAC controls whether subnet routers should be
|
||||
// disabled if WPAD is present on the network.
|
||||
DisableSubnetsIfPAC opt.Bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (k MachineKey) String() string { return fmt.Sprintf("mkey:%x", k[:]) }
|
||||
func (k MachineKey) MarshalText() ([]byte, error) { return keyMarshalText("mkey:", k), nil }
|
||||
func (k MachineKey) HexString() string { return fmt.Sprintf("%x", k[:]) }
|
||||
func (k *MachineKey) UnmarshalText(text []byte) error { return keyUnmarshalText(k[:], "mkey:", text) }
|
||||
|
||||
func keyMarshalText(prefix string, k [32]byte) []byte {
|
||||
@@ -615,6 +701,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)
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
package tstest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
@@ -35,7 +37,16 @@ func UnfixLogs(t *testing.T) {
|
||||
type panicLogWriter struct{}
|
||||
|
||||
func (panicLogWriter) Write(b []byte) (int, error) {
|
||||
panic("please use tailscale.com/logger.Logf instead of the log package")
|
||||
// Allow certain phrases for now, in the interest of getting
|
||||
// CI working on Windows and not having to refactor all the
|
||||
// interfaces.GetState & tshttpproxy code to allow pushing
|
||||
// down a Logger yet. TODO(bradfitz): do that refactoring once
|
||||
// 1.2.0 is out.
|
||||
if bytes.Contains(b, []byte("tshttpproxy: ")) {
|
||||
os.Stderr.Write(b)
|
||||
return len(b), nil
|
||||
}
|
||||
panic(fmt.Sprintf("please use tailscale.com/logger.Logf instead of the log package (tried to log: %q)", b))
|
||||
}
|
||||
|
||||
// PanicOnLog modifies the standard library log package's default output to
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ type response struct {
|
||||
// JSONHandlerFunc is an HTTP ReturnHandler that writes JSON responses to the client.
|
||||
//
|
||||
// Return a HTTPError to show an error message, otherwise JSONHandlerFunc will
|
||||
// only report "internal server error" to the user.
|
||||
// only report "internal server error" to the user with status code 500.
|
||||
type JSONHandlerFunc func(r *http.Request) (status int, data interface{}, err error)
|
||||
|
||||
// ServeHTTPReturn implements the ReturnHandler interface.
|
||||
@@ -31,23 +31,12 @@ type JSONHandlerFunc func(r *http.Request) (status int, data interface{}, err er
|
||||
// return http.StatusBadRequest, nil, err
|
||||
// }
|
||||
//
|
||||
// See jsonhandler_text.go for examples.
|
||||
// See jsonhandler_test.go for examples.
|
||||
func (fn JSONHandlerFunc) ServeHTTPReturn(w http.ResponseWriter, r *http.Request) error {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
var resp *response
|
||||
status, data, err := fn(r)
|
||||
if status == 0 {
|
||||
status = http.StatusInternalServerError
|
||||
resp = &response{
|
||||
Status: "error",
|
||||
Error: "internal server error",
|
||||
}
|
||||
} else if err == nil {
|
||||
resp = &response{
|
||||
Status: "success",
|
||||
Data: data,
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
if werr, ok := err.(HTTPError); ok {
|
||||
resp = &response{
|
||||
Status: "error",
|
||||
@@ -58,12 +47,32 @@ 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)
|
||||
}
|
||||
// take status from the HTTPError to encourage error handling in one location
|
||||
if status != 0 && status != werr.Code {
|
||||
err = fmt.Errorf("[unexpected] non-zero status that does not match HTTPError status, status: %d, HTTPError.code: %d: %w", status, werr.Code, err)
|
||||
}
|
||||
status = werr.Code
|
||||
} else {
|
||||
status = http.StatusInternalServerError
|
||||
resp = &response{
|
||||
Status: "error",
|
||||
Error: "internal server error",
|
||||
}
|
||||
}
|
||||
} else if status == 0 {
|
||||
status = http.StatusInternalServerError
|
||||
resp = &response{
|
||||
Status: "error",
|
||||
Error: "internal server error",
|
||||
}
|
||||
} else if err == nil {
|
||||
resp = &response{
|
||||
Status: "success",
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
b, jerr := json.Marshal(resp)
|
||||
|
||||
@@ -11,6 +11,8 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
type Data struct {
|
||||
@@ -40,11 +42,11 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
if d.Status == status {
|
||||
t.Logf("ok: %s", d.Status)
|
||||
} else {
|
||||
t.Fatalf("wrong status: %s %s", d.Status, status)
|
||||
t.Fatalf("wrong status: got: %s, want: %s", d.Status, status)
|
||||
}
|
||||
|
||||
if w.Code != code {
|
||||
t.Fatalf("wrong status code: %d %d", w.Code, code)
|
||||
t.Fatalf("wrong status code: got: %d, want: %d", w.Code, code)
|
||||
}
|
||||
|
||||
if w.Header().Get("Content-Type") != "application/json" {
|
||||
@@ -67,7 +69,7 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
|
||||
t.Run("403 HTTPError", func(t *testing.T) {
|
||||
h := JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
return http.StatusForbidden, nil, fmt.Errorf("forbidden")
|
||||
return 0, nil, Error(http.StatusForbidden, "forbidden", nil)
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
@@ -90,11 +92,11 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
h31 := JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
body := new(Data)
|
||||
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
return http.StatusBadRequest, nil, err
|
||||
return 0, nil, Error(http.StatusBadRequest, err.Error(), err)
|
||||
}
|
||||
|
||||
if body.Name == "" {
|
||||
return http.StatusBadRequest, nil, Error(http.StatusBadGateway, "name is empty", nil)
|
||||
return 0, nil, Error(http.StatusBadRequest, "name is empty", nil)
|
||||
}
|
||||
|
||||
return http.StatusOK, nil, nil
|
||||
@@ -126,13 +128,13 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
h32 := JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
body := new(Data)
|
||||
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
return http.StatusBadRequest, nil, err
|
||||
return 0, nil, Error(http.StatusBadRequest, err.Error(), err)
|
||||
}
|
||||
if body.Name == "root" {
|
||||
return http.StatusInternalServerError, nil, fmt.Errorf("invalid name")
|
||||
return 0, nil, fmt.Errorf("invalid name")
|
||||
}
|
||||
if body.Price == 0 {
|
||||
return http.StatusBadRequest, nil, Error(http.StatusBadGateway, "price is empty", nil)
|
||||
return 0, nil, Error(http.StatusBadRequest, "price is empty", nil)
|
||||
}
|
||||
|
||||
return http.StatusOK, &Data{Price: body.Price * 2}, nil
|
||||
@@ -159,7 +161,7 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("500 internal server error", func(t *testing.T) {
|
||||
t.Run("500 internal server error (unspecified error, not of type HTTPError)", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{"Name": "root"}`))
|
||||
h32.ServeHTTPReturn(w, r)
|
||||
@@ -189,4 +191,41 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
}).ServeHTTPReturn(w, r)
|
||||
checkStatus(w, "error", http.StatusInternalServerError)
|
||||
})
|
||||
|
||||
t.Run("403 forbidden, status returned by JSONHandlerFunc and HTTPError agree", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", nil)
|
||||
JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
return http.StatusForbidden, nil, Error(http.StatusForbidden, "403 forbidden", nil)
|
||||
}).ServeHTTPReturn(w, r)
|
||||
want := &Response{
|
||||
Status: "error",
|
||||
Data: &Data{},
|
||||
Error: "403 forbidden",
|
||||
}
|
||||
got := checkStatus(w, "error", http.StatusForbidden)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Fatalf(diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("403 forbidden, status returned by JSONHandlerFunc and HTTPError do not agree", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", nil)
|
||||
err := JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
return http.StatusInternalServerError, nil, Error(http.StatusForbidden, "403 forbidden", nil)
|
||||
}).ServeHTTPReturn(w, r)
|
||||
if !strings.HasPrefix(err.Error(), "[unexpected]") {
|
||||
t.Fatalf("returned error should have `[unexpected]` to note the disagreeing status codes: %v", err)
|
||||
}
|
||||
want := &Response{
|
||||
Status: "error",
|
||||
Data: &Data{},
|
||||
Error: "403 forbidden",
|
||||
}
|
||||
got := checkStatus(w, "error", http.StatusForbidden)
|
||||
if diff := cmp.Diff(want, got); diff != "" {
|
||||
t.Fatalf("(-want,+got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -157,11 +157,22 @@ type ReturnHandler interface {
|
||||
ServeHTTPReturn(http.ResponseWriter, *http.Request) error
|
||||
}
|
||||
|
||||
type HandlerOptions struct {
|
||||
Quiet200s bool // if set, do not log successfully handled HTTP requests
|
||||
Logf logger.Logf
|
||||
Now func() time.Time // if nil, defaults to time.Now
|
||||
|
||||
// If non-nil, StatusCodeCounters maintains counters
|
||||
// of status codes for handled responses.
|
||||
// The keys are "1xx", "2xx", "3xx", "4xx", and "5xx".
|
||||
StatusCodeCounters *expvar.Map
|
||||
}
|
||||
|
||||
// StdHandler converts a ReturnHandler into a standard http.Handler.
|
||||
// Handled requests are logged using logf, as are any errors. Errors
|
||||
// are handled as specified by the Handler interface.
|
||||
func StdHandler(h ReturnHandler, logf logger.Logf) http.Handler {
|
||||
return stdHandler(h, logf, time.Now, true)
|
||||
return StdHandlerOpts(h, HandlerOptions{Logf: logf, Now: time.Now})
|
||||
}
|
||||
|
||||
// ReturnHandlerFunc is an adapter to allow the use of ordinary
|
||||
@@ -178,27 +189,32 @@ func (f ReturnHandlerFunc) ServeHTTPReturn(w http.ResponseWriter, r *http.Reques
|
||||
// StdHandlerNo200s is like StdHandler, but successfully handled HTTP
|
||||
// requests don't write an access log entry to logf.
|
||||
//
|
||||
// TODO(danderson): quick stopgap, probably want ...Options on StdHandler instead?
|
||||
// TODO(josharian): eliminate this and StdHandler in favor of StdHandlerOpts,
|
||||
// rename StdHandlerOpts to StdHandler. Will be a breaking API change.
|
||||
func StdHandlerNo200s(h ReturnHandler, logf logger.Logf) http.Handler {
|
||||
return stdHandler(h, logf, time.Now, false)
|
||||
return StdHandlerOpts(h, HandlerOptions{Logf: logf, Now: time.Now, Quiet200s: true})
|
||||
}
|
||||
|
||||
func stdHandler(h ReturnHandler, logf logger.Logf, now func() time.Time, log200s bool) http.Handler {
|
||||
return retHandler{h, logf, now, log200s}
|
||||
// StdHandlerOpts converts a ReturnHandler into a standard http.Handler.
|
||||
// Handled requests are logged using opts.Logf, as are any errors.
|
||||
// Errors are handled as specified by the Handler interface.
|
||||
func StdHandlerOpts(h ReturnHandler, opts HandlerOptions) http.Handler {
|
||||
if opts.Now == nil {
|
||||
opts.Now = time.Now
|
||||
}
|
||||
return retHandler{h, opts}
|
||||
}
|
||||
|
||||
// retHandler is an http.Handler that wraps a Handler and handles errors.
|
||||
type retHandler struct {
|
||||
rh ReturnHandler
|
||||
logf logger.Logf
|
||||
timeNow func() time.Time
|
||||
log200s bool
|
||||
rh ReturnHandler
|
||||
opts HandlerOptions
|
||||
}
|
||||
|
||||
// ServeHTTP implements the http.Handler interface.
|
||||
func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
msg := AccessLogRecord{
|
||||
When: h.timeNow(),
|
||||
When: h.opts.Now(),
|
||||
RemoteAddr: r.RemoteAddr,
|
||||
Proto: r.Proto,
|
||||
TLS: r.TLS != nil,
|
||||
@@ -209,7 +225,7 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
Referer: r.Referer(),
|
||||
}
|
||||
|
||||
lw := &loggingResponseWriter{ResponseWriter: w, logf: h.logf}
|
||||
lw := &loggingResponseWriter{ResponseWriter: w, logf: h.opts.Logf}
|
||||
err := h.rh.ServeHTTPReturn(lw, r)
|
||||
hErr, hErrOK := err.(HTTPError)
|
||||
|
||||
@@ -219,7 +235,7 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
lw.code = 200
|
||||
}
|
||||
|
||||
msg.Seconds = h.timeNow().Sub(msg.When).Seconds()
|
||||
msg.Seconds = h.opts.Now().Sub(msg.When).Seconds()
|
||||
msg.Code = lw.code
|
||||
msg.Bytes = lw.bytes
|
||||
|
||||
@@ -236,16 +252,21 @@ 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)
|
||||
h.opts.Logf("[unexpected] handler returned HTTPError %v, but already sent a response with code %d", hErr, lw.code)
|
||||
break
|
||||
}
|
||||
msg.Code = hErr.Code
|
||||
if msg.Code == 0 {
|
||||
h.logf("[unexpected] HTTPError %v did not contain an HTTP status code, sending internal server error", hErr)
|
||||
h.opts.Logf("[unexpected] HTTPError %v did not contain an HTTP status code, sending internal server error", hErr)
|
||||
msg.Code = http.StatusInternalServerError
|
||||
}
|
||||
http.Error(lw, hErr.Msg, msg.Code)
|
||||
@@ -259,8 +280,13 @@ func (h retHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
if msg.Code != 200 || h.log200s {
|
||||
h.logf("%s", msg)
|
||||
if msg.Code != 200 || !h.opts.Quiet200s {
|
||||
h.opts.Logf("%s", msg)
|
||||
}
|
||||
|
||||
if h.opts.StatusCodeCounters != nil {
|
||||
key := fmt.Sprintf("%dxx", msg.Code/100)
|
||||
h.opts.StatusCodeCounters.Add(key, 1)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
@@ -247,7 +248,7 @@ func TestStdHandler(t *testing.T) {
|
||||
clock.Reset()
|
||||
|
||||
rec := noopHijacker{httptest.NewRecorder(), false}
|
||||
h := stdHandler(test.rh, logf, clock.Now, true)
|
||||
h := StdHandlerOpts(test.rh, HandlerOptions{Logf: logf, Now: clock.Now})
|
||||
h.ServeHTTP(&rec, test.r)
|
||||
res := rec.Result()
|
||||
if res.StatusCode != test.wantCode {
|
||||
|
||||
46
types/flagtype/flagtype.go
Normal file
46
types/flagtype/flagtype.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package flagtype defines flag.Value types.
|
||||
package flagtype
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type portValue struct{ n *uint16 }
|
||||
|
||||
func PortValue(dst *uint16, defaultPort uint16) flag.Value {
|
||||
*dst = defaultPort
|
||||
return portValue{dst}
|
||||
}
|
||||
|
||||
func (p portValue) String() string {
|
||||
if p.n == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprint(*p.n)
|
||||
}
|
||||
func (p portValue) Set(v string) error {
|
||||
if v == "" {
|
||||
return errors.New("can't be the empty string")
|
||||
}
|
||||
if strings.Contains(v, ":") {
|
||||
return errors.New("expecting just a port number, without a colon")
|
||||
}
|
||||
n, err := strconv.ParseUint(v, 10, 64) // use 64 instead of 16 to return nicer error message
|
||||
if err != nil {
|
||||
return fmt.Errorf("not a valid number")
|
||||
}
|
||||
if n > math.MaxUint16 {
|
||||
return errors.New("out of range for port number")
|
||||
}
|
||||
*p.n = uint16(n)
|
||||
return nil
|
||||
}
|
||||
@@ -158,7 +158,10 @@ func LogOnChange(logf Logf, maxInterval time.Duration, timeNow func() time.Time)
|
||||
tLastLogged = timeNow()
|
||||
mu.Unlock()
|
||||
|
||||
logf(s)
|
||||
// Re-stringify it (instead of using "%s", s) so something like "%s"
|
||||
// doesn't end up getting rate-limited. (And can't use 's' as the pattern,
|
||||
// as it might contain formatting directives.)
|
||||
logf(format, args...)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,19 +2,17 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wgengine
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// RusagePrefixLog returns a Logf func wrapping the provided logf func that adds
|
||||
// a prefixed log message to each line with the current binary memory usage
|
||||
// and max RSS.
|
||||
func RusagePrefixLog(logf logger.Logf) logger.Logf {
|
||||
func RusagePrefixLog(logf Logf) Logf {
|
||||
return func(f string, argv ...interface{}) {
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
// +build !windows
|
||||
|
||||
package wgengine
|
||||
package logger
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
@@ -19,7 +19,7 @@ func rusageMaxRSS() float64 {
|
||||
}
|
||||
|
||||
rss := float64(ru.Maxrss)
|
||||
if runtime.GOOS == "darwin" {
|
||||
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
|
||||
rss /= 1 << 20 // ru_maxrss is bytes on darwin
|
||||
} else {
|
||||
// ru_maxrss is kilobytes elsewhere (linux, openbsd, etc)
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package wgengine
|
||||
package logger
|
||||
|
||||
func rusageMaxRSS() float64 {
|
||||
// TODO(apenwarr): Substitute Windows equivalent of Getrusage() here.
|
||||
10
util/endian/big.go
Normal file
10
util/endian/big.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 mips mips64 ppc64 s390x
|
||||
|
||||
package endian
|
||||
|
||||
// Big is whether the current platform is big endian.
|
||||
const Big = true
|
||||
6
util/endian/endian.go
Normal file
6
util/endian/endian.go
Normal file
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package endian exports a constant about whether the machine is big endian.
|
||||
package endian
|
||||
10
util/endian/little.go
Normal file
10
util/endian/little.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 386 amd64 arm arm64 mips64le mipsle ppc64le riscv64 wasm
|
||||
|
||||
package endian
|
||||
|
||||
// Big is whether the current platform is big endian.
|
||||
const Big = false
|
||||
9
util/racebuild/off.go
Normal file
9
util/racebuild/off.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// 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 !race
|
||||
|
||||
package racebuild
|
||||
|
||||
const On = false
|
||||
9
util/racebuild/on.go
Normal file
9
util/racebuild/on.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// 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 race
|
||||
|
||||
package racebuild
|
||||
|
||||
const On = true
|
||||
7
util/racebuild/racebuild.go
Normal file
7
util/racebuild/racebuild.go
Normal file
@@ -0,0 +1,7 @@
|
||||
// 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 racebuild exports a constant about whether the current binary
|
||||
// was built with the race detector.
|
||||
package racebuild
|
||||
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] })
|
||||
}
|
||||
}
|
||||
3
version/.gitignore
vendored
3
version/.gitignore
vendored
@@ -1,6 +1,9 @@
|
||||
describe.txt
|
||||
long.txt
|
||||
short.txt
|
||||
gitcommit.txt
|
||||
extragitcommit.txt
|
||||
version-info.sh
|
||||
version.h
|
||||
version.xcconfig
|
||||
ver.go
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
describe=$(cd ../.. && git describe --long --abbrev=9)
|
||||
echo "$describe" >$3
|
||||
redo-always
|
||||
redo-stamp <$3
|
||||
@@ -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 ""
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
redo-ifchange mkversion.sh describe.txt
|
||||
read -r describe <describe.txt
|
||||
ver=$(./mkversion.sh long "$describe")
|
||||
echo "$ver" >$3
|
||||
@@ -1,111 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
mode=$1
|
||||
describe=$2
|
||||
|
||||
# Git describe output overall looks like
|
||||
# MAJOR.MINOR.PATCH-NUMCOMMITS-GITHASH. Depending on the tag being
|
||||
# described and the state of the repo, ver can be missing PATCH,
|
||||
# NUMCOMMITS, or both.
|
||||
#
|
||||
# Valid values look like: 1.2.3-1234-abcdef, 0.98-1234-abcdef,
|
||||
# 1.0.0-abcdef, 0.99-abcdef.
|
||||
ver="${describe#v}"
|
||||
stem="${ver%%-*}" # Just the semver-ish bit e.g. 1.2.3, 0.98
|
||||
suffix="${ver#$stem}" # The rest e.g. -23-abcdef, -abcdef
|
||||
|
||||
# Normalize the stem into a full major.minor.patch semver. We might
|
||||
# not use all those pieces depending on what kind of version we're
|
||||
# making, but it's good to have them all on hand.
|
||||
case "$stem" in
|
||||
*.*.*)
|
||||
# Full SemVer, nothing to do
|
||||
stem="$stem"
|
||||
;;
|
||||
*.*)
|
||||
# Old style major.minor, add a .0
|
||||
stem="${stem}.0"
|
||||
;;
|
||||
*)
|
||||
echo "Unparseable version $stem" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
major=$(echo "$stem" | cut -f1 -d.)
|
||||
minor=$(echo "$stem" | cut -f2 -d.)
|
||||
patch=$(echo "$stem" | cut -f3 -d.)
|
||||
|
||||
# Extract the change count and git ID from the suffix.
|
||||
case "$suffix" in
|
||||
-*-*)
|
||||
# Has both a change count and a commit hash.
|
||||
changecount=$(echo "$suffix" | cut -f2 -d-)
|
||||
githash=$(echo "$suffix" | cut -f3 -d-)
|
||||
;;
|
||||
-*)
|
||||
# Git hash only, change count is zero.
|
||||
changecount="0"
|
||||
githash=$(echo "$suffix" | cut -f2 -d-)
|
||||
;;
|
||||
*)
|
||||
echo "Unparseable version suffix $suffix" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Validate that the version data makes sense. Rules:
|
||||
# - Odd number minors are unstable. Patch must be 0, and gets
|
||||
# replaced by changecount.
|
||||
# - Even number minors are stable. Changecount must be 0, and
|
||||
# gets removed.
|
||||
#
|
||||
# After this section, we only use major/minor/patch, which have been
|
||||
# tweaked as needed.
|
||||
if expr "$minor" : "[0-9]*[13579]$" >/dev/null; then
|
||||
# Unstable
|
||||
if [ "$patch" != "0" ]; then
|
||||
# This is a fatal error, because a non-zero patch number
|
||||
# indicates that we created an unstable git tag in violation
|
||||
# of our versioning policy, and we want to blow up loudly to
|
||||
# get that fixed.
|
||||
echo "Unstable release $describe has a non-zero patch number, which is not allowed" >&2
|
||||
exit 1
|
||||
fi
|
||||
patch="$changecount"
|
||||
else
|
||||
# Stable
|
||||
if [ "$changecount" != "0" ]; then
|
||||
# This is a commit that's sitting between two stable
|
||||
# releases. We never want to release such a commit to the
|
||||
# pbulic, but it's useful to be able to build it for
|
||||
# debugging. Just force the version to 0.0.0, so that we're
|
||||
# forced to rely on the git commit hash.
|
||||
major=0
|
||||
minor=0
|
||||
patch=0
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$1" in
|
||||
long)
|
||||
echo "${major}.${minor}.${patch}-${githash}"
|
||||
;;
|
||||
short)
|
||||
echo "${major}.${minor}.${patch}"
|
||||
;;
|
||||
xcode)
|
||||
# CFBundleShortVersionString: the "short name" used in the App
|
||||
# Store. eg. 0.92.98
|
||||
echo "VERSION_NAME = ${major}.${minor}.${patch}"
|
||||
# CFBundleVersion: the build number. Needs to be 3 numeric
|
||||
# sections that increment for each release according to SemVer
|
||||
# rules.
|
||||
#
|
||||
# We start counting at 100 because we submitted using raw
|
||||
# build numbers before, and Apple doesn't let you start over.
|
||||
# e.g. 0.98.3 -> 100.98.3
|
||||
echo "VERSION_ID = $((major + 100)).${minor}.${patch}"
|
||||
;;
|
||||
esac
|
||||
@@ -7,69 +7,96 @@ package version
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
)
|
||||
|
||||
func xcode(short, long string) string {
|
||||
return fmt.Sprintf("VERSION_NAME = %s\nVERSION_ID = %s", short, long)
|
||||
}
|
||||
|
||||
func mkversion(t *testing.T, mode, in string) (string, bool) {
|
||||
func mkversion(t *testing.T, gitHash, otherHash string, major, minor, patch, changeCount int) (string, bool) {
|
||||
t.Helper()
|
||||
bs, err := exec.Command("./mkversion.sh", mode, in).CombinedOutput()
|
||||
bs, err := exec.Command("./version.sh", gitHash, otherHash, strconv.Itoa(major), strconv.Itoa(minor), strconv.Itoa(patch), strconv.Itoa(changeCount)).CombinedOutput()
|
||||
out := strings.TrimSpace(string(bs))
|
||||
if err != nil {
|
||||
t.Logf("mkversion.sh output: %s", string(bs))
|
||||
return "", false
|
||||
return out, false
|
||||
}
|
||||
return strings.TrimSpace(string(bs)), true
|
||||
return out, true
|
||||
}
|
||||
|
||||
func TestMkversion(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skip test on Windows, because there is no shell to execute mkversion.sh.")
|
||||
}
|
||||
tests := []struct {
|
||||
in string
|
||||
ok bool
|
||||
long string
|
||||
short string
|
||||
xcode string
|
||||
gitHash, otherHash string
|
||||
major, minor, patch, changeCount int
|
||||
want string
|
||||
}{
|
||||
{"v0.98-abcdef", true, "0.98.0-abcdef", "0.98.0", xcode("0.98.0", "100.98.0")},
|
||||
{"v0.98.1-abcdef", true, "0.98.1-abcdef", "0.98.1", xcode("0.98.1", "100.98.1")},
|
||||
{"v1.1.0-37-abcdef", true, "1.1.37-abcdef", "1.1.37", xcode("1.1.37", "101.1.37")},
|
||||
{"v1.2.9-abcdef", true, "1.2.9-abcdef", "1.2.9", xcode("1.2.9", "101.2.9")},
|
||||
{"v1.2.9-0-abcdef", true, "1.2.9-abcdef", "1.2.9", xcode("1.2.9", "101.2.9")},
|
||||
{"v1.15.0-129-abcdef", true, "1.15.129-abcdef", "1.15.129", xcode("1.15.129", "101.15.129")},
|
||||
|
||||
{"v0.98-123-abcdef", true, "0.0.0-abcdef", "0.0.0", xcode("0.0.0", "100.0.0")},
|
||||
{"v1.0.0-37-abcdef", true, "0.0.0-abcdef", "0.0.0", xcode("0.0.0", "100.0.0")},
|
||||
|
||||
{"v0.99.5-0-abcdef", false, "", "", ""}, // unstable, patch not allowed
|
||||
{"v0.99.5-123-abcdef", false, "", "", ""}, // unstable, patch not allowed
|
||||
{"v1-abcdef", false, "", "", ""}, // bad semver
|
||||
{"v1.0", false, "", "", ""}, // missing suffix
|
||||
{"abcdef", "", 0, 98, 0, 0, `
|
||||
VERSION_SHORT="0.98.0"
|
||||
VERSION_LONG="0.98.0-tabcdef"
|
||||
VERSION_GIT_HASH="abcdef"
|
||||
VERSION_EXTRA_HASH=""
|
||||
VERSION_XCODE="100.98.0"
|
||||
VERSION_WINRES="0,98,0,0"`},
|
||||
{"abcdef", "", 0, 98, 1, 0, `
|
||||
VERSION_SHORT="0.98.1"
|
||||
VERSION_LONG="0.98.1-tabcdef"
|
||||
VERSION_GIT_HASH="abcdef"
|
||||
VERSION_EXTRA_HASH=""
|
||||
VERSION_XCODE="100.98.1"
|
||||
VERSION_WINRES="0,98,1,0"`},
|
||||
{"abcdef", "", 1, 1, 0, 37, `
|
||||
VERSION_SHORT="1.1.1037"
|
||||
VERSION_LONG="1.1.1037-tabcdef"
|
||||
VERSION_GIT_HASH="abcdef"
|
||||
VERSION_EXTRA_HASH=""
|
||||
VERSION_XCODE="101.1.1037"
|
||||
VERSION_WINRES="1,1,1037,0"`},
|
||||
{"abcdef", "", 1, 2, 9, 0, `
|
||||
VERSION_SHORT="1.2.9"
|
||||
VERSION_LONG="1.2.9-tabcdef"
|
||||
VERSION_GIT_HASH="abcdef"
|
||||
VERSION_EXTRA_HASH=""
|
||||
VERSION_XCODE="101.2.9"
|
||||
VERSION_WINRES="1,2,9,0"`},
|
||||
{"abcdef", "", 1, 15, 0, 129, `
|
||||
VERSION_SHORT="1.15.129"
|
||||
VERSION_LONG="1.15.129-tabcdef"
|
||||
VERSION_GIT_HASH="abcdef"
|
||||
VERSION_EXTRA_HASH=""
|
||||
VERSION_XCODE="101.15.129"
|
||||
VERSION_WINRES="1,15,129,0"`},
|
||||
{"abcdef", "", 1, 2, 0, 17, `
|
||||
VERSION_SHORT="1.2.0"
|
||||
VERSION_LONG="1.2.0-17-tabcdef"
|
||||
VERSION_GIT_HASH="abcdef"
|
||||
VERSION_EXTRA_HASH=""
|
||||
VERSION_XCODE="101.2.0"
|
||||
VERSION_WINRES="1,2,0,0"`},
|
||||
{"abcdef", "defghi", 1, 15, 0, 129, `
|
||||
VERSION_SHORT="1.15.129"
|
||||
VERSION_LONG="1.15.129-tabcdef-gdefghi"
|
||||
VERSION_GIT_HASH="abcdef"
|
||||
VERSION_EXTRA_HASH="defghi"
|
||||
VERSION_XCODE="101.15.129"
|
||||
VERSION_WINRES="1,15,129,0"`},
|
||||
{"abcdef", "", 0, 99, 5, 0, ""}, // unstable, patch number not allowed
|
||||
{"abcdef", "", 0, 99, 5, 123, ""}, // unstable, patch number not allowed
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
gotlong, longOK := mkversion(t, "long", test.in)
|
||||
if longOK != test.ok {
|
||||
t.Errorf("mkversion.sh long %q ok=%v, want %v", test.in, longOK, test.ok)
|
||||
want := strings.ReplaceAll(strings.TrimSpace(test.want), " ", "")
|
||||
got, ok := mkversion(t, test.gitHash, test.otherHash, test.major, test.minor, test.patch, test.changeCount)
|
||||
invoc := fmt.Sprintf("version.sh %s %s %d %d %d %d", test.gitHash, test.otherHash, test.major, test.minor, test.patch, test.changeCount)
|
||||
if want == "" && ok {
|
||||
t.Errorf("%s ok=true, want false", invoc)
|
||||
continue
|
||||
}
|
||||
gotshort, shortOK := mkversion(t, "short", test.in)
|
||||
if shortOK != test.ok {
|
||||
t.Errorf("mkversion.sh short %q ok=%v, want %v", test.in, shortOK, test.ok)
|
||||
}
|
||||
gotxcode, xcodeOK := mkversion(t, "xcode", test.in)
|
||||
if xcodeOK != test.ok {
|
||||
t.Errorf("mkversion.sh xcode %q ok=%v, want %v", test.in, xcodeOK, test.ok)
|
||||
}
|
||||
if longOK && gotlong != test.long {
|
||||
t.Errorf("mkversion.sh long %q: got %q, want %q", test.in, gotlong, test.long)
|
||||
}
|
||||
if shortOK && gotshort != test.short {
|
||||
t.Errorf("mkversion.sh short %q: got %q, want %q", test.in, gotshort, test.short)
|
||||
}
|
||||
if xcodeOK && gotxcode != test.xcode {
|
||||
t.Errorf("mkversion.sh xcode %q: got %q, want %q", test.in, gotxcode, test.xcode)
|
||||
if diff := cmp.Diff(got, want); want != "" && diff != "" {
|
||||
t.Errorf("%s wrong output (-got+want):\n%s", invoc, diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
25
version/print.go
Normal file
25
version/print.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 version
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func String() string {
|
||||
var ret strings.Builder
|
||||
ret.WriteString(Short)
|
||||
ret.WriteByte('\n')
|
||||
if GitCommit != "" {
|
||||
fmt.Fprintf(&ret, " tailscale commit: %s\n", GitCommit)
|
||||
}
|
||||
if ExtraGitCommit != "" {
|
||||
fmt.Fprintf(&ret, " other commit: %s\n", ExtraGitCommit)
|
||||
}
|
||||
fmt.Fprintf(&ret, " go version: %s\n", runtime.Version())
|
||||
return strings.TrimSpace(ret.String())
|
||||
}
|
||||
@@ -10,17 +10,16 @@ import "runtime"
|
||||
func IsMobile() bool {
|
||||
// Good enough heuristic for now, at least until Apple makes
|
||||
// ARM laptops...
|
||||
return runtime.GOOS == "android" ||
|
||||
(runtime.GOOS == "darwin" && (runtime.GOARCH == "arm" || runtime.GOARCH == "arm64"))
|
||||
return runtime.GOOS == "android" || isIOS
|
||||
}
|
||||
|
||||
// OS returns runtime.GOOS, except instead of returning "darwin" it
|
||||
// returns "iOS" or "macOS".
|
||||
func OS() string {
|
||||
if isIOS {
|
||||
return "iOS"
|
||||
}
|
||||
if runtime.GOOS == "darwin" {
|
||||
if IsMobile() {
|
||||
return "iOS"
|
||||
}
|
||||
return "macOS"
|
||||
}
|
||||
return runtime.GOOS
|
||||
|
||||
9
version/prop_ios.go
Normal file
9
version/prop_ios.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// 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 ios
|
||||
|
||||
package version
|
||||
|
||||
const isIOS = true
|
||||
9
version/prop_notios.go
Normal file
9
version/prop_notios.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// 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 !ios
|
||||
|
||||
package version
|
||||
|
||||
const isIOS = false
|
||||
@@ -1,4 +0,0 @@
|
||||
redo-ifchange mkversion.sh describe.txt
|
||||
read -r describe <describe.txt
|
||||
ver=$(./mkversion.sh short "$describe")
|
||||
echo "$ver" >$3
|
||||
@@ -1,8 +1,9 @@
|
||||
redo-ifchange long.txt short.txt ver.go.in
|
||||
redo-ifchange version-info.sh ver.go.in
|
||||
|
||||
read -r LONGVER <long.txt
|
||||
read -r SHORTVER <short.txt
|
||||
. ./version-info.sh
|
||||
|
||||
sed -e "s/{LONGVER}/$LONGVER/g" \
|
||||
-e "s/{SHORTVER}/$SHORTVER/g" \
|
||||
sed -e "s/{LONGVER}/$VERSION_LONG/g" \
|
||||
-e "s/{SHORTVER}/$VERSION_SHORT/g" \
|
||||
-e "s/{GITCOMMIT}/$VERSION_GIT_HASH/g" \
|
||||
-e "s/{EXTRAGITCOMMIT}/$VERSION_EXTRA_HASH/g" \
|
||||
<ver.go.in >$3
|
||||
|
||||
@@ -6,5 +6,9 @@
|
||||
|
||||
package version
|
||||
|
||||
const LONG = "{LONGVER}"
|
||||
const SHORT = "{SHORTVER}"
|
||||
const Long = "{LONGVER}"
|
||||
const Short = "{SHORTVER}"
|
||||
const LONG = Long
|
||||
const SHORT = Short
|
||||
const GitCommit = "{GITCOMMIT}"
|
||||
const ExtraGitCommit = "{EXTRAGITCOMMIT}"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user