Compare commits
182 Commits
c22wen/mag
...
v1.2.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d7ae3146c | ||
|
|
76c2982d88 | ||
|
|
3d64eef37b | ||
|
|
4f292740b0 | ||
|
|
e1327154bb | ||
|
|
a702921620 | ||
|
|
b9b7fbdd21 | ||
|
|
9446e5c170 | ||
|
|
75cd82791e | ||
|
|
28f6552646 | ||
|
|
1036f51a56 | ||
|
|
07b6ffd55c | ||
|
|
de5da37a22 | ||
|
|
65bad9a8bd | ||
|
|
20a357b386 | ||
|
|
4d5d5f89a3 | ||
|
|
437142daa5 | ||
|
|
710b105f38 | ||
|
|
f3aa08de76 | ||
|
|
cc3259f8d9 | ||
|
|
01ee638cca | ||
|
|
037daad47a | ||
|
|
3b46655dbb | ||
|
|
e98f2c57d6 | ||
|
|
eab6e9ea4e | ||
|
|
bb058703ee | ||
|
|
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 | ||
|
|
dea3ef0597 | ||
|
|
3aeb2e204c | ||
|
|
acafe9811f | ||
|
|
48fbe93e72 | ||
|
|
96fd20e3c0 | ||
|
|
7f97cf654d | ||
|
|
3fa863e6d9 | ||
|
|
e862f90e34 | ||
|
|
761fe19e5f | ||
|
|
88107b1287 | ||
|
|
931bcd44cb | ||
|
|
7e9d1f7808 | ||
|
|
8f5b52e571 | ||
|
|
3f4d93feb2 | ||
|
|
a5d701095b | ||
|
|
0c0239242c | ||
|
|
6e38d29485 | ||
|
|
41f6c78c53 | ||
|
|
662c19551a | ||
|
|
4f7751e025 | ||
|
|
4f71319f7c | ||
|
|
3af64765fd | ||
|
|
a084c44afc | ||
|
|
31c13013ae | ||
|
|
9ab2b32569 | ||
|
|
5a94317628 | ||
|
|
37b40b035b | ||
|
|
bc1751a376 | ||
|
|
b14288f96c | ||
|
|
23f01174ea | ||
|
|
40e12c17ec | ||
|
|
f65eb4e5c1 | ||
|
|
8b60936913 | ||
|
|
edb47b98a8 |
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'
|
||||
|
||||
2
Makefile
2
Makefile
@@ -7,12 +7,10 @@ vet:
|
||||
updatedeps:
|
||||
go run github.com/tailscale/depaware --update tailscale.com/cmd/tailscaled
|
||||
go run github.com/tailscale/depaware --update tailscale.com/cmd/tailscale
|
||||
go run github.com/tailscale/depaware --update tailscale.com/cmd/tsshd
|
||||
|
||||
depaware:
|
||||
go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscaled
|
||||
go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscale
|
||||
go run github.com/tailscale/depaware --check tailscale.com/cmd/tsshd
|
||||
|
||||
check: staticcheck vet depaware
|
||||
|
||||
|
||||
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.2.3
|
||||
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)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"expvar"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@@ -62,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)
|
||||
@@ -229,10 +230,10 @@ func debugHandler(s *derp.Server) http.Handler {
|
||||
<h1>DERP debug</h1>
|
||||
<ul>
|
||||
`)
|
||||
f("<li><b>Hostname:</b> %v</li>\n", *hostname)
|
||||
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", 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>
|
||||
|
||||
@@ -34,6 +34,7 @@ var (
|
||||
logCollection = flag.String("logcollection", "", "If non-empty, logtail collection to log to")
|
||||
nodeExporter = flag.String("node-exporter", "http://localhost:9100", "URL of the local prometheus node exporter")
|
||||
goVarsURL = flag.String("go-vars-url", "http://localhost:8383/debug/vars", "URL of a local Go server's /debug/vars endpoint")
|
||||
insecure = flag.Bool("insecure", false, "serve over http, for development")
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -66,12 +67,15 @@ func main() {
|
||||
httpsrv := &http.Server{
|
||||
Addr: *addr,
|
||||
Handler: mux,
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: ch.GetCertificate,
|
||||
},
|
||||
}
|
||||
|
||||
if err := httpsrv.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
|
||||
if !*insecure {
|
||||
httpsrv.TLSConfig = &tls.Config{GetCertificate: ch.GetCertificate}
|
||||
err = httpsrv.ListenAndServeTLS("", "")
|
||||
} else {
|
||||
err = httpsrv.ListenAndServe()
|
||||
}
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@ func runDown(ctx context.Context, args []string) error {
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Printf("Notify: %#v", n)
|
||||
})
|
||||
|
||||
bc.RequestStatus()
|
||||
|
||||
@@ -7,6 +7,7 @@ package cli
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -23,18 +24,10 @@ import (
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
"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]",
|
||||
@@ -58,19 +51,25 @@ 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)")
|
||||
}
|
||||
if runtime.GOOS == "linux" {
|
||||
upf.BoolVar(&upArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with -advertise-routes")
|
||||
upf.StringVar(&upArgs.netfilterMode, "netfilter-mode", "on", "netfilter mode (one of on, nodivert, off)")
|
||||
upf.BoolVar(&upArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
|
||||
upf.StringVar(&upArgs.netfilterMode, "netfilter-mode", defaultNetfilterMode(), "netfilter mode (one of on, nodivert, off)")
|
||||
}
|
||||
return upf
|
||||
})(),
|
||||
Exec: runUp,
|
||||
}
|
||||
|
||||
func defaultNetfilterMode() string {
|
||||
if distro.Get() == distro.Synology {
|
||||
return "off"
|
||||
}
|
||||
return "on"
|
||||
}
|
||||
|
||||
var upArgs struct {
|
||||
server string
|
||||
acceptRoutes bool
|
||||
@@ -80,7 +79,6 @@ var upArgs struct {
|
||||
forceReauth bool
|
||||
advertiseRoutes string
|
||||
advertiseTags string
|
||||
enableDERP bool
|
||||
snat bool
|
||||
netfilterMode string
|
||||
authKey string
|
||||
@@ -151,6 +149,19 @@ func runUp(ctx context.Context, args []string) error {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
if distro.Get() == distro.Synology {
|
||||
notSupported := "not yet supported on Synology; see https://github.com/tailscale/tailscale/issues/451"
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
return errors.New("--advertise-routes is " + notSupported)
|
||||
}
|
||||
if upArgs.acceptRoutes {
|
||||
return errors.New("--accept-routes is " + notSupported)
|
||||
}
|
||||
if upArgs.netfilterMode != "off" {
|
||||
return errors.New("--netfilter-mode values besides \"off\" " + notSupported)
|
||||
}
|
||||
}
|
||||
|
||||
var routes []wgcfg.CIDR
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
|
||||
@@ -171,11 +182,19 @@ func runUp(ctx context.Context, args []string) error {
|
||||
var tags []string
|
||||
if upArgs.advertiseTags != "" {
|
||||
tags = strings.Split(upArgs.advertiseTags, ",")
|
||||
for _, tag := range tags {
|
||||
err := tailcfg.CheckTag(tag)
|
||||
if err != nil {
|
||||
fatalf("tag: %q: %s", tag, err)
|
||||
for i, tag := range tags {
|
||||
if strings.HasPrefix(tag, "tag:") {
|
||||
// Accept fully-qualified tags (starting with
|
||||
// "tag:"), as we do in the ACL file.
|
||||
if err := tailcfg.CheckTag(tag); err != nil {
|
||||
fatalf("tag: %q: %v", tag, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := tailcfg.CheckTagSuffix(tag); err != nil {
|
||||
fatalf("tag: %q: %v", tag, err)
|
||||
}
|
||||
tags[i] = "tag:" + tag
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,8 +213,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":
|
||||
@@ -219,8 +239,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 {
|
||||
@@ -248,6 +269,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/wgengine/router
|
||||
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
|
||||
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
|
||||
@@ -36,6 +36,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
|
||||
💣 go4.org/mem from tailscale.com/control/controlclient+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
@@ -50,12 +51,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn
|
||||
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
||||
tailscale.com/logtail/backoff from tailscale.com/control/controlclient
|
||||
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
|
||||
tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/net/dnscache from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netns from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/netns from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
@@ -63,9 +64,10 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/portlist from tailscale.com/ipn
|
||||
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
|
||||
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/key from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+
|
||||
@@ -75,12 +77,13 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
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+
|
||||
tailscale.com/wgengine from tailscale.com/ipn
|
||||
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 from tailscale.com/cmd/tailscale/cli+
|
||||
💣 tailscale.com/wgengine/router/dns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tstun from tailscale.com/wgengine
|
||||
@@ -88,18 +91,26 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/curve25519 from github.com/tailscale/wireguard-go/wgcfg+
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
golang.org/x/crypto/curve25519 from crypto/tls+
|
||||
golang.org/x/crypto/hkdf from crypto/tls
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/control/controlclient+
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/context/ctxhttp from golang.org/x/oauth2/internal
|
||||
golang.org/x/net/dns/dnsmessage from tailscale.com/wgengine/tsdns
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from net/http
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device
|
||||
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net
|
||||
golang.org/x/oauth2 from tailscale.com/control/controlclient+
|
||||
golang.org/x/oauth2/internal from golang.org/x/oauth2
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp
|
||||
@@ -108,27 +119,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
|
||||
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
|
||||
W golang.org/x/sys/windows/registry from github.com/tailscale/wireguard-go/tun/wintun+
|
||||
W golang.org/x/text/transform from golang.org/x/text/unicode/norm
|
||||
W golang.org/x/text/unicode/norm from github.com/tailscale/wireguard-go/tun/wintun
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
golang.org/x/text/unicode/norm from github.com/tailscale/wireguard-go/tun/wintun+
|
||||
golang.org/x/time/rate from tailscale.com/types/logger+
|
||||
vendor/golang.org/x/crypto/chacha20 from vendor/golang.org/x/crypto/chacha20poly1305
|
||||
vendor/golang.org/x/crypto/chacha20poly1305 from crypto/tls
|
||||
vendor/golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
vendor/golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
vendor/golang.org/x/crypto/curve25519 from crypto/tls
|
||||
vendor/golang.org/x/crypto/hkdf from crypto/tls
|
||||
vendor/golang.org/x/crypto/poly1305 from vendor/golang.org/x/crypto/chacha20poly1305
|
||||
vendor/golang.org/x/net/dns/dnsmessage from net
|
||||
vendor/golang.org/x/net/http/httpguts from net/http
|
||||
vendor/golang.org/x/net/http/httpproxy from net/http
|
||||
vendor/golang.org/x/net/http2/hpack from net/http
|
||||
vendor/golang.org/x/net/idna from net/http+
|
||||
D vendor/golang.org/x/net/route from net
|
||||
vendor/golang.org/x/sys/cpu from vendor/golang.org/x/crypto/chacha20poly1305
|
||||
vendor/golang.org/x/text/secure/bidirule from vendor/golang.org/x/net/idna
|
||||
vendor/golang.org/x/text/transform from vendor/golang.org/x/text/secure/bidirule+
|
||||
vendor/golang.org/x/text/unicode/bidi from vendor/golang.org/x/net/idna+
|
||||
vendor/golang.org/x/text/unicode/norm from vendor/golang.org/x/net/idna
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
@@ -190,7 +185,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
net/http from expvar+
|
||||
net/http/httptrace from github.com/tcnksm/go-httpstat+
|
||||
net/http/internal from net/http
|
||||
net/textproto from mime/multipart+
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
net/url from crypto/x509+
|
||||
os from crypto/rand+
|
||||
os/exec from github.com/coreos/go-iptables/iptables+
|
||||
@@ -201,7 +196,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
reflect from crypto/x509+
|
||||
regexp from github.com/coreos/go-iptables/iptables+
|
||||
regexp/syntax from regexp
|
||||
LD runtime/cgo
|
||||
runtime/pprof from tailscale.com/log/logheap+
|
||||
sort from compress/flate+
|
||||
strconv from compress/flate+
|
||||
@@ -214,4 +208,3 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
unicode from bytes+
|
||||
unicode/utf16 from encoding/asn1+
|
||||
unicode/utf8 from bufio+
|
||||
unsafe from crypto/internal/subtle+
|
||||
|
||||
@@ -5,6 +5,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscaled
|
||||
W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
|
||||
LW github.com/go-multierror/multierror from tailscale.com/wgengine/router
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/wgengine/router/dns
|
||||
@@ -18,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/wgengine/router
|
||||
💣 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
|
||||
@@ -39,6 +38,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/conn+
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
💣 go4.org/mem from tailscale.com/control/controlclient+
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr from tailscale.com/control/controlclient+
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
@@ -51,6 +51,7 @@ 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
|
||||
@@ -58,9 +59,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/logtail/filch from tailscale.com/logpolicy
|
||||
tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/net/dnscache from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/interfaces from tailscale.com/ipn+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/ipn+
|
||||
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/net/netns from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/netns from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
@@ -69,10 +71,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/portlist from tailscale.com/ipn
|
||||
tailscale.com/safesocket from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/control/controlclient+
|
||||
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
|
||||
@@ -80,13 +84,15 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/types/strbuilder from tailscale.com/wgengine/packet
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/version from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/version from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/version/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 from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/wgengine/router/dns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tstun from tailscale.com/wgengine
|
||||
@@ -94,8 +100,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+
|
||||
@@ -103,10 +112,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
|
||||
@@ -115,27 +129,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+
|
||||
@@ -176,7 +174,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
|
||||
@@ -199,18 +197,17 @@ 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+
|
||||
os/signal from tailscale.com/cmd/tailscaled+
|
||||
L os/user from github.com/godbus/dbus/v5
|
||||
os/user from github.com/godbus/dbus/v5+
|
||||
path from debug/dwarf+
|
||||
path/filepath from crypto/x509+
|
||||
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
|
||||
@@ -227,4 +224,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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -45,8 +47,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 +74,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 +83,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 +92,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 +112,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 +122,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 +153,9 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
if opts.ServerURL == "" {
|
||||
return nil, errors.New("controlclient.New: no server URL specified")
|
||||
}
|
||||
if opts.MachinePrivateKey.IsZero() {
|
||||
return nil, errors.New("controlclient.New: no MachinePrivateKey specified")
|
||||
}
|
||||
opts.ServerURL = strings.TrimRight(opts.ServerURL, "/")
|
||||
serverURL, err := url.Parse(opts.ServerURL)
|
||||
if err != nil {
|
||||
@@ -158,6 +184,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 +193,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 +212,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 +233,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 +279,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 +317,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 +345,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 +358,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 +383,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 +400,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 +489,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 +504,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 +520,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 +528,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 +536,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 +591,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 +630,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 +680,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 +711,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 +720,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,13 +789,14 @@ func decode(res *http.Response, v interface{}, serverKey *wgcfg.Key, mkey *wgcfg
|
||||
return decodeMsg(msg, v, serverKey, mkey)
|
||||
}
|
||||
|
||||
var debugMap, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_MAP"))
|
||||
|
||||
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
|
||||
}
|
||||
@@ -741,6 +814,11 @@ func (c *Direct) decodeMsg(msg []byte, v interface{}) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if debugMap {
|
||||
var buf bytes.Buffer
|
||||
json.Indent(&buf, b, "", " ")
|
||||
log.Printf("MapResponse: %s", buf.Bytes())
|
||||
}
|
||||
if err := json.Unmarshal(b, v); err != nil {
|
||||
return fmt.Errorf("response: %v", err)
|
||||
}
|
||||
@@ -770,7 +848,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
|
||||
}
|
||||
@@ -780,8 +858,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)
|
||||
}
|
||||
@@ -836,25 +913,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 {
|
||||
@@ -989,3 +1061,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
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/util/lineread"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -23,13 +24,22 @@ func init() {
|
||||
}
|
||||
|
||||
func osVersionLinux() string {
|
||||
dist := distro.Get()
|
||||
propFile := "/etc/os-release"
|
||||
switch dist {
|
||||
case distro.Synology:
|
||||
propFile = "/etc.defaults/VERSION"
|
||||
case distro.OpenWrt:
|
||||
propFile = "/etc/openwrt_release"
|
||||
}
|
||||
|
||||
m := map[string]string{}
|
||||
lineread.File("/etc/os-release", func(line []byte) error {
|
||||
lineread.File(propFile, func(line []byte) error {
|
||||
eq := bytes.IndexByte(line, '=')
|
||||
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
|
||||
})
|
||||
@@ -71,6 +81,12 @@ func osVersionLinux() string {
|
||||
return fmt.Sprintf("%s%s", v, attr)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,10 @@ func osVersionWindows() string {
|
||||
s := strings.TrimSpace(string(out))
|
||||
s = strings.TrimPrefix(s, "Microsoft Windows [")
|
||||
s = strings.TrimSuffix(s, "]")
|
||||
s = strings.TrimPrefix(s, "Version ") // is this localized? do it last in case.
|
||||
return s // "10.0.19041.388", ideally
|
||||
|
||||
// "Version 10.x.y.z", with "Version" localized. Keep only stuff after the space.
|
||||
if sp := strings.Index(s, " "); sp != -1 {
|
||||
s = s[sp+1:]
|
||||
}
|
||||
return s // "10.0.19041.388", ideally
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ type NetworkMap struct {
|
||||
Addresses []wgcfg.CIDR
|
||||
LocalPort uint16 // used for debugging
|
||||
MachineStatus tailcfg.MachineStatus
|
||||
MachineKey tailcfg.MachineKey
|
||||
Peers []*tailcfg.Node // sorted by Node.ID
|
||||
DNS tailcfg.DNSConfig
|
||||
Hostinfo tailcfg.Hostinfo
|
||||
@@ -49,7 +50,6 @@ type NetworkMap struct {
|
||||
// TODO(crawshaw): reduce UserProfiles to []tailcfg.UserProfile?
|
||||
// There are lots of ways to slice this data, leave it up to users.
|
||||
UserProfiles map[tailcfg.UserID]tailcfg.UserProfile
|
||||
Roles []tailcfg.Role
|
||||
// TODO(crawshaw): Groups []tailcfg.Group
|
||||
// TODO(crawshaw): Capabilities []tailcfg.Capability
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -219,7 +229,6 @@ const (
|
||||
AllowSingleHosts WGConfigFlags = 1 << iota
|
||||
AllowSubnetRoutes
|
||||
AllowDefaultRoute
|
||||
HackDefaultRoute
|
||||
)
|
||||
|
||||
// EndpointDiscoSuffix is appended to the hex representation of a peer's discovery key
|
||||
@@ -274,11 +283,7 @@ func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Confi
|
||||
logf("wgcfg: %v skipping default route", peer.Key.ShortString())
|
||||
continue
|
||||
}
|
||||
if (flags & HackDefaultRoute) != 0 {
|
||||
allowedIP = wgcfg.CIDR{IP: wgcfg.IPv4(10, 0, 0, 0), Mask: 8}
|
||||
logf("wgcfg: %v converting default route => %v", peer.Key.ShortString(), allowedIP.String())
|
||||
}
|
||||
} else if allowedIP.Mask < 32 {
|
||||
} else if cidrIsSubnet(peer, allowedIP) {
|
||||
if (flags & AllowSubnetRoutes) == 0 {
|
||||
logf("wgcfg: %v skipping subnet route", peer.Key.ShortString())
|
||||
continue
|
||||
@@ -291,6 +296,29 @@ func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Confi
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// cidrIsSubnet reports whether cidr is a non-default-route subnet
|
||||
// exported by node that is not one of its own self addresses.
|
||||
func cidrIsSubnet(node *tailcfg.Node, cidr wgcfg.CIDR) bool {
|
||||
if cidr.Mask == 0 {
|
||||
return false
|
||||
}
|
||||
if cidr.Mask < 32 {
|
||||
// Fast path for IPv4, to avoid loop below.
|
||||
//
|
||||
// TODO: if cidr.IP is an IPv6 address, we could do "< 128"
|
||||
// to avoid the range over node.Addresses. Or we could
|
||||
// just remove this fast path and unconditionally do the range
|
||||
// loop.
|
||||
return true
|
||||
}
|
||||
for _, selfCIDR := range node.Addresses {
|
||||
if cidr == selfCIDR {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func appendEndpoint(peer *wgcfg.Peer, epStr string) error {
|
||||
if epStr == "" {
|
||||
return nil
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
15
go.mod
15
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-20200909185729-8ca448326e3a
|
||||
github.com/tailscale/winipcfg-go v0.0.0-20200413171540-609dcf2df55f
|
||||
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-20200709230013-948cd5f35899
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208
|
||||
golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425
|
||||
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201004085714-dd60d0447f81
|
||||
honnef.co/go/tools v0.0.1-2020.1.4
|
||||
inet.af/netaddr v0.0.0-20200810144936-56928fe48a98
|
||||
rsc.io/goversion v1.2.0
|
||||
|
||||
66
go.sum
66
go.sum
@@ -3,11 +3,8 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/Masterminds/semver/v3 v3.0.3 h1:znjIyLfpXEDQjOIEWh+ehwpTU14UzUPub3c3sm36u14=
|
||||
github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/alecthomas/kingpin v2.2.6+incompatible h1:5svnBTFgJjZvGKyYBtMB0+m5wvrbUHiqye8wRJMlnYI=
|
||||
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 h1:P5U+E4x5OkVEKQDklVPmzs71WM56RTTRqV4OrDC//Y4=
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5/go.mod h1:976q2ETgjT2snVCf2ZaBnyBbVoPERGjUz+0sofzEfro=
|
||||
@@ -30,6 +27,8 @@ github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjr
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-multierror/multierror v1.0.2 h1:AwsKbEXkmf49ajdFJgcFXqSG0aLo0HEyAE9zk9JguJo=
|
||||
github.com/go-multierror/multierror v1.0.2/go.mod h1:U7SZR/D9jHgt2nkSj8XcbCWdmVM2igraCHQ3HC1HiKY=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
|
||||
@@ -61,6 +60,8 @@ github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lxn/walk v0.0.0-20191128110447-55ccb3a9f5c1/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20191128105842-2da648fda5b4/go.mod h1:ouWl4wViUNh8tPSIwxTVMuS014WakR1hqvBc2I0bMoA=
|
||||
github.com/mattn/go-zglob v0.0.1 h1:xsEx/XUoVlI6yXjqBK062zYhRTZltCNmYPx6v+8DNaY=
|
||||
github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
|
||||
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
|
||||
@@ -76,6 +77,8 @@ github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3/go.mod h1:85jBQOZwp
|
||||
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
||||
github.com/peterbourgon/ff/v2 v2.0.0 h1:lx0oYI5qr/FU1xnpNhQ+EZM04gKgn46jyYvGEEqBBbY=
|
||||
github.com/peterbourgon/ff/v2 v2.0.0/go.mod h1:xjwr+t+SjWm4L46fcj/D+Ap+6ME7+HqFzaP22pP5Ggk=
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 h1:+/+DxvQaYifJ+grD4klzrS5y+KJXldn/2YTl5JG+vZ8=
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -83,15 +86,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/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/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=
|
||||
@@ -100,16 +107,22 @@ github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
|
||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go4.org/mem v0.0.0-20200706164138-185c595c3ecc h1:paujszgN6SpsO/UsXC7xax3gQAKz/XQKCYZLQdU34Tw=
|
||||
go4.org/mem v0.0.0-20200706164138-185c595c3ecc/go.mod h1:NEYvpHWemiG/E5UWfaN5QAIGZeT1sa0Z2UNk6oeMb/k=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88=
|
||||
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899 h1:DZhuSZLsGlFL4CmhA8BcRA0mnthyA/nZ00AqCUo7vHg=
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@@ -117,48 +130,57 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 h1:WQ8q63x+f/zpC8Ac1s9wLElVoHhm32p6tudrU72n1QA=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202 h1:VvcQYSHwXgi7W+TpUR6A9g6Up98WAHf3f/ulnJ62IyA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-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/windows v0.1.2-0.20201004085714-dd60d0447f81 h1:cT2oWlz8v9g7bjFZclT362akxJJfGv9d7ccKu6GQUbA=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201004085714-dd60d0447f81/go.mod h1:GaK5zcgr5XE98WaRzIDilumDBp5/yP8j2kG/LCDnvAM=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -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,23 +7,33 @@ package ipnserver
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/pidowner"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
@@ -61,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.
|
||||
@@ -74,21 +90,185 @@ 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) {
|
||||
s.addConn(c)
|
||||
logf("incoming control connection")
|
||||
// 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{})
|
||||
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
|
||||
}
|
||||
|
||||
defer s.removeAndCloseConn(c)
|
||||
logf("incoming control connection")
|
||||
|
||||
for ctx.Err() == nil {
|
||||
msg, err := ipn.ReadMsg(c)
|
||||
msg, err := ipn.ReadMsg(br)
|
||||
if err != nil {
|
||||
if ctx.Err() == nil {
|
||||
logf("ReadMsg: %v", err)
|
||||
@@ -107,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()
|
||||
}
|
||||
@@ -140,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)
|
||||
}
|
||||
@@ -160,6 +480,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
}
|
||||
|
||||
server := &server{
|
||||
logf: logf,
|
||||
resetOnZero: !opts.SurviveDisconnects,
|
||||
}
|
||||
|
||||
@@ -176,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 {
|
||||
@@ -189,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()
|
||||
@@ -217,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{}
|
||||
}
|
||||
@@ -232,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,
|
||||
@@ -274,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()
|
||||
@@ -281,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
|
||||
@@ -394,3 +743,69 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
func FixedEngine(eng wgengine.Engine) func() (wgengine.Engine, error) {
|
||||
return func() (wgengine.Engine, error) { return eng, nil }
|
||||
}
|
||||
|
||||
type dummyAddr string
|
||||
type oneConnListener struct {
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func (l *oneConnListener) Accept() (c net.Conn, err error) {
|
||||
c = l.conn
|
||||
if c == nil {
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
l.conn = nil
|
||||
return
|
||||
}
|
||||
|
||||
func (l *oneConnListener) Close() error { return nil }
|
||||
|
||||
func (l *oneConnListener) Addr() net.Addr { return dummyAddr("unused-address") }
|
||||
|
||||
func (a dummyAddr) Network() string { return string(a) }
|
||||
func (a dummyAddr) String() string { return string(a) }
|
||||
|
||||
// protoSwitchConn is a net.Conn that's we want to speak HTTP to but
|
||||
// it's already had a few bytes read from it to determine that it's
|
||||
// HTTP. So we Read from its bufio.Reader. On Close, we we tell the
|
||||
// server it's closed, so the server can account the who's connected.
|
||||
type protoSwitchConn struct {
|
||||
s *server
|
||||
net.Conn
|
||||
br *bufio.Reader
|
||||
closeOnce sync.Once
|
||||
}
|
||||
|
||||
func (psc *protoSwitchConn) Read(p []byte) (int, error) { return psc.br.Read(p) }
|
||||
func (psc *protoSwitchConn) Close() error {
|
||||
psc.closeOnce.Do(func() { psc.s.removeAndCloseConn(psc.Conn) })
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) localhostHandler(ci connIdentity) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if ci.Unknown {
|
||||
io.WriteString(w, "<html><title>Tailscale</title><body><h1>Tailscale</h1>This is the local Tailscale daemon.")
|
||||
return
|
||||
}
|
||||
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 {
|
||||
return e.Pid
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -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{}) {
|
||||
|
||||
501
ipn/local.go
501
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
|
||||
@@ -555,7 +615,7 @@ func (b *LocalBackend) updateDNSMap(netMap *controlclient.NetworkMap) {
|
||||
|
||||
nameToIP := make(map[string]netaddr.IP)
|
||||
set := func(name string, addrs []wgcfg.CIDR) {
|
||||
if len(addrs) == 0 {
|
||||
if len(addrs) == 0 || name == "" {
|
||||
return
|
||||
}
|
||||
nameToIP[name] = netaddr.IPFrom16(addrs[0].IP.Addr)
|
||||
@@ -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,7 @@ func (b *LocalBackend) authReconfig() {
|
||||
blocked := b.blocked
|
||||
uc := b.prefs
|
||||
nm := b.netMap
|
||||
hasPAC := b.prevIfState.HasPAC()
|
||||
b.mu.Unlock()
|
||||
|
||||
if blocked {
|
||||
@@ -953,16 +1174,22 @@ func (b *LocalBackend) authReconfig() {
|
||||
flags |= controlclient.AllowDefaultRoute
|
||||
// TODO(apenwarr): Make subnet routes a different pref?
|
||||
flags |= controlclient.AllowSubnetRoutes
|
||||
// TODO(apenwarr): Remove this once we sort out subnet routes.
|
||||
// Right now default routes are broken in Windows, but
|
||||
// controlclient doesn't properly send subnet routes. So
|
||||
// let's convert a default route into a subnet route in order
|
||||
// to allow experimentation.
|
||||
flags |= controlclient.HackDefaultRoute
|
||||
}
|
||||
if uc.AllowSingleHosts {
|
||||
flags |= controlclient.AllowSingleHosts
|
||||
}
|
||||
if hasPAC {
|
||||
// TODO(bradfitz): make this policy configurable per
|
||||
// domain, flesh out all the edge cases where subnet
|
||||
// routes might shadow corp HTTP proxies, DNS servers,
|
||||
// domain controllers, etc. For now we just want
|
||||
// Tailscale to stay enabled while laptops roam
|
||||
// between corp & non-corp networks.
|
||||
if flags&controlclient.AllowSubnetRoutes != 0 {
|
||||
b.logf("authReconfig: have PAC; disabling subnet routes")
|
||||
flags &^= controlclient.AllowSubnetRoutes
|
||||
}
|
||||
}
|
||||
|
||||
cfg, err := nm.WGCfg(b.logf, flags)
|
||||
if err != nil {
|
||||
@@ -1112,6 +1339,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 {
|
||||
@@ -1124,7 +1352,7 @@ func (b *LocalBackend) enterState(newState State) {
|
||||
}
|
||||
|
||||
if bc != nil {
|
||||
bc.SetPaused(newState == Stopped)
|
||||
bc.SetPaused(newState == Stopped || !networkUp)
|
||||
}
|
||||
|
||||
switch newState {
|
||||
@@ -1251,15 +1479,25 @@ func (b *LocalBackend) requestEngineStatusAndWait() {
|
||||
// rebooting will fix it.
|
||||
func (b *LocalBackend) Logout() {
|
||||
b.mu.Lock()
|
||||
b.assertClientLocked()
|
||||
c := b.c
|
||||
b.netMap = nil
|
||||
b.setNetMapLocked(nil)
|
||||
b.mu.Unlock()
|
||||
|
||||
if c == nil {
|
||||
// Double Logout can happen via repeated IPN
|
||||
// connections to ipnserver making it repeatedly
|
||||
// transition from 1->0 total connections, which on
|
||||
// Windows by default ("client mode") causes a Logout
|
||||
// on the transition to zero.
|
||||
// Previously this crashed when we asserted that c was non-nil
|
||||
// here.
|
||||
return
|
||||
}
|
||||
|
||||
c.Logout()
|
||||
|
||||
b.mu.Lock()
|
||||
b.netMap = nil
|
||||
b.setNetMapLocked(nil)
|
||||
b.mu.Unlock()
|
||||
|
||||
b.stateMachine()
|
||||
@@ -1288,19 +1526,52 @@ 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
|
||||
if runtime.GOOS == "darwin" || runtime.GOOS == "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())
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ 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)
|
||||
@@ -92,6 +92,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 +117,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.
|
||||
@@ -197,9 +208,9 @@ func (bc *BackendClient) GotNotifyMsg(b []byte) {
|
||||
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,7 +225,7 @@ 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)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
33
ipn/store.go
33
ipn/store.go
@@ -5,8 +5,10 @@
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -19,6 +21,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 +60,8 @@ type MemoryStore struct {
|
||||
cache map[StateKey][]byte
|
||||
}
|
||||
|
||||
func (s *MemoryStore) String() string { return "MemoryStore" }
|
||||
|
||||
// ReadState implements the StateStore interface.
|
||||
func (s *MemoryStore) ReadState(id StateKey) ([]byte, error) {
|
||||
s.mu.Lock()
|
||||
@@ -67,6 +95,8 @@ type FileStore struct {
|
||||
cache map[StateKey][]byte
|
||||
}
|
||||
|
||||
func (s *FileStore) String() string { return fmt.Sprintf("FileStore(%q)", s.path) }
|
||||
|
||||
// NewFileStore returns a new file store that persists to path.
|
||||
func NewFileStore(path string) (*FileStore, error) {
|
||||
bs, err := ioutil.ReadFile(path)
|
||||
@@ -112,6 +142,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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,6 +315,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 +359,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,7 +395,7 @@ func New(collection string) *Policy {
|
||||
log.SetOutput(lw)
|
||||
|
||||
log.Printf("Program starting: v%v, Go %v: %#v",
|
||||
version.LONG,
|
||||
version.Long,
|
||||
strings.TrimPrefix(runtime.Version(), "go"),
|
||||
os.Args)
|
||||
log.Printf("LogID: %v", newc.PublicID)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,16 +5,23 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
func init() {
|
||||
likelyHomeRouterIP = likelyHomeRouterIPWindows
|
||||
getPAC = getPACWindows
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -71,3 +78,113 @@ func likelyHomeRouterIPWindows() (ret netaddr.IP, ok bool) {
|
||||
})
|
||||
return ret, !ret.IsZero()
|
||||
}
|
||||
|
||||
// NonTailscaleMTUs returns a map of interface LUID to interface MTU,
|
||||
// for all interfaces except Tailscale tunnels.
|
||||
func NonTailscaleMTUs() (map[winipcfg.LUID]uint32, error) {
|
||||
mtus := map[winipcfg.LUID]uint32{}
|
||||
ifs, err := NonTailscaleInterfaces()
|
||||
for luid, iface := range ifs {
|
||||
mtus[luid] = iface.MTU
|
||||
}
|
||||
return mtus, err
|
||||
}
|
||||
|
||||
// NonTailscaleInterfaces returns a map of interface LUID to interface
|
||||
// for all interfaces except Tailscale tunnels.
|
||||
func NonTailscaleInterfaces() (map[winipcfg.LUID]*winipcfg.IPAdapterAddresses, error) {
|
||||
ifs, err := winipcfg.GetAdaptersAddresses(windows.AF_UNSPEC, winipcfg.GAAFlagIncludeAllInterfaces)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ret := map[winipcfg.LUID]*winipcfg.IPAdapterAddresses{}
|
||||
for _, iface := range ifs {
|
||||
if iface.Description() == tsconst.WintunInterfaceDesc {
|
||||
continue
|
||||
}
|
||||
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)))
|
||||
return windows.UTF16PtrToString(res)
|
||||
}
|
||||
const (
|
||||
ERROR_WINHTTP_AUTODETECTION_FAILED = 12180
|
||||
)
|
||||
if e == syscall.Errno(ERROR_WINHTTP_AUTODETECTION_FAILED) {
|
||||
// Common case on networks without advertised PAC.
|
||||
return ""
|
||||
}
|
||||
log.Printf("getPACWindows: %T=%v", e, e) // syscall.Errno=0x....
|
||||
return ""
|
||||
}
|
||||
|
||||
17
net/interfaces/interfaces_windows_test.go
Normal file
17
net/interfaces/interfaces_windows_test.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package interfaces
|
||||
|
||||
import "testing"
|
||||
|
||||
func BenchmarkGetPACWindows(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
v := getPACWindows()
|
||||
if i == 0 {
|
||||
b.Logf("Got: %q", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -156,6 +156,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
|
||||
@@ -236,7 +245,6 @@ func (c *Client) ReceiveSTUNPacket(pkt []byte, src netaddr.IPPort) {
|
||||
|
||||
tx, addr, port, err := stun.ParseResponse(pkt)
|
||||
if err != nil {
|
||||
c.mu.Unlock()
|
||||
if _, err := stun.ParseBindingRequest(pkt); err == nil {
|
||||
// This was probably our own netcheck hairpin
|
||||
// check probe coming in late. Ignore.
|
||||
@@ -773,6 +781,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.
|
||||
@@ -832,8 +847,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
|
||||
@@ -851,7 +868,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
|
||||
@@ -864,7 +881,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 {
|
||||
@@ -903,8 +920,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)
|
||||
|
||||
@@ -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 netns
|
||||
|
||||
|
||||
120
net/netns/netns_windows.go
Normal file
120
net/netns/netns_windows.go
Normal file
@@ -0,0 +1,120 @@
|
||||
// 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 netns
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"tailscale.com/net/interfaces"
|
||||
)
|
||||
|
||||
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":
|
||||
canV4, canV6 = true, true
|
||||
case "tcp4", "udp4":
|
||||
canV4 = true
|
||||
case "tcp6", "udp6":
|
||||
canV6 = true
|
||||
}
|
||||
|
||||
if canV4 {
|
||||
iface, err := interfaces.GetWindowsDefault(windows.AF_INET)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bindSocket4(c, interfaceIndex(iface)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if canV6 {
|
||||
iface, err := interfaces.GetWindowsDefault(windows.AF_INET6)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bindSocket6(c, interfaceIndex(iface)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sockoptBoundInterface is the value of IP_UNICAST_IF and IPV6_UNICAST_IF.
|
||||
//
|
||||
// See https://docs.microsoft.com/en-us/windows/win32/winsock/ipproto-ip-socket-options
|
||||
// and https://docs.microsoft.com/en-us/windows/win32/winsock/ipproto-ipv6-socket-options
|
||||
const sockoptBoundInterface = 31
|
||||
|
||||
// bindSocket4 binds the given RawConn to the network interface with
|
||||
// index ifidx, for IPv4 traffic only.
|
||||
func bindSocket4(c syscall.RawConn, ifidx uint32) error {
|
||||
// For IPv4 (but NOT IPv6) the interface index must be passed
|
||||
// as a big-endian integer (regardless of platform endianness)
|
||||
// because the underlying sockopt takes either an IPv4 address
|
||||
// or an index shoved into IPv4 address representation (an IP
|
||||
// in 0.0.0.0/8 means it's actually an index).
|
||||
//
|
||||
// See https://docs.microsoft.com/en-us/windows/win32/winsock/ipproto-ip-socket-options
|
||||
// and IP_UNICAST_IF.
|
||||
indexAsAddr := nativeToBigEndian(ifidx)
|
||||
var controlErr error
|
||||
err := c.Control(func(fd uintptr) {
|
||||
controlErr = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IP, sockoptBoundInterface, int(indexAsAddr))
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return controlErr
|
||||
}
|
||||
|
||||
// bindSocket6 binds the given RawConn to the network interface with
|
||||
// index ifidx, for IPv6 traffic only.
|
||||
func bindSocket6(c syscall.RawConn, ifidx uint32) error {
|
||||
var controlErr error
|
||||
err := c.Control(func(fd uintptr) {
|
||||
controlErr = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_IPV6, sockoptBoundInterface, int(ifidx))
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return controlErr
|
||||
}
|
||||
|
||||
// nativeToBigEndian returns i converted into big-endian
|
||||
// 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]))
|
||||
}
|
||||
36
net/netstat/netstat.go
Normal file
36
net/netstat/netstat.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// 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 netstat returns the local machine's network connection table.
|
||||
package netstat
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
var ErrNotImplemented = errors.New("not implemented for GOOS=" + runtime.GOOS)
|
||||
|
||||
type Entry struct {
|
||||
Local, Remote netaddr.IPPort
|
||||
Pid int
|
||||
State string // TODO: type?
|
||||
}
|
||||
|
||||
// Table contains local machine's TCP connection entries.
|
||||
//
|
||||
// Currently only TCP (IPv4 and IPv6) are included.
|
||||
type Table struct {
|
||||
Entries []Entry
|
||||
}
|
||||
|
||||
// Get returns the connection table.
|
||||
//
|
||||
// It returns ErrNotImplemented if the table is not available for the
|
||||
// current operating system.
|
||||
func Get() (*Table, error) {
|
||||
return get()
|
||||
}
|
||||
11
net/netstat/netstat_noimpl.go
Normal file
11
net/netstat/netstat_noimpl.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// 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 !windows
|
||||
|
||||
package netstat
|
||||
|
||||
func get() (*Table, error) {
|
||||
return nil, ErrNotImplemented
|
||||
}
|
||||
22
net/netstat/netstat_test.go
Normal file
22
net/netstat/netstat_test.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// 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 netstat
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGet(t *testing.T) {
|
||||
nt, err := Get()
|
||||
if err == ErrNotImplemented {
|
||||
t.Skip("TODO: not implemented")
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, e := range nt.Entries {
|
||||
t.Logf("Entry: %+v", e)
|
||||
}
|
||||
}
|
||||
178
net/netstat/netstat_windows.go
Normal file
178
net/netstat/netstat_windows.go
Normal file
@@ -0,0 +1,178 @@
|
||||
// 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 netstat returns the local machine's network connection table.
|
||||
package netstat
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// See https://docs.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getextendedtcptable
|
||||
|
||||
// TCP_TABLE_OWNER_PID_ALL means to include the PID info. The table type
|
||||
// we get back from Windows depends on AF_INET vs AF_INET6:
|
||||
// MIB_TCPTABLE_OWNER_PID for v4 or MIB_TCP6TABLE_OWNER_PID for v6.
|
||||
const tcpTableOwnerPidAll = 5
|
||||
|
||||
var (
|
||||
iphlpapi = syscall.NewLazyDLL("iphlpapi.dll")
|
||||
getTCPTable = iphlpapi.NewProc("GetExtendedTcpTable")
|
||||
// TODO: GetExtendedUdpTable also? if/when needed.
|
||||
)
|
||||
|
||||
type _MIB_TCPROW_OWNER_PID struct {
|
||||
state uint32
|
||||
localAddr uint32
|
||||
localPort uint32
|
||||
remoteAddr uint32
|
||||
remotePort uint32
|
||||
pid uint32
|
||||
}
|
||||
|
||||
type _MIB_TCP6ROW_OWNER_PID struct {
|
||||
localAddr [16]byte
|
||||
localScope uint32
|
||||
localPort uint32
|
||||
remoteAddr [16]byte
|
||||
remoteScope uint32
|
||||
remotePort uint32
|
||||
state uint32
|
||||
pid uint32
|
||||
}
|
||||
|
||||
func get() (*Table, error) {
|
||||
t := new(Table)
|
||||
if err := t.addEntries(windows.AF_INET); err != nil {
|
||||
return nil, fmt.Errorf("failed to get IPv4 entries: %w", err)
|
||||
}
|
||||
if err := t.addEntries(windows.AF_INET6); err != nil {
|
||||
return nil, fmt.Errorf("failed to get IPv6 entries: %w", err)
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (t *Table) addEntries(fam int) error {
|
||||
var size uint32
|
||||
var addr unsafe.Pointer
|
||||
var buf []byte
|
||||
for {
|
||||
err, _, _ := getTCPTable.Call(
|
||||
uintptr(addr),
|
||||
uintptr(unsafe.Pointer(&size)),
|
||||
1, // sorted
|
||||
uintptr(fam),
|
||||
tcpTableOwnerPidAll,
|
||||
0, // reserved; "must be zero"
|
||||
)
|
||||
if err == 0 {
|
||||
break
|
||||
}
|
||||
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)
|
||||
}
|
||||
buf = make([]byte, size)
|
||||
addr = unsafe.Pointer(&buf[0])
|
||||
continue
|
||||
}
|
||||
return syscall.Errno(err)
|
||||
}
|
||||
if len(buf) < int(size) {
|
||||
return errors.New("unexpected size growth from system call")
|
||||
}
|
||||
buf = buf[:size]
|
||||
|
||||
numEntries := *(*uint32)(unsafe.Pointer(&buf[0]))
|
||||
buf = buf[4:]
|
||||
|
||||
var recSize int
|
||||
switch fam {
|
||||
case windows.AF_INET:
|
||||
recSize = 6 * 4
|
||||
case windows.AF_INET6:
|
||||
recSize = 6*4 + 16*2
|
||||
}
|
||||
dataLen := numEntries * uint32(recSize)
|
||||
if uint32(len(buf)) > dataLen {
|
||||
buf = buf[:dataLen]
|
||||
}
|
||||
for len(buf) >= recSize {
|
||||
switch fam {
|
||||
case windows.AF_INET:
|
||||
row := (*_MIB_TCPROW_OWNER_PID)(unsafe.Pointer(&buf[0]))
|
||||
t.Entries = append(t.Entries, Entry{
|
||||
Local: ipport4(row.localAddr, port(&row.localPort)),
|
||||
Remote: ipport4(row.remoteAddr, port(&row.remotePort)),
|
||||
Pid: int(row.pid),
|
||||
State: state(row.state),
|
||||
})
|
||||
case windows.AF_INET6:
|
||||
row := (*_MIB_TCP6ROW_OWNER_PID)(unsafe.Pointer(&buf[0]))
|
||||
t.Entries = append(t.Entries, Entry{
|
||||
Local: ipport6(row.localAddr, row.localScope, port(&row.localPort)),
|
||||
Remote: ipport6(row.remoteAddr, row.remoteScope, port(&row.remotePort)),
|
||||
Pid: int(row.pid),
|
||||
State: state(row.state),
|
||||
})
|
||||
}
|
||||
buf = buf[recSize:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var states = []string{
|
||||
"",
|
||||
"CLOSED",
|
||||
"LISTEN",
|
||||
"SYN-SENT",
|
||||
"SYN-RECEIVED",
|
||||
"ESTABLISHED",
|
||||
"FIN-WAIT-1",
|
||||
"FIN-WAIT-2",
|
||||
"CLOSE-WAIT",
|
||||
"CLOSING",
|
||||
"LAST-ACK",
|
||||
"DELETE-TCB",
|
||||
}
|
||||
|
||||
func state(v uint32) string {
|
||||
if v < uint32(len(states)) {
|
||||
return states[v]
|
||||
}
|
||||
return fmt.Sprintf("unknown-state-%d", v)
|
||||
}
|
||||
|
||||
func ipport4(addr uint32, port uint16) netaddr.IPPort {
|
||||
a4 := (*[4]byte)(unsafe.Pointer(&addr))
|
||||
return netaddr.IPPort{
|
||||
IP: netaddr.IPv4(a4[0], a4[1], a4[2], a4[3]),
|
||||
Port: port,
|
||||
}
|
||||
}
|
||||
|
||||
func ipport6(addr [16]byte, scope uint32, port uint16) netaddr.IPPort {
|
||||
ip := netaddr.IPFrom16(addr)
|
||||
if scope != 0 {
|
||||
// TODO: something better here?
|
||||
ip = ip.WithZone(fmt.Sprint(scope))
|
||||
}
|
||||
return netaddr.IPPort{
|
||||
IP: ip,
|
||||
Port: port,
|
||||
}
|
||||
}
|
||||
|
||||
func port(v *uint32) uint16 {
|
||||
p := (*[4]byte)(unsafe.Pointer(v))
|
||||
return binary.BigEndian.Uint16(p[:2])
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ if [ $# != 1 ]; then
|
||||
fi
|
||||
|
||||
fail=0
|
||||
for file in $(find $1 -name '*.go'); do
|
||||
for file in $(find $1 -name '*.go' -not -path '*/.git/*'); do
|
||||
case $file in
|
||||
$1/tempfork/*)
|
||||
# Skip, tempfork of third-party code
|
||||
|
||||
95
syncs/watchdog.go
Normal file
95
syncs/watchdog.go
Normal file
@@ -0,0 +1,95 @@
|
||||
// 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 syncs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Watch monitors mu for contention.
|
||||
// On first call, and at every tick, Watch locks and unlocks mu.
|
||||
// (Tick should be large to avoid adding contention to mu.)
|
||||
// Max is the maximum length of time Watch will wait to acquire the lock.
|
||||
// The time required to lock mu is sent on the returned channel.
|
||||
// Watch exits when ctx is done, and closes the returned channel.
|
||||
func Watch(ctx context.Context, mu sync.Locker, tick, max time.Duration) chan time.Duration {
|
||||
// Set up the return channel.
|
||||
c := make(chan time.Duration)
|
||||
var (
|
||||
closemu sync.Mutex
|
||||
closed bool
|
||||
)
|
||||
sendc := func(d time.Duration) {
|
||||
closemu.Lock()
|
||||
defer closemu.Unlock()
|
||||
if closed {
|
||||
// Drop values written after c is closed.
|
||||
return
|
||||
}
|
||||
c <- d
|
||||
}
|
||||
closec := func() {
|
||||
closemu.Lock()
|
||||
defer closemu.Unlock()
|
||||
close(c)
|
||||
closed = true
|
||||
}
|
||||
|
||||
// check locks the mutex and writes how long it took to c.
|
||||
// check returns ~immediately.
|
||||
check := func() {
|
||||
// Start a race between two goroutines.
|
||||
// One locks the mutex; the other times out.
|
||||
// Ensure that only one of the two gets to write its result.
|
||||
// Since the common case is that locking the mutex is fast,
|
||||
// let the timeout goroutine exit early when that happens.
|
||||
var sendonce sync.Once
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
start := time.Now()
|
||||
mu.Lock()
|
||||
mu.Unlock() //lint:ignore SA2001 ignore the empty critical section
|
||||
elapsed := time.Since(start)
|
||||
if elapsed > max {
|
||||
elapsed = max
|
||||
}
|
||||
close(done)
|
||||
sendonce.Do(func() { sendc(elapsed) })
|
||||
}()
|
||||
go func() {
|
||||
select {
|
||||
case <-time.After(max):
|
||||
// the other goroutine may not have sent a value
|
||||
sendonce.Do(func() { sendc(max) })
|
||||
case <-done:
|
||||
// the other goroutine sent a value
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Check once at startup.
|
||||
// This is mainly to make testing easier.
|
||||
check()
|
||||
|
||||
// Start the watchdog goroutine.
|
||||
// It checks the mutex every tick, until ctx is done.
|
||||
go func() {
|
||||
ticker := time.NewTicker(tick)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
closec()
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
check()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return c
|
||||
}
|
||||
71
syncs/watchdog_test.go
Normal file
71
syncs/watchdog_test.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// 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 syncs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Time-based tests are fundamentally flaky.
|
||||
// We use exaggerated durations in the hopes of minimizing such issues.
|
||||
|
||||
func TestWatchUncontended(t *testing.T) {
|
||||
mu := new(sync.Mutex)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
// Once an hour, and now, check whether we can lock mu in under an hour.
|
||||
tick := time.Hour
|
||||
max := time.Hour
|
||||
c := Watch(ctx, mu, tick, max)
|
||||
d := <-c
|
||||
if d == max {
|
||||
t.Errorf("uncontended mutex did not lock in under %v", max)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchContended(t *testing.T) {
|
||||
mu := new(sync.Mutex)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
// Every hour, and now, check whether we can lock mu in under a millisecond,
|
||||
// which is enough time for an uncontended mutex by several orders of magnitude.
|
||||
tick := time.Hour
|
||||
max := time.Millisecond
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
c := Watch(ctx, mu, tick, max)
|
||||
d := <-c
|
||||
if d != max {
|
||||
t.Errorf("contended mutex locked in under %v", max)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWatchMultipleValues(t *testing.T) {
|
||||
mu := new(sync.Mutex)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel() // not necessary, but keep vet happy
|
||||
// Check the mutex every millisecond.
|
||||
// The goal is to see that we get a sufficient number of values out of the channel.
|
||||
tick := time.Millisecond
|
||||
max := time.Millisecond
|
||||
c := Watch(ctx, mu, tick, max)
|
||||
start := time.Now()
|
||||
n := 0
|
||||
for d := range c {
|
||||
n++
|
||||
if d == max {
|
||||
t.Errorf("uncontended mutex did not lock in under %v", max)
|
||||
}
|
||||
if n == 10 {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
if elapsed := time.Since(start); elapsed > 100*time.Millisecond {
|
||||
t.Errorf("expected 1 event per millisecond, got only %v events in %v", n, elapsed)
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"go4.org/mem"
|
||||
@@ -27,14 +28,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 +135,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 {
|
||||
@@ -190,13 +211,8 @@ func (m MachineStatus) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
func isNum(b byte) bool {
|
||||
return b >= '0' && b <= '9'
|
||||
}
|
||||
|
||||
func isAlpha(b byte) bool {
|
||||
return (b >= 'A' && b <= 'Z') || (b >= 'a' && b <= 'z')
|
||||
}
|
||||
func isNum(r rune) bool { return r >= '0' && r <= '9' }
|
||||
func isAlpha(r rune) bool { return (r >= 'A' && r <= 'Z') || (r >= 'a' && r <= 'z') }
|
||||
|
||||
// CheckTag valids whether a given string can be used as an ACL tag.
|
||||
// For now we allow only ascii alphanumeric tags, and they need to start
|
||||
@@ -211,20 +227,34 @@ func CheckTag(tag string) error {
|
||||
if !strings.HasPrefix(tag, "tag:") {
|
||||
return errors.New("tags must start with 'tag:'")
|
||||
}
|
||||
tag = tag[4:]
|
||||
suffix := tag[len("tag:"):]
|
||||
if err := CheckTagSuffix(suffix); err != nil {
|
||||
return fmt.Errorf("invalid tag %q: %w", tag, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckTagSuffix checks whether tag is a valid tag suffix (the part
|
||||
// appearing after "tag:"). The error message does not reference
|
||||
// "tag:", so it's suitable for use by the "tailscale up" CLI tool
|
||||
// where the "tag:" isn't required. The returned error also does not
|
||||
// reference the tag itself, so the caller can wrap it as needed with
|
||||
// either the full or short form.
|
||||
func CheckTagSuffix(tag string) error {
|
||||
if tag == "" {
|
||||
return errors.New("tag names must not be empty")
|
||||
}
|
||||
if !isAlpha(tag[0]) {
|
||||
return errors.New("tag names must start with a letter, after 'tag:'")
|
||||
if i := strings.IndexFunc(tag, func(r rune) bool { return r >= utf8.RuneSelf }); i != -1 {
|
||||
return errors.New("tag names must only contain ASCII")
|
||||
}
|
||||
|
||||
for _, b := range []byte(tag) {
|
||||
if !isNum(b) && !isAlpha(b) && b != '-' {
|
||||
if !isAlpha(rune(tag[0])) {
|
||||
return errors.New("tag name must start with a letter")
|
||||
}
|
||||
for _, r := range tag {
|
||||
if !isNum(r) && !isAlpha(r) && r != '-' {
|
||||
return errors.New("tag names can only contain numbers, letters, or dashes")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -239,7 +269,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 +285,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 +338,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 +471,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 +529,23 @@ 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.
|
||||
type FilterRule struct {
|
||||
SrcIPs []string
|
||||
SrcIPs []string // "*" means all
|
||||
SrcBits []int
|
||||
DstPorts []NetPortRange
|
||||
}
|
||||
|
||||
var FilterAllowAll = []FilterRule{
|
||||
FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"*"},
|
||||
SrcBits: nil,
|
||||
DstPorts: []NetPortRange{NetPortRange{
|
||||
DstPorts: []NetPortRange{{
|
||||
IP: "*",
|
||||
Bits: nil,
|
||||
Ports: PortRange{0, 65535},
|
||||
@@ -545,8 +604,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
|
||||
|
||||
@@ -586,6 +645,7 @@ type Debug struct {
|
||||
|
||||
func (k MachineKey) String() string { return fmt.Sprintf("mkey:%x", k[:]) }
|
||||
func (k MachineKey) MarshalText() ([]byte, error) { return keyMarshalText("mkey:", k), nil }
|
||||
func (k MachineKey) HexString() string { return fmt.Sprintf("%x", k[:]) }
|
||||
func (k *MachineKey) UnmarshalText(text []byte) error { return keyUnmarshalText(k[:], "mkey:", text) }
|
||||
|
||||
func keyMarshalText(prefix string, k [32]byte) []byte {
|
||||
@@ -615,6 +675,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)
|
||||
|
||||
11
tsconst/interface.go
Normal file
11
tsconst/interface.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// 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 tsconst exports some constants used elsewhere in the
|
||||
// codebase.
|
||||
package tsconst
|
||||
|
||||
// WintunInterfaceDesc is the description attached to Tailscale
|
||||
// interfaces on Windows. This is set by our modified WinTun driver.
|
||||
const WintunInterfaceDesc = "Tailscale Tunnel"
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ package tsweb
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -15,23 +16,23 @@ type response struct {
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// TODO: Header
|
||||
|
||||
// JSONHandlerFunc only take *http.Request as argument to avoid any misuse of http.ResponseWriter.
|
||||
// The function's results must be (status int, data interface{}, err error).
|
||||
// Return a HTTPError to show an error message, otherwise JSONHandler will only show "internal server error".
|
||||
// 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.
|
||||
type JSONHandlerFunc func(r *http.Request) (status int, data interface{}, err error)
|
||||
|
||||
// ServeHTTP calls the JSONHandlerFunc and automatically marshals http responses.
|
||||
// ServeHTTPReturn implements the ReturnHandler interface.
|
||||
//
|
||||
// Use the following code to unmarshal the request body
|
||||
//
|
||||
// body := new(DataType)
|
||||
// if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
// return http.StatusBadRequest, nil, err
|
||||
// }
|
||||
//
|
||||
// Check jsonhandler_text.go for examples
|
||||
func (fn JSONHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// See jsonhandler_text.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)
|
||||
@@ -53,6 +54,13 @@ func (fn JSONHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
Error: werr.Msg,
|
||||
Data: data,
|
||||
}
|
||||
// Unwrap the HTTPError here because we are communicating with
|
||||
// the client in this handler. We don't want the wrapping
|
||||
// ReturnHandler to do it too.
|
||||
err = werr.Err
|
||||
if werr.Msg != "" {
|
||||
err = fmt.Errorf("%s: %w", werr.Msg, err)
|
||||
}
|
||||
} else {
|
||||
resp = &response{
|
||||
Status: "error",
|
||||
@@ -61,13 +69,17 @@ func (fn JSONHandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
b, jerr := json.Marshal(resp)
|
||||
if jerr != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(`{"status":"error","error":"json marshal error"}`))
|
||||
return
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w, and then we could not respond: %v", err, jerr)
|
||||
}
|
||||
return jerr
|
||||
}
|
||||
|
||||
w.WriteHeader(status)
|
||||
w.Write(b)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
t.Run("200 simple", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
h21.ServeHTTP(w, r)
|
||||
h21.ServeHTTPReturn(w, r)
|
||||
checkStatus(w, "success", http.StatusOK)
|
||||
})
|
||||
|
||||
@@ -72,7 +72,7 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
h.ServeHTTP(w, r)
|
||||
h.ServeHTTPReturn(w, r)
|
||||
checkStatus(w, "error", http.StatusForbidden)
|
||||
})
|
||||
|
||||
@@ -83,7 +83,7 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
t.Run("200 get data", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
h22.ServeHTTP(w, r)
|
||||
h22.ServeHTTPReturn(w, r)
|
||||
checkStatus(w, "success", http.StatusOK)
|
||||
})
|
||||
|
||||
@@ -102,21 +102,21 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
t.Run("200 post data", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{"Name": "tailscale"}`))
|
||||
h31.ServeHTTP(w, r)
|
||||
h31.ServeHTTPReturn(w, r)
|
||||
checkStatus(w, "success", http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("400 bad json", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{`))
|
||||
h31.ServeHTTP(w, r)
|
||||
h31.ServeHTTPReturn(w, r)
|
||||
checkStatus(w, "error", http.StatusBadRequest)
|
||||
})
|
||||
|
||||
t.Run("400 post data error", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{}`))
|
||||
h31.ServeHTTP(w, r)
|
||||
h31.ServeHTTPReturn(w, r)
|
||||
resp := checkStatus(w, "error", http.StatusBadRequest)
|
||||
if resp.Error != "name is empty" {
|
||||
t.Fatalf("wrong error")
|
||||
@@ -141,7 +141,7 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
t.Run("200 post data", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{"Price": 10}`))
|
||||
h32.ServeHTTP(w, r)
|
||||
h32.ServeHTTPReturn(w, r)
|
||||
resp := checkStatus(w, "success", http.StatusOK)
|
||||
t.Log(resp.Data)
|
||||
if resp.Data.Price != 20 {
|
||||
@@ -152,7 +152,7 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
t.Run("400 post data error", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{}`))
|
||||
h32.ServeHTTP(w, r)
|
||||
h32.ServeHTTPReturn(w, r)
|
||||
resp := checkStatus(w, "error", http.StatusBadRequest)
|
||||
if resp.Error != "price is empty" {
|
||||
t.Fatalf("wrong error")
|
||||
@@ -162,7 +162,7 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
t.Run("500 internal server error", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{"Name": "root"}`))
|
||||
h32.ServeHTTP(w, r)
|
||||
h32.ServeHTTPReturn(w, r)
|
||||
resp := checkStatus(w, "error", http.StatusInternalServerError)
|
||||
if resp.Error != "internal server error" {
|
||||
t.Fatalf("wrong error")
|
||||
@@ -174,7 +174,7 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
r := httptest.NewRequest("POST", "/", nil)
|
||||
JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
return http.StatusOK, make(chan int), nil
|
||||
}).ServeHTTP(w, r)
|
||||
}).ServeHTTPReturn(w, r)
|
||||
resp := checkStatus(w, "error", http.StatusInternalServerError)
|
||||
if resp.Error != "json marshal error" {
|
||||
t.Fatalf("wrong error")
|
||||
@@ -186,7 +186,7 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
r := httptest.NewRequest("POST", "/", nil)
|
||||
JSONHandlerFunc(func(r *http.Request) (status int, data interface{}, err error) {
|
||||
return
|
||||
}).ServeHTTP(w, r)
|
||||
}).ServeHTTPReturn(w, r)
|
||||
checkStatus(w, "error", http.StatusInternalServerError)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
25
util/pidowner/pidowner.go
Normal file
25
util/pidowner/pidowner.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 pidowner handles lookups from process ID to its owning user.
|
||||
package pidowner
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var ErrNotImplemented = errors.New("not implemented for GOOS=" + runtime.GOOS)
|
||||
|
||||
var ErrProcessNotFound = errors.New("process not found")
|
||||
|
||||
// OwnerOfPID returns the user ID that owns the given process ID.
|
||||
//
|
||||
// The returned user ID is suitable to passing to os/user.LookupId.
|
||||
//
|
||||
// The returned error will be ErrNotImplemented for operating systems where
|
||||
// this isn't supported.
|
||||
func OwnerOfPID(pid int) (userID string, err error) {
|
||||
return ownerOfPID(pid)
|
||||
}
|
||||
37
util/pidowner/pidowner_linux.go
Normal file
37
util/pidowner/pidowner_linux.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// 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 pidowner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/util/lineread"
|
||||
)
|
||||
|
||||
func ownerOfPID(pid int) (userID string, err error) {
|
||||
file := fmt.Sprintf("/proc/%d/status", pid)
|
||||
err = lineread.File(file, func(line []byte) error {
|
||||
if len(line) < 4 || string(line[:4]) != "Uid:" {
|
||||
return nil
|
||||
}
|
||||
f := strings.Fields(string(line))
|
||||
if len(f) >= 2 {
|
||||
userID = f[1] // real userid
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if os.IsNotExist(err) {
|
||||
return "", ErrProcessNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if userID == "" {
|
||||
return "", fmt.Errorf("missing Uid line in %s", file)
|
||||
}
|
||||
return userID, nil
|
||||
}
|
||||
9
util/pidowner/pidowner_noimpl.go
Normal file
9
util/pidowner/pidowner_noimpl.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 !windows,!linux
|
||||
|
||||
package pidowner
|
||||
|
||||
func ownerOfPID(pid int) (userID string, err error) { return "", ErrNotImplemented }
|
||||
51
util/pidowner/pidowner_test.go
Normal file
51
util/pidowner/pidowner_test.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 pidowner
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"os"
|
||||
"os/user"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestOwnerOfPID(t *testing.T) {
|
||||
id, err := OwnerOfPID(os.Getpid())
|
||||
if err == ErrNotImplemented {
|
||||
t.Skip(err)
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("id=%q", id)
|
||||
|
||||
u, err := user.LookupId(id)
|
||||
if err != nil {
|
||||
t.Fatalf("LookupId: %v", err)
|
||||
}
|
||||
t.Logf("Got: %+v", u)
|
||||
}
|
||||
|
||||
// validate that OS implementation returns ErrProcessNotFound.
|
||||
func TestNotFoundError(t *testing.T) {
|
||||
// Try a bunch of times to stumble upon a pid that doesn't exist...
|
||||
const tries = 50
|
||||
for i := 0; i < tries; i++ {
|
||||
_, err := OwnerOfPID(rand.Intn(1e9))
|
||||
if err == ErrNotImplemented {
|
||||
t.Skip(err)
|
||||
}
|
||||
if err == nil {
|
||||
// We got unlucky and this pid existed. Try again.
|
||||
continue
|
||||
}
|
||||
if err == ErrProcessNotFound {
|
||||
// Pass.
|
||||
return
|
||||
}
|
||||
t.Fatalf("Error is not ErrProcessNotFound: %T %v", err, err)
|
||||
}
|
||||
t.Errorf("after %d tries, couldn't find a process that didn't exist", tries)
|
||||
}
|
||||
36
util/pidowner/pidowner_windows.go
Normal file
36
util/pidowner/pidowner_windows.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// 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 pidowner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func ownerOfPID(pid int) (userID string, err error) {
|
||||
procHnd, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION, false, uint32(pid))
|
||||
if err == syscall.Errno(0x57) { // invalid parameter, for PIDs that don't exist
|
||||
return "", ErrProcessNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("OpenProcess: %T %#v", err, err)
|
||||
}
|
||||
defer windows.CloseHandle(procHnd)
|
||||
|
||||
var tok windows.Token
|
||||
if err := windows.OpenProcessToken(procHnd, windows.TOKEN_QUERY, &tok); err != nil {
|
||||
return "", fmt.Errorf("OpenProcessToken: %w", err)
|
||||
}
|
||||
|
||||
tokUser, err := tok.GetTokenUser()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("GetTokenUser: %w", err)
|
||||
}
|
||||
|
||||
sid := tokUser.User.Sid
|
||||
return sid.String(), nil
|
||||
}
|
||||
65
util/uniq/slice.go
Normal file
65
util/uniq/slice.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package uniq provides removal of adjacent duplicate elements in slices.
|
||||
// It is similar to the unix command uniq.
|
||||
package uniq
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type badTypeError struct {
|
||||
typ reflect.Type
|
||||
}
|
||||
|
||||
func (e badTypeError) Error() string {
|
||||
return fmt.Sprintf("uniq.ModifySlice's first argument must have type *[]T, got %v", e.typ)
|
||||
}
|
||||
|
||||
// ModifySlice removes adjacent duplicate elements from the slice pointed to by sliceptr.
|
||||
// It adjusts the length of the slice appropriately and zeros the tail.
|
||||
// eq reports whether (*sliceptr)[i] and (*sliceptr)[j] are equal.
|
||||
// ModifySlice does O(len(*sliceptr)) operations.
|
||||
func ModifySlice(sliceptr interface{}, eq func(i, j int) bool) {
|
||||
rvp := reflect.ValueOf(sliceptr)
|
||||
if rvp.Type().Kind() != reflect.Ptr {
|
||||
panic(badTypeError{rvp.Type()})
|
||||
}
|
||||
rv := rvp.Elem()
|
||||
if rv.Type().Kind() != reflect.Slice {
|
||||
panic(badTypeError{rvp.Type()})
|
||||
}
|
||||
|
||||
length := rv.Len()
|
||||
dst := 0
|
||||
for i := 1; i < length; i++ {
|
||||
if eq(dst, i) {
|
||||
continue
|
||||
}
|
||||
dst++
|
||||
// slice[dst] = slice[i]
|
||||
rv.Index(dst).Set(rv.Index(i))
|
||||
}
|
||||
|
||||
end := dst + 1
|
||||
var zero reflect.Value
|
||||
if end < length {
|
||||
zero = reflect.Zero(rv.Type().Elem())
|
||||
}
|
||||
|
||||
// for i := range slice[end:] {
|
||||
// size[i] = 0/nil/{}
|
||||
// }
|
||||
for i := end; i < length; i++ {
|
||||
// slice[i] = 0/nil/{}
|
||||
rv.Index(i).Set(zero)
|
||||
}
|
||||
|
||||
// slice = slice[:end]
|
||||
if end < length {
|
||||
rv.SetLen(end)
|
||||
}
|
||||
}
|
||||
88
util/uniq/slice_test.go
Normal file
88
util/uniq/slice_test.go
Normal file
@@ -0,0 +1,88 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package uniq_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/util/uniq"
|
||||
)
|
||||
|
||||
func TestModifySlice(t *testing.T) {
|
||||
tests := []struct {
|
||||
in []int
|
||||
want []int
|
||||
}{
|
||||
{in: []int{0, 1, 2}, want: []int{0, 1, 2}},
|
||||
{in: []int{0, 1, 2, 2}, want: []int{0, 1, 2}},
|
||||
{in: []int{0, 0, 1, 2}, want: []int{0, 1, 2}},
|
||||
{in: []int{0, 1, 0, 2}, want: []int{0, 1, 0, 2}},
|
||||
{in: []int{0}, want: []int{0}},
|
||||
{in: []int{0, 0}, want: []int{0}},
|
||||
{in: []int{}, want: []int{}},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
in := make([]int, len(test.in))
|
||||
copy(in, test.in)
|
||||
uniq.ModifySlice(&test.in, func(i, j int) bool { return test.in[i] == test.in[j] })
|
||||
if !reflect.DeepEqual(test.in, test.want) {
|
||||
t.Errorf("uniq.Slice(%v) = %v, want %v", in, test.in, test.want)
|
||||
}
|
||||
start := len(test.in)
|
||||
test.in = test.in[:cap(test.in)]
|
||||
for i := start; i < len(in); i++ {
|
||||
if test.in[i] != 0 {
|
||||
t.Errorf("uniq.Slice(%v): non-0 in tail of %v at index %v", in, test.in, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark(b *testing.B) {
|
||||
benches := []struct {
|
||||
name string
|
||||
reset func(s []byte)
|
||||
}{
|
||||
{name: "AllDups",
|
||||
reset: func(s []byte) {
|
||||
for i := range s {
|
||||
s[i] = '*'
|
||||
}
|
||||
},
|
||||
},
|
||||
{name: "NoDups",
|
||||
reset: func(s []byte) {
|
||||
for i := range s {
|
||||
s[i] = byte(i)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, bb := range benches {
|
||||
b.Run(bb.name, func(b *testing.B) {
|
||||
for size := 1; size <= 4096; size *= 16 {
|
||||
b.Run(strconv.Itoa(size), func(b *testing.B) {
|
||||
benchmark(b, 64, bb.reset)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func benchmark(b *testing.B, size int64, reset func(s []byte)) {
|
||||
b.ReportAllocs()
|
||||
b.SetBytes(size)
|
||||
s := make([]byte, size)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
s = s[:size]
|
||||
reset(s)
|
||||
uniq.ModifySlice(&s, func(i, j int) bool { return s[i] == s[j] })
|
||||
}
|
||||
}
|
||||
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
|
||||
44
version/distro/distro.go
Normal file
44
version/distro/distro.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 distro reports which distro we're running on.
|
||||
package distro
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
type Distro string
|
||||
|
||||
const (
|
||||
Debian = Distro("debian")
|
||||
Arch = Distro("arch")
|
||||
Synology = Distro("synology")
|
||||
OpenWrt = Distro("openwrt")
|
||||
)
|
||||
|
||||
// Get returns the current distro, or the empty string if unknown.
|
||||
func Get() Distro {
|
||||
if runtime.GOOS == "linux" {
|
||||
return linuxDistro()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func linuxDistro() Distro {
|
||||
if fi, err := os.Stat("/usr/syno"); err == nil && fi.IsDir() {
|
||||
return Synology
|
||||
}
|
||||
if _, err := os.Stat("/etc/debian_version"); err == nil {
|
||||
return Debian
|
||||
}
|
||||
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="0.0.0"
|
||||
VERSION_LONG="0.0.0-tabcdef"
|
||||
VERSION_GIT_HASH="abcdef"
|
||||
VERSION_EXTRA_HASH=""
|
||||
VERSION_XCODE="100.0.0"
|
||||
VERSION_WINRES="0,0,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())
|
||||
}
|
||||
@@ -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}"
|
||||
|
||||
3
version/version-info.sh.do
Normal file
3
version/version-info.sh.do
Normal file
@@ -0,0 +1,3 @@
|
||||
./version.sh ../.. >$3
|
||||
redo-always
|
||||
redo-stamp <$3
|
||||
@@ -7,5 +7,35 @@
|
||||
// Package version provides the version that the binary was built at.
|
||||
package version
|
||||
|
||||
const LONG = "date.20200820"
|
||||
const SHORT = LONG
|
||||
// Long is a full version number for this build, of the form
|
||||
// "x.y.z-commithash", or "date.yyyymmdd" if no actual version was
|
||||
// provided.
|
||||
const Long = "date.20200921"
|
||||
|
||||
// Short is a short version number for this build, of the form
|
||||
// "x.y.z", or "date.yyyymmdd" if no actual version was provided.
|
||||
const Short = Long
|
||||
|
||||
// LONG is a deprecated alias for Long. Don't use it.
|
||||
const LONG = Long
|
||||
|
||||
// SHORT is a deprecated alias for Short. Don't use it.
|
||||
const SHORT = Short
|
||||
|
||||
// GitCommit, if non-empty, is the git commit of the
|
||||
// github.com/tailscale/tailscale repository at which Tailscale was
|
||||
// built. Its format is the one returned by `git describe --always
|
||||
// --exclude "*" --dirty --abbrev=200`.
|
||||
const GitCommit = ""
|
||||
|
||||
// ExtraGitCommit, if non-empty, is the git commit of a "supplemental"
|
||||
// repository at which Tailscale was built. Its format is the same as
|
||||
// gitCommit.
|
||||
//
|
||||
// ExtraGitCommit is used to track the source revision when the main
|
||||
// Tailscale repository is integrated into and built from another
|
||||
// repository (for example, Tailscale's proprietary code, or the
|
||||
// Android OSS repository). Together, GitCommit and ExtraGitCommit
|
||||
// exactly describe what repositories and commits were used in a
|
||||
// build.
|
||||
const ExtraGitCommit = ""
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
redo-ifchange long.txt short.txt
|
||||
read -r long <long.txt
|
||||
read -r short <short.txt
|
||||
redo-ifchange version-info.sh
|
||||
|
||||
# get it into "major.minor.patch" format
|
||||
ver=$(echo "$ver" | sed -e 's/-/./')
|
||||
. ./version-info.sh
|
||||
|
||||
(
|
||||
printf '#define TAILSCALE_VERSION_LONG "%s"\n' "$long"
|
||||
printf '#define TAILSCALE_VERSION_SHORT "%s"\n' "$short"
|
||||
) >$3
|
||||
cat >$3 <<EOF
|
||||
#define TAILSCALE_VERSION_LONG "$VERSION_LONG"
|
||||
#define TAILSCALE_VERSION_SHORT "$VERSION_SHORT"
|
||||
#define TAILSCALE_VERSION_WIN_RES $VERSION_WINRES
|
||||
EOF
|
||||
|
||||
109
version/version.sh
Executable file
109
version/version.sh
Executable file
@@ -0,0 +1,109 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
case $# in
|
||||
0|1)
|
||||
# extra_hash describes a git repository other than the current
|
||||
# one. It gets embedded as an additional commit hash in built
|
||||
# binaries, to help us locate the exact set of tools and code
|
||||
# that were used.
|
||||
extra_hash="${1:-}"
|
||||
if [ -z "$extra_hash" ]; then
|
||||
# Nothing, empty extra hash is fine.
|
||||
extra_hash=""
|
||||
elif [ -d "$extra_hash/.git" ]; then
|
||||
extra_hash=$(cd "$extra_hash" && git describe --always --dirty --exclude '*' --abbrev=200)
|
||||
elif ! expr "$extra_hash" : "^[0-9a-f]*$"; then
|
||||
echo "Invalid extra hash '$extra_hash', must be a git commit hash or path to a git repo" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Load the base version and optional corresponding git hash
|
||||
# from the VERSION file. If there is no git hash in the file,
|
||||
# we use the hash of the last change to the VERSION file.
|
||||
version_file="$(dirname $0)/../VERSION.txt"
|
||||
IFS=".$IFS" read -r major minor patch base_git_hash <"$version_file"
|
||||
if [ -z "$base_git_hash" ]; then
|
||||
base_git_hash=$(git rev-list --max-count=1 HEAD -- $version_file)
|
||||
fi
|
||||
|
||||
# The full git has we're currently building at. --abbrev=200 is an
|
||||
# arbitrary large number larger than all currently-known hashes, so
|
||||
# that git displays the full commit hash.
|
||||
git_hash=$(git describe --always --dirty --exclude '*' --abbrev=200)
|
||||
# The number of extra commits between the release base to git_hash.
|
||||
change_count=$(git rev-list ${base_git_hash}..HEAD | wc -l)
|
||||
;;
|
||||
6)
|
||||
# Test mode: rather than run git commands and whatnot, take in
|
||||
# all the version pieces as arguments.
|
||||
git_hash=$1
|
||||
extra_hash=$2
|
||||
major=$3
|
||||
minor=$4
|
||||
patch=$5
|
||||
change_count=$6
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 [extra-git-hash-or-checkout]"
|
||||
exit 1
|
||||
esac
|
||||
|
||||
# Shortened versions of git hashes, so that they fit neatly into an
|
||||
# "elongated" but still human-readable version number.
|
||||
short_git_hash=$(echo $git_hash | cut -c-9)
|
||||
short_extra_hash=$(echo $extra_hash | cut -c-9)
|
||||
|
||||
# Convert major/minor/patch/change_count into an adjusted
|
||||
# major/minor/patch. This block is where all our policies on
|
||||
# versioning are.
|
||||
if expr "$minor" : "[0-9]*[13579]$" >/dev/null; then
|
||||
# Odd minor numbers are unstable builds.
|
||||
if [ "$patch" != "0" ]; then
|
||||
# This is a fatal error, because a non-zero patch number
|
||||
# indicates that we created an unstable git tag in violation
|
||||
# of our versioning policy, and we want to blow up loudly to
|
||||
# get that fixed.
|
||||
echo "Unstable release $major.$minor.$patch has a non-zero patch number, which is not allowed" >&2
|
||||
exit 1
|
||||
fi
|
||||
patch="$change_count"
|
||||
elif [ "$change_count" != "0" ]; then
|
||||
# Even minor numbers are stable builds, but stable builds are
|
||||
# supposed to have a zero change count. Therefore, we're currently
|
||||
# describing a commit that's on a release branch, but hasn't been
|
||||
# tagged as a patch release yet. We allow these commits to build
|
||||
# for testing purposes, but force their version number to 0.0.0,
|
||||
# to reflect that they're an unreleasable build. The git hashes
|
||||
# still completely describe the build commit, so we can still
|
||||
# figure out what this build is if it escapes into the wild.
|
||||
major="0"
|
||||
minor="0"
|
||||
patch="0"
|
||||
fi
|
||||
|
||||
# Hack for 1.1: add 1000 to the patch number. We switched from using
|
||||
# the proprietary repo's change_count over to using the OSS repo's
|
||||
# change_count, and this was necessary to avoid a backwards jump in
|
||||
# release numbers.
|
||||
if [ "$major.$minor" = "1.1" ]; then
|
||||
patch="$((patch + 1000))"
|
||||
fi
|
||||
|
||||
# At this point, the version number correctly reflects our
|
||||
# policies. All that remains is to output the various vars that other
|
||||
# code can use to embed version data.
|
||||
if [ -z "$extra_hash" ]; then
|
||||
long_version_suffix="-t$short_git_hash"
|
||||
else
|
||||
long_version_suffix="-t${short_git_hash}-g${short_extra_hash}"
|
||||
fi
|
||||
cat <<EOF
|
||||
VERSION_SHORT="${major}.${minor}.${patch}"
|
||||
VERSION_LONG="${major}.${minor}.${patch}${long_version_suffix}"
|
||||
VERSION_GIT_HASH="${git_hash}"
|
||||
VERSION_EXTRA_HASH="${extra_hash}"
|
||||
VERSION_XCODE="$((major + 100)).${minor}.${patch}"
|
||||
VERSION_WINRES="${major},${minor},${patch},0"
|
||||
EOF
|
||||
@@ -1,4 +1,14 @@
|
||||
redo-ifchange mkversion.sh describe.txt
|
||||
read -r describe <describe.txt
|
||||
ver=$(./mkversion.sh xcode "$describe")
|
||||
echo "$ver" >$3
|
||||
redo-ifchange version-info.sh
|
||||
|
||||
. ./version-info.sh
|
||||
|
||||
# CFBundleShortVersionString: the "short name" used in the App Store.
|
||||
# eg. 0.92.98
|
||||
echo "VERSION_NAME = $VERSION_SHORT"
|
||||
# CFBundleVersion: the build number. Needs to be 3 numeric sections
|
||||
# that increment for each release according to SemVer rules.
|
||||
#
|
||||
# We start counting at 100 because we submitted using raw build
|
||||
# numbers before, and Apple doesn't let you start over. e.g. 0.98.3
|
||||
# -> 100.98.3
|
||||
echo "VERSION_ID = $VERSION_XCODE"
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
|
||||
package version
|
||||
|
||||
// Replaced at build time with the Go linker flag -X.
|
||||
var LONG string = "<not set>"
|
||||
var SHORT string = "<not set>"
|
||||
// Replaced at build time with the Go linker flag -X. See
|
||||
// ../build_dist.sh for example usage, and version.go for field
|
||||
// documentation.
|
||||
var Long string = "<not set>"
|
||||
var Short string = "<not set>"
|
||||
var LONG = Long
|
||||
var SHORT = Short
|
||||
var GitCommit = ""
|
||||
var ExtraGitCommit = ""
|
||||
|
||||
@@ -367,6 +367,15 @@ func (f *Filter) pre(q *packet.ParsedPacket, rf RunFlags, dir direction) Respons
|
||||
f.logRateLimit(rf, q, dir, Drop, "ipv6")
|
||||
return Drop
|
||||
}
|
||||
if q.DstIP.IsMulticast() {
|
||||
f.logRateLimit(rf, q, dir, Drop, "multicast")
|
||||
return Drop
|
||||
}
|
||||
if q.DstIP.IsLinkLocalUnicast() {
|
||||
f.logRateLimit(rf, q, dir, Drop, "link-local-unicast")
|
||||
return Drop
|
||||
}
|
||||
|
||||
switch q.IPProto {
|
||||
case packet.Unknown:
|
||||
// Unknown packets are dangerous; always drop them.
|
||||
@@ -409,6 +418,9 @@ func omitDropLogging(p *packet.ParsedPacket, dir direction) bool {
|
||||
if ipProto == packet.IGMP {
|
||||
return true
|
||||
}
|
||||
if p.DstIP.IsMulticast() || p.DstIP.IsLinkLocalUnicast() {
|
||||
return true
|
||||
}
|
||||
case 6:
|
||||
if len(b) < 40 {
|
||||
return false
|
||||
|
||||
@@ -379,6 +379,24 @@ func TestOmitDropLogging(t *testing.T) {
|
||||
dir: out,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "v4_multicast_out_low",
|
||||
pkt: &packet.ParsedPacket{IPVersion: 4, DstIP: packet.NewIP(net.ParseIP("224.0.0.0"))},
|
||||
dir: out,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "v4_multicast_out_high",
|
||||
pkt: &packet.ParsedPacket{IPVersion: 4, DstIP: packet.NewIP(net.ParseIP("239.255.255.255"))},
|
||||
dir: out,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "v4_link_local_unicast",
|
||||
pkt: &packet.ParsedPacket{IPVersion: 4, DstIP: packet.NewIP(net.ParseIP("169.254.1.2"))},
|
||||
dir: out,
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
29
wgengine/magicsock/discopingpurpose_string.go
Normal file
29
wgengine/magicsock/discopingpurpose_string.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by "stringer -type=discoPingPurpose -trimprefix=ping"; DO NOT EDIT.
|
||||
|
||||
package magicsock
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[pingDiscovery-0]
|
||||
_ = x[pingHeartbeat-1]
|
||||
_ = x[pingCLI-2]
|
||||
}
|
||||
|
||||
const _discoPingPurpose_name = "DiscoveryHeartbeatCLI"
|
||||
|
||||
var _discoPingPurpose_index = [...]uint8{0, 9, 18, 21}
|
||||
|
||||
func (i discoPingPurpose) String() string {
|
||||
if i < 0 || i >= discoPingPurpose(len(_discoPingPurpose_index)-1) {
|
||||
return "discoPingPurpose(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _discoPingPurpose_name[_discoPingPurpose_index[i]:_discoPingPurpose_index[i+1]]
|
||||
}
|
||||
@@ -40,6 +40,7 @@ import (
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/disco"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netcheck"
|
||||
@@ -119,6 +120,7 @@ type Conn struct {
|
||||
netChecker *netcheck.Client
|
||||
idleFunc func() time.Duration // nil means unknown
|
||||
noteRecvActivity func(tailcfg.DiscoKey) // or nil, see Options.NoteRecvActivity
|
||||
simulatedNetwork bool
|
||||
|
||||
// bufferedIPv4From and bufferedIPv4Packet are owned by
|
||||
// ReceiveIPv4, and used when both a DERP and IPv4 packet arrive
|
||||
@@ -208,6 +210,11 @@ type Conn struct {
|
||||
// necessarily have a netcheck.Report and don't want to skip
|
||||
// logging.
|
||||
noV4, noV6 syncs.AtomicBool
|
||||
|
||||
// networkUp is whether the network is up (some interface is up
|
||||
// with IPv4 or IPv6). It's used to suppress log spam and prevent
|
||||
// new connection that'll fail.
|
||||
networkUp syncs.AtomicBool
|
||||
}
|
||||
|
||||
// derpRoute is a route entry for a public key, saying that a certain
|
||||
@@ -301,11 +308,18 @@ type Options struct {
|
||||
// sole user just doesn't need or want it called on every
|
||||
// packet, just every minute or two for Wireguard timeouts,
|
||||
// and 10 seconds seems like a good trade-off between often
|
||||
// enough and not too often.) The provided func is called while
|
||||
// holding userspaceEngine.wgLock and likely calls
|
||||
// Conn.CreateEndpoint, which acquires Conn.mu. As such, you should
|
||||
// not hold
|
||||
// enough and not too often.) The provided func is called
|
||||
// while holding userspaceEngine.wgLock and likely calls
|
||||
// Conn.CreateEndpoint, which acquires Conn.mu. As such, you
|
||||
// should not hold Conn.mu while calling it.
|
||||
NoteRecvActivity func(tailcfg.DiscoKey)
|
||||
|
||||
// SimulatedNetwork can be set true in tests to signal that
|
||||
// the network is simulated and thus it's okay to bind on the
|
||||
// unspecified address (which we'd normally avoid to avoid
|
||||
// triggering macOS and Windows firwall dialog boxes during
|
||||
// "go test").
|
||||
SimulatedNetwork bool
|
||||
}
|
||||
|
||||
func (o *Options) logf() logger.Logf {
|
||||
@@ -345,6 +359,7 @@ func newConn() *Conn {
|
||||
discoOfAddr: make(map[netaddr.IPPort]tailcfg.DiscoKey),
|
||||
}
|
||||
c.muCond = sync.NewCond(&c.mu)
|
||||
c.networkUp.Set(true) // assume up until told otherwise
|
||||
return c
|
||||
}
|
||||
|
||||
@@ -362,6 +377,7 @@ func NewConn(opts Options) (*Conn, error) {
|
||||
c.idleFunc = opts.IdleFunc
|
||||
c.packetListener = opts.PacketListener
|
||||
c.noteRecvActivity = opts.NoteRecvActivity
|
||||
c.simulatedNetwork = opts.SimulatedNetwork
|
||||
|
||||
if err := c.initialBind(); err != nil {
|
||||
return nil, err
|
||||
@@ -369,8 +385,9 @@ func NewConn(opts Options) (*Conn, error) {
|
||||
|
||||
c.connCtx, c.connCtxCancel = context.WithCancel(context.Background())
|
||||
c.netChecker = &netcheck.Client{
|
||||
Logf: logger.WithPrefix(c.logf, "netcheck: "),
|
||||
GetSTUNConn4: func() netcheck.STUNConn { return c.pconn4 },
|
||||
Logf: logger.WithPrefix(c.logf, "netcheck: "),
|
||||
GetSTUNConn4: func() netcheck.STUNConn { return c.pconn4 },
|
||||
SkipExternalNetwork: inTest(),
|
||||
}
|
||||
if c.pconn6 != nil {
|
||||
c.netChecker.GetSTUNConn6 = func() netcheck.STUNConn { return c.pconn6 }
|
||||
@@ -431,7 +448,7 @@ func (c *Conn) updateEndpoints(why string) {
|
||||
return
|
||||
}
|
||||
|
||||
if c.setEndpoints(endpoints) {
|
||||
if c.setEndpoints(endpoints, reasons) {
|
||||
c.logEndpointChange(endpoints, reasons)
|
||||
c.epFunc(endpoints)
|
||||
}
|
||||
@@ -439,9 +456,31 @@ func (c *Conn) updateEndpoints(why string) {
|
||||
|
||||
// setEndpoints records the new endpoints, reporting whether they're changed.
|
||||
// It takes ownership of the slice.
|
||||
func (c *Conn) setEndpoints(endpoints []string) (changed bool) {
|
||||
func (c *Conn) setEndpoints(endpoints []string, reasons map[string]string) (changed bool) {
|
||||
anySTUN := false
|
||||
for _, reason := range reasons {
|
||||
if reason == "stun" {
|
||||
anySTUN = true
|
||||
}
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if !anySTUN && c.derpMap == nil && !inTest() {
|
||||
// Don't bother storing or reporting this yet. We
|
||||
// don't have a DERP map or any STUN entries, so we're
|
||||
// just starting up. A DERP map should arrive shortly
|
||||
// and then we'll have more interesting endpoints to
|
||||
// report. This saves a map update.
|
||||
// TODO(bradfitz): this optimization is currently
|
||||
// skipped during the e2e tests because they depend
|
||||
// too much on the exact sequence of updates. Fix the
|
||||
// tests. But a protocol rewrite might happen first.
|
||||
c.logf("magicsock: ignoring pre-DERP map, STUN-less endpoint update: %v", endpoints)
|
||||
return false
|
||||
}
|
||||
|
||||
if stringsEqual(endpoints, c.lastEndpoints) {
|
||||
return false
|
||||
}
|
||||
@@ -454,7 +493,7 @@ func (c *Conn) updateNetInfo(ctx context.Context) (*netcheck.Report, error) {
|
||||
dm := c.derpMap
|
||||
c.mu.Unlock()
|
||||
|
||||
if dm == nil {
|
||||
if dm == nil || c.networkDown() {
|
||||
return new(netcheck.Report), nil
|
||||
}
|
||||
|
||||
@@ -648,8 +687,8 @@ func (c *Conn) Ping(ip netaddr.IP, cb func(*ipnstate.PingResult)) {
|
||||
}
|
||||
|
||||
dk, ok := c.discoOfNode[peer.Key]
|
||||
if !ok {
|
||||
res.Err = "no discovery key for peer (pre 0.100?)"
|
||||
if !ok { // peer is using outdated Tailscale version (pre-0.100)
|
||||
res.Err = "no discovery key for peer (pre Tailscale 0.100 version?). Try: ping 100.x.y.z"
|
||||
cb(res)
|
||||
return
|
||||
}
|
||||
@@ -969,8 +1008,15 @@ func (as *AddrSet) appendDests(dsts []netaddr.IPPort, b []byte) (_ []netaddr.IPP
|
||||
}
|
||||
|
||||
var errNoDestinations = errors.New("magicsock: no destinations")
|
||||
var errNetworkDown = errors.New("magicsock: network down")
|
||||
|
||||
func (c *Conn) networkDown() bool { return !c.networkUp.Get() }
|
||||
|
||||
func (c *Conn) Send(b []byte, ep conn.Endpoint) error {
|
||||
if c.networkDown() {
|
||||
return errNetworkDown
|
||||
}
|
||||
|
||||
var as *AddrSet
|
||||
switch v := ep.(type) {
|
||||
default:
|
||||
@@ -1111,6 +1157,10 @@ func (c *Conn) derpWriteChanOfAddr(addr netaddr.IPPort, peer key.Public) chan<-
|
||||
}
|
||||
regionID := int(addr.Port)
|
||||
|
||||
if c.networkDown() {
|
||||
return nil
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if !c.wantDerpLocked() || c.closed {
|
||||
@@ -1301,36 +1351,45 @@ func (c *Conn) runDerpReader(ctx context.Context, derpFakeAddr netaddr.IPPort, d
|
||||
// peerPresent is the set of senders we know are present on this
|
||||
// connection, based on messages we've received from the server.
|
||||
peerPresent := map[key.Public]bool{}
|
||||
|
||||
bo := backoff.NewBackoff(fmt.Sprintf("derp-%d", regionID), c.logf, 5*time.Second)
|
||||
for {
|
||||
msg, err := dc.Recv()
|
||||
if err == derphttp.ErrClientClosed {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
// Forget that all these peers have routes.
|
||||
for peer := range peerPresent {
|
||||
delete(peerPresent, peer)
|
||||
c.removeDerpPeerRoute(peer, regionID, dc)
|
||||
}
|
||||
if err == derphttp.ErrClientClosed {
|
||||
return
|
||||
}
|
||||
if c.networkDown() {
|
||||
c.logf("magicsock: derp.Recv(derp-%d): network down, closing", regionID)
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
c.ReSTUN("derp-close")
|
||||
|
||||
c.logf("magicsock: [%p] derp.Recv(derp-%d): %v", dc, regionID, err)
|
||||
|
||||
// Avoid excessive spinning.
|
||||
// TODO: use a backoff timer, perhaps between 10ms and 500ms?
|
||||
// Don't want to sleep too long. For now 250ms seems fine.
|
||||
// If our DERP connection broke, it might be because our network
|
||||
// conditions changed. Start that check.
|
||||
c.ReSTUN("derp-recv-error")
|
||||
|
||||
// Back off a bit before reconnecting.
|
||||
bo.BackOff(ctx, err)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(250 * time.Millisecond):
|
||||
default:
|
||||
}
|
||||
continue
|
||||
}
|
||||
bo.BackOff(ctx, nil) // reset
|
||||
|
||||
switch m := msg.(type) {
|
||||
case derp.ReceivedPacket:
|
||||
pkt = m
|
||||
@@ -1691,7 +1750,9 @@ func (c *Conn) sendDiscoMessage(dst netaddr.IPPort, dstKey tailcfg.NodeKey, dstD
|
||||
} else if err == nil {
|
||||
// Can't send. (e.g. no IPv6 locally)
|
||||
} else {
|
||||
c.logf("magicsock: disco: failed to send %T to %v: %v", m, dst, err)
|
||||
if !c.networkDown() {
|
||||
c.logf("magicsock: disco: failed to send %T to %v: %v", m, dst, err)
|
||||
}
|
||||
}
|
||||
return sent, err
|
||||
}
|
||||
@@ -1956,6 +2017,21 @@ func (c *Conn) sharedDiscoKeyLocked(k tailcfg.DiscoKey) *[32]byte {
|
||||
return shared
|
||||
}
|
||||
|
||||
func (c *Conn) SetNetworkUp(up bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.networkUp.Get() == up {
|
||||
return
|
||||
}
|
||||
|
||||
c.logf("magicsock: SetNetworkUp(%v)", up)
|
||||
c.networkUp.Set(up)
|
||||
|
||||
if !up {
|
||||
c.closeAllDerpLocked("network-down")
|
||||
}
|
||||
}
|
||||
|
||||
// SetPrivateKey sets the connection's private key.
|
||||
//
|
||||
// This is only used to be able prove our identity when connecting to
|
||||
@@ -2282,6 +2358,10 @@ func maxIdleBeforeSTUNShutdown() time.Duration {
|
||||
}
|
||||
|
||||
func (c *Conn) shouldDoPeriodicReSTUN() bool {
|
||||
if c.networkDown() {
|
||||
return false
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if len(c.peerSet) == 0 {
|
||||
@@ -2410,7 +2490,7 @@ func (c *Conn) listenPacket(ctx context.Context, network, addr string) (net.Pack
|
||||
|
||||
func (c *Conn) bind1(ruc **RebindingUDPConn, which string) error {
|
||||
host := ""
|
||||
if inTest() {
|
||||
if inTest() && !c.simulatedNetwork {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
var pc net.PacketConn
|
||||
@@ -2440,7 +2520,7 @@ func (c *Conn) bind1(ruc **RebindingUDPConn, which string) error {
|
||||
// It should be followed by a call to ReSTUN.
|
||||
func (c *Conn) Rebind() {
|
||||
host := ""
|
||||
if inTest() {
|
||||
if inTest() && !c.simulatedNetwork {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
listenCtx := context.Background() // unused without DNS name to resolve
|
||||
@@ -3465,6 +3545,7 @@ func (de *discoEndpoint) sendDiscoPing(ep netaddr.IPPort, txid stun.TxID, logLev
|
||||
// discoPingPurpose is the reason why a discovery ping message was sent.
|
||||
type discoPingPurpose int
|
||||
|
||||
//go:generate stringer -type=discoPingPurpose -trimprefix=ping
|
||||
const (
|
||||
// pingDiscovery means that purpose of a ping was to see if a
|
||||
// path was valid.
|
||||
|
||||
@@ -46,6 +46,10 @@ import (
|
||||
"tailscale.com/wgengine/tstun"
|
||||
)
|
||||
|
||||
func init() {
|
||||
os.Setenv("IN_TS_TEST", "1")
|
||||
}
|
||||
|
||||
// WaitReady waits until the magicsock is entirely initialized and connected
|
||||
// to its home DERP server. This is normally not necessary, since magicsock
|
||||
// is intended to be entirely asynchronous, but it helps eliminate race
|
||||
@@ -118,7 +122,7 @@ type magicStack struct {
|
||||
privateKey wgcfg.PrivateKey
|
||||
epCh chan []string // endpoint updates produced by this peer
|
||||
conn *Conn // the magicsock itself
|
||||
tun *tuntest.ChannelTUN // tuntap device to send/receive packets
|
||||
tun *tuntest.ChannelTUN // TUN device to send/receive packets
|
||||
tsTun *tstun.TUN // wrapped tun that implements filtering and wgengine hooks
|
||||
dev *device.Device // the wireguard-go Device that connects the previous things
|
||||
}
|
||||
@@ -141,6 +145,7 @@ func newMagicStack(t *testing.T, logf logger.Logf, l nettype.PacketListener, der
|
||||
EndpointsFunc: func(eps []string) {
|
||||
epCh <- eps
|
||||
},
|
||||
SimulatedNetwork: l != nettype.Std{},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("constructing magicsock: %v", err)
|
||||
@@ -374,7 +379,7 @@ collectEndpoints:
|
||||
|
||||
func pickPort(t *testing.T) uint16 {
|
||||
t.Helper()
|
||||
conn, err := net.ListenPacket("udp4", ":0")
|
||||
conn, err := net.ListenPacket("udp4", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -783,7 +788,8 @@ func testActiveDiscovery(t *testing.T, d *devices) {
|
||||
|
||||
start := time.Now()
|
||||
logf := func(msg string, args ...interface{}) {
|
||||
msg = fmt.Sprintf("%s: %s", time.Since(start), msg)
|
||||
t.Helper()
|
||||
msg = fmt.Sprintf("%s: %s", time.Since(start).Truncate(time.Microsecond), msg)
|
||||
tlogf(msg, args...)
|
||||
}
|
||||
|
||||
@@ -811,7 +817,8 @@ func testActiveDiscovery(t *testing.T, d *devices) {
|
||||
|
||||
mustDirect := func(m1, m2 *magicStack) {
|
||||
lastLog := time.Now().Add(-time.Minute)
|
||||
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(10 * time.Millisecond) {
|
||||
// See https://github.com/tailscale/tailscale/issues/654 for a discussion of this deadline.
|
||||
for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline); time.Sleep(10 * time.Millisecond) {
|
||||
pst := m1.Status().Peer[m2.Public()]
|
||||
if pst.CurAddr != "" {
|
||||
logf("direct link %s->%s found with addr %s", m1, m2, pst.CurAddr)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user