Compare commits
177 Commits
clone
...
bradfitz/g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d27301638a | ||
|
|
468bb3afce | ||
|
|
9c25968b63 | ||
|
|
82a3721661 | ||
|
|
b026a638c7 | ||
|
|
a570c27577 | ||
|
|
3b05cbacfb | ||
|
|
57e642648f | ||
|
|
6d14678009 | ||
|
|
09d56f54a7 | ||
|
|
74ee374667 | ||
|
|
1e0be5a458 | ||
|
|
3af2d671e6 | ||
|
|
9b07517f18 | ||
|
|
bd37e40d2b | ||
|
|
cb5f3c0819 | ||
|
|
5acbb149a2 | ||
|
|
2bac125cad | ||
|
|
aa1da24f18 | ||
|
|
7541982635 | ||
|
|
34a7e7c12b | ||
|
|
bc34788e65 | ||
|
|
28f9cd06f5 | ||
|
|
756d6a72bd | ||
|
|
483141094c | ||
|
|
f27a57911b | ||
|
|
f915ab6552 | ||
|
|
dd2c61a519 | ||
|
|
a67b174da1 | ||
|
|
a3fb422a39 | ||
|
|
cd7bc02ab1 | ||
|
|
5e0375808b | ||
|
|
24d1a38e81 | ||
|
|
1be6c6dd70 | ||
|
|
169ff22a84 | ||
|
|
a903d6c2ed | ||
|
|
10cad39abd | ||
|
|
9be1917c5b | ||
|
|
44598e3e89 | ||
|
|
9e2e8c80af | ||
|
|
7841c97af5 | ||
|
|
557c23517b | ||
|
|
6c71e5b851 | ||
|
|
1886dfdca3 | ||
|
|
309c15dfdd | ||
|
|
e415991256 | ||
|
|
9337a99dff | ||
|
|
4d56d19b46 | ||
|
|
9cb2df4ddd | ||
|
|
1e562886f5 | ||
|
|
461db356b9 | ||
|
|
805850add9 | ||
|
|
1af70e2468 | ||
|
|
a583e498b0 | ||
|
|
287522730d | ||
|
|
862d223c39 | ||
|
|
c5eb57f4d6 | ||
|
|
1835bb6f85 | ||
|
|
93ffc565e5 | ||
|
|
6b80bcf112 | ||
|
|
f6dc47efe4 | ||
|
|
771e9541c7 | ||
|
|
337c86b89d | ||
|
|
e64ab89712 | ||
|
|
adf4f3cce0 | ||
|
|
80d0b88a89 | ||
|
|
f90f35c123 | ||
|
|
3e2bfe48c3 | ||
|
|
062bd67d3b | ||
|
|
dbb4c246fa | ||
|
|
85c3d17b3c | ||
|
|
0512fd89a1 | ||
|
|
37c19970b3 | ||
|
|
909c165382 | ||
|
|
b983e5340f | ||
|
|
6fa7a9a055 | ||
|
|
95a18f815c | ||
|
|
b97aac1718 | ||
|
|
75225368a4 | ||
|
|
15949ad77d | ||
|
|
13661e195a | ||
|
|
1b5b59231b | ||
|
|
c2b63ba363 | ||
|
|
5a0c37aafd | ||
|
|
1f7a7a4ffe | ||
|
|
4e63a4fda3 | ||
|
|
a9b1e3f9e8 | ||
|
|
e577303dc7 | ||
|
|
355c6296f0 | ||
|
|
25b021388b | ||
|
|
84dc891843 | ||
|
|
d65e2632ab | ||
|
|
87cbc067c2 | ||
|
|
a275b9d7aa | ||
|
|
dd97111d06 | ||
|
|
696020227c | ||
|
|
b23f2263c1 | ||
|
|
c64a43a734 | ||
|
|
9318b4758c | ||
|
|
6818bb843d | ||
|
|
24f78eff62 | ||
|
|
5590daa97d | ||
|
|
b840e7dd5b | ||
|
|
1b27eb431a | ||
|
|
2622e8e082 | ||
|
|
b62b07bc2d | ||
|
|
cb01058a53 | ||
|
|
9a346fd8b4 | ||
|
|
78c2e1ff83 | ||
|
|
41c4560592 | ||
|
|
cff737786e | ||
|
|
43bc86588e | ||
|
|
2bd9ad4b40 | ||
|
|
5db529a655 | ||
|
|
934c63115e | ||
|
|
7c38db0c97 | ||
|
|
a16a793605 | ||
|
|
08949d4ef1 | ||
|
|
4987a7d46c | ||
|
|
bfcb0aa0be | ||
|
|
c3467fbadb | ||
|
|
6298018704 | ||
|
|
da3b50ad88 | ||
|
|
9e26ffecf8 | ||
|
|
d64de1ddf7 | ||
|
|
358cd3fd92 | ||
|
|
28e52a0492 | ||
|
|
43b271cb26 | ||
|
|
3e493e0417 | ||
|
|
c253d4f948 | ||
|
|
8c850947db | ||
|
|
cb970539a6 | ||
|
|
92e9a5ac15 | ||
|
|
915f65ddae | ||
|
|
c180abd7cf | ||
|
|
7cc8fcb784 | ||
|
|
b4d97d2532 | ||
|
|
ff8c8db9d3 | ||
|
|
2072dcc127 | ||
|
|
6013462e9e | ||
|
|
60c00605d3 | ||
|
|
f81233524f | ||
|
|
2ce2b63239 | ||
|
|
154d1cde05 | ||
|
|
cbf71d5eba | ||
|
|
b3fc61b132 | ||
|
|
9ff5b380cb | ||
|
|
4aba86cc03 | ||
|
|
d55fdd4669 | ||
|
|
d96d26c22a | ||
|
|
c7582dc234 | ||
|
|
3e3c24b8f6 | ||
|
|
91d95dafd2 | ||
|
|
77cad13c70 | ||
|
|
84f2320972 | ||
|
|
f8e4c75f6b | ||
|
|
33a748bec1 | ||
|
|
b77d752623 | ||
|
|
cd21ba0a71 | ||
|
|
58b721f374 | ||
|
|
ec4feaf31c | ||
|
|
41d0c81859 | ||
|
|
9beea8b314 | ||
|
|
b62341d308 | ||
|
|
9265296b33 | ||
|
|
0249236cc0 | ||
|
|
c3958898f1 | ||
|
|
7578c815be | ||
|
|
c3994fd77c | ||
|
|
5455c64f1d | ||
|
|
f794493b4f | ||
|
|
f582eeabd1 | ||
|
|
a2b4ad839b | ||
|
|
25288567ec | ||
|
|
5a370d545a | ||
|
|
37903a9056 | ||
|
|
bca9fe35ba |
2
.github/workflows/cross-darwin.yml
vendored
2
.github/workflows/cross-darwin.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14
|
||||
go-version: 1.15
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/cross-freebsd.yml
vendored
2
.github/workflows/cross-freebsd.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14
|
||||
go-version: 1.15
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/cross-openbsd.yml
vendored
2
.github/workflows/cross-openbsd.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14
|
||||
go-version: 1.15
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/cross-windows.yml
vendored
2
.github/workflows/cross-windows.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14
|
||||
go-version: 1.15
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/license.yml
vendored
2
.github/workflows/license.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14
|
||||
go-version: 1.15
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14
|
||||
go-version: 1.15
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/linux32.yml
vendored
2
.github/workflows/linux32.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14
|
||||
go-version: 1.15
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/staticcheck.yml
vendored
2
.github/workflows/staticcheck.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.14
|
||||
go-version: 1.15
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
5
Makefile
5
Makefile
@@ -1,7 +1,10 @@
|
||||
usage:
|
||||
echo "See Makefile"
|
||||
|
||||
check: staticcheck
|
||||
vet:
|
||||
go vet ./...
|
||||
|
||||
check: staticcheck vet
|
||||
|
||||
staticcheck:
|
||||
go run honnef.co/go/tools/cmd/staticcheck -- $$(go list ./... | grep -v tempfork)
|
||||
|
||||
25
README.md
25
README.md
@@ -6,17 +6,24 @@ Private WireGuard® networks made easy
|
||||
|
||||
## Overview
|
||||
|
||||
This repository contains all the open source Tailscale code.
|
||||
It currently includes the Linux client.
|
||||
This repository contains all the open source Tailscale client code and
|
||||
the `tailscaled` daemon and `tailscale` CLI tool. The `tailscaled`
|
||||
daemon runs primarily on Linux; it also works to varying degrees on
|
||||
FreeBSD, OpenBSD, Darwin, and Windows.
|
||||
|
||||
The Linux client is currently `cmd/relaynode`, but will
|
||||
soon be replaced by `cmd/tailscaled`.
|
||||
The Android app is at https://github.com/tailscale/tailscale-android
|
||||
|
||||
## Using
|
||||
|
||||
We serve packages for a variety of distros at
|
||||
https://pkgs.tailscale.com .
|
||||
|
||||
## Other clients
|
||||
|
||||
The [macOS, iOS, and Windows clients](https://tailscale.com/download)
|
||||
use the code in this repository but additionally include small GUI
|
||||
wrappers that are not open source.
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
@@ -24,7 +31,7 @@ go install tailscale.com/cmd/tailscale{,d}
|
||||
```
|
||||
|
||||
We only guarantee to support the latest Go release and any Go beta or
|
||||
release candidate builds (currently Go 1.14) in module mode. It might
|
||||
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
|
||||
effort to keep those working.
|
||||
|
||||
@@ -35,10 +42,8 @@ Please file any issues about this code or the hosted service on
|
||||
|
||||
## Contributing
|
||||
|
||||
`under_construction.gif`
|
||||
|
||||
PRs welcome, but we are still working out our contribution process and
|
||||
tooling.
|
||||
PRs welcome! But please file bugs. Commit messages should [reference
|
||||
bugs](https://docs.github.com/en/github/writing-on-github/autolinked-references-and-urls).
|
||||
|
||||
We require [Developer Certificate of
|
||||
Origin](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin)
|
||||
@@ -46,7 +51,7 @@ Origin](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin)
|
||||
|
||||
## About Us
|
||||
|
||||
We are apenwarr, bradfitz, crawshaw, danderson, dfcarney,
|
||||
We are apenwarr, bradfitz, crawshaw, danderson, dfcarney, josharian
|
||||
from Tailscale Inc.
|
||||
You can learn more about us from [our website](https://tailscale.com).
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ package main // import "tailscale.com/cmd/derper"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"expvar"
|
||||
@@ -33,6 +34,7 @@ import (
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -184,6 +186,15 @@ func main() {
|
||||
certManager.Email = "security@tailscale.com"
|
||||
}
|
||||
httpsrv.TLSConfig = certManager.TLSConfig()
|
||||
letsEncryptGetCert := httpsrv.TLSConfig.GetCertificate
|
||||
httpsrv.TLSConfig.GetCertificate = func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
cert, err := letsEncryptGetCert(hi)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert.Certificate = append(cert.Certificate, s.MetaCert())
|
||||
return cert, nil
|
||||
}
|
||||
go func() {
|
||||
err := http.ListenAndServe(":80", certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}))
|
||||
if err != nil {
|
||||
@@ -221,6 +232,7 @@ func debugHandler(s *derp.Server) http.Handler {
|
||||
f("<li><b>Hostname:</b> %v</li>\n", *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><a href="/debug/vars">/debug/vars</a> (Go)</li>
|
||||
<li><a href="/debug/varz">/debug/varz</a> (Prometheus)</li>
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -88,7 +89,16 @@ func promPrint(w io.Writer, prefix string, obj map[string]interface{}) {
|
||||
case map[string]interface{}:
|
||||
promPrint(w, k, v)
|
||||
case float64:
|
||||
fmt.Fprintf(w, "%s %f\n", k, v)
|
||||
const saveConfigReject = "control_save_config_rejected_"
|
||||
const saveConfig = "control_save_config_"
|
||||
switch {
|
||||
case strings.HasPrefix(k, saveConfigReject):
|
||||
fmt.Fprintf(w, "control_save_config_rejected{reason=%q} %f\n", k[len(saveConfigReject):], v)
|
||||
case strings.HasPrefix(k, saveConfig):
|
||||
fmt.Fprintf(w, "control_save_config{reason=%q} %f\n", k[len(saveConfig):], v)
|
||||
default:
|
||||
fmt.Fprintf(w, "%s %f\n", k, v)
|
||||
}
|
||||
default:
|
||||
fmt.Fprintf(w, "# Skipping key %q, unhandled type %T\n", k, v)
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
# placeholder to work around redo bug
|
||||
@@ -31,7 +31,8 @@ func ActLikeCLI() bool {
|
||||
return false
|
||||
}
|
||||
switch os.Args[1] {
|
||||
case "up", "status", "netcheck", "version",
|
||||
case "up", "down", "status", "netcheck", "ping", "version",
|
||||
"debug",
|
||||
"-V", "--version", "-h", "--help":
|
||||
return true
|
||||
}
|
||||
@@ -57,14 +58,21 @@ change in the future.
|
||||
`),
|
||||
Subcommands: []*ffcli.Command{
|
||||
upCmd,
|
||||
downCmd,
|
||||
netcheckCmd,
|
||||
statusCmd,
|
||||
pingCmd,
|
||||
versionCmd,
|
||||
},
|
||||
FlagSet: rootfs,
|
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||
}
|
||||
|
||||
// Don't advertise the debug command, but it exists.
|
||||
if strSliceContains(args, "debug") {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
|
||||
}
|
||||
|
||||
if err := rootCmd.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -76,6 +84,11 @@ change in the future.
|
||||
return err
|
||||
}
|
||||
|
||||
func fatalf(format string, a ...interface{}) {
|
||||
log.SetFlags(0)
|
||||
log.Fatalf(format, a...)
|
||||
}
|
||||
|
||||
var rootArgs struct {
|
||||
socket string
|
||||
}
|
||||
@@ -84,9 +97,9 @@ func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context
|
||||
c, err := safesocket.Connect(rootArgs.socket, 41112)
|
||||
if err != nil {
|
||||
if runtime.GOOS != "windows" && rootArgs.socket == "" {
|
||||
log.Fatalf("--socket cannot be empty")
|
||||
fatalf("--socket cannot be empty")
|
||||
}
|
||||
log.Fatalf("Failed to connect to connect to tailscaled. (safesocket.Connect: %v)\n", err)
|
||||
fatalf("Failed to connect to connect to tailscaled. (safesocket.Connect: %v)\n", err)
|
||||
}
|
||||
clientToServer := func(b []byte) {
|
||||
ipn.WriteMsg(c, b)
|
||||
@@ -121,3 +134,12 @@ func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) {
|
||||
bc.GotNotifyMsg(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func strSliceContains(ss []string, s string) bool {
|
||||
for _, v := range ss {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
175
cmd/tailscale/cli/debug.go
Normal file
175
cmd/tailscale/cli/debug.go
Normal file
@@ -0,0 +1,175 @@
|
||||
// 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 cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/derp/derpmap"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
var debugCmd = &ffcli.Command{
|
||||
Name: "debug",
|
||||
Exec: runDebug,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("debug", flag.ExitOnError)
|
||||
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
|
||||
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
|
||||
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var debugArgs struct {
|
||||
monitor bool
|
||||
getURL string
|
||||
derpCheck string
|
||||
}
|
||||
|
||||
func runDebug(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("unknown arguments")
|
||||
}
|
||||
if debugArgs.derpCheck != "" {
|
||||
return checkDerp(ctx, debugArgs.derpCheck)
|
||||
}
|
||||
if debugArgs.monitor {
|
||||
return runMonitor(ctx)
|
||||
}
|
||||
if debugArgs.getURL != "" {
|
||||
return getURL(ctx, debugArgs.getURL)
|
||||
}
|
||||
return errors.New("only --monitor is available at the moment")
|
||||
}
|
||||
|
||||
func runMonitor(ctx context.Context) error {
|
||||
dump := func() {
|
||||
st, err := interfaces.GetState()
|
||||
if err != nil {
|
||||
log.Printf("error getting state: %v", err)
|
||||
return
|
||||
}
|
||||
j, _ := json.MarshalIndent(st, "", " ")
|
||||
os.Stderr.Write(j)
|
||||
}
|
||||
mon, err := monitor.New(log.Printf, func() {
|
||||
log.Printf("Link monitor fired. State:")
|
||||
dump()
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Starting link change monitor; initial state:")
|
||||
dump()
|
||||
mon.Start()
|
||||
log.Printf("Started link change monitor; waiting...")
|
||||
select {}
|
||||
}
|
||||
|
||||
func getURL(ctx context.Context, urlStr string) error {
|
||||
if urlStr == "login" {
|
||||
urlStr = "https://login.tailscale.com"
|
||||
}
|
||||
log.SetOutput(os.Stdout)
|
||||
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
|
||||
GetConn: func(hostPort string) { log.Printf("GetConn(%q)", hostPort) },
|
||||
GotConn: func(info httptrace.GotConnInfo) { log.Printf("GotConn: %+v", info) },
|
||||
DNSStart: func(info httptrace.DNSStartInfo) { log.Printf("DNSStart: %+v", info) },
|
||||
DNSDone: func(info httptrace.DNSDoneInfo) { log.Printf("DNSDoneInfo: %+v", info) },
|
||||
TLSHandshakeStart: func() { log.Printf("TLSHandshakeStart") },
|
||||
TLSHandshakeDone: func(cs tls.ConnectionState, err error) { log.Printf("TLSHandshakeDone: %+v, %v", cs, err) },
|
||||
WroteRequest: func(info httptrace.WroteRequestInfo) { log.Printf("WroteRequest: %+v", info) },
|
||||
})
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("http.NewRequestWithContext: %v", err)
|
||||
}
|
||||
proxyURL, err := tshttpproxy.ProxyFromEnvironment(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tshttpproxy.ProxyFromEnvironment: %v", err)
|
||||
}
|
||||
log.Printf("proxy: %v", proxyURL)
|
||||
tr := &http.Transport{
|
||||
Proxy: func(*http.Request) (*url.URL, error) { return proxyURL, nil },
|
||||
ProxyConnectHeader: http.Header{},
|
||||
DisableKeepAlives: true,
|
||||
}
|
||||
if proxyURL != nil {
|
||||
auth, err := tshttpproxy.GetAuthHeader(proxyURL)
|
||||
if err == nil && auth != "" {
|
||||
tr.ProxyConnectHeader.Set("Proxy-Authorization", auth)
|
||||
}
|
||||
const truncLen = 20
|
||||
if len(auth) > truncLen {
|
||||
auth = fmt.Sprintf("%s...(%d total bytes)", auth[:truncLen], len(auth))
|
||||
}
|
||||
log.Printf("tshttpproxy.GetAuthHeader(%v) for Proxy-Auth: = %q, %v", proxyURL, auth, err)
|
||||
}
|
||||
res, err := tr.RoundTrip(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Transport.RoundTrip: %v", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
return res.Write(os.Stdout)
|
||||
}
|
||||
|
||||
func checkDerp(ctx context.Context, derpRegion string) error {
|
||||
dmap := derpmap.Prod()
|
||||
getRegion := func() *tailcfg.DERPRegion {
|
||||
for _, r := range dmap.Regions {
|
||||
if r.RegionCode == derpRegion {
|
||||
return r
|
||||
}
|
||||
}
|
||||
for _, r := range dmap.Regions {
|
||||
log.Printf("Known region: %q", r.RegionCode)
|
||||
}
|
||||
log.Fatalf("unknown region %q", derpRegion)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
priv1 := key.NewPrivate()
|
||||
priv2 := key.NewPrivate()
|
||||
|
||||
c1 := derphttp.NewRegionClient(priv1, log.Printf, getRegion)
|
||||
c2 := derphttp.NewRegionClient(priv2, log.Printf, getRegion)
|
||||
|
||||
c2.NotePreferred(true) // just to open it
|
||||
|
||||
m, err := c2.Recv()
|
||||
log.Printf("c2 got %T, %v", m, err)
|
||||
|
||||
t0 := time.Now()
|
||||
if err := c1.Send(priv2.Public(), []byte("hello")); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(time.Since(t0))
|
||||
|
||||
m, err = c2.Recv()
|
||||
log.Printf("c2 got %T, %v", m, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("ok")
|
||||
return err
|
||||
}
|
||||
67
cmd/tailscale/cli/down.go
Normal file
67
cmd/tailscale/cli/down.go
Normal file
@@ -0,0 +1,67 @@
|
||||
// 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 cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
|
||||
var downCmd = &ffcli.Command{
|
||||
Name: "down",
|
||||
ShortUsage: "down",
|
||||
ShortHelp: "Disconnect from Tailscale",
|
||||
|
||||
Exec: runDown,
|
||||
}
|
||||
|
||||
func runDown(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
timer := time.AfterFunc(5*time.Second, func() {
|
||||
log.Fatalf("timeout running stop")
|
||||
})
|
||||
defer timer.Stop()
|
||||
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if n.Status != nil {
|
||||
cur := n.Status.BackendState
|
||||
switch cur {
|
||||
case "Stopped":
|
||||
log.Printf("already stopped")
|
||||
cancel()
|
||||
default:
|
||||
log.Printf("was in state %q", cur)
|
||||
}
|
||||
return
|
||||
}
|
||||
if n.State != nil {
|
||||
log.Printf("now in state %q", *n.State)
|
||||
if *n.State == ipn.Stopped {
|
||||
cancel()
|
||||
}
|
||||
return
|
||||
}
|
||||
log.Printf("Notify: %#v", n)
|
||||
})
|
||||
|
||||
bc.RequestStatus()
|
||||
bc.SetWantRunning(false)
|
||||
pump(ctx, bc, c)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -125,20 +125,35 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
|
||||
if len(report.RegionLatency) == 0 {
|
||||
fmt.Printf("\t* Nearest DERP: unknown (no response to latency probes)\n")
|
||||
} else {
|
||||
fmt.Printf("\t* Nearest DERP: %v (%v)\n", report.PreferredDERP, dm.Regions[report.PreferredDERP].RegionCode)
|
||||
fmt.Printf("\t* Nearest DERP: %v\n", dm.Regions[report.PreferredDERP].RegionName)
|
||||
fmt.Printf("\t* DERP latency:\n")
|
||||
var rids []int
|
||||
for rid := range dm.Regions {
|
||||
rids = append(rids, rid)
|
||||
}
|
||||
sort.Ints(rids)
|
||||
sort.Slice(rids, func(i, j int) bool {
|
||||
l1, ok1 := report.RegionLatency[rids[i]]
|
||||
l2, ok2 := report.RegionLatency[rids[j]]
|
||||
if ok1 != ok2 {
|
||||
return ok1 // defined things sort first
|
||||
}
|
||||
if !ok1 {
|
||||
return rids[i] < rids[j]
|
||||
}
|
||||
return l1 < l2
|
||||
})
|
||||
for _, rid := range rids {
|
||||
d, ok := report.RegionLatency[rid]
|
||||
var latency string
|
||||
if ok {
|
||||
latency = d.Round(time.Millisecond / 10).String()
|
||||
}
|
||||
fmt.Printf("\t\t- %v, %3s = %s\n", rid, dm.Regions[rid].RegionCode, latency)
|
||||
r := dm.Regions[rid]
|
||||
var derpNum string
|
||||
if netcheckArgs.verbose {
|
||||
derpNum = fmt.Sprintf("derp%d, ", rid)
|
||||
}
|
||||
fmt.Printf("\t\t- %3s: %-7s (%s%s)\n", r.RegionCode, latency, derpNum, r.RegionName)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
130
cmd/tailscale/cli/ping.go
Normal file
130
cmd/tailscale/cli/ping.go
Normal file
@@ -0,0 +1,130 @@
|
||||
// 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 cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
)
|
||||
|
||||
var pingCmd = &ffcli.Command{
|
||||
Name: "ping",
|
||||
ShortUsage: "ping <hostname-or-IP>",
|
||||
ShortHelp: "Ping a host at the Tailscale layer, see how it routed",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
|
||||
The 'tailscale ping' command pings a peer node at the Tailscale layer
|
||||
and reports which route it took for each response. The first ping or
|
||||
so will likely go over DERP (Tailscale's TCP relay protocol) while NAT
|
||||
traversal finds a direct path through.
|
||||
|
||||
If 'tailscale ping' works but a normal ping does not, that means one
|
||||
side's operating system firewall is blocking packets; 'tailscale ping'
|
||||
does not inject packets into either side's TUN devices.
|
||||
|
||||
By default, 'tailscale ping' stops after 10 pings or once a direct
|
||||
(non-DERP) path has been established, whichever comes first.
|
||||
|
||||
The provided hostname must resolve to or be a Tailscale IP
|
||||
(e.g. 100.x.y.z) or a subnet IP advertised by a Tailscale
|
||||
relay node.
|
||||
|
||||
`),
|
||||
Exec: runPing,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("ping", flag.ExitOnError)
|
||||
fs.BoolVar(&pingArgs.verbose, "verbose", false, "verbose output")
|
||||
fs.BoolVar(&pingArgs.untilDirect, "until-direct", true, "stop once a direct path is established")
|
||||
fs.IntVar(&pingArgs.num, "c", 10, "max number of pings to send")
|
||||
fs.DurationVar(&pingArgs.timeout, "timeout", 5*time.Second, "timeout before giving up on a ping")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var pingArgs struct {
|
||||
num int
|
||||
untilDirect bool
|
||||
verbose bool
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func runPing(ctx context.Context, args []string) error {
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
if len(args) != 1 {
|
||||
return errors.New("usage: ping <hostname-or-IP>")
|
||||
}
|
||||
hostOrIP := args[0]
|
||||
var ip string
|
||||
var res net.Resolver
|
||||
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
|
||||
return fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
|
||||
} else if len(addrs) == 0 {
|
||||
return fmt.Errorf("no IPs found for %q", hostOrIP)
|
||||
} else {
|
||||
ip = addrs[0]
|
||||
}
|
||||
if pingArgs.verbose && ip != hostOrIP {
|
||||
log.Printf("lookup %q => %q", hostOrIP, ip)
|
||||
}
|
||||
|
||||
ch := make(chan *ipnstate.PingResult, 1)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if pr := n.PingResult; pr != nil && pr.IP == ip {
|
||||
ch <- pr
|
||||
}
|
||||
})
|
||||
go pump(ctx, bc, c)
|
||||
|
||||
n := 0
|
||||
anyPong := false
|
||||
for {
|
||||
n++
|
||||
bc.Ping(ip)
|
||||
timer := time.NewTimer(pingArgs.timeout)
|
||||
select {
|
||||
case <-timer.C:
|
||||
fmt.Printf("timeout waiting for ping reply\n")
|
||||
case pr := <-ch:
|
||||
timer.Stop()
|
||||
if pr.Err != "" {
|
||||
return errors.New(pr.Err)
|
||||
}
|
||||
latency := time.Duration(pr.LatencySeconds * float64(time.Second)).Round(time.Millisecond)
|
||||
via := pr.Endpoint
|
||||
if pr.DERPRegionID != 0 {
|
||||
via = fmt.Sprintf("DERP(%s)", pr.DERPRegionCode)
|
||||
}
|
||||
anyPong = true
|
||||
fmt.Printf("pong from %s (%s) via %v in %v\n", pr.NodeName, pr.NodeIP, via, latency)
|
||||
if pr.Endpoint != "" && pingArgs.untilDirect {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
if n == pingArgs.num {
|
||||
if !anyPong {
|
||||
return errors.New("no reply")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,7 @@ var statusCmd = &ffcli.Command{
|
||||
fs.BoolVar(&statusArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
|
||||
fs.BoolVar(&statusArgs.web, "web", false, "run webserver with HTML showing status")
|
||||
fs.BoolVar(&statusArgs.active, "active", false, "filter output to only peers with active sessions (not applicable to web mode)")
|
||||
fs.BoolVar(&statusArgs.self, "self", true, "show status of local machine")
|
||||
fs.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address; use port 0 for automatic")
|
||||
fs.BoolVar(&statusArgs.browser, "browser", true, "Open a browser in web mode")
|
||||
return fs
|
||||
@@ -45,6 +46,7 @@ var statusArgs struct {
|
||||
listen string // in web mode, webserver address to listen on, empty means auto
|
||||
browser bool // in web mode, whether to open browser
|
||||
active bool // in CLI mode, filter output to only peers with active sessions
|
||||
self bool // in CLI mode, show status of local machine
|
||||
}
|
||||
|
||||
func runStatus(ctx context.Context, args []string) error {
|
||||
@@ -125,16 +127,17 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if st.BackendState == ipn.Stopped.String() {
|
||||
fmt.Println("Tailscale is stopped.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
f := func(format string, a ...interface{}) { fmt.Fprintf(&buf, format, a...) }
|
||||
for _, peer := range st.Peers() {
|
||||
ps := st.Peer[peer]
|
||||
printPS := func(ps *ipnstate.PeerStatus) {
|
||||
active := peerActive(ps)
|
||||
if statusArgs.active && !active {
|
||||
continue
|
||||
}
|
||||
f("%s %-7s %-15s %-18s tx=%8d rx=%8d ",
|
||||
peer.ShortString(),
|
||||
ps.PublicKey.ShortString(),
|
||||
ps.OS,
|
||||
ps.TailAddr,
|
||||
ps.SimpleHostName(),
|
||||
@@ -160,6 +163,18 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
}
|
||||
f("\n")
|
||||
}
|
||||
|
||||
if statusArgs.self && st.Self != nil {
|
||||
printPS(st.Self)
|
||||
}
|
||||
for _, peer := range st.Peers() {
|
||||
ps := st.Peer[peer]
|
||||
active := peerActive(ps)
|
||||
if statusArgs.active && !active {
|
||||
continue
|
||||
}
|
||||
printPS(ps)
|
||||
}
|
||||
os.Stdout.Write(buf.Bytes())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -15,11 +15,14 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
@@ -51,11 +54,12 @@ specify any flags, options are reset to their default.
|
||||
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
|
||||
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "install host routes to other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
||||
upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication")
|
||||
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) {
|
||||
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" {
|
||||
@@ -73,6 +77,7 @@ var upArgs struct {
|
||||
acceptDNS bool
|
||||
singleRoutes bool
|
||||
shieldsUp bool
|
||||
forceReauth bool
|
||||
advertiseRoutes string
|
||||
advertiseTags string
|
||||
enableDERP bool
|
||||
@@ -109,18 +114,18 @@ func isBSD(s string) bool {
|
||||
return s == "dragonfly" || s == "freebsd" || s == "netbsd" || s == "openbsd"
|
||||
}
|
||||
|
||||
func warning(format string, args ...interface{}) {
|
||||
func warnf(format string, args ...interface{}) {
|
||||
fmt.Printf("Warning: "+format+"\n", args...)
|
||||
}
|
||||
|
||||
// checkIPForwarding prints warnings on linux if IP forwarding is not
|
||||
// checkIPForwarding prints warnings if IP forwarding is not
|
||||
// enabled, or if we were unable to verify the state of IP forwarding.
|
||||
func checkIPForwarding() {
|
||||
var key string
|
||||
|
||||
if runtime.GOOS == "linux" {
|
||||
key = "net.ipv4.ip_forward"
|
||||
} else if isBSD(runtime.GOOS) {
|
||||
} else if isBSD(runtime.GOOS) || version.OS() == "macOS" {
|
||||
key = "net.inet.ip.forwarding"
|
||||
} else {
|
||||
return
|
||||
@@ -128,16 +133,16 @@ func checkIPForwarding() {
|
||||
|
||||
bs, err := exec.Command("sysctl", "-n", key).Output()
|
||||
if err != nil {
|
||||
warning("couldn't check %s (%v).\nSubnet routes won't work without IP forwarding.", key, err)
|
||||
warnf("couldn't check %s (%v).\nSubnet routes won't work without IP forwarding.", key, err)
|
||||
return
|
||||
}
|
||||
on, err := strconv.ParseBool(string(bytes.TrimSpace(bs)))
|
||||
if err != nil {
|
||||
warning("couldn't parse %s (%v).\nSubnet routes won't work without IP forwarding.", key, err)
|
||||
warnf("couldn't parse %s (%v).\nSubnet routes won't work without IP forwarding.", key, err)
|
||||
return
|
||||
}
|
||||
if !on {
|
||||
warning("%s is disabled. Subnet routes won't work.", key)
|
||||
warnf("%s is disabled. Subnet routes won't work.", key)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,15 +153,19 @@ func runUp(ctx context.Context, args []string) error {
|
||||
|
||||
var routes []wgcfg.CIDR
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
checkIPForwarding()
|
||||
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
|
||||
for _, s := range advroutes {
|
||||
cidr, ok := parseIPOrCIDR(s)
|
||||
if !ok {
|
||||
log.Fatalf("%q is not a valid IP address or CIDR prefix", s)
|
||||
ipp, err := netaddr.ParseIPPrefix(s) // parse it with other pawith both packages
|
||||
if !ok || err != nil {
|
||||
fatalf("%q is not a valid IP address or CIDR prefix", s)
|
||||
}
|
||||
if ipp != ipp.Masked() {
|
||||
fatalf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
|
||||
}
|
||||
routes = append(routes, cidr)
|
||||
}
|
||||
checkIPForwarding()
|
||||
}
|
||||
|
||||
var tags []string
|
||||
@@ -165,17 +174,16 @@ func runUp(ctx context.Context, args []string) error {
|
||||
for _, tag := range tags {
|
||||
err := tailcfg.CheckTag(tag)
|
||||
if err != nil {
|
||||
log.Fatalf("tag: %q: %s", tag, err)
|
||||
fatalf("tag: %q: %s", tag, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(upArgs.hostname) > 256 {
|
||||
log.Fatalf("hostname too long: %d bytes (max 256)", len(upArgs.hostname))
|
||||
fatalf("hostname too long: %d bytes (max 256)", len(upArgs.hostname))
|
||||
}
|
||||
|
||||
// TODO(apenwarr): fix different semantics between prefs and uflags
|
||||
// TODO(apenwarr): allow setting/using CorpDNS
|
||||
prefs := ipn.NewPrefs()
|
||||
prefs.ControlURL = upArgs.server
|
||||
prefs.WantRunning = true
|
||||
@@ -194,12 +202,12 @@ func runUp(ctx context.Context, args []string) error {
|
||||
prefs.NetfilterMode = router.NetfilterOn
|
||||
case "nodivert":
|
||||
prefs.NetfilterMode = router.NetfilterNoDivert
|
||||
warning("netfilter=nodivert; add iptables calls to ts-* chains manually.")
|
||||
warnf("netfilter=nodivert; add iptables calls to ts-* chains manually.")
|
||||
case "off":
|
||||
prefs.NetfilterMode = router.NetfilterOff
|
||||
warning("netfilter=off; configure iptables yourself.")
|
||||
warnf("netfilter=off; configure iptables yourself.")
|
||||
default:
|
||||
log.Fatalf("invalid value --netfilter-mode: %q", upArgs.netfilterMode)
|
||||
fatalf("invalid value --netfilter-mode: %q", upArgs.netfilterMode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,6 +215,8 @@ func runUp(ctx context.Context, args []string) error {
|
||||
defer cancel()
|
||||
|
||||
var printed bool
|
||||
var loginOnce sync.Once
|
||||
startLoginInteractive := func() { loginOnce.Do(func() { bc.StartLoginInteractive() }) }
|
||||
|
||||
bc.SetPrefs(prefs)
|
||||
opts := ipn.Options{
|
||||
@@ -214,13 +224,13 @@ func runUp(ctx context.Context, args []string) error {
|
||||
AuthKey: upArgs.authKey,
|
||||
Notify: func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatalf("backend error: %v\n", *n.ErrMessage)
|
||||
fatalf("backend error: %v\n", *n.ErrMessage)
|
||||
}
|
||||
if s := n.State; s != nil {
|
||||
switch *s {
|
||||
case ipn.NeedsLogin:
|
||||
printed = true
|
||||
bc.StartLoginInteractive()
|
||||
startLoginInteractive()
|
||||
case ipn.NeedsMachineAuth:
|
||||
printed = true
|
||||
fmt.Fprintf(os.Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s/admin/machines\n\n", upArgs.server)
|
||||
@@ -246,6 +256,10 @@ func runUp(ctx context.Context, args []string) error {
|
||||
// ephemeral frontends that read/modify/write state, once
|
||||
// Windows/Mac state is moved into backend.
|
||||
bc.Start(opts)
|
||||
if upArgs.forceReauth {
|
||||
printed = true
|
||||
startLoginInteractive()
|
||||
}
|
||||
pump(ctx, bc, c)
|
||||
|
||||
return nil
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
@@ -136,7 +137,11 @@ func run() error {
|
||||
|
||||
var e wgengine.Engine
|
||||
if args.fake {
|
||||
e, err = wgengine.NewFakeUserspaceEngine(logf, 0)
|
||||
impl := netstack.Impl
|
||||
if args.tunname != "netstack" {
|
||||
impl = nil
|
||||
}
|
||||
e, err = wgengine.NewFakeUserspaceEngine(logf, 0, impl)
|
||||
} else {
|
||||
e, err = wgengine.NewUserspaceEngine(logf, args.tunname, args.port)
|
||||
}
|
||||
@@ -149,11 +154,16 @@ func run() error {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
// Exit gracefully by cancelling the ipnserver context in most common cases:
|
||||
// interrupted from the TTY or killed by a service manager.
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
// SIGPIPE sometimes gets generated when CLIs disconnect from
|
||||
// tailscaled. The default action is to terminate the process, we
|
||||
// want to keep running.
|
||||
signal.Ignore(syscall.SIGPIPE)
|
||||
go func() {
|
||||
interrupt := make(chan os.Signal, 1)
|
||||
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
select {
|
||||
case <-interrupt:
|
||||
case s := <-interrupt:
|
||||
logf("tailscaled got signal %v; shutting down", s)
|
||||
cancel()
|
||||
case <-ctx.Done():
|
||||
// continue
|
||||
@@ -169,7 +179,7 @@ func run() error {
|
||||
SurviveDisconnects: true,
|
||||
DebugMux: debugMux,
|
||||
}
|
||||
err = ipnserver.Run(ctx, logf, pol.PublicID.String(), opts, e)
|
||||
err = ipnserver.Run(ctx, logf, pol.PublicID.String(), ipnserver.FixedEngine(e), opts)
|
||||
// Cancelation is not an error: it is the only way to stop ipnserver.
|
||||
if err != nil && err != context.Canceled {
|
||||
logf("ipnserver.Run: %v", err)
|
||||
|
||||
@@ -3,8 +3,6 @@ Description=Tailscale node agent
|
||||
Documentation=https://tailscale.com/kb/
|
||||
Wants=network-pre.target
|
||||
After=network-pre.target
|
||||
StartLimitIntervalSec=0
|
||||
StartLimitBurst=0
|
||||
|
||||
[Service]
|
||||
EnvironmentFile=/etc/default/tailscaled
|
||||
|
||||
@@ -117,13 +117,15 @@ type Client struct {
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
statusFunc func(Status) // called to update Client status
|
||||
|
||||
loggedIn bool // true if currently logged in
|
||||
loginGoal *LoginGoal // non-nil if some login activity is desired
|
||||
synced bool // true if our netmap is up-to-date
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
inPollNetMap bool // true if currently running a PollNetMap
|
||||
inSendStatus int // number of sendStatus calls currently in progress
|
||||
state State
|
||||
paused bool // whether we should stop making HTTP requests
|
||||
unpauseWaiters []chan struct{}
|
||||
loggedIn bool // true if currently logged in
|
||||
loginGoal *LoginGoal // non-nil if some login activity is desired
|
||||
synced bool // true if our netmap is up-to-date
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
inPollNetMap bool // true if currently running a PollNetMap
|
||||
inSendStatus int // number of sendStatus calls currently in progress
|
||||
state State
|
||||
|
||||
authCtx context.Context // context used for auth requests
|
||||
mapCtx context.Context // context used for netmap requests
|
||||
@@ -169,6 +171,27 @@ func NewNoStart(opts Options) (*Client, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// SetPaused controls whether HTTP activity should be paused.
|
||||
//
|
||||
// The client can be paused and unpaused repeatedly, unlike Start and Shutdown, which can only be used once.
|
||||
func (c *Client) SetPaused(paused bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if paused == c.paused {
|
||||
return
|
||||
}
|
||||
c.paused = paused
|
||||
if paused {
|
||||
// Just cancel the map routine. The auth routine isn't expensive.
|
||||
c.cancelMapLocked()
|
||||
} else {
|
||||
for _, ch := range c.unpauseWaiters {
|
||||
close(ch)
|
||||
}
|
||||
c.unpauseWaiters = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the client's goroutines.
|
||||
//
|
||||
// It should only be called for clients created by NewNoStart.
|
||||
@@ -241,7 +264,7 @@ func (c *Client) cancelMapSafely() {
|
||||
|
||||
func (c *Client) authRoutine() {
|
||||
defer close(c.authDone)
|
||||
bo := backoff.NewBackoff("authRoutine", c.logf)
|
||||
bo := backoff.NewBackoff("authRoutine", c.logf, 30*time.Second)
|
||||
|
||||
for {
|
||||
c.mu.Lock()
|
||||
@@ -272,6 +295,7 @@ func (c *Client) authRoutine() {
|
||||
if goal == nil {
|
||||
// Wait for something interesting to happen
|
||||
var exp <-chan time.Time
|
||||
var expTimer *time.Timer
|
||||
if expiry != nil && !expiry.IsZero() {
|
||||
// if expiry is in the future, don't delay
|
||||
// past that time.
|
||||
@@ -284,11 +308,15 @@ func (c *Client) authRoutine() {
|
||||
if delay > 5*time.Second {
|
||||
delay = time.Second
|
||||
}
|
||||
exp = time.After(delay)
|
||||
expTimer = time.NewTimer(delay)
|
||||
exp = expTimer.C
|
||||
}
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if expTimer != nil {
|
||||
expTimer.Stop()
|
||||
}
|
||||
c.logf("authRoutine: context done.")
|
||||
case <-exp:
|
||||
// Unfortunately the key expiry isn't provided
|
||||
@@ -310,7 +338,7 @@ func (c *Client) authRoutine() {
|
||||
}
|
||||
}
|
||||
} else if !goal.wantLoggedIn {
|
||||
err := c.direct.TryLogout(c.authCtx)
|
||||
err := c.direct.TryLogout(ctx)
|
||||
if err != nil {
|
||||
report(err, "TryLogout")
|
||||
bo.BackOff(ctx, err)
|
||||
@@ -399,12 +427,35 @@ func (c *Client) Direct() *Direct {
|
||||
return c.direct
|
||||
}
|
||||
|
||||
// unpausedChanLocked returns a new channel that is closed when the
|
||||
// current Client pause is unpaused.
|
||||
//
|
||||
// c.mu must be held
|
||||
func (c *Client) unpausedChanLocked() <-chan struct{} {
|
||||
unpaused := make(chan struct{})
|
||||
c.unpauseWaiters = append(c.unpauseWaiters, unpaused)
|
||||
return unpaused
|
||||
}
|
||||
|
||||
func (c *Client) mapRoutine() {
|
||||
defer close(c.mapDone)
|
||||
bo := backoff.NewBackoff("mapRoutine", c.logf)
|
||||
bo := backoff.NewBackoff("mapRoutine", c.logf, 30*time.Second)
|
||||
|
||||
for {
|
||||
c.mu.Lock()
|
||||
if c.paused {
|
||||
unpaused := c.unpausedChanLocked()
|
||||
c.mu.Unlock()
|
||||
c.logf("mapRoutine: awaiting unpause")
|
||||
select {
|
||||
case <-unpaused:
|
||||
c.logf("mapRoutine: unpaused")
|
||||
case <-c.quit:
|
||||
c.logf("mapRoutine: quit")
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
c.logf("mapRoutine: %s", c.state)
|
||||
loggedIn := c.loggedIn
|
||||
ctx := c.mapCtx
|
||||
@@ -487,8 +538,14 @@ func (c *Client) mapRoutine() {
|
||||
if c.state == StateSynchronized {
|
||||
c.state = StateAuthenticated
|
||||
}
|
||||
paused := c.paused
|
||||
c.mu.Unlock()
|
||||
|
||||
if paused {
|
||||
c.logf("mapRoutine: paused")
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
report(err, "PollNetMap")
|
||||
bo.BackOff(ctx, err)
|
||||
@@ -517,7 +574,7 @@ func (c *Client) SetHostinfo(hi *tailcfg.Hostinfo) {
|
||||
panic("nil Hostinfo")
|
||||
}
|
||||
if !c.direct.SetHostinfo(hi) {
|
||||
c.logf("[unexpected] duplicate Hostinfo: %v", hi)
|
||||
// No changes. Don't log.
|
||||
return
|
||||
}
|
||||
c.logf("Hostinfo: %v", hi)
|
||||
|
||||
@@ -70,3 +70,10 @@ func TestStatusEqual(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSVersion(t *testing.T) {
|
||||
if osVersion == nil {
|
||||
t.Skip("not available for OS")
|
||||
}
|
||||
t.Logf("Got: %#q", osVersion())
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
package controlclient
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=Persist -output=direct_clone.go
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
@@ -19,19 +21,25 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/oauth2"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/log/logheap"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
@@ -140,6 +148,8 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
if httpc == nil {
|
||||
dialer := netns.NewDialer()
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
tr.DialContext = dialer.DialContext
|
||||
tr.ForceAttemptHTTP2 = true
|
||||
tr.TLSClientConfig = tlsdial.Config(serverURL.Host, tr.TLSClientConfig)
|
||||
@@ -165,12 +175,20 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
var osVersion func() string // non-nil on some platforms
|
||||
|
||||
func NewHostinfo() *tailcfg.Hostinfo {
|
||||
hostname, _ := os.Hostname()
|
||||
var osv string
|
||||
if osVersion != nil {
|
||||
osv = osVersion()
|
||||
}
|
||||
return &tailcfg.Hostinfo{
|
||||
IPNVersion: version.LONG,
|
||||
Hostname: hostname,
|
||||
OS: version.OS(),
|
||||
OSVersion: osv,
|
||||
GoArch: runtime.GOARCH,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -477,6 +495,7 @@ 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,
|
||||
@@ -561,6 +580,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
// the same format before just closing the connection.
|
||||
// We can use this same read loop either way.
|
||||
var msg []byte
|
||||
var previousPeers []*tailcfg.Node // for delta-purposes
|
||||
for i := 0; i < maxPolls || maxPolls < 0; i++ {
|
||||
vlogf("netmap: starting size read after %v (poll %v)", time.Since(t0).Round(time.Millisecond), i)
|
||||
var siz [4]byte
|
||||
@@ -582,25 +602,36 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
vlogf("netmap: decode error: %v")
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.KeepAlive {
|
||||
vlogf("netmap: got keep-alive")
|
||||
select {
|
||||
case timeoutReset <- struct{}{}:
|
||||
vlogf("netmap: sent keep-alive timer reset")
|
||||
case <-ctx.Done():
|
||||
c.logf("netmap: not resetting timer for keep-alive due to: %v", ctx.Err())
|
||||
return ctx.Err()
|
||||
}
|
||||
} else {
|
||||
vlogf("netmap: got new map")
|
||||
}
|
||||
select {
|
||||
case timeoutReset <- struct{}{}:
|
||||
vlogf("netmap: sent timer reset")
|
||||
case <-ctx.Done():
|
||||
c.logf("netmap: not resetting timer; context done: %v", ctx.Err())
|
||||
return ctx.Err()
|
||||
}
|
||||
if resp.KeepAlive {
|
||||
continue
|
||||
}
|
||||
vlogf("netmap: got new map")
|
||||
|
||||
undeltaPeers(&resp, previousPeers)
|
||||
previousPeers = cloneNodes(resp.Peers) // defensive/lazy clone, since this escapes to who knows where
|
||||
|
||||
if resp.DERPMap != nil {
|
||||
vlogf("netmap: new map contains DERP map")
|
||||
lastDERPMap = resp.DERPMap
|
||||
}
|
||||
if resp.Debug != nil && resp.Debug.LogHeapPprof {
|
||||
go logheap.LogHeap(resp.Debug.LogHeapURL)
|
||||
if resp.Debug != nil {
|
||||
if resp.Debug.LogHeapPprof {
|
||||
go logheap.LogHeap(resp.Debug.LogHeapURL)
|
||||
}
|
||||
setControlAtomic(&controlUseDERPRoute, resp.Debug.DERPRoute)
|
||||
setControlAtomic(&controlTrimWGConfig, resp.Debug.TrimWGConfig)
|
||||
}
|
||||
// Temporarily (2020-06-29) support removing all but
|
||||
// discovery-supporting nodes during development, for
|
||||
@@ -619,6 +650,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
PrivateKey: persist.PrivateNodeKey,
|
||||
Expiry: resp.Node.KeyExpiry,
|
||||
Name: resp.Node.Name,
|
||||
Addresses: resp.Node.Addresses,
|
||||
Peers: resp.Peers,
|
||||
LocalPort: localPort,
|
||||
@@ -626,8 +658,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
|
||||
Domain: resp.Domain,
|
||||
Roles: resp.Roles,
|
||||
DNS: resp.DNS,
|
||||
DNSDomains: resp.SearchPaths,
|
||||
DNS: resp.DNSConfig,
|
||||
Hostinfo: resp.Node.Hostinfo,
|
||||
PacketFilter: c.parsePacketFilter(resp.PacketFilter),
|
||||
DERPMap: lastDERPMap,
|
||||
@@ -641,6 +672,15 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
} else {
|
||||
nm.MachineStatus = tailcfg.MachineUnauthorized
|
||||
}
|
||||
if len(resp.DNS) > 0 {
|
||||
nm.DNS.Nameservers = wgIPToNetaddr(resp.DNS)
|
||||
}
|
||||
if len(resp.SearchPaths) > 0 {
|
||||
nm.DNS.Domains = resp.SearchPaths
|
||||
}
|
||||
if Debug.ProxyDNS {
|
||||
nm.DNS.Proxied = true
|
||||
}
|
||||
|
||||
// Printing the netmap can be extremely verbose, but is very
|
||||
// handy for debugging. Let's limit how often we do it.
|
||||
@@ -780,12 +820,24 @@ func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (w
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func wgIPToNetaddr(ips []wgcfg.IP) (ret []netaddr.IP) {
|
||||
for _, ip := range ips {
|
||||
nip, ok := netaddr.FromStdIP(ip.IP())
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("conversion of %s from wgcfg to netaddr IP failed", ip))
|
||||
}
|
||||
ret = append(ret, nip.Unmap())
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Debug contains temporary internal-only debug knobs.
|
||||
// They're unexported to not draw attention to them.
|
||||
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
|
||||
@@ -794,6 +846,7 @@ type debug struct {
|
||||
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"),
|
||||
}
|
||||
@@ -815,3 +868,124 @@ func envBool(k string) bool {
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// undeltaPeers updates mapRes.Peers to be complete based on the provided previous peer list
|
||||
// and the PeersRemoved and PeersChanged fields in mapRes.
|
||||
// It then also nils out the delta fields.
|
||||
func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
|
||||
if len(mapRes.Peers) > 0 {
|
||||
// Not delta encoded.
|
||||
if !nodesSorted(mapRes.Peers) {
|
||||
log.Printf("netmap: undeltaPeers: MapResponse.Peers not sorted; sorting")
|
||||
sortNodes(mapRes.Peers)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var removed map[tailcfg.NodeID]bool
|
||||
if pr := mapRes.PeersRemoved; len(pr) > 0 {
|
||||
removed = make(map[tailcfg.NodeID]bool, len(pr))
|
||||
for _, id := range pr {
|
||||
removed[id] = true
|
||||
}
|
||||
}
|
||||
changed := mapRes.PeersChanged
|
||||
|
||||
if len(removed) == 0 && len(changed) == 0 {
|
||||
// No changes fast path.
|
||||
mapRes.Peers = prev
|
||||
return
|
||||
}
|
||||
|
||||
if !nodesSorted(changed) {
|
||||
log.Printf("netmap: undeltaPeers: MapResponse.PeersChanged not sorted; sorting")
|
||||
sortNodes(changed)
|
||||
}
|
||||
if !nodesSorted(prev) {
|
||||
// Internal error (unrelated to the network) if we get here.
|
||||
log.Printf("netmap: undeltaPeers: [unexpected] prev not sorted; sorting")
|
||||
sortNodes(prev)
|
||||
}
|
||||
|
||||
newFull := make([]*tailcfg.Node, 0, len(prev)-len(removed))
|
||||
for len(prev) > 0 && len(changed) > 0 {
|
||||
pID := prev[0].ID
|
||||
cID := changed[0].ID
|
||||
if removed[pID] {
|
||||
prev = prev[1:]
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case pID < cID:
|
||||
newFull = append(newFull, prev[0])
|
||||
prev = prev[1:]
|
||||
case pID == cID:
|
||||
newFull = append(newFull, changed[0])
|
||||
prev, changed = prev[1:], changed[1:]
|
||||
case cID < pID:
|
||||
newFull = append(newFull, changed[0])
|
||||
changed = changed[1:]
|
||||
}
|
||||
}
|
||||
newFull = append(newFull, changed...)
|
||||
for _, n := range prev {
|
||||
if !removed[n.ID] {
|
||||
newFull = append(newFull, n)
|
||||
}
|
||||
}
|
||||
sortNodes(newFull)
|
||||
mapRes.Peers = newFull
|
||||
mapRes.PeersChanged = nil
|
||||
mapRes.PeersRemoved = nil
|
||||
}
|
||||
|
||||
func nodesSorted(v []*tailcfg.Node) bool {
|
||||
for i, n := range v {
|
||||
if i > 0 && n.ID <= v[i-1].ID {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func sortNodes(v []*tailcfg.Node) {
|
||||
sort.Slice(v, func(i, j int) bool { return v[i].ID < v[j].ID })
|
||||
}
|
||||
|
||||
func cloneNodes(v1 []*tailcfg.Node) []*tailcfg.Node {
|
||||
if v1 == nil {
|
||||
return nil
|
||||
}
|
||||
v2 := make([]*tailcfg.Node, len(v1))
|
||||
for i, n := range v1 {
|
||||
v2[i] = n.Clone()
|
||||
}
|
||||
return v2
|
||||
}
|
||||
|
||||
// opt.Bool configs from control.
|
||||
var (
|
||||
controlUseDERPRoute atomic.Value
|
||||
controlTrimWGConfig atomic.Value
|
||||
)
|
||||
|
||||
func setControlAtomic(dst *atomic.Value, v opt.Bool) {
|
||||
old, ok := dst.Load().(opt.Bool)
|
||||
if !ok || old != v {
|
||||
dst.Store(v)
|
||||
}
|
||||
}
|
||||
|
||||
// DERPRouteFlag reports the last reported value from control for whether
|
||||
// DERP route optimization (Issue 150) should be enabled.
|
||||
func DERPRouteFlag() opt.Bool {
|
||||
v, _ := controlUseDERPRoute.Load().(opt.Bool)
|
||||
return v
|
||||
}
|
||||
|
||||
// TrimWGConfig reports the last reported value from control for whether
|
||||
// we should do lazy wireguard configuration.
|
||||
func TrimWGConfig() opt.Bool {
|
||||
v, _ := controlTrimWGConfig.Load().(opt.Bool)
|
||||
return v
|
||||
}
|
||||
|
||||
20
control/controlclient/direct_clone.go
Normal file
20
control/controlclient/direct_clone.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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 Persist; DO NOT EDIT.
|
||||
|
||||
package controlclient
|
||||
|
||||
import ()
|
||||
|
||||
// Clone makes a deep copy of Persist.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Persist) Clone() *Persist {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Persist)
|
||||
*dst = *src
|
||||
return dst
|
||||
}
|
||||
93
control/controlclient/direct_test.go
Normal file
93
control/controlclient/direct_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
// 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 controlclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func TestUndeltaPeers(t *testing.T) {
|
||||
n := func(id tailcfg.NodeID, name string) *tailcfg.Node {
|
||||
return &tailcfg.Node{ID: id, Name: name}
|
||||
}
|
||||
peers := func(nv ...*tailcfg.Node) []*tailcfg.Node { return nv }
|
||||
tests := []struct {
|
||||
name string
|
||||
mapRes *tailcfg.MapResponse
|
||||
prev []*tailcfg.Node
|
||||
want []*tailcfg.Node
|
||||
}{
|
||||
{
|
||||
name: "full_peers",
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
Peers: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
want: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
{
|
||||
name: "full_peers_ignores_deltas",
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
Peers: peers(n(1, "foo"), n(2, "bar")),
|
||||
PeersRemoved: []tailcfg.NodeID{2},
|
||||
},
|
||||
want: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
{
|
||||
name: "add_and_update",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChanged: peers(n(0, "zero"), n(2, "bar2"), n(3, "three")),
|
||||
},
|
||||
want: peers(n(0, "zero"), n(1, "foo"), n(2, "bar2"), n(3, "three")),
|
||||
},
|
||||
{
|
||||
name: "remove",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersRemoved: []tailcfg.NodeID{1},
|
||||
},
|
||||
want: peers(n(2, "bar")),
|
||||
},
|
||||
{
|
||||
name: "add_and_remove",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChanged: peers(n(1, "foo2")),
|
||||
PeersRemoved: []tailcfg.NodeID{2},
|
||||
},
|
||||
want: peers(n(1, "foo2")),
|
||||
},
|
||||
{
|
||||
name: "unchanged",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{},
|
||||
want: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
undeltaPeers(tt.mapRes, tt.prev)
|
||||
if !reflect.DeepEqual(tt.mapRes.Peers, tt.want) {
|
||||
t.Errorf("wrong results\n got: %s\nwant: %s", formatNodes(tt.mapRes.Peers), formatNodes(tt.want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func formatNodes(nodes []*tailcfg.Node) string {
|
||||
var sb strings.Builder
|
||||
for i, n := range nodes {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
fmt.Fprintf(&sb, "(%d, %q)", n.ID, n.Name)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
@@ -5,80 +5,16 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
func parseIP(host string, defaultBits int) (filter.Net, error) {
|
||||
ip := net.ParseIP(host)
|
||||
if ip != nil && ip.IsUnspecified() {
|
||||
// For clarity, reject 0.0.0.0 as an input
|
||||
return filter.NetNone, fmt.Errorf("ports=%#v: to allow all IP addresses, use *:port, not 0.0.0.0:port", host)
|
||||
} else if ip == nil && host == "*" {
|
||||
// User explicitly requested wildcard dst ip
|
||||
return filter.NetAny, nil
|
||||
} else {
|
||||
if ip != nil {
|
||||
ip = ip.To4()
|
||||
}
|
||||
if ip == nil || len(ip) != 4 {
|
||||
return filter.NetNone, fmt.Errorf("ports=%#v: invalid IPv4 address", host)
|
||||
}
|
||||
return filter.Net{
|
||||
IP: filter.NewIP(ip),
|
||||
Mask: filter.Netmask(defaultBits),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Parse a backward-compatible FilterRule used by control's wire format,
|
||||
// producing the most current filter.Matches format.
|
||||
func (c *Direct) parsePacketFilter(pf []tailcfg.FilterRule) filter.Matches {
|
||||
mm := make([]filter.Match, 0, len(pf))
|
||||
var erracc error
|
||||
|
||||
for _, r := range pf {
|
||||
m := filter.Match{}
|
||||
|
||||
for i, s := range r.SrcIPs {
|
||||
bits := 32
|
||||
if len(r.SrcBits) > i {
|
||||
bits = r.SrcBits[i]
|
||||
}
|
||||
net, err := parseIP(s, bits)
|
||||
if err != nil && erracc == nil {
|
||||
erracc = err
|
||||
continue
|
||||
}
|
||||
m.Srcs = append(m.Srcs, net)
|
||||
}
|
||||
|
||||
for _, d := range r.DstPorts {
|
||||
bits := 32
|
||||
if d.Bits != nil {
|
||||
bits = *d.Bits
|
||||
}
|
||||
net, err := parseIP(d.IP, bits)
|
||||
if err != nil && erracc == nil {
|
||||
erracc = err
|
||||
continue
|
||||
}
|
||||
m.Dsts = append(m.Dsts, filter.NetPortRange{
|
||||
Net: net,
|
||||
Ports: filter.PortRange{
|
||||
First: d.Ports.First,
|
||||
Last: d.Ports.Last,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
mm = append(mm, m)
|
||||
}
|
||||
|
||||
if erracc != nil {
|
||||
c.logf("parsePacketFilter: %s\n", erracc)
|
||||
mm, err := filter.MatchesFromFilterRules(pf)
|
||||
if err != nil {
|
||||
c.logf("parsePacketFilter: %s\n", err)
|
||||
}
|
||||
return mm
|
||||
}
|
||||
|
||||
87
control/controlclient/hostinfo_linux.go
Normal file
87
control/controlclient/hostinfo_linux.go
Normal file
@@ -0,0 +1,87 @@
|
||||
// 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 linux,!android
|
||||
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/util/lineread"
|
||||
)
|
||||
|
||||
func init() {
|
||||
osVersion = osVersionLinux
|
||||
}
|
||||
|
||||
func osVersionLinux() string {
|
||||
m := map[string]string{}
|
||||
lineread.File("/etc/os-release", func(line []byte) error {
|
||||
eq := bytes.IndexByte(line, '=')
|
||||
if eq == -1 {
|
||||
return nil
|
||||
}
|
||||
k, v := string(line[:eq]), strings.Trim(string(line[eq+1:]), `"`)
|
||||
m[k] = v
|
||||
return nil
|
||||
})
|
||||
|
||||
var un syscall.Utsname
|
||||
syscall.Uname(&un)
|
||||
|
||||
var attrBuf strings.Builder
|
||||
attrBuf.WriteString("; kernel=")
|
||||
for _, b := range un.Release {
|
||||
if b == 0 {
|
||||
break
|
||||
}
|
||||
attrBuf.WriteByte(byte(b))
|
||||
}
|
||||
if inContainer() {
|
||||
attrBuf.WriteString("; container")
|
||||
}
|
||||
attr := attrBuf.String()
|
||||
|
||||
id := m["ID"]
|
||||
|
||||
switch id {
|
||||
case "debian":
|
||||
slurp, _ := ioutil.ReadFile("/etc/debian_version")
|
||||
return fmt.Sprintf("Debian %s (%s)%s", bytes.TrimSpace(slurp), m["VERSION_CODENAME"], attr)
|
||||
case "ubuntu":
|
||||
return fmt.Sprintf("Ubuntu %s%s", m["VERSION"], attr)
|
||||
case "", "centos": // CentOS 6 has no /etc/os-release, so its id is ""
|
||||
if cr, _ := ioutil.ReadFile("/etc/centos-release"); len(cr) > 0 { // "CentOS release 6.10 (Final)
|
||||
return fmt.Sprintf("%s%s", bytes.TrimSpace(cr), attr)
|
||||
}
|
||||
fallthrough
|
||||
case "fedora", "rhel", "alpine":
|
||||
// Their PRETTY_NAME is fine as-is for all versions I tested.
|
||||
fallthrough
|
||||
default:
|
||||
if v := m["PRETTY_NAME"]; v != "" {
|
||||
return fmt.Sprintf("%s%s", v, attr)
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("Other%s", attr)
|
||||
}
|
||||
|
||||
func inContainer() (ret bool) {
|
||||
lineread.File("/proc/1/cgroup", func(line []byte) error {
|
||||
if mem.Contains(mem.B(line), mem.S("/docker/")) ||
|
||||
mem.Contains(mem.B(line), mem.S("/lxc/")) {
|
||||
ret = true
|
||||
return io.EOF // arbitrary non-nil error to stop loop
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
26
control/controlclient/hostinfo_windows.go
Normal file
26
control/controlclient/hostinfo_windows.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// 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 controlclient
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func init() {
|
||||
osVersion = osVersionWindows
|
||||
}
|
||||
|
||||
func osVersionWindows() string {
|
||||
cmd := exec.Command("cmd", "/c", "ver")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
out, _ := cmd.Output() // "\nMicrosoft Windows [Version 10.0.19041.388]\n\n"
|
||||
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
|
||||
}
|
||||
@@ -7,7 +7,6 @@ package controlclient
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
@@ -23,15 +22,16 @@ import (
|
||||
type NetworkMap struct {
|
||||
// Core networking
|
||||
|
||||
NodeKey tailcfg.NodeKey
|
||||
PrivateKey wgcfg.PrivateKey
|
||||
Expiry time.Time
|
||||
NodeKey tailcfg.NodeKey
|
||||
PrivateKey wgcfg.PrivateKey
|
||||
Expiry time.Time
|
||||
// Name is the DNS name assigned to this node.
|
||||
Name string
|
||||
Addresses []wgcfg.CIDR
|
||||
LocalPort uint16 // used for debugging
|
||||
MachineStatus tailcfg.MachineStatus
|
||||
Peers []*tailcfg.Node // sorted by Node.ID
|
||||
DNS []wgcfg.IP
|
||||
DNSDomains []string
|
||||
DNS tailcfg.DNSConfig
|
||||
Hostinfo tailcfg.Hostinfo
|
||||
PacketFilter filter.Matches
|
||||
|
||||
@@ -131,12 +131,18 @@ func printPeerConcise(buf *strings.Builder, p *tailcfg.Node) {
|
||||
if strings.HasPrefix(derp, derpPrefix) {
|
||||
derp = "D" + derp[len(derpPrefix):]
|
||||
}
|
||||
var discoShort string
|
||||
if !p.DiscoKey.IsZero() {
|
||||
discoShort = p.DiscoKey.ShortString() + " "
|
||||
}
|
||||
|
||||
// Most of the time, aip is just one element, so format the
|
||||
// table to look good in that case. This will also make multi-
|
||||
// subnet nodes stand out visually.
|
||||
fmt.Fprintf(buf, " %v %-2v %-15v : %v\n",
|
||||
p.Key.ShortString(), derp,
|
||||
fmt.Fprintf(buf, " %v %s%-2v %-15v : %v\n",
|
||||
p.Key.ShortString(),
|
||||
discoShort,
|
||||
derp,
|
||||
strings.Join(aip, " "),
|
||||
strings.Join(ep, " "))
|
||||
}
|
||||
@@ -145,6 +151,7 @@ func printPeerConcise(buf *strings.Builder, p *tailcfg.Node) {
|
||||
func nodeConciseEqual(a, b *tailcfg.Node) bool {
|
||||
return a.Key == b.Key &&
|
||||
a.DERP == b.DERP &&
|
||||
a.DiscoKey == b.DiscoKey &&
|
||||
eqCIDRsIgnoreNil(a.AllowedIPs, b.AllowedIPs) &&
|
||||
eqStringsIgnoreNil(a.Endpoints, b.Endpoints)
|
||||
}
|
||||
@@ -215,33 +222,18 @@ const (
|
||||
HackDefaultRoute
|
||||
)
|
||||
|
||||
// TODO(bradfitz): UAPI seems to only be used by the old confnode and
|
||||
// pingnode; delete this when those are deleted/rewritten?
|
||||
func (nm *NetworkMap) UAPI(flags WGConfigFlags, dnsOverride []wgcfg.IP) string {
|
||||
wgcfg, err := nm.WGCfg(log.Printf, flags, dnsOverride)
|
||||
if err != nil {
|
||||
log.Fatalf("WGCfg() failed unexpectedly: %v", err)
|
||||
}
|
||||
s, err := wgcfg.ToUAPI()
|
||||
if err != nil {
|
||||
log.Fatalf("ToUAPI() failed unexpectedly: %v", err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// EndpointDiscoSuffix is appended to the hex representation of a peer's discovery key
|
||||
// and is then the sole wireguard endpoint for peers with a non-zero discovery key.
|
||||
// This form is then recognize by magicsock's CreateEndpoint.
|
||||
const EndpointDiscoSuffix = ".disco.tailscale:12345"
|
||||
|
||||
// WGCfg returns the NetworkMaps's Wireguard configuration.
|
||||
func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags, dnsOverride []wgcfg.IP) (*wgcfg.Config, error) {
|
||||
func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Config, error) {
|
||||
cfg := &wgcfg.Config{
|
||||
Name: "tailscale",
|
||||
PrivateKey: nm.PrivateKey,
|
||||
Addresses: nm.Addresses,
|
||||
ListenPort: nm.LocalPort,
|
||||
DNS: append([]wgcfg.IP(nil), dnsOverride...),
|
||||
Peers: make([]wgcfg.Peer, 0, len(nm.Peers)),
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
@@ -17,6 +19,15 @@ func testNodeKey(b byte) (ret tailcfg.NodeKey) {
|
||||
return
|
||||
}
|
||||
|
||||
func testDiscoKey(hexPrefix string) (ret tailcfg.DiscoKey) {
|
||||
b, err := hex.DecodeString(hexPrefix)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
copy(ret[:], b)
|
||||
return
|
||||
}
|
||||
|
||||
func TestNetworkMapConcise(t *testing.T) {
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
@@ -202,6 +213,62 @@ func TestConciseDiffFrom(t *testing.T) {
|
||||
},
|
||||
want: "- [AQEBA] D1 : 192.168.0.100:12 192.168.0.100:12354\n- [AwMDA] D3 : 192.168.0.100:12 192.168.0.100:12354\n",
|
||||
},
|
||||
{
|
||||
name: "peer_port_change",
|
||||
a: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
{
|
||||
ID: 2,
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:12", "1.1.1.1:1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
b: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
{
|
||||
ID: 2,
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:12", "1.1.1.1:2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "- [AgICA] D2 : 192.168.0.100:12 1.1.1.1:1 \n+ [AgICA] D2 : 192.168.0.100:12 1.1.1.1:2 \n",
|
||||
},
|
||||
{
|
||||
name: "disco_key_only_change",
|
||||
a: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
{
|
||||
ID: 2,
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:41641", "1.1.1.1:41641"},
|
||||
DiscoKey: testDiscoKey("f00f00f00f"),
|
||||
AllowedIPs: []wgcfg.CIDR{{IP: wgcfg.IPv4(100, 102, 103, 104), Mask: 32}},
|
||||
},
|
||||
},
|
||||
},
|
||||
b: &NetworkMap{
|
||||
NodeKey: testNodeKey(1),
|
||||
Peers: []*tailcfg.Node{
|
||||
{
|
||||
ID: 2,
|
||||
Key: testNodeKey(2),
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:41641", "1.1.1.1:41641"},
|
||||
DiscoKey: testDiscoKey("ba4ba4ba4b"),
|
||||
AllowedIPs: []wgcfg.CIDR{{IP: wgcfg.IPv4(100, 102, 103, 104), Mask: 32}},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "- [AgICA] d:f00f00f00f000000 D2 100.102.103.104 : 192.168.0.100:41641 1.1.1.1:41641\n+ [AgICA] d:ba4ba4ba4b000000 D2 100.102.103.104 : 192.168.0.100:41641 1.1.1.1:41641\n",
|
||||
},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var got string
|
||||
|
||||
42
derp/derp.go
42
derp/derp.go
@@ -39,14 +39,10 @@ const (
|
||||
keepAlive = 60 * time.Second
|
||||
)
|
||||
|
||||
// protocolVersion is bumped whenever there's a wire-incompatible change.
|
||||
// ProtocolVersion is bumped whenever there's a wire-incompatible change.
|
||||
// * version 1 (zero on wire): consistent box headers, in use by employee dev nodes a bit
|
||||
// * version 2: received packets have src addrs in frameRecvPacket at beginning
|
||||
const protocolVersion = 2
|
||||
|
||||
const (
|
||||
protocolSrcAddrs = 2 // protocol version at which client expects src addresses
|
||||
)
|
||||
const ProtocolVersion = 2
|
||||
|
||||
// frameType is the one byte frame type at the beginning of the frame
|
||||
// header. The second field is a big-endian uint32 describing the
|
||||
@@ -108,16 +104,31 @@ var bin = binary.BigEndian
|
||||
func writeUint32(bw *bufio.Writer, v uint32) error {
|
||||
var b [4]byte
|
||||
bin.PutUint32(b[:], v)
|
||||
_, err := bw.Write(b[:])
|
||||
return err
|
||||
// Writing a byte at a time is a bit silly,
|
||||
// but it causes b not to escape,
|
||||
// which more than pays for the silliness.
|
||||
for _, c := range &b {
|
||||
err := bw.WriteByte(c)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readUint32(br *bufio.Reader) (uint32, error) {
|
||||
b := make([]byte, 4)
|
||||
if _, err := io.ReadFull(br, b); err != nil {
|
||||
return 0, err
|
||||
var b [4]byte
|
||||
// Reading a byte at a time is a bit silly,
|
||||
// but it causes b not to escape,
|
||||
// which more than pays for the silliness.
|
||||
for i := range &b {
|
||||
c, err := br.ReadByte()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
b[i] = c
|
||||
}
|
||||
return bin.Uint32(b), nil
|
||||
return bin.Uint32(b[:]), nil
|
||||
}
|
||||
|
||||
func readFrameTypeHeader(br *bufio.Reader, wantType frameType) (frameLen uint32, err error) {
|
||||
@@ -194,13 +205,6 @@ func writeFrame(bw *bufio.Writer, t frameType, b []byte) error {
|
||||
return bw.Flush()
|
||||
}
|
||||
|
||||
func minInt(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func minUint32(a, b uint32) uint32 {
|
||||
if a < b {
|
||||
return a
|
||||
|
||||
@@ -21,14 +21,13 @@ import (
|
||||
|
||||
// Client is a DERP client.
|
||||
type Client struct {
|
||||
serverKey key.Public // of the DERP server; not a machine or node key
|
||||
privateKey key.Private
|
||||
publicKey key.Public // of privateKey
|
||||
protoVersion int // min of server+client
|
||||
logf logger.Logf
|
||||
nc Conn
|
||||
br *bufio.Reader
|
||||
meshKey string
|
||||
serverKey key.Public // of the DERP server; not a machine or node key
|
||||
privateKey key.Private
|
||||
publicKey key.Public // of privateKey
|
||||
logf logger.Logf
|
||||
nc Conn
|
||||
br *bufio.Reader
|
||||
meshKey string
|
||||
|
||||
wmu sync.Mutex // hold while writing to bw
|
||||
bw *bufio.Writer
|
||||
@@ -49,7 +48,8 @@ func (f clientOptFunc) update(o *clientOpt) { f(o) }
|
||||
|
||||
// clientOpt are the options passed to newClient.
|
||||
type clientOpt struct {
|
||||
MeshKey string
|
||||
MeshKey string
|
||||
ServerPub key.Public
|
||||
}
|
||||
|
||||
// MeshKey returns a ClientOpt to pass to the DERP server during connect to get
|
||||
@@ -58,6 +58,12 @@ type clientOpt struct {
|
||||
// An empty key means to not use a mesh key.
|
||||
func MeshKey(key string) ClientOpt { return clientOptFunc(func(o *clientOpt) { o.MeshKey = key }) }
|
||||
|
||||
// ServerPublicKey returns a ClientOpt to declare that the server's DERP public key is known.
|
||||
// If key is the zero value, the returned ClientOpt is a no-op.
|
||||
func ServerPublicKey(key key.Public) ClientOpt {
|
||||
return clientOptFunc(func(o *clientOpt) { o.ServerPub = key })
|
||||
}
|
||||
|
||||
func NewClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opts ...ClientOpt) (*Client, error) {
|
||||
var opt clientOpt
|
||||
for _, o := range opts {
|
||||
@@ -79,17 +85,16 @@ func newClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logg
|
||||
bw: brw.Writer,
|
||||
meshKey: opt.MeshKey,
|
||||
}
|
||||
if err := c.recvServerKey(); err != nil {
|
||||
return nil, fmt.Errorf("derp.Client: failed to receive server key: %v", err)
|
||||
if opt.ServerPub.IsZero() {
|
||||
if err := c.recvServerKey(); err != nil {
|
||||
return nil, fmt.Errorf("derp.Client: failed to receive server key: %v", err)
|
||||
}
|
||||
} else {
|
||||
c.serverKey = opt.ServerPub
|
||||
}
|
||||
if err := c.sendClientKey(); err != nil {
|
||||
return nil, fmt.Errorf("derp.Client: failed to send client key: %v", err)
|
||||
}
|
||||
info, err := c.recvServerInfo()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("derp.Client: failed to receive server info: %v", err)
|
||||
}
|
||||
c.protoVersion = minInt(protocolVersion, info.Version)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@@ -110,12 +115,9 @@ func (c *Client) recvServerKey() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) recvServerInfo() (*serverInfo, error) {
|
||||
fl, err := readFrameTypeHeader(c.br, frameServerInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
func (c *Client) parseServerInfo(b []byte) (*serverInfo, error) {
|
||||
const maxLength = nonceLen + maxInfoLen
|
||||
fl := len(b)
|
||||
if fl < nonceLen {
|
||||
return nil, fmt.Errorf("short serverInfo frame")
|
||||
}
|
||||
@@ -124,33 +126,27 @@ func (c *Client) recvServerInfo() (*serverInfo, error) {
|
||||
}
|
||||
// TODO: add a read-nonce-and-box helper
|
||||
var nonce [nonceLen]byte
|
||||
if _, err := io.ReadFull(c.br, nonce[:]); err != nil {
|
||||
return nil, fmt.Errorf("nonce: %v", err)
|
||||
}
|
||||
msgLen := fl - nonceLen
|
||||
msgbox := make([]byte, msgLen)
|
||||
if _, err := io.ReadFull(c.br, msgbox); err != nil {
|
||||
return nil, fmt.Errorf("msgbox: %v", err)
|
||||
}
|
||||
copy(nonce[:], b)
|
||||
msgbox := b[nonceLen:]
|
||||
msg, ok := box.Open(nil, msgbox, &nonce, c.serverKey.B32(), c.privateKey.B32())
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("msgbox: cannot open len=%d with server key %x", msgLen, c.serverKey[:])
|
||||
return nil, fmt.Errorf("failed to open naclbox from server key %x", c.serverKey[:])
|
||||
}
|
||||
info := new(serverInfo)
|
||||
if err := json.Unmarshal(msg, info); err != nil {
|
||||
return nil, fmt.Errorf("msg: %v", err)
|
||||
return nil, fmt.Errorf("invalid JSON: %v", err)
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
type clientInfo struct {
|
||||
Version int // `json:"version,omitempty"`
|
||||
Version int `json:"version,omitempty"`
|
||||
|
||||
// MeshKey optionally specifies a pre-shared key used by
|
||||
// trusted clients. It's required to subscribe to the
|
||||
// connection list & forward packets. It's empty for regular
|
||||
// users.
|
||||
MeshKey string // `json:"meshKey,omitempty"`
|
||||
MeshKey string `json:"meshKey,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) sendClientKey() error {
|
||||
@@ -159,7 +155,7 @@ func (c *Client) sendClientKey() error {
|
||||
return err
|
||||
}
|
||||
msg, err := json.Marshal(clientInfo{
|
||||
Version: protocolVersion,
|
||||
Version: ProtocolVersion,
|
||||
MeshKey: c.meshKey,
|
||||
})
|
||||
if err != nil {
|
||||
@@ -318,6 +314,11 @@ type PeerPresentMessage key.Public
|
||||
|
||||
func (PeerPresentMessage) msg() {}
|
||||
|
||||
// ServerInfoMessage is sent by the server upon first connect.
|
||||
type ServerInfoMessage struct{}
|
||||
|
||||
func (ServerInfoMessage) msg() {}
|
||||
|
||||
// Recv reads a message from the DERP server.
|
||||
//
|
||||
// The returned message may alias memory owned by the Client; it
|
||||
@@ -364,7 +365,7 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
// If the frame fits in our bufio.Reader buffer, just use it.
|
||||
// In practice it's 4KB (from derphttp.Client's bufio.NewReader(httpConn)) and
|
||||
// in practive, WireGuard packets (and thus DERP frames) are under 1.5KB.
|
||||
// So This is the common path.
|
||||
// So this is the common path.
|
||||
if int(n) <= c.br.Size() {
|
||||
b, err = c.br.Peek(int(n))
|
||||
c.peeked = int(n)
|
||||
@@ -382,6 +383,19 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
switch t {
|
||||
default:
|
||||
continue
|
||||
case frameServerInfo:
|
||||
// Server sends this at start-up. Currently unused.
|
||||
// Just has a JSON message saying "version: 2",
|
||||
// but the protocol seems extensible enough as-is without
|
||||
// needing to wait an RTT to discover the version at startup.
|
||||
// We'd prefer to give the connection to the client (magicsock)
|
||||
// to start writing as soon as possible.
|
||||
_, err := c.parseServerInfo(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid server info frame: %v", err)
|
||||
}
|
||||
// TODO: add the results of parseServerInfo to ServerInfoMessage if we ever need it.
|
||||
return ServerInfoMessage{}, nil
|
||||
case frameKeepAlive:
|
||||
// TODO: eventually we'll have server->client pings that
|
||||
// require ack pongs.
|
||||
@@ -406,16 +420,12 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
|
||||
case frameRecvPacket:
|
||||
var rp ReceivedPacket
|
||||
if c.protoVersion < protocolSrcAddrs {
|
||||
rp.Data = b[:n]
|
||||
} else {
|
||||
if n < keyLen {
|
||||
c.logf("[unexpected] dropping short packet from DERP server")
|
||||
continue
|
||||
}
|
||||
copy(rp.Source[:], b[:keyLen])
|
||||
rp.Data = b[keyLen:n]
|
||||
if n < keyLen {
|
||||
c.logf("[unexpected] dropping short packet from DERP server")
|
||||
continue
|
||||
}
|
||||
copy(rp.Source[:], b[:keyLen])
|
||||
rp.Data = b[keyLen:n]
|
||||
return rp, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,19 @@ package derp
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
crand "crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
@@ -24,16 +29,41 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"tailscale.com/disco"
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var debug, _ = strconv.ParseBool(os.Getenv("DERP_DEBUG_LOGS"))
|
||||
|
||||
// verboseDropKeys is the set of destination public keys that should
|
||||
// verbosely log whenever DERP drops a packet.
|
||||
var verboseDropKeys = map[key.Public]bool{}
|
||||
|
||||
func init() {
|
||||
keys := os.Getenv("TS_DEBUG_VERBOSE_DROPS")
|
||||
if keys == "" {
|
||||
return
|
||||
}
|
||||
for _, keyStr := range strings.Split(keys, ",") {
|
||||
k, err := key.NewPublicFromHexMem(mem.S(keyStr))
|
||||
if err != nil {
|
||||
log.Printf("ignoring invalid debug key %q: %v", keyStr, err)
|
||||
} else {
|
||||
verboseDropKeys[k] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
const (
|
||||
perClientSendQueueDepth = 32 // packets buffered for sending
|
||||
writeTimeout = 2 * time.Second
|
||||
@@ -52,23 +82,22 @@ type Server struct {
|
||||
// before failing when writing to a client.
|
||||
WriteTimeout time.Duration
|
||||
|
||||
// OnlyDisco controls whether, for tests, non-discovery packets
|
||||
// are dropped. This is used by magicsock tests to verify that
|
||||
// NAT traversal works (using DERP for out-of-band messaging)
|
||||
// but the packets themselves aren't going via DERP.
|
||||
OnlyDisco bool
|
||||
_ [pad32bit]byte
|
||||
|
||||
privateKey key.Private
|
||||
publicKey key.Public
|
||||
logf logger.Logf
|
||||
memSys0 uint64 // runtime.MemStats.Sys at start (or early-ish)
|
||||
meshKey string
|
||||
privateKey key.Private
|
||||
publicKey key.Public
|
||||
logf logger.Logf
|
||||
memSys0 uint64 // runtime.MemStats.Sys at start (or early-ish)
|
||||
meshKey string
|
||||
limitedLogf logger.Logf
|
||||
metaCert []byte // the encoded x509 cert to send after LetsEncrypt cert+intermediate
|
||||
|
||||
// Counters:
|
||||
_ [pad32bit]byte
|
||||
packetsSent, bytesSent expvar.Int
|
||||
packetsRecv, bytesRecv expvar.Int
|
||||
packetsRecvByKind metrics.LabelMap
|
||||
packetsRecvDisco *expvar.Int
|
||||
packetsRecvOther *expvar.Int
|
||||
_ [pad32bit]byte
|
||||
packetsDropped expvar.Int
|
||||
packetsDroppedReason metrics.LabelMap
|
||||
packetsDroppedUnknown *expvar.Int // unknown dst pubkey
|
||||
@@ -143,6 +172,8 @@ func NewServer(privateKey key.Private, logf logger.Logf) *Server {
|
||||
privateKey: privateKey,
|
||||
publicKey: privateKey.Public(),
|
||||
logf: logf,
|
||||
limitedLogf: logger.RateLimitedFn(logf, 30*time.Second, 5, 100),
|
||||
packetsRecvByKind: metrics.LabelMap{Label: "kind"},
|
||||
packetsDroppedReason: metrics.LabelMap{Label: "reason"},
|
||||
clients: map[key.Public]*sclient{},
|
||||
clientsEver: map[key.Public]bool{},
|
||||
@@ -152,6 +183,9 @@ func NewServer(privateKey key.Private, logf logger.Logf) *Server {
|
||||
watchers: map[*sclient]bool{},
|
||||
sentTo: map[key.Public]map[key.Public]int64{},
|
||||
}
|
||||
s.initMetacert()
|
||||
s.packetsRecvDisco = s.packetsRecvByKind.Get("disco")
|
||||
s.packetsRecvOther = s.packetsRecvByKind.Get("other")
|
||||
s.packetsDroppedUnknown = s.packetsDroppedReason.Get("unknown_dest")
|
||||
s.packetsDroppedFwdUnknown = s.packetsDroppedReason.Get("unknown_dest_on_fwd")
|
||||
s.packetsDroppedGone = s.packetsDroppedReason.Get("gone")
|
||||
@@ -243,6 +277,50 @@ func (s *Server) Accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string) {
|
||||
}
|
||||
}
|
||||
|
||||
// initMetacert initialized s.metaCert with a self-signed x509 cert
|
||||
// encoding this server's public key and protocol version. cmd/derper
|
||||
// then sends this after the Let's Encrypt leaf + intermediate certs
|
||||
// after the ServerHello (encrypted in TLS 1.3, not that it matters
|
||||
// much).
|
||||
//
|
||||
// Then the client can save a round trip getting that and can start
|
||||
// speaking DERP right away. (We don't use ALPN because that's sent in
|
||||
// the clear and we're being paranoid to not look too weird to any
|
||||
// middleboxes, given that DERP is an ultimate fallback path). But
|
||||
// since the post-ServerHello certs are encrypted we can have the
|
||||
// client also use them as a signal to be able to start speaking DERP
|
||||
// right away, starting with its identity proof, encrypted to the
|
||||
// server's public key.
|
||||
//
|
||||
// This RTT optimization fails where there's a corp-mandated
|
||||
// TLS proxy with corp-mandated root certs on employee machines and
|
||||
// and TLS proxy cleans up unnecessary certs. In that case we just fall
|
||||
// back to the extra RTT.
|
||||
func (s *Server) initMetacert() {
|
||||
pub, priv, err := ed25519.GenerateKey(crand.Reader)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
tmpl := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(ProtocolVersion),
|
||||
Subject: pkix.Name{
|
||||
CommonName: fmt.Sprintf("derpkey%x", s.publicKey[:]),
|
||||
},
|
||||
// Windows requires NotAfter and NotBefore set:
|
||||
NotAfter: time.Now().Add(30 * 24 * time.Hour),
|
||||
NotBefore: time.Now().Add(-30 * 24 * time.Hour),
|
||||
}
|
||||
cert, err := x509.CreateCertificate(crand.Reader, tmpl, tmpl, pub, priv)
|
||||
if err != nil {
|
||||
log.Fatalf("CreateCertificate: %v", err)
|
||||
}
|
||||
s.metaCert = cert
|
||||
}
|
||||
|
||||
// MetaCert returns the server metadata cert that can be sent by the
|
||||
// TLS server to let the client skip a round trip during start-up.
|
||||
func (s *Server) MetaCert() []byte { return s.metaCert }
|
||||
|
||||
// registerClient notes that client c is now authenticated and ready for packets.
|
||||
// If c's public key was already connected with a different connection, the prior one is closed.
|
||||
func (s *Server) registerClient(c *sclient) {
|
||||
@@ -559,11 +637,6 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
|
||||
return fmt.Errorf("client %x: recvPacket: %v", c.key, err)
|
||||
}
|
||||
|
||||
if s.OnlyDisco && !disco.LooksLikeDiscoWrapper(contents) {
|
||||
s.packetsDropped.Add(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
var fwd PacketForwarder
|
||||
s.mu.Lock()
|
||||
dst := s.clients[dstKey]
|
||||
@@ -592,10 +665,8 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
|
||||
}
|
||||
|
||||
p := pkt{
|
||||
bs: contents,
|
||||
}
|
||||
if dst.info.Version >= protocolSrcAddrs {
|
||||
p.src = c.key
|
||||
bs: contents,
|
||||
src: c.key,
|
||||
}
|
||||
return c.sendPkt(dst, p)
|
||||
}
|
||||
@@ -628,6 +699,12 @@ func (c *sclient) sendPkt(dst *sclient, p pkt) error {
|
||||
case <-dst.sendQueue:
|
||||
s.packetsDropped.Add(1)
|
||||
s.packetsDroppedQueueHead.Add(1)
|
||||
if verboseDropKeys[dstKey] {
|
||||
// Generate a full string including src and dst, so
|
||||
// the limiter kicks in once per src.
|
||||
msg := fmt.Sprintf("tail drop %s -> %s", p.src.ShortString(), dstKey.ShortString())
|
||||
c.s.limitedLogf(msg)
|
||||
}
|
||||
if debug {
|
||||
c.logf("dropping packet from client %x queue head", dstKey)
|
||||
}
|
||||
@@ -639,6 +716,12 @@ func (c *sclient) sendPkt(dst *sclient, p pkt) error {
|
||||
// this case to keep reader unblocked.
|
||||
s.packetsDropped.Add(1)
|
||||
s.packetsDroppedQueueTail.Add(1)
|
||||
if verboseDropKeys[dstKey] {
|
||||
// Generate a full string including src and dst, so
|
||||
// the limiter kicks in once per src.
|
||||
msg := fmt.Sprintf("head drop %s -> %s", p.src.ShortString(), dstKey.ShortString())
|
||||
c.s.limitedLogf(msg)
|
||||
}
|
||||
if debug {
|
||||
c.logf("dropping packet from client %x queue tail", dstKey)
|
||||
}
|
||||
@@ -680,7 +763,7 @@ func (s *Server) sendServerKey(bw *bufio.Writer) error {
|
||||
}
|
||||
|
||||
type serverInfo struct {
|
||||
Version int // `json:"version,omitempty"`
|
||||
Version int `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Server) sendServerInfo(bw *bufio.Writer, clientKey key.Public) error {
|
||||
@@ -688,7 +771,7 @@ func (s *Server) sendServerInfo(bw *bufio.Writer, clientKey key.Public) error {
|
||||
if _, err := crand.Read(nonce[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
msg, err := json.Marshal(serverInfo{Version: protocolVersion})
|
||||
msg, err := json.Marshal(serverInfo{Version: ProtocolVersion})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -750,7 +833,7 @@ func (s *Server) recvPacket(br *bufio.Reader, frameLen uint32) (dstKey key.Publi
|
||||
if frameLen < keyLen {
|
||||
return zpub, nil, errors.New("short send packet frame")
|
||||
}
|
||||
if _, err := io.ReadFull(br, dstKey[:]); err != nil {
|
||||
if err := readPublicKey(br, &dstKey); err != nil {
|
||||
return zpub, nil, err
|
||||
}
|
||||
packetLen := frameLen - keyLen
|
||||
@@ -763,6 +846,11 @@ func (s *Server) recvPacket(br *bufio.Reader, frameLen uint32) (dstKey key.Publi
|
||||
}
|
||||
s.packetsRecv.Add(1)
|
||||
s.bytesRecv.Add(int64(len(contents)))
|
||||
if disco.LooksLikeDiscoWrapper(contents) {
|
||||
s.packetsRecvDisco.Add(1)
|
||||
} else {
|
||||
s.packetsRecvOther.Add(1)
|
||||
}
|
||||
return dstKey, contents, nil
|
||||
}
|
||||
|
||||
@@ -892,11 +980,7 @@ func (c *sclient) sendLoop(ctx context.Context) error {
|
||||
}
|
||||
}()
|
||||
|
||||
jitterMs, err := crand.Int(crand.Reader, big.NewInt(5000))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
jitter := time.Duration(jitterMs.Int64()) * time.Millisecond
|
||||
jitter := time.Duration(rand.Intn(5000)) * time.Millisecond
|
||||
keepAliveTick := time.NewTicker(keepAlive + jitter)
|
||||
defer keepAliveTick.Stop()
|
||||
|
||||
@@ -1052,7 +1136,8 @@ func (c *sclient) sendPacket(srcKey key.Public, contents []byte) (err error) {
|
||||
return err
|
||||
}
|
||||
if withKey {
|
||||
if _, err = c.bw.Write(srcKey[:]); err != nil {
|
||||
err := writePublicKey(c.bw, &srcKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -1193,6 +1278,7 @@ func (s *Server) ExpVar() expvar.Var {
|
||||
m.Set("bytes_sent", &s.bytesSent)
|
||||
m.Set("packets_dropped", &s.packetsDropped)
|
||||
m.Set("counter_packets_dropped_reason", &s.packetsDroppedReason)
|
||||
m.Set("counter_packets_received_kind", &s.packetsRecvByKind)
|
||||
m.Set("packets_sent", &s.packetsSent)
|
||||
m.Set("packets_received", &s.packetsRecv)
|
||||
m.Set("unknown_frames", &s.unknownFrames)
|
||||
@@ -1204,6 +1290,9 @@ func (s *Server) ExpVar() expvar.Var {
|
||||
m.Set("multiforwarder_created", &s.multiForwarderCreated)
|
||||
m.Set("multiforwarder_deleted", &s.multiForwarderDeleted)
|
||||
m.Set("packet_forwarder_delete_other_value", &s.removePktForwardOther)
|
||||
var expvarVersion expvar.String
|
||||
expvarVersion.Set(version.LONG)
|
||||
m.Set("version", &expvarVersion)
|
||||
return m
|
||||
}
|
||||
|
||||
@@ -1245,3 +1334,34 @@ func (s *Server) ConsistencyCheck() error {
|
||||
}
|
||||
return errors.New(strings.Join(errs, ", "))
|
||||
}
|
||||
|
||||
// readPublicKey reads key from br.
|
||||
// It is ~4x slower than io.ReadFull(br, key),
|
||||
// but it prevents key from escaping and thus being allocated.
|
||||
// If io.ReadFull(br, key) does not cause key to escape, use that instead.
|
||||
func readPublicKey(br *bufio.Reader, key *key.Public) error {
|
||||
// Do io.ReadFull(br, key), but one byte at a time, to avoid allocation.
|
||||
for i := range key {
|
||||
b, err := br.ReadByte()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key[i] = b
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writePublicKey writes key to bw.
|
||||
// It is ~3x slower than bw.Write(key[:]),
|
||||
// but it prevents key from escaping and thus being allocated.
|
||||
// If bw.Write(key[:]) does not cause key to escape, use that instead.
|
||||
func writePublicKey(bw *bufio.Writer, key *key.Public) error {
|
||||
// Do bw.Write(key[:]), but one byte at a time to avoid allocation.
|
||||
for _, b := range key {
|
||||
err := bw.WriteByte(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,10 +8,14 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"expvar"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"reflect"
|
||||
"sync"
|
||||
@@ -23,14 +27,30 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func newPrivateKey(t *testing.T) (k key.Private) {
|
||||
t.Helper()
|
||||
func newPrivateKey(tb testing.TB) (k key.Private) {
|
||||
tb.Helper()
|
||||
if _, err := crand.Read(k[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
tb.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func TestClientInfoUnmarshal(t *testing.T) {
|
||||
for i, in := range []string{
|
||||
`{"Version":5,"MeshKey":"abc"}`,
|
||||
`{"version":5,"meshKey":"abc"}`,
|
||||
} {
|
||||
var got clientInfo
|
||||
if err := json.Unmarshal([]byte(in), &got); err != nil {
|
||||
t.Fatalf("[%d]: %v", i, err)
|
||||
}
|
||||
want := clientInfo{Version: 5, MeshKey: "abc"}
|
||||
if got != want {
|
||||
t.Errorf("[%d]: got %+v; want %+v", i, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendRecv(t *testing.T) {
|
||||
serverPrivateKey := newPrivateKey(t)
|
||||
s := NewServer(serverPrivateKey, t.Logf)
|
||||
@@ -79,6 +99,8 @@ func TestSendRecv(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("client %d: %v", i, err)
|
||||
}
|
||||
waitConnect(t, c)
|
||||
|
||||
clients = append(clients, c)
|
||||
recvChs = append(recvChs, make(chan []byte))
|
||||
t.Logf("Connected client %d.", i)
|
||||
@@ -118,7 +140,7 @@ func TestSendRecv(t *testing.T) {
|
||||
if got := string(b); got != want {
|
||||
t.Errorf("client1.Recv=%q, want %q", got, want)
|
||||
}
|
||||
case <-time.After(1 * time.Second):
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Errorf("client%d.Recv, got nothing, want %q", i, want)
|
||||
}
|
||||
}
|
||||
@@ -224,6 +246,7 @@ func TestSendFreeze(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
waitConnect(t, c)
|
||||
return c, c2
|
||||
}
|
||||
|
||||
@@ -502,7 +525,13 @@ func newTestClient(t *testing.T, ts *testServer, name string, newClient func(net
|
||||
func newRegularClient(t *testing.T, ts *testServer, name string) *testClient {
|
||||
return newTestClient(t, ts, name, func(nc net.Conn, priv key.Private, logf logger.Logf) (*Client, error) {
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(nc), bufio.NewWriter(nc))
|
||||
return NewClient(priv, nc, brw, logf)
|
||||
c, err := NewClient(priv, nc, brw, logf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
waitConnect(t, c)
|
||||
return c, nil
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
@@ -513,6 +542,7 @@ func newTestWatcher(t *testing.T, ts *testServer, name string) *testClient {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
waitConnect(t, c)
|
||||
if err := c.WatchConnectionChanges(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -742,3 +772,121 @@ func TestForwarderRegistration(t *testing.T) {
|
||||
u1: testFwd(3),
|
||||
})
|
||||
}
|
||||
|
||||
func TestMetaCert(t *testing.T) {
|
||||
priv := newPrivateKey(t)
|
||||
pub := priv.Public()
|
||||
s := NewServer(priv, t.Logf)
|
||||
|
||||
certBytes := s.MetaCert()
|
||||
cert, err := x509.ParseCertificate(certBytes)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if fmt.Sprint(cert.SerialNumber) != fmt.Sprint(ProtocolVersion) {
|
||||
t.Errorf("serial = %v; want %v", cert.SerialNumber, ProtocolVersion)
|
||||
}
|
||||
if g, w := cert.Subject.CommonName, fmt.Sprintf("derpkey%x", pub[:]); g != w {
|
||||
t.Errorf("CommonName = %q; want %q", g, w)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSendRecv(b *testing.B) {
|
||||
for _, size := range []int{10, 100, 1000, 10000} {
|
||||
b.Run(fmt.Sprintf("msgsize=%d", size), func(b *testing.B) { benchmarkSendRecvSize(b, size) })
|
||||
}
|
||||
}
|
||||
|
||||
func benchmarkSendRecvSize(b *testing.B, packetSize int) {
|
||||
serverPrivateKey := newPrivateKey(b)
|
||||
s := NewServer(serverPrivateKey, logger.Discard)
|
||||
defer s.Close()
|
||||
|
||||
key := newPrivateKey(b)
|
||||
clientKey := key.Public()
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
connOut, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer connOut.Close()
|
||||
|
||||
connIn, err := ln.Accept()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
defer connIn.Close()
|
||||
|
||||
brwServer := bufio.NewReadWriter(bufio.NewReader(connIn), bufio.NewWriter(connIn))
|
||||
go s.Accept(connIn, brwServer, "test-client")
|
||||
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(connOut), bufio.NewWriter(connOut))
|
||||
client, err := NewClient(key, connOut, brw, logger.Discard)
|
||||
if err != nil {
|
||||
b.Fatalf("client: %v", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
_, err := client.Recv()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
msg := make([]byte, packetSize)
|
||||
b.SetBytes(int64(len(msg)))
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := client.Send(clientKey, msg); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWriteUint32(b *testing.B) {
|
||||
w := bufio.NewWriter(ioutil.Discard)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
writeUint32(w, 0x0ba3a)
|
||||
}
|
||||
}
|
||||
|
||||
type nopRead struct{}
|
||||
|
||||
func (r nopRead) Read(p []byte) (int, error) {
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
var sinkU32 uint32
|
||||
|
||||
func BenchmarkReadUint32(b *testing.B) {
|
||||
r := bufio.NewReader(nopRead{})
|
||||
var err error
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
sinkU32, err = readUint32(r)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func waitConnect(t testing.TB, c *Client) {
|
||||
t.Helper()
|
||||
if m, err := c.Recv(); err != nil {
|
||||
t.Fatalf("client first Recv: %v", err)
|
||||
} else if v, ok := m.(ServerInfoMessage); !ok {
|
||||
t.Fatalf("client first Recv was unexpected type %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,21 +14,27 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -252,14 +258,41 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
}
|
||||
}()
|
||||
|
||||
var httpConn net.Conn // a TCP conn or a TLS conn; what we speak HTTP to
|
||||
var httpConn net.Conn // a TCP conn or a TLS conn; what we speak HTTP to
|
||||
var serverPub key.Public // or zero if unknown (if not using TLS or TLS middlebox eats it)
|
||||
var serverProtoVersion int
|
||||
if c.useHTTPS() {
|
||||
httpConn = c.tlsClient(tcpConn, node)
|
||||
tlsConn := c.tlsClient(tcpConn, node)
|
||||
httpConn = tlsConn
|
||||
|
||||
// Force a handshake now (instead of waiting for it to
|
||||
// be done implicitly on read/write) so we can check
|
||||
// the ConnectionState.
|
||||
if err := tlsConn.Handshake(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// We expect to be using TLS 1.3 to our own servers, and only
|
||||
// starting at TLS 1.3 are the server's returned certificates
|
||||
// encrypted, so only look for and use our "meta cert" if we're
|
||||
// using TLS 1.3. If we're not using TLS 1.3, it might be a user
|
||||
// running cmd/derper themselves with a different configuration,
|
||||
// in which case we can avoid this fast-start optimization.
|
||||
// (If a corporate proxy is MITM'ing TLS 1.3 connections with
|
||||
// corp-mandated TLS root certs than all bets are off anyway.)
|
||||
// Note that we're not specifically concerned about TLS downgrade
|
||||
// attacks. TLS handles that fine:
|
||||
// https://blog.gypsyengineer.com/en/security/how-does-tls-1-3-protect-against-downgrade-attacks.html
|
||||
connState := tlsConn.ConnectionState()
|
||||
if connState.Version >= tls.VersionTLS13 {
|
||||
serverPub, serverProtoVersion = parseMetaCert(connState.PeerCertificates)
|
||||
}
|
||||
} else {
|
||||
httpConn = tcpConn
|
||||
}
|
||||
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(httpConn), bufio.NewWriter(httpConn))
|
||||
var derpClient *derp.Client
|
||||
|
||||
req, err := http.NewRequest("GET", c.urlString(node), nil)
|
||||
if err != nil {
|
||||
@@ -268,24 +301,39 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
req.Header.Set("Upgrade", "DERP")
|
||||
req.Header.Set("Connection", "Upgrade")
|
||||
|
||||
if err := req.Write(brw); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := brw.Flush(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if !serverPub.IsZero() && serverProtoVersion != 0 {
|
||||
// parseMetaCert found the server's public key (no TLS
|
||||
// middlebox was in the way), so skip the HTTP upgrade
|
||||
// exchange. See https://github.com/tailscale/tailscale/issues/693
|
||||
// for an overview. We still send the HTTP request
|
||||
// just to get routed into the server's HTTP Handler so it
|
||||
// can Hijack the request, but we signal with a special header
|
||||
// that we don't want to deal with its HTTP response.
|
||||
req.Header.Set(fastStartHeader, "1") // suppresses the server's HTTP response
|
||||
if err := req.Write(brw); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
// No need to flush the HTTP request. the derp.Client's initial
|
||||
// client auth frame will flush it.
|
||||
} else {
|
||||
if err := req.Write(brw); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := brw.Flush(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
resp, err := http.ReadResponse(brw.Reader, req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
resp, err := http.ReadResponse(brw.Reader, req)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
return nil, 0, fmt.Errorf("GET failed: %v: %s", err, b)
|
||||
}
|
||||
}
|
||||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
return nil, 0, fmt.Errorf("GET failed: %v: %s", err, b)
|
||||
}
|
||||
|
||||
derpClient, err := derp.NewClient(c.privateKey, httpConn, brw, c.logf, derp.MeshKey(c.MeshKey))
|
||||
derpClient, err = derp.NewClient(c.privateKey, httpConn, brw, c.logf, derp.MeshKey(c.MeshKey), derp.ServerPublicKey(serverPub))
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -364,6 +412,14 @@ func (c *Client) tlsClient(nc net.Conn, node *tailcfg.DERPNode) *tls.Conn {
|
||||
tlsdial.SetConfigExpectedCert(tlsConf, node.CertName)
|
||||
}
|
||||
}
|
||||
if n := os.Getenv("SSLKEYLOGFILE"); n != "" {
|
||||
f, err := os.OpenFile(n, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("WARNING: writing to SSLKEYLOGFILE %v", n)
|
||||
tlsConf.KeyLogWriter = f
|
||||
}
|
||||
return tls.Client(nc, tlsConf)
|
||||
}
|
||||
|
||||
@@ -420,6 +476,19 @@ const dialNodeTimeout = 1500 * time.Millisecond
|
||||
// TODO(bradfitz): longer if no options remain perhaps? ... Or longer
|
||||
// overall but have dialRegion start overlapping races?
|
||||
func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, error) {
|
||||
// First see if we need to use an HTTP proxy.
|
||||
proxyReq := &http.Request{
|
||||
Method: "GET", // doesn't really matter
|
||||
URL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: c.tlsServerName(n),
|
||||
Path: "/", // unused
|
||||
},
|
||||
}
|
||||
if proxyURL, err := tshttpproxy.ProxyFromEnvironment(proxyReq); err == nil && proxyURL != nil {
|
||||
return c.dialNodeUsingProxy(ctx, n, proxyURL)
|
||||
}
|
||||
|
||||
type res struct {
|
||||
c net.Conn
|
||||
err error
|
||||
@@ -480,6 +549,77 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
|
||||
}
|
||||
}
|
||||
|
||||
func firstStr(a, b string) string {
|
||||
if a != "" {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// dialNodeUsingProxy connects to n using a CONNECT to the HTTP(s) proxy in proxyURL.
|
||||
func (c *Client) dialNodeUsingProxy(ctx context.Context, n *tailcfg.DERPNode, proxyURL *url.URL) (proxyConn net.Conn, err error) {
|
||||
pu := proxyURL
|
||||
if pu.Scheme == "https" {
|
||||
var d tls.Dialer
|
||||
proxyConn, err = d.DialContext(ctx, "tcp", net.JoinHostPort(pu.Hostname(), firstStr(pu.Port(), "443")))
|
||||
} else {
|
||||
var d net.Dialer
|
||||
proxyConn, err = d.DialContext(ctx, "tcp", net.JoinHostPort(pu.Hostname(), firstStr(pu.Port(), "80")))
|
||||
}
|
||||
defer func() {
|
||||
if err != nil && proxyConn != nil {
|
||||
// In a goroutine in case it's a *tls.Conn (that can block on Close)
|
||||
// TODO(bradfitz): track the underlying tcp.Conn and just close that instead.
|
||||
go proxyConn.Close()
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
go func() {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case <-ctx.Done():
|
||||
proxyConn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
target := net.JoinHostPort(n.HostName, "443")
|
||||
|
||||
var authHeader string
|
||||
if v, err := tshttpproxy.GetAuthHeader(pu); err != nil {
|
||||
c.logf("derphttp: error getting proxy auth header for %v: %v", proxyURL, err)
|
||||
} else if v != "" {
|
||||
authHeader = fmt.Sprintf("Proxy-Authorization: %s\r\n", v)
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintf(proxyConn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n%s\r\n", target, pu.Hostname(), authHeader); err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
br := bufio.NewReader(proxyConn)
|
||||
res, err := http.ReadResponse(br, nil)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
c.logf("derphttp: CONNECT dial to %s: %v", target, err)
|
||||
return nil, err
|
||||
}
|
||||
c.logf("derphttp: CONNECT dial to %s: %v", target, res.Status)
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("invalid response status from HTTP proxy %s on CONNECT to %s: %v", pu, target, res.Status)
|
||||
}
|
||||
return proxyConn, nil
|
||||
}
|
||||
|
||||
func (c *Client) Send(dstKey key.Public, b []byte) error {
|
||||
client, _, err := c.connect(context.TODO(), "derphttp.Client.Send")
|
||||
if err != nil {
|
||||
@@ -614,3 +754,16 @@ func (c *Client) closeForReconnect(brokenClient *derp.Client) {
|
||||
}
|
||||
|
||||
var ErrClientClosed = errors.New("derphttp.Client closed")
|
||||
|
||||
func parseMetaCert(certs []*x509.Certificate) (serverPub key.Public, serverProtoVersion int) {
|
||||
for _, cert := range certs {
|
||||
if cn := cert.Subject.CommonName; strings.HasPrefix(cn, "derpkey") {
|
||||
var err error
|
||||
serverPub, err = key.NewPublicFromHexMem(mem.S(strings.TrimPrefix(cn, "derpkey")))
|
||||
if err == nil && cert.SerialNumber.BitLen() <= 8 { // supports up to version 255
|
||||
return serverPub, int(cert.SerialNumber.Int64())
|
||||
}
|
||||
}
|
||||
}
|
||||
return key.Public{}, 0
|
||||
}
|
||||
|
||||
@@ -5,33 +5,51 @@
|
||||
package derphttp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"tailscale.com/derp"
|
||||
)
|
||||
|
||||
// fastStartHeader is the header (with value "1") that signals to the HTTP
|
||||
// server that the DERP HTTP client does not want the HTTP 101 response
|
||||
// headers and it will begin writing & reading the DERP protocol immediately
|
||||
// following its HTTP request.
|
||||
const fastStartHeader = "Derp-Fast-Start"
|
||||
|
||||
func Handler(s *derp.Server) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if p := r.Header.Get("Upgrade"); p != "WebSocket" && p != "DERP" {
|
||||
http.Error(w, "DERP requires connection upgrade", http.StatusUpgradeRequired)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Upgrade", "DERP")
|
||||
w.Header().Set("Connection", "Upgrade")
|
||||
w.WriteHeader(http.StatusSwitchingProtocols)
|
||||
fastStart := r.Header.Get(fastStartHeader) == "1"
|
||||
|
||||
h, ok := w.(http.Hijacker)
|
||||
if !ok {
|
||||
http.Error(w, "HTTP does not support general TCP support", 500)
|
||||
return
|
||||
}
|
||||
|
||||
netConn, conn, err := h.Hijack()
|
||||
if err != nil {
|
||||
log.Printf("Hijack failed: %v", err)
|
||||
http.Error(w, "HTTP does not support general TCP support", 500)
|
||||
return
|
||||
}
|
||||
|
||||
if !fastStart {
|
||||
pubKey := s.PublicKey()
|
||||
fmt.Fprintf(conn, "HTTP/1.1 101 Switching Protocols\r\n"+
|
||||
"Upgrade: DERP\r\n"+
|
||||
"Connection: Upgrade\r\n"+
|
||||
"Derp-Version: %v\r\n"+
|
||||
"Derp-Public-Key: %x\r\n\r\n",
|
||||
derp.ProtocolVersion,
|
||||
pubKey[:])
|
||||
}
|
||||
|
||||
s.Accept(netConn, conn, netConn.RemoteAddr().String())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ package derphttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -19,22 +18,15 @@ import (
|
||||
)
|
||||
|
||||
func TestSendRecv(t *testing.T) {
|
||||
serverPrivateKey := key.NewPrivate()
|
||||
|
||||
const numClients = 3
|
||||
var serverPrivateKey key.Private
|
||||
if _, err := crand.Read(serverPrivateKey[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var clientPrivateKeys []key.Private
|
||||
for i := 0; i < numClients; i++ {
|
||||
var key key.Private
|
||||
if _, err := crand.Read(key[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
clientPrivateKeys = append(clientPrivateKeys, key)
|
||||
}
|
||||
var clientKeys []key.Public
|
||||
for _, privKey := range clientPrivateKeys {
|
||||
clientKeys = append(clientKeys, privKey.Public())
|
||||
for i := 0; i < numClients; i++ {
|
||||
priv := key.NewPrivate()
|
||||
clientPrivateKeys = append(clientPrivateKeys, priv)
|
||||
clientKeys = append(clientKeys, priv.Public())
|
||||
}
|
||||
|
||||
s := derp.NewServer(serverPrivateKey, t.Logf)
|
||||
@@ -81,6 +73,7 @@ func TestSendRecv(t *testing.T) {
|
||||
if err := c.Connect(context.Background()); err != nil {
|
||||
t.Fatalf("client %d Connect: %v", i, err)
|
||||
}
|
||||
waitConnect(t, c)
|
||||
clients = append(clients, c)
|
||||
recvChs = append(recvChs, make(chan []byte))
|
||||
|
||||
@@ -95,6 +88,11 @@ func TestSendRecv(t *testing.T) {
|
||||
}
|
||||
m, err := c.Recv()
|
||||
if err != nil {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
default:
|
||||
}
|
||||
t.Logf("client%d: %v", i, err)
|
||||
break
|
||||
}
|
||||
@@ -118,7 +116,7 @@ func TestSendRecv(t *testing.T) {
|
||||
if got := string(b); got != want {
|
||||
t.Errorf("client1.Recv=%q, want %q", got, want)
|
||||
}
|
||||
case <-time.After(1 * time.Second):
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Errorf("client%d.Recv, got nothing, want %q", i, want)
|
||||
}
|
||||
}
|
||||
@@ -146,5 +144,13 @@ func TestSendRecv(t *testing.T) {
|
||||
recv(2, string(msg2))
|
||||
recvNothing(0)
|
||||
recvNothing(1)
|
||||
|
||||
}
|
||||
|
||||
func waitConnect(t testing.TB, c *Client) {
|
||||
t.Helper()
|
||||
if m, err := c.Recv(); err != nil {
|
||||
t.Fatalf("client first Recv: %v", err)
|
||||
} else if v, ok := m.(derp.ServerInfoMessage); !ok {
|
||||
t.Fatalf("client first Recv was unexpected type %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,13 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package derpmap contains information about Tailscale.com's production DERP nodes.
|
||||
//
|
||||
// This package is only used by the "tailscale netcheck" command for debugging.
|
||||
// In normal operation the Tailscale nodes get this sent to them from the control
|
||||
// server.
|
||||
//
|
||||
// TODO: remove this package and make "tailscale netcheck" get the
|
||||
// list from the control server too.
|
||||
package derpmap
|
||||
|
||||
import (
|
||||
@@ -21,9 +28,10 @@ func derpNode(suffix, v4, v6 string) *tailcfg.DERPNode {
|
||||
}
|
||||
}
|
||||
|
||||
func derpRegion(id int, code string, nodes ...*tailcfg.DERPNode) *tailcfg.DERPRegion {
|
||||
func derpRegion(id int, code, name string, nodes ...*tailcfg.DERPNode) *tailcfg.DERPRegion {
|
||||
region := &tailcfg.DERPRegion{
|
||||
RegionID: id,
|
||||
RegionName: name,
|
||||
RegionCode: code,
|
||||
Nodes: nodes,
|
||||
}
|
||||
@@ -45,21 +53,36 @@ func derpRegion(id int, code string, nodes ...*tailcfg.DERPNode) *tailcfg.DERPRe
|
||||
func Prod() *tailcfg.DERPMap {
|
||||
return &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: derpRegion(1, "nyc",
|
||||
1: derpRegion(1, "nyc", "New York City",
|
||||
derpNode("a", "159.89.225.99", "2604:a880:400:d1::828:b001"),
|
||||
),
|
||||
2: derpRegion(2, "sfo",
|
||||
2: derpRegion(2, "sfo", "San Francisco",
|
||||
derpNode("a", "167.172.206.31", "2604:a880:2:d1::c5:7001"),
|
||||
),
|
||||
3: derpRegion(3, "sin",
|
||||
3: derpRegion(3, "sin", "Singapore",
|
||||
derpNode("a", "68.183.179.66", "2400:6180:0:d1::67d:8001"),
|
||||
),
|
||||
4: derpRegion(4, "fra",
|
||||
4: derpRegion(4, "fra", "Frankfurt",
|
||||
derpNode("a", "167.172.182.26", "2a03:b0c0:3:e0::36e:9001"),
|
||||
),
|
||||
5: derpRegion(5, "syd",
|
||||
5: derpRegion(5, "syd", "Sydney",
|
||||
derpNode("a", "103.43.75.49", "2001:19f0:5801:10b7:5400:2ff:feaa:284c"),
|
||||
),
|
||||
6: derpRegion(6, "blr", "Bangalore",
|
||||
derpNode("a", "68.183.90.120", "2400:6180:100:d0::982:d001"),
|
||||
),
|
||||
7: derpRegion(7, "tok", "Tokyo",
|
||||
derpNode("a", "167.179.89.145", "2401:c080:1000:467f:5400:2ff:feee:22aa"),
|
||||
),
|
||||
8: derpRegion(8, "lhr", "London",
|
||||
derpNode("a", "167.71.139.179", "2a03:b0c0:1:e0::3cc:e001"),
|
||||
),
|
||||
9: derpRegion(9, "dfw", "Dallas",
|
||||
derpNode("a", "207.148.3.137", "2001:19f0:6401:1d9c:5400:2ff:feef:bb82"),
|
||||
),
|
||||
10: derpRegion(10, "sea", "Seattle",
|
||||
derpNode("a", "137.220.36.168", "2001:19f0:8001:2d9:5400:2ff:feef:bbb1"),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
20
go.mod
20
go.mod
@@ -3,6 +3,7 @@ module tailscale.com
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
|
||||
github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29
|
||||
github.com/coreos/go-iptables v0.4.5
|
||||
@@ -11,28 +12,29 @@ require (
|
||||
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
|
||||
github.com/google/go-cmp v0.4.0
|
||||
github.com/google/go-cmp v0.5.0
|
||||
github.com/goreleaser/nfpm v1.1.10
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4
|
||||
github.com/klauspost/compress v1.10.10
|
||||
github.com/kr/pty v1.1.1
|
||||
github.com/kr/pty v1.1.4-0.20190131011033-7dc38fb350b1
|
||||
github.com/mdlayher/netlink v1.1.0
|
||||
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/winipcfg-go v0.0.0-20200413171540-609dcf2df55f
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200724155040-d554a2a5e7e1
|
||||
github.com/tailscale/wireguard-go v0.0.0-20200902185615-1997cf6f9fe4
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
go4.org/mem v0.0.0-20200706164138-185c595c3ecc
|
||||
golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e
|
||||
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-20200707200213-416e8f4faf8a
|
||||
gvisor.dev/gvisor v0.0.0-20200903175658-a842a338ecd9
|
||||
honnef.co/go/tools v0.0.1-2020.1.4
|
||||
inet.af/netaddr v0.0.0-20200718043157-99321d6ad24c
|
||||
inet.af/netaddr v0.0.0-20200810144936-56928fe48a98
|
||||
rsc.io/goversion v1.2.0
|
||||
)
|
||||
|
||||
310
go.sum
310
go.sum
@@ -1,14 +1,32 @@
|
||||
bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.52.1-0.20200122224058-0482b626c726/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
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/Microsoft/go-winio v0.4.15-0.20190919025122-fc70bd9a86b5/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
|
||||
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=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
|
||||
github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29 h1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4=
|
||||
@@ -19,44 +37,139 @@ github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e h1:hHg27A0RSSp2Om9lubZpiMgVbvn39bsUmW9U5h0twqc=
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A=
|
||||
github.com/cenkalti/backoff v1.1.1-0.20190506075156-2146c9339422 h1:8eZxmY1yvxGHzdzTEhI09npjMVGzNAdrqzruTX6jcK4=
|
||||
github.com/cenkalti/backoff v1.1.1-0.20190506075156-2146c9339422/go.mod h1:b6Nc7NRH5C4aCISLry0tLnTjcuTEvoiqcWDdsU0sOGM=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/containerd/cgroups v0.0.0-20181219155423-39b18af02c41 h1:5yg0k8gqOssNLsjjCtXIADoPbAtUtQZJfC8hQ4r2oFY=
|
||||
github.com/containerd/cgroups v0.0.0-20181219155423-39b18af02c41/go.mod h1:X9rLEHIqSf/wfK8NsPqxJmeZgW4pcfzdXITDrUSJ6uI=
|
||||
github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e h1:GdiIYd8ZDOrT++e1NjhSD4rGt9zaJukHm4rt5F4mRQc=
|
||||
github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE=
|
||||
github.com/containerd/containerd v1.3.4 h1:3o0smo5SKY7H6AJCmJhsnCjR2/V2T8VmiHt7seN2/kI=
|
||||
github.com/containerd/containerd v1.3.4/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe/go.mod h1:cECdGN1O8G9bgKTlLhuPJimka6Xb/Gg7vYzCTNVxhvo=
|
||||
github.com/containerd/fifo v0.0.0-20191213151349-ff969a566b00 h1:lsjC5ENBl+Zgf38+B0ymougXFp0BaubeIVETltYZTQw=
|
||||
github.com/containerd/fifo v0.0.0-20191213151349-ff969a566b00/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=
|
||||
github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328 h1:PRTagVMbJcCezLcHXe8UJvR1oBzp2lG3CEumeFOLOds=
|
||||
github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g=
|
||||
github.com/containerd/ttrpc v0.0.0-20200121165050-0be804eadb15 h1:+jgiLE5QylzgADj0Yldb4id1NQNRrDOROj7KDvY9PEc=
|
||||
github.com/containerd/ttrpc v0.0.0-20200121165050-0be804eadb15/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=
|
||||
github.com/containerd/typeurl v0.0.0-20200205145503-b45ef1f1f737 h1:HovfQDS/K3Mr7eyS0QJLxE1CbVUhjZCl6g3OhFJgP1o=
|
||||
github.com/containerd/typeurl v0.0.0-20200205145503-b45ef1f1f737/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg=
|
||||
github.com/coreos/go-iptables v0.4.5 h1:DpHb9vJrZQEFMcVLFKAAGMUVX0XoRC0ptCthinRYm38=
|
||||
github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v1.4.2-0.20191028175130-9e7d5ac5ea55/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dpjacques/clockwork v0.1.1-0.20190114191937-d864eecc357b/go.mod h1:D8mP2A8vVT2GkXqPorSBmhnshhkFBYgzhA90KmJt25Y=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
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-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
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 v0.0.0-20190422162347-ade71ed3457e h1:BWhy2j3IXJhjCbC68FptL43tDKIq8FladmaTs3Xs7Z8=
|
||||
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
|
||||
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
|
||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/gofrs/flock v0.6.1-0.20180915234121-886344bea079 h1:JFTFz3HZTGmgMz4E1TabNBNJljROSYgja1b4l50FNVs=
|
||||
github.com/gofrs/flock v0.6.1-0.20180915234121-886344bea079/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=
|
||||
github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-github/v28 v28.1.2-0.20191108005307-e555eab49ce8/go.mod h1:g82e6OHbJ0WYrYeOrid1MMfHAtqjxBz+N74tfAt9KrQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0 h1:BW6OvS3kpT5UEPbCZ+KyX/OB4Ks9/MNMhWjqPPkZxsE=
|
||||
github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg=
|
||||
github.com/google/subcommands v1.0.2-0.20190508160503-636abe8753b8 h1:8nlgEAjIalk6uj/CGKCdOO8CQqTeysvcW4RFZ6HbkGM=
|
||||
github.com/google/subcommands v1.0.2-0.20190508160503-636abe8753b8/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/goreleaser/nfpm v1.1.10 h1:0nwzKUJTcygNxTzVKq2Dh9wpVP1W2biUH6SNKmoxR3w=
|
||||
github.com/goreleaser/nfpm v1.1.10/go.mod h1:oOcoGRVwvKIODz57NUfiRwFWGfn00NXdgnn6MrYtO5k=
|
||||
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
|
||||
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4 h1:nwOc1YaOrYJ37sEBrtWZrdqzK22hiJs3GpDmP3sR2Yw=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I=
|
||||
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
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/pty v1.1.4-0.20190131011033-7dc38fb350b1 h1:zc0R6cOw98cMengLA0fvU55mqbnN7sd/tBMLzSejp+M=
|
||||
github.com/kr/pty v1.1.4-0.20190131011033-7dc38fb350b1/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/mattn/go-zglob v0.0.1 h1:xsEx/XUoVlI6yXjqBK062zYhRTZltCNmYPx6v+8DNaY=
|
||||
@@ -67,105 +180,282 @@ github.com/mdlayher/netlink v1.1.0 h1:mpdLgm+brq10nI9zM1BpX1kpDbh3NLl3RSnVq6ZSkf
|
||||
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
|
||||
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
|
||||
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/mohae/deepcopy v0.0.0-20170308212314-bb9b5e7adda9 h1:Sha2bQdoWE5YQPTlJOL31rmce94/tYi113SlFo1xQ2c=
|
||||
github.com/mohae/deepcopy v0.0.0-20170308212314-bb9b5e7adda9/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y=
|
||||
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||
github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/runtime-spec v1.0.2-0.20181111125026-1722abf79c2f h1:Pyp2f/uuhJIcUgnIeZaAbwOcyNz8TBlEe6mPpC8kXq8=
|
||||
github.com/opencontainers/runtime-spec v1.0.2-0.20181111125026-1722abf79c2f/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3 h1:YtFkrqsMEj7YqpIhRteVxJxCeC3jJBieuLr0d4C4rSA=
|
||||
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
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/errors v0.8.1-0.20171018195549-f15c970de5b7/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
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/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2 h1:b6uOv7YOFK0TYG7HtkIgExQo+2RdLuwRft63jn2HWj8=
|
||||
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
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/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=
|
||||
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
|
||||
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
|
||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
|
||||
github.com/vishvananda/netlink v1.0.1-0.20190930145447-2ec5bdc52b86 h1:7SWt9pGCMaw+N1ZhRsaLKaYNviFhxambdoaoYlDqz1w=
|
||||
github.com/vishvananda/netlink v1.0.1-0.20190930145447-2ec5bdc52b86/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
|
||||
github.com/vishvananda/netns v0.0.0-20200520041808-52d707b772fe h1:mjAZxE1nh8yvuwhGHpdDqdhtNu2dgbpk93TwoXuk5so=
|
||||
github.com/vishvananda/netns v0.0.0-20200520041808-52d707b772fe/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
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.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
go.uber.org/multierr v1.2.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
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-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
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-20190605123033-f99c8df09eb5/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-20200317142112-1b76d66859c6 h1:TjszyFsQsyZNHwdVdZ5m7bjmreu0znc2kRYsEml9/Ww=
|
||||
golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
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/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
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-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/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-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
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-20200114155413-6afb5195e5aa/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-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
|
||||
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/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-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
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-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/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-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/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-20200501052902-10377860bb8e h1:hq86ru83GdWTlfQFZGO4nZJTU4Bs2wfHl8oFHRaXsfc=
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/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-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.1-0.20180807135948-17ff2d5776d2/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/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
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-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d h1:/iIZNFGxc/a7C3yWjGcnboV+Tkc7mxr+p6fDztwoxuM=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200707200213-416e8f4faf8a h1:YAl/dx/kLsMMIWGqfhFHW9ckqGhmq7Ki0dfoKAgvFTE=
|
||||
golang.org/x/tools v0.0.0-20200707200213-416e8f4faf8a/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
|
||||
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=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24 h1:wDju+RU97qa0FZT0QnZDg9Uc2dH0Ql513kFvHocz+WM=
|
||||
google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.29.0 h1:2pJjwYOdkZ9HlN4sWRYBg9ttH5bCOlsueaM+b/oYjwo=
|
||||
google.golang.org/grpc v1.29.0/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
gvisor.dev/gvisor v0.0.0-20200903175658-a842a338ecd9 h1:w2M4YLwjhea3cX8qp7pOvMg97svEgPzpAcGZHb7BVwc=
|
||||
gvisor.dev/gvisor v0.0.0-20200903175658-a842a338ecd9/go.mod h1:n17AP1iZxpRCzqyHLJdpa2e1SzY1rNZ9tt3/MePxlGs=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK8=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
inet.af/netaddr v0.0.0-20200718043157-99321d6ad24c h1:si3Owrfem175Ry6gKqnh59eOXxDojyBTIHxUKuvK/Eo=
|
||||
inet.af/netaddr v0.0.0-20200718043157-99321d6ad24c/go.mod h1:qqYzz/2whtrbWJvt+DNWQyvekNN4ePQZcg2xc2/Yjww=
|
||||
inet.af/netaddr v0.0.0-20200810144936-56928fe48a98 h1:bWyWDZP0l6VnQ1TDKf6yNwuiEDV6Q3q1Mv34m+lzT1I=
|
||||
inet.af/netaddr v0.0.0-20200810144936-56928fe48a98/go.mod h1:qqYzz/2whtrbWJvt+DNWQyvekNN4ePQZcg2xc2/Yjww=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w=
|
||||
rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=
|
||||
|
||||
@@ -18,13 +18,23 @@ import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func Hash(v interface{}) string {
|
||||
func Hash(v ...interface{}) string {
|
||||
h := sha256.New()
|
||||
Print(h, v)
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
func Print(w io.Writer, v interface{}) {
|
||||
// UpdateHash sets last to the hash of v and reports whether its value changed.
|
||||
func UpdateHash(last *string, v ...interface{}) (changed bool) {
|
||||
sig := Hash(v)
|
||||
if *last != sig {
|
||||
*last = sig
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func Print(w io.Writer, v ...interface{}) {
|
||||
print(w, reflect.ValueOf(v), make(map[uintptr]bool))
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/router/dns"
|
||||
)
|
||||
|
||||
func TestDeepPrint(t *testing.T) {
|
||||
@@ -50,7 +51,7 @@ func getVal() []interface{} {
|
||||
},
|
||||
},
|
||||
&router.Config{
|
||||
DNSConfig: router.DNSConfig{
|
||||
DNS: dns.Config{
|
||||
Nameservers: []netaddr.IP{netaddr.IPv4(8, 8, 8, 8)},
|
||||
Domains: []string{"tailscale.net"},
|
||||
},
|
||||
|
||||
@@ -62,6 +62,7 @@ type Notify struct {
|
||||
Status *ipnstate.Status // full status
|
||||
BrowseToURL *string // UI should open a browser right now
|
||||
BackendLogID *string // public logtail id used by backend
|
||||
PingResult *ipnstate.PingResult
|
||||
|
||||
// LocalTCPPort, if non-nil, informs the UI frontend which
|
||||
// (non-zero) localhost TCP port it's listening on.
|
||||
@@ -143,6 +144,9 @@ type Backend interface {
|
||||
// WantRunning. This may cause the wireguard engine to
|
||||
// reconfigure or stop.
|
||||
SetPrefs(*Prefs)
|
||||
// SetWantRunning is like SetPrefs but sets only the
|
||||
// WantRunning field.
|
||||
SetWantRunning(wantRunning bool)
|
||||
// RequestEngineStatus polls for an update from the wireguard
|
||||
// engine. Only needed if you want to display byte
|
||||
// counts. Connection events are emitted automatically without
|
||||
@@ -156,4 +160,8 @@ type Backend interface {
|
||||
// make sure they react properly with keys that are going to
|
||||
// expire.
|
||||
FakeExpireAfter(x time.Duration)
|
||||
// Ping attempts to start connecting to the given IP and sends a Notify
|
||||
// with its PingResult. If the host is down, there might never
|
||||
// be a PingResult sent. The cmd/tailscale CLI client adds a timeout.
|
||||
Ping(ip string)
|
||||
}
|
||||
|
||||
@@ -79,6 +79,10 @@ func (b *FakeBackend) SetPrefs(new *Prefs) {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *FakeBackend) SetWantRunning(v bool) {
|
||||
b.SetPrefs(&Prefs{WantRunning: v})
|
||||
}
|
||||
|
||||
func (b *FakeBackend) RequestEngineStatus() {
|
||||
b.notify(Notify{Engine: &EngineStatus{}})
|
||||
}
|
||||
@@ -90,3 +94,7 @@ func (b *FakeBackend) RequestStatus() {
|
||||
func (b *FakeBackend) FakeExpireAfter(x time.Duration) {
|
||||
b.notify(Notify{NetMap: &controlclient.NetworkMap{}})
|
||||
}
|
||||
|
||||
func (b *FakeBackend) Ping(ip string) {
|
||||
b.notify(Notify{PingResult: &ipnstate.PingResult{}})
|
||||
}
|
||||
|
||||
@@ -69,10 +69,6 @@ type Options struct {
|
||||
// DebugMux, if non-nil, specifies an HTTP ServeMux in which
|
||||
// to register a debug handler.
|
||||
DebugMux *http.ServeMux
|
||||
|
||||
// ErrorMessage, if not empty, signals that the server will exist
|
||||
// only to relay the provided critical error message to the user.
|
||||
ErrorMessage string
|
||||
}
|
||||
|
||||
// server is an IPN backend and its set of 0 or more active connections
|
||||
@@ -152,7 +148,9 @@ func (s *server) writeToClients(b []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
func Run(ctx context.Context, logf logger.Logf, logid string, opts Options, e wgengine.Engine) error {
|
||||
// Run runs a Tailscale backend service.
|
||||
// The getEngine func is called repeatedly, once per connection, until it returns an engine successfully.
|
||||
func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (wgengine.Engine, error), opts Options) error {
|
||||
runDone := make(chan struct{})
|
||||
defer close(runDone)
|
||||
|
||||
@@ -177,27 +175,40 @@ func Run(ctx context.Context, logf logger.Logf, logid string, opts Options, e wg
|
||||
}()
|
||||
logf("Listening on %v", listen.Addr())
|
||||
|
||||
bo := backoff.NewBackoff("ipnserver", logf)
|
||||
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
|
||||
|
||||
if opts.ErrorMessage != "" {
|
||||
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)
|
||||
for i := 1; ctx.Err() == nil; i++ {
|
||||
s, err := listen.Accept()
|
||||
c, err := listen.Accept()
|
||||
if err != nil {
|
||||
logf("%d: Accept: %v", i, err)
|
||||
bo.BackOff(ctx, err)
|
||||
continue
|
||||
}
|
||||
serverToClient := func(b []byte) {
|
||||
ipn.WriteMsg(s, b)
|
||||
logf("%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)
|
||||
errMsg := err.Error()
|
||||
go func() {
|
||||
defer s.Close()
|
||||
defer c.Close()
|
||||
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
|
||||
bs := ipn.NewBackendServer(logf, nil, serverToClient)
|
||||
bs.SendErrorMessage(opts.ErrorMessage)
|
||||
s.Read(make([]byte, 1))
|
||||
bs.SendErrorMessage(errMsg)
|
||||
time.Sleep(time.Second)
|
||||
}()
|
||||
}
|
||||
return ctx.Err()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var store ipn.StateStore
|
||||
@@ -210,7 +221,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, opts Options, e wg
|
||||
store = &ipn.MemoryStore{}
|
||||
}
|
||||
|
||||
b, err := ipn.NewLocalBackend(logf, logid, store, e)
|
||||
b, err := ipn.NewLocalBackend(logf, logid, store, eng)
|
||||
if err != nil {
|
||||
return fmt.Errorf("NewLocalBackend: %v", err)
|
||||
}
|
||||
@@ -243,7 +254,14 @@ func Run(ctx context.Context, logf logger.Logf, logid string, opts Options, e wg
|
||||
}
|
||||
|
||||
for i := 1; ctx.Err() == nil; i++ {
|
||||
c, err := listen.Accept()
|
||||
var c net.Conn
|
||||
var err error
|
||||
if unservedConn != nil {
|
||||
c = unservedConn
|
||||
unservedConn = nil
|
||||
} else {
|
||||
c, err = listen.Accept()
|
||||
}
|
||||
if err != nil {
|
||||
if ctx.Err() == nil {
|
||||
logf("ipnserver: Accept: %v", err)
|
||||
@@ -288,7 +306,7 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
proc.mu.Unlock()
|
||||
}()
|
||||
|
||||
bo := backoff.NewBackoff("BabysitProc", logf)
|
||||
bo := backoff.NewBackoff("BabysitProc", logf, 30*time.Second)
|
||||
|
||||
for {
|
||||
startTime := time.Now()
|
||||
@@ -371,3 +389,8 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FixedEngine returns a func that returns eng and a nil error.
|
||||
func FixedEngine(eng wgengine.Engine) func() (wgengine.Engine, error) {
|
||||
return func() (wgengine.Engine, error) { return eng, nil }
|
||||
}
|
||||
|
||||
@@ -72,6 +72,6 @@ func TestRunMultipleAccepts(t *testing.T) {
|
||||
SocketPath: socketPath,
|
||||
}
|
||||
t.Logf("pre-Run")
|
||||
err = ipnserver.Run(ctx, logTriggerTestf, "dummy_logid", opts, eng)
|
||||
err = ipnserver.Run(ctx, logTriggerTestf, "dummy_logid", ipnserver.FixedEngine(eng), opts)
|
||||
t.Logf("ipnserver.Run = %v", err)
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
@@ -25,8 +26,11 @@ import (
|
||||
// Status represents the entire state of the IPN network.
|
||||
type Status struct {
|
||||
BackendState string
|
||||
Peer map[key.Public]*PeerStatus
|
||||
User map[tailcfg.UserID]tailcfg.UserProfile
|
||||
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
||||
Self *PeerStatus
|
||||
|
||||
Peer map[key.Public]*PeerStatus
|
||||
User map[tailcfg.UserID]tailcfg.UserProfile
|
||||
}
|
||||
|
||||
func (s *Status) Peers() []key.Public {
|
||||
@@ -41,6 +45,7 @@ func (s *Status) Peers() []key.Public {
|
||||
type PeerStatus struct {
|
||||
PublicKey key.Public
|
||||
HostName string // HostInfo's Hostname (not a DNS name or necessarily unique)
|
||||
DNSName string
|
||||
OS string // HostInfo.OS
|
||||
UserID tailcfg.UserID
|
||||
|
||||
@@ -86,6 +91,12 @@ type StatusBuilder struct {
|
||||
st Status
|
||||
}
|
||||
|
||||
func (sb *StatusBuilder) SetBackendState(v string) {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
sb.st.BackendState = v
|
||||
}
|
||||
|
||||
func (sb *StatusBuilder) Status() *Status {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
@@ -93,6 +104,13 @@ func (sb *StatusBuilder) Status() *Status {
|
||||
return &sb.st
|
||||
}
|
||||
|
||||
// SetSelfStatus sets the status of the local machine.
|
||||
func (sb *StatusBuilder) SetSelfStatus(ss *PeerStatus) {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
sb.st.Self = ss
|
||||
}
|
||||
|
||||
// AddUser adds a user profile to the status.
|
||||
func (sb *StatusBuilder) AddUser(id tailcfg.UserID, up tailcfg.UserProfile) {
|
||||
sb.mu.Lock()
|
||||
@@ -109,6 +127,18 @@ func (sb *StatusBuilder) AddUser(id tailcfg.UserID, up tailcfg.UserProfile) {
|
||||
sb.st.User[id] = up
|
||||
}
|
||||
|
||||
// AddIP adds a Tailscale IP address to the status.
|
||||
func (sb *StatusBuilder) AddTailscaleIP(ip netaddr.IP) {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
if sb.locked {
|
||||
log.Printf("[unexpected] ipnstate: AddIP after Locked")
|
||||
return
|
||||
}
|
||||
|
||||
sb.st.TailscaleIPs = append(sb.st.TailscaleIPs, ip)
|
||||
}
|
||||
|
||||
// AddPeer adds a peer node to the status.
|
||||
//
|
||||
// Its PeerStatus is mixed with any previous status already added.
|
||||
@@ -137,6 +167,9 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
|
||||
if v := st.HostName; v != "" {
|
||||
e.HostName = v
|
||||
}
|
||||
if v := st.DNSName; v != "" {
|
||||
e.DNSName = v
|
||||
}
|
||||
if v := st.Relay; v != "" {
|
||||
e.Relay = v
|
||||
}
|
||||
@@ -218,6 +251,12 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
|
||||
//f("<p><b>logid:</b> %s</p>\n", logid)
|
||||
//f("<p><b>opts:</b> <code>%s</code></p>\n", html.EscapeString(fmt.Sprintf("%+v", opts)))
|
||||
|
||||
ips := make([]string, 0, len(st.TailscaleIPs))
|
||||
for _, ip := range st.TailscaleIPs {
|
||||
ips = append(ips, ip.String())
|
||||
}
|
||||
f("<p>Tailscale IP: %s", strings.Join(ips, ", "))
|
||||
|
||||
f("<table>\n<thead>\n")
|
||||
f("<tr><th>Peer</th><th>Node</th><th>Owner</th><th>Rx</th><th>Tx</th><th>Activity</th><th>Endpoints</th></tr>\n")
|
||||
f("</thead>\n<tbody>\n")
|
||||
@@ -302,3 +341,21 @@ func osEmoji(os string) string {
|
||||
}
|
||||
return "👽"
|
||||
}
|
||||
|
||||
// PingResult contains response information for the "tailscale ping" subcommand,
|
||||
// saying how Tailscale can reach a Tailscale IP or subnet-routed IP.
|
||||
type PingResult struct {
|
||||
IP string // ping destination
|
||||
NodeIP string // Tailscale IP of node handling IP (different for subnet routers)
|
||||
NodeName string // DNS name base or (possibly not unique) hostname
|
||||
|
||||
Err string
|
||||
LatencySeconds float64
|
||||
|
||||
Endpoint string // ip:port if direct UDP was used
|
||||
|
||||
DERPRegionID int // non-zero if DERP was used
|
||||
DERPRegionCode string // three-letter airport/region code if DERP was used
|
||||
|
||||
// TODO(bradfitz): details like whether port mapping was used on either side? (Once supported)
|
||||
}
|
||||
|
||||
378
ipn/local.go
378
ipn/local.go
@@ -16,8 +16,11 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/internal/deepprint"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn/policy"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/portlist"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/empty"
|
||||
@@ -27,6 +30,7 @@ import (
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/router/dns"
|
||||
"tailscale.com/wgengine/tsdns"
|
||||
)
|
||||
|
||||
@@ -51,12 +55,10 @@ type LocalBackend struct {
|
||||
backendLogID string
|
||||
portpoll *portlist.Poller // may be nil
|
||||
portpollOnce sync.Once
|
||||
serverURL string // tailcontrol URL
|
||||
newDecompressor func() (controlclient.Decompressor, error)
|
||||
|
||||
// TODO: these fields are accessed unsafely by concurrent
|
||||
// goroutines. They need to be protected.
|
||||
serverURL string // tailcontrol URL
|
||||
lastFilterPrint time.Time
|
||||
filterHash string
|
||||
|
||||
// The mutex protects the following elements.
|
||||
mu sync.Mutex
|
||||
@@ -108,11 +110,23 @@ func NewLocalBackend(logf logger.Logf, logid string, store StateStore, e wgengin
|
||||
state: NoState,
|
||||
portpoll: portpoll,
|
||||
}
|
||||
e.SetLinkChangeCallback(b.linkChange)
|
||||
b.statusChanged = sync.NewCond(&b.statusLock)
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
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.
|
||||
}
|
||||
|
||||
// Shutdown halts the backend and all its sub-components. The backend
|
||||
// can no longer be used after Shutdown returns.
|
||||
func (b *LocalBackend) Shutdown() {
|
||||
@@ -143,6 +157,8 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
sb.SetBackendState(b.state.String())
|
||||
|
||||
// TODO: hostinfo, and its networkinfo
|
||||
// TODO: EngineStatus copy (and deprecate it?)
|
||||
if b.netMap != nil {
|
||||
@@ -163,6 +179,7 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
UserID: p.User,
|
||||
TailAddr: tailAddr,
|
||||
HostName: p.Hostinfo.Hostname,
|
||||
DNSName: p.Name,
|
||||
OS: p.Hostinfo.OS,
|
||||
KeepAlive: p.KeepAlive,
|
||||
Created: p.Created,
|
||||
@@ -186,21 +203,56 @@ func (b *LocalBackend) SetDecompressor(fn func() (controlclient.Decompressor, er
|
||||
// setClientStatus is the callback invoked by the control client whenever it posts a new status.
|
||||
// Among other things, this is where we update the netmap, packet filters, DNS and DERP maps.
|
||||
func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
// The following do not depend on any data for which we need to lock b.
|
||||
if st.Err != "" {
|
||||
// TODO(crawshaw): display in the UI.
|
||||
b.logf("Received error: %v", st.Err)
|
||||
return
|
||||
}
|
||||
if st.LoginFinished != nil {
|
||||
// Auth completed, unblock the engine
|
||||
b.blockEngineUpdates(false)
|
||||
b.authReconfig()
|
||||
b.send(Notify{LoginFinished: &empty.Message{}})
|
||||
}
|
||||
|
||||
prefsChanged := false
|
||||
|
||||
// Lock b once and do only the things that require locking.
|
||||
b.mu.Lock()
|
||||
|
||||
prefs := b.prefs
|
||||
stateKey := b.stateKey
|
||||
netMap := b.netMap
|
||||
interact := b.interact
|
||||
|
||||
if st.Persist != nil {
|
||||
persist := *st.Persist // copy
|
||||
if !b.prefs.Persist.Equals(st.Persist) {
|
||||
prefsChanged = true
|
||||
b.prefs.Persist = st.Persist.Clone()
|
||||
}
|
||||
}
|
||||
if st.NetMap != nil {
|
||||
b.netMap = st.NetMap
|
||||
}
|
||||
if st.URL != "" {
|
||||
b.authURL = st.URL
|
||||
}
|
||||
if b.state == NeedsLogin {
|
||||
if !b.prefs.WantRunning {
|
||||
prefsChanged = true
|
||||
}
|
||||
b.prefs.WantRunning = true
|
||||
}
|
||||
// Prefs will be written out; this is not safe unless locked or cloned.
|
||||
if prefsChanged {
|
||||
prefs = b.prefs.Clone()
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
b.prefs.Persist = &persist
|
||||
prefs := b.prefs.Clone()
|
||||
stateKey := b.stateKey
|
||||
b.mu.Unlock()
|
||||
b.mu.Unlock()
|
||||
|
||||
// Now complete the lock-free parts of what we started while locked.
|
||||
if prefsChanged {
|
||||
if stateKey != "" {
|
||||
if err := b.store.WriteState(stateKey, prefs.ToBytes()); err != nil {
|
||||
b.logf("Failed to save new controlclient state: %v", err)
|
||||
@@ -209,63 +261,41 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
b.send(Notify{Prefs: prefs})
|
||||
}
|
||||
if st.NetMap != nil {
|
||||
// Netmap is unchanged only when the diff is empty.
|
||||
changed := true
|
||||
b.mu.Lock()
|
||||
if b.netMap != nil {
|
||||
diff := st.NetMap.ConciseDiffFrom(b.netMap)
|
||||
if netMap != nil {
|
||||
diff := st.NetMap.ConciseDiffFrom(netMap)
|
||||
if strings.TrimSpace(diff) == "" {
|
||||
changed = false
|
||||
b.logf("netmap diff: (none)")
|
||||
} else {
|
||||
b.logf("netmap diff:\n%v", diff)
|
||||
}
|
||||
}
|
||||
disableDERP := b.prefs != nil && b.prefs.DisableDERP
|
||||
b.netMap = st.NetMap
|
||||
b.mu.Unlock()
|
||||
|
||||
b.send(Notify{NetMap: st.NetMap})
|
||||
// There is nothing to update if the map hasn't changed.
|
||||
if changed {
|
||||
b.updateFilter(st.NetMap)
|
||||
b.updateFilter(st.NetMap, prefs)
|
||||
b.e.SetNetworkMap(st.NetMap)
|
||||
|
||||
if !dnsMapsEqual(st.NetMap, netMap) {
|
||||
b.updateDNSMap(st.NetMap)
|
||||
b.e.SetNetworkMap(st.NetMap)
|
||||
}
|
||||
|
||||
disableDERP := prefs != nil && prefs.DisableDERP
|
||||
if disableDERP {
|
||||
b.e.SetDERPMap(nil)
|
||||
} else {
|
||||
b.e.SetDERPMap(st.NetMap.DERPMap)
|
||||
}
|
||||
|
||||
b.send(Notify{NetMap: st.NetMap})
|
||||
}
|
||||
if st.URL != "" {
|
||||
b.logf("Received auth URL: %.20v...", st.URL)
|
||||
|
||||
b.mu.Lock()
|
||||
interact := b.interact
|
||||
b.authURL = st.URL
|
||||
b.mu.Unlock()
|
||||
|
||||
if interact > 0 {
|
||||
b.popBrowserAuthNow()
|
||||
}
|
||||
}
|
||||
if st.Err != "" {
|
||||
// TODO(crawshaw): display in the UI.
|
||||
b.logf("Received error: %v", st.Err)
|
||||
return
|
||||
}
|
||||
if st.NetMap != nil {
|
||||
b.mu.Lock()
|
||||
if b.state == NeedsLogin {
|
||||
b.prefs.WantRunning = true
|
||||
}
|
||||
prefs := b.prefs
|
||||
b.mu.Unlock()
|
||||
|
||||
b.SetPrefs(prefs)
|
||||
}
|
||||
b.stateMachine()
|
||||
// This is currently (2020-07-28) necessary; conditionally disabling it is fragile!
|
||||
// This is where netmap information gets propagated to router and magicsock.
|
||||
b.authReconfig()
|
||||
}
|
||||
|
||||
// setWgengineStatus is the callback by the wireguard engine whenever it posts a new status.
|
||||
@@ -360,7 +390,7 @@ func (b *LocalBackend) Start(opts Options) error {
|
||||
persist := b.prefs.Persist
|
||||
b.mu.Unlock()
|
||||
|
||||
b.updateFilter(nil)
|
||||
b.updateFilter(nil, nil)
|
||||
|
||||
var discoPublic tailcfg.DiscoKey
|
||||
if controlclient.Debug.Disco {
|
||||
@@ -424,65 +454,121 @@ func (b *LocalBackend) Start(opts Options) error {
|
||||
|
||||
// updateFilter updates the packet filter in wgengine based on the
|
||||
// given netMap and user preferences.
|
||||
func (b *LocalBackend) updateFilter(netMap *controlclient.NetworkMap) {
|
||||
if netMap == nil {
|
||||
// Not configured yet, block everything
|
||||
func (b *LocalBackend) updateFilter(netMap *controlclient.NetworkMap, prefs *Prefs) {
|
||||
// NOTE(danderson): keep change detection as the first thing in
|
||||
// this function. Don't try to optimize by returning early, more
|
||||
// likely than not you'll just end up breaking the change
|
||||
// detection and end up with the wrong filter installed. This is
|
||||
// quite hard to debug, so save yourself the trouble.
|
||||
var (
|
||||
haveNetmap = netMap != nil
|
||||
addrs []wgcfg.CIDR
|
||||
packetFilter filter.Matches
|
||||
advRoutes []wgcfg.CIDR
|
||||
shieldsUp = prefs == nil || prefs.ShieldsUp // Be conservative when not ready
|
||||
)
|
||||
if haveNetmap {
|
||||
addrs = netMap.Addresses
|
||||
packetFilter = netMap.PacketFilter
|
||||
}
|
||||
if prefs != nil {
|
||||
advRoutes = prefs.AdvertiseRoutes
|
||||
}
|
||||
|
||||
changed := deepprint.UpdateHash(&b.filterHash, haveNetmap, addrs, packetFilter, advRoutes, shieldsUp)
|
||||
if !changed {
|
||||
return
|
||||
}
|
||||
|
||||
if !haveNetmap {
|
||||
b.logf("netmap packet filter: (not ready yet)")
|
||||
b.e.SetFilter(filter.NewAllowNone(b.logf))
|
||||
return
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
advRoutes := b.prefs.AdvertiseRoutes
|
||||
b.mu.Unlock()
|
||||
localNets := wgCIDRsToFilter(netMap.Addresses, advRoutes)
|
||||
|
||||
if b.shieldsAreUp() {
|
||||
// Shields up, block everything
|
||||
if shieldsUp {
|
||||
b.logf("netmap packet filter: (shields up)")
|
||||
var prevFilter *filter.Filter // don't reuse old filter state
|
||||
b.e.SetFilter(filter.New(filter.Matches{}, localNets, prevFilter, b.logf))
|
||||
return
|
||||
} else {
|
||||
b.logf("netmap packet filter: %v", packetFilter)
|
||||
b.e.SetFilter(filter.New(packetFilter, localNets, b.e.GetFilter(), b.logf))
|
||||
}
|
||||
}
|
||||
|
||||
// dnsCIDRsEqual determines whether two CIDR lists are equal
|
||||
// for DNS map construction purposes (that is, only the first entry counts).
|
||||
func dnsCIDRsEqual(newAddr, oldAddr []wgcfg.CIDR) bool {
|
||||
if len(newAddr) != len(oldAddr) {
|
||||
return false
|
||||
}
|
||||
if len(newAddr) == 0 || newAddr[0] == oldAddr[0] {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// dnsMapsEqual determines whether the new and the old network map
|
||||
// induce the same DNS map. It does so without allocating memory,
|
||||
// at the expense of giving false negatives if peers are reordered.
|
||||
func dnsMapsEqual(new, old *controlclient.NetworkMap) bool {
|
||||
if (old == nil) != (new == nil) {
|
||||
return false
|
||||
}
|
||||
if old == nil && new == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// TODO(apenwarr): don't replace filter at all if unchanged.
|
||||
// TODO(apenwarr): print a diff instead of full filter.
|
||||
now := time.Now()
|
||||
if now.Sub(b.lastFilterPrint) > 1*time.Minute {
|
||||
b.logf("netmap packet filter: %v", netMap.PacketFilter)
|
||||
b.lastFilterPrint = now
|
||||
} else {
|
||||
b.logf("netmap packet filter: (length %d)", len(netMap.PacketFilter))
|
||||
if len(new.Peers) != len(old.Peers) {
|
||||
return false
|
||||
}
|
||||
b.e.SetFilter(filter.New(netMap.PacketFilter, localNets, b.e.GetFilter(), b.logf))
|
||||
|
||||
if new.Name != old.Name {
|
||||
return false
|
||||
}
|
||||
if !dnsCIDRsEqual(new.Addresses, old.Addresses) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i, newPeer := range new.Peers {
|
||||
oldPeer := old.Peers[i]
|
||||
if newPeer.Name != oldPeer.Name {
|
||||
return false
|
||||
}
|
||||
if !dnsCIDRsEqual(newPeer.Addresses, oldPeer.Addresses) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// updateDNSMap updates the domain map in the DNS resolver in wgengine
|
||||
// based on the given netMap and user preferences.
|
||||
func (b *LocalBackend) updateDNSMap(netMap *controlclient.NetworkMap) {
|
||||
if netMap == nil {
|
||||
b.logf("dns map: (not ready)")
|
||||
return
|
||||
}
|
||||
|
||||
domainToIP := make(map[string]netaddr.IP)
|
||||
set := func(hostname string, addrs []wgcfg.CIDR) {
|
||||
nameToIP := make(map[string]netaddr.IP)
|
||||
set := func(name string, addrs []wgcfg.CIDR) {
|
||||
if len(addrs) == 0 {
|
||||
return
|
||||
}
|
||||
domain := hostname
|
||||
// Like PeerStatus.SimpleHostName()
|
||||
domain = strings.TrimSuffix(domain, ".local")
|
||||
domain = strings.TrimSuffix(domain, ".localdomain")
|
||||
domain = domain + ".b.tailscale.net"
|
||||
domainToIP[domain] = netaddr.IPFrom16(addrs[0].IP.Addr)
|
||||
nameToIP[name] = netaddr.IPFrom16(addrs[0].IP.Addr)
|
||||
}
|
||||
|
||||
for _, peer := range netMap.Peers {
|
||||
set(peer.Hostinfo.Hostname, peer.Addresses)
|
||||
set(peer.Name, peer.Addresses)
|
||||
}
|
||||
set(netMap.Hostinfo.Hostname, netMap.Addresses)
|
||||
set(netMap.Name, netMap.Addresses)
|
||||
|
||||
b.e.SetDNSMap(tsdns.NewMap(domainToIP))
|
||||
dnsMap := tsdns.NewMap(nameToIP, domainsForProxying(netMap))
|
||||
// map diff will be logged in tsdns.Resolver.SetMap.
|
||||
b.e.SetDNSMap(dnsMap)
|
||||
}
|
||||
|
||||
// readPoller is a goroutine that receives service lists from
|
||||
@@ -675,6 +761,17 @@ func (b *LocalBackend) FakeExpireAfter(x time.Duration) {
|
||||
b.send(Notify{NetMap: b.netMap})
|
||||
}
|
||||
|
||||
func (b *LocalBackend) Ping(ipStr string) {
|
||||
ip, err := netaddr.ParseIP(ipStr)
|
||||
if err != nil {
|
||||
b.logf("ignoring Ping request to invalid IP %q", ipStr)
|
||||
return
|
||||
}
|
||||
b.e.Ping(ip, func(pr *ipnstate.PingResult) {
|
||||
b.send(Notify{PingResult: pr})
|
||||
})
|
||||
}
|
||||
|
||||
func (b *LocalBackend) parseWgStatus(s *wgengine.Status) (ret EngineStatus) {
|
||||
var (
|
||||
peerStats []string
|
||||
@@ -695,7 +792,9 @@ func (b *LocalBackend) parseWgStatus(s *wgengine.Status) (ret EngineStatus) {
|
||||
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, " "))
|
||||
}
|
||||
return ret
|
||||
@@ -713,6 +812,18 @@ func (b *LocalBackend) shieldsAreUp() bool {
|
||||
return b.prefs.ShieldsUp
|
||||
}
|
||||
|
||||
func (b *LocalBackend) SetWantRunning(wantRunning bool) {
|
||||
b.mu.Lock()
|
||||
new := b.prefs.Clone()
|
||||
b.mu.Unlock()
|
||||
if new.WantRunning == wantRunning {
|
||||
return
|
||||
}
|
||||
new.WantRunning = wantRunning
|
||||
b.logf("SetWantRunning: %v", wantRunning)
|
||||
b.SetPrefs(new)
|
||||
}
|
||||
|
||||
// SetPrefs saves new user preferences and propagates them throughout
|
||||
// the system. Implements Backend.
|
||||
func (b *LocalBackend) SetPrefs(new *Prefs) {
|
||||
@@ -721,37 +832,46 @@ func (b *LocalBackend) SetPrefs(new *Prefs) {
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
|
||||
netMap := b.netMap
|
||||
stateKey := b.stateKey
|
||||
|
||||
old := b.prefs
|
||||
new.Persist = old.Persist // caller isn't allowed to override this
|
||||
b.prefs = new
|
||||
if b.stateKey != "" {
|
||||
if err := b.store.WriteState(b.stateKey, b.prefs.ToBytes()); err != nil {
|
||||
b.logf("Failed to save new controlclient state: %v", err)
|
||||
}
|
||||
}
|
||||
// We do this to avoid holding the lock while doing everything else.
|
||||
new = b.prefs.Clone()
|
||||
|
||||
oldHi := b.hostinfo
|
||||
newHi := oldHi.Clone()
|
||||
newHi.RoutableIPs = append([]wgcfg.CIDR(nil), b.prefs.AdvertiseRoutes...)
|
||||
applyPrefsToHostinfo(newHi, new)
|
||||
b.hostinfo = newHi
|
||||
hostInfoChanged := !oldHi.Equal(newHi)
|
||||
|
||||
b.mu.Unlock()
|
||||
|
||||
if stateKey != "" {
|
||||
if err := b.store.WriteState(stateKey, new.ToBytes()); err != nil {
|
||||
b.logf("Failed to save new controlclient state: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// [GRINDER STATS LINE] - please don't remove (used for log parsing)
|
||||
b.logf("SetPrefs: %v", new.Pretty())
|
||||
|
||||
if old.ShieldsUp != new.ShieldsUp || hostInfoChanged {
|
||||
b.doSetHostinfoFilterServices(newHi)
|
||||
}
|
||||
|
||||
b.updateFilter(b.netMap)
|
||||
// TODO(dmytro): when Prefs gain an EnableTailscaleDNS toggle, updateDNSMap here.
|
||||
b.updateFilter(netMap, new)
|
||||
|
||||
turnDERPOff := new.DisableDERP && !old.DisableDERP
|
||||
turnDERPOn := !new.DisableDERP && old.DisableDERP
|
||||
if turnDERPOff {
|
||||
b.e.SetDERPMap(nil)
|
||||
} else if turnDERPOn && b.netMap != nil {
|
||||
b.e.SetDERPMap(b.netMap.DERPMap)
|
||||
} else if turnDERPOn && netMap != nil {
|
||||
b.e.SetDERPMap(netMap.DERPMap)
|
||||
}
|
||||
|
||||
if old.WantRunning != new.WantRunning {
|
||||
@@ -844,28 +964,72 @@ func (b *LocalBackend) authReconfig() {
|
||||
flags |= controlclient.AllowSingleHosts
|
||||
}
|
||||
|
||||
dns := nm.DNS
|
||||
dom := nm.DNSDomains
|
||||
if !uc.CorpDNS {
|
||||
dns = []wgcfg.IP{}
|
||||
dom = []string{}
|
||||
}
|
||||
cfg, err := nm.WGCfg(b.logf, flags, dns)
|
||||
cfg, err := nm.WGCfg(b.logf, flags)
|
||||
if err != nil {
|
||||
b.logf("wgcfg: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
err = b.e.Reconfig(cfg, routerConfig(cfg, uc, dom))
|
||||
rcfg := routerConfig(cfg, uc)
|
||||
|
||||
// If CorpDNS is false, rcfg.DNS remains the zero value.
|
||||
if uc.CorpDNS {
|
||||
domains := nm.DNS.Domains
|
||||
proxied := nm.DNS.Proxied
|
||||
if proxied {
|
||||
if len(nm.DNS.Nameservers) == 0 {
|
||||
b.logf("[unexpected] dns proxied but no nameservers")
|
||||
proxied = false
|
||||
} else {
|
||||
// Domains for proxying should come first to avoid leaking queries.
|
||||
domains = append(domainsForProxying(nm), domains...)
|
||||
}
|
||||
}
|
||||
rcfg.DNS = dns.Config{
|
||||
Nameservers: nm.DNS.Nameservers,
|
||||
Domains: domains,
|
||||
PerDomain: nm.DNS.PerDomain,
|
||||
Proxied: proxied,
|
||||
}
|
||||
}
|
||||
|
||||
err = b.e.Reconfig(cfg, rcfg)
|
||||
if err == wgengine.ErrNoChanges {
|
||||
return
|
||||
}
|
||||
b.logf("authReconfig: ra=%v dns=%v 0x%02x: %v", uc.RouteAll, uc.CorpDNS, flags, err)
|
||||
}
|
||||
|
||||
// routerConfig produces a router.Config from a wireguard config,
|
||||
// IPN prefs, and the dnsDomains pulled from control's network map.
|
||||
func routerConfig(cfg *wgcfg.Config, prefs *Prefs, dnsDomains []string) *router.Config {
|
||||
// domainsForProxying produces a list of search domains for proxied DNS.
|
||||
func domainsForProxying(nm *controlclient.NetworkMap) []string {
|
||||
var domains []string
|
||||
if idx := strings.IndexByte(nm.Name, '.'); idx != -1 {
|
||||
domains = append(domains, nm.Name[idx+1:])
|
||||
}
|
||||
for _, peer := range nm.Peers {
|
||||
idx := strings.IndexByte(peer.Name, '.')
|
||||
if idx == -1 {
|
||||
continue
|
||||
}
|
||||
domain := peer.Name[idx+1:]
|
||||
seen := false
|
||||
// In theory this makes the function O(n^2) worst case,
|
||||
// but in practice we expect domains to contain very few elements
|
||||
// (only one until invitations are introduced).
|
||||
for _, seenDomain := range domains {
|
||||
if domain == seenDomain {
|
||||
seen = true
|
||||
}
|
||||
}
|
||||
if !seen {
|
||||
domains = append(domains, domain)
|
||||
}
|
||||
}
|
||||
return domains
|
||||
}
|
||||
|
||||
// routerConfig produces a router.Config from a wireguard config and IPN prefs.
|
||||
func routerConfig(cfg *wgcfg.Config, prefs *Prefs) *router.Config {
|
||||
var addrs []wgcfg.CIDR
|
||||
for _, addr := range cfg.Addresses {
|
||||
addrs = append(addrs, wgcfg.CIDR{
|
||||
@@ -879,20 +1043,14 @@ func routerConfig(cfg *wgcfg.Config, prefs *Prefs, dnsDomains []string) *router.
|
||||
SubnetRoutes: wgCIDRToNetaddr(prefs.AdvertiseRoutes),
|
||||
SNATSubnetRoutes: !prefs.NoSNAT,
|
||||
NetfilterMode: prefs.NetfilterMode,
|
||||
DNSConfig: router.DNSConfig{
|
||||
Nameservers: wgIPToNetaddr(cfg.DNS),
|
||||
Domains: dnsDomains,
|
||||
},
|
||||
}
|
||||
|
||||
for _, peer := range cfg.Peers {
|
||||
rs.Routes = append(rs.Routes, wgCIDRToNetaddr(peer.AllowedIPs)...)
|
||||
}
|
||||
|
||||
// The Tailscale DNS IP.
|
||||
// TODO(dmytro): make this configurable.
|
||||
rs.Routes = append(rs.Routes, netaddr.IPPrefix{
|
||||
IP: netaddr.IPv4(100, 100, 100, 100),
|
||||
IP: tsaddr.TailscaleServiceIP(),
|
||||
Bits: 32,
|
||||
})
|
||||
|
||||
@@ -916,17 +1074,6 @@ func wgCIDRsToFilter(cidrLists ...[]wgcfg.CIDR) (ret []filter.Net) {
|
||||
return ret
|
||||
}
|
||||
|
||||
func wgIPToNetaddr(ips []wgcfg.IP) (ret []netaddr.IP) {
|
||||
for _, ip := range ips {
|
||||
nip, ok := netaddr.FromStdIP(ip.IP())
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("conversion of %s from wgcfg to netaddr IP failed", ip))
|
||||
}
|
||||
ret = append(ret, nip.Unmap())
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func wgCIDRToNetaddr(cidrs []wgcfg.CIDR) (ret []netaddr.IPPrefix) {
|
||||
for _, cidr := range cidrs {
|
||||
ncidr, ok := netaddr.FromStdIPNet(cidr.IPNet())
|
||||
@@ -964,6 +1111,7 @@ func (b *LocalBackend) enterState(newState State) {
|
||||
b.state = newState
|
||||
prefs := b.prefs
|
||||
notify := b.notify
|
||||
bc := b.c
|
||||
b.mu.Unlock()
|
||||
|
||||
if state == newState {
|
||||
@@ -975,6 +1123,10 @@ func (b *LocalBackend) enterState(newState State) {
|
||||
b.send(Notify{State: &newState})
|
||||
}
|
||||
|
||||
if bc != nil {
|
||||
bc.SetPaused(newState == Stopped)
|
||||
}
|
||||
|
||||
switch newState {
|
||||
case NeedsLogin:
|
||||
b.blockEngineUpdates(true)
|
||||
|
||||
107
ipn/loglines_test.go
Normal file
107
ipn/loglines_test.go
Normal file
@@ -0,0 +1,107 @@
|
||||
// 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 ipn
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
// TestLocalLogLines tests to make sure that the log lines required for log parsing are
|
||||
// being logged by the expected functions. Update these tests if moving log lines between
|
||||
// functions.
|
||||
func TestLocalLogLines(t *testing.T) {
|
||||
logListen := tstest.ListenFor(t.Logf, []string{
|
||||
"SetPrefs: %v",
|
||||
"peer keys: %s",
|
||||
"v%v peers: %v",
|
||||
})
|
||||
|
||||
logid := func(hex byte) logtail.PublicID {
|
||||
var ret logtail.PublicID
|
||||
for i := 0; i < len(ret); i++ {
|
||||
ret[i] = hex
|
||||
}
|
||||
return ret
|
||||
}
|
||||
idA := logid(0xaa)
|
||||
|
||||
// set up a LocalBackend, super bare bones. No functional data.
|
||||
store := &MemoryStore{
|
||||
cache: make(map[StateKey][]byte),
|
||||
}
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logListen.Logf, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
lb, err := NewLocalBackend(logListen.Logf, idA.String(), store, e)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// custom adjustments for required non-nil fields
|
||||
lb.prefs = NewPrefs()
|
||||
lb.hostinfo = &tailcfg.Hostinfo{}
|
||||
// hacky manual override of the usual log-on-change behaviour of keylogf
|
||||
lb.keyLogf = logListen.Logf
|
||||
|
||||
// testing infrastructure
|
||||
type linesTest struct {
|
||||
name string
|
||||
want []string
|
||||
}
|
||||
|
||||
tests := []linesTest{
|
||||
{
|
||||
name: "after prefs",
|
||||
want: []string{
|
||||
"peer keys: %s",
|
||||
"v%v peers: %v",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "after peers",
|
||||
want: []string{},
|
||||
},
|
||||
}
|
||||
|
||||
testLogs := func(want linesTest) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
if linesLeft := logListen.Check(); len(linesLeft) != len(want.want) {
|
||||
t.Errorf("got %v, expected %v", linesLeft, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// log prefs line
|
||||
persist := &controlclient.Persist{}
|
||||
prefs := NewPrefs()
|
||||
prefs.Persist = persist
|
||||
lb.SetPrefs(prefs)
|
||||
|
||||
t.Run(tests[0].name, testLogs(tests[0]))
|
||||
|
||||
// log peers, peer keys
|
||||
status := &wgengine.Status{
|
||||
Peers: []wgengine.PeerStatus{wgengine.PeerStatus{
|
||||
TxBytes: 10,
|
||||
RxBytes: 10,
|
||||
LastHandshake: time.Now(),
|
||||
NodeKey: tailcfg.NodeKey(key.NewPrivate()),
|
||||
}},
|
||||
LocalAddrs: []string{"idk an address"},
|
||||
}
|
||||
lb.parseWgStatus(status)
|
||||
|
||||
t.Run(tests[1].name, testLogs(tests[1]))
|
||||
}
|
||||
@@ -33,6 +33,10 @@ type FakeExpireAfterArgs struct {
|
||||
Duration time.Duration
|
||||
}
|
||||
|
||||
type PingArgs struct {
|
||||
IP string
|
||||
}
|
||||
|
||||
// Command is a command message that is JSON encoded and sent by a
|
||||
// frontend to a backend.
|
||||
type Command struct {
|
||||
@@ -53,9 +57,11 @@ type Command struct {
|
||||
Login *oauth2.Token
|
||||
Logout *NoArgs
|
||||
SetPrefs *SetPrefsArgs
|
||||
SetWantRunning *bool
|
||||
RequestEngineStatus *NoArgs
|
||||
RequestStatus *NoArgs
|
||||
FakeExpireAfter *FakeExpireAfterArgs
|
||||
Ping *PingArgs
|
||||
}
|
||||
|
||||
type BackendServer struct {
|
||||
@@ -139,6 +145,9 @@ func (bs *BackendServer) GotCommand(cmd *Command) error {
|
||||
} else if c := cmd.SetPrefs; c != nil {
|
||||
bs.b.SetPrefs(c.New)
|
||||
return nil
|
||||
} else if c := cmd.SetWantRunning; c != nil {
|
||||
bs.b.SetWantRunning(*c)
|
||||
return nil
|
||||
} else if c := cmd.RequestEngineStatus; c != nil {
|
||||
bs.b.RequestEngineStatus()
|
||||
return nil
|
||||
@@ -148,6 +157,9 @@ func (bs *BackendServer) GotCommand(cmd *Command) error {
|
||||
} else if c := cmd.FakeExpireAfter; c != nil {
|
||||
bs.b.FakeExpireAfter(c.Duration)
|
||||
return nil
|
||||
} else if c := cmd.Ping; c != nil {
|
||||
bs.b.Ping(c.IP)
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("BackendServer.Do: no command specified")
|
||||
}
|
||||
@@ -254,8 +266,16 @@ func (bc *BackendClient) FakeExpireAfter(x time.Duration) {
|
||||
bc.send(Command{FakeExpireAfter: &FakeExpireAfterArgs{Duration: x}})
|
||||
}
|
||||
|
||||
func (bc *BackendClient) Ping(ip string) {
|
||||
bc.send(Command{Ping: &PingArgs{IP: ip}})
|
||||
}
|
||||
|
||||
func (bc *BackendClient) SetWantRunning(v bool) {
|
||||
bc.send(Command{SetWantRunning: &v})
|
||||
}
|
||||
|
||||
// MaxMessageSize is the maximum message size, in bytes.
|
||||
const MaxMessageSize = 1 << 20
|
||||
const MaxMessageSize = 10 << 20
|
||||
|
||||
// TODO(apenwarr): incremental json decode?
|
||||
// That would let us avoid storing the whole byte array uselessly in RAM.
|
||||
|
||||
@@ -31,7 +31,10 @@ import (
|
||||
"tailscale.com/logtail/filch"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -54,7 +57,7 @@ type Policy struct {
|
||||
func (c *Config) ToBytes() []byte {
|
||||
data, err := json.MarshalIndent(c, "", "\t")
|
||||
if err != nil {
|
||||
log.Fatalf("logpolicy.Config marshal: %v\n", err)
|
||||
log.Fatalf("logpolicy.Config marshal: %v", err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -101,21 +104,36 @@ func (l logWriter) Write(buf []byte) (int, error) {
|
||||
|
||||
// logsDir returns the directory to use for log configuration and
|
||||
// buffer storage.
|
||||
func logsDir() string {
|
||||
func logsDir(logf logger.Logf) string {
|
||||
// STATE_DIRECTORY is set by systemd 240+ but we support older
|
||||
// systems-d. For example, Ubuntu 18.04 (Bionic Beaver) is 237.
|
||||
systemdStateDir := os.Getenv("STATE_DIRECTORY")
|
||||
if systemdStateDir != "" {
|
||||
logf("logpolicy: using $STATE_DIRECTORY, %q", systemdStateDir)
|
||||
return systemdStateDir
|
||||
}
|
||||
|
||||
// Default to e.g. /var/lib/tailscale or /var/db/tailscale on Unix.
|
||||
if d := paths.DefaultTailscaledStateFile(); d != "" {
|
||||
d = filepath.Dir(d) // directory of e.g. "/var/lib/tailscale/tailscaled.state"
|
||||
if err := os.MkdirAll(d, 0700); err == nil {
|
||||
logf("logpolicy: using system state directory %q", d)
|
||||
return d
|
||||
}
|
||||
}
|
||||
|
||||
cacheDir, err := os.UserCacheDir()
|
||||
if err == nil {
|
||||
return filepath.Join(cacheDir, "Tailscale")
|
||||
d := filepath.Join(cacheDir, "Tailscale")
|
||||
logf("logpolicy: using UserCacheDir, %q", d)
|
||||
return d
|
||||
}
|
||||
|
||||
// Use the current working directory, unless we're being run by a
|
||||
// service manager that sets it to /.
|
||||
wd, err := os.Getwd()
|
||||
if err == nil && wd != "/" {
|
||||
logf("logpolicy: using current directory, %q", wd)
|
||||
return wd
|
||||
}
|
||||
|
||||
@@ -126,6 +144,7 @@ func logsDir() string {
|
||||
if err != nil {
|
||||
panic("no safe place found to store log state")
|
||||
}
|
||||
logf("logpolicy: using temp directory, %q", tmp)
|
||||
return tmp
|
||||
}
|
||||
|
||||
@@ -149,6 +168,14 @@ func runningUnderSystemd() bool {
|
||||
// moved from whereever it does exist, into dir. Leftover logs state
|
||||
// in / and $CACHE_DIRECTORY is deleted.
|
||||
func tryFixLogStateLocation(dir, cmdname string) {
|
||||
switch runtime.GOOS {
|
||||
case "linux", "freebsd", "openbsd":
|
||||
// These are the OSes where we might have written stuff into
|
||||
// root. Others use different logic to find the logs storage
|
||||
// dir.
|
||||
default:
|
||||
return
|
||||
}
|
||||
if cmdname == "" {
|
||||
log.Printf("[unexpected] no cmdname given to tryFixLogStateLocation, please file a bug at https://github.com/tailscale/tailscale")
|
||||
return
|
||||
@@ -163,14 +190,6 @@ func tryFixLogStateLocation(dir, cmdname string) {
|
||||
// Only root could have written log configs to weird places.
|
||||
return
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "linux", "freebsd", "openbsd":
|
||||
// These are the OSes where we might have written stuff into
|
||||
// root. Others use different logic to find the logs storage
|
||||
// dir.
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
// We stored logs in 2 incorrect places: either /, or CACHE_DIR
|
||||
// (aka /var/cache/tailscale). We want to move files into the
|
||||
@@ -303,23 +322,28 @@ func New(collection string) *Policy {
|
||||
}
|
||||
console := log.New(stderrWriter{}, "", lflags)
|
||||
|
||||
dir := logsDir()
|
||||
|
||||
if runtime.GOOS != "windows" { // version.CmdName call was blowing some Windows stack limit via goversion DLL loading
|
||||
tryFixLogStateLocation(dir, version.CmdName())
|
||||
var earlyErrBuf bytes.Buffer
|
||||
earlyLogf := func(format string, a ...interface{}) {
|
||||
fmt.Fprintf(&earlyErrBuf, format, a...)
|
||||
earlyErrBuf.WriteByte('\n')
|
||||
}
|
||||
|
||||
cfgPath := filepath.Join(dir, fmt.Sprintf("%s.log.conf", version.CmdName()))
|
||||
dir := logsDir(earlyLogf)
|
||||
|
||||
cmdName := version.CmdName()
|
||||
tryFixLogStateLocation(dir, cmdName)
|
||||
|
||||
cfgPath := filepath.Join(dir, fmt.Sprintf("%s.log.conf", cmdName))
|
||||
var oldc *Config
|
||||
data, err := ioutil.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
log.Printf("logpolicy.Read %v: %v\n", cfgPath, err)
|
||||
earlyLogf("logpolicy.Read %v: %v", cfgPath, err)
|
||||
oldc = &Config{}
|
||||
oldc.Collection = collection
|
||||
} else {
|
||||
oldc, err = ConfigFromBytes(data)
|
||||
if err != nil {
|
||||
log.Printf("logpolicy.Config unmarshal: %v\n", err)
|
||||
earlyLogf("logpolicy.Config unmarshal: %v", err)
|
||||
oldc = &Config{}
|
||||
}
|
||||
}
|
||||
@@ -341,7 +365,7 @@ func New(collection string) *Policy {
|
||||
newc.PublicID = newc.PrivateID.Public()
|
||||
if newc != *oldc {
|
||||
if err := newc.save(cfgPath); err != nil {
|
||||
log.Printf("logpolicy.Config.Save: %v\n", err)
|
||||
earlyLogf("logpolicy.Config.Save: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,7 +383,7 @@ func New(collection string) *Policy {
|
||||
HTTPC: &http.Client{Transport: newLogtailTransport(logtail.DefaultHost)},
|
||||
}
|
||||
|
||||
filchBuf, filchErr := filch.New(filepath.Join(dir, version.CmdName()), filch.Options{})
|
||||
filchBuf, filchErr := filch.New(filepath.Join(dir, cmdName), filch.Options{})
|
||||
if filchBuf != nil {
|
||||
c.Buffer = filchBuf
|
||||
}
|
||||
@@ -367,14 +391,17 @@ func New(collection string) *Policy {
|
||||
log.SetFlags(0) // other logflags are set on console, not here
|
||||
log.SetOutput(lw)
|
||||
|
||||
log.Printf("Program starting: v%v, Go %v: %#v\n",
|
||||
log.Printf("Program starting: v%v, Go %v: %#v",
|
||||
version.LONG,
|
||||
strings.TrimPrefix(runtime.Version(), "go"),
|
||||
os.Args)
|
||||
log.Printf("LogID: %v\n", newc.PublicID)
|
||||
log.Printf("LogID: %v", newc.PublicID)
|
||||
if filchErr != nil {
|
||||
log.Printf("filch failed: %v", filchErr)
|
||||
}
|
||||
if earlyErrBuf.Len() != 0 {
|
||||
log.Printf("%s", earlyErrBuf.Bytes())
|
||||
}
|
||||
|
||||
return &Policy{
|
||||
Logtail: lw,
|
||||
@@ -393,7 +420,7 @@ func (p *Policy) Close() {
|
||||
// log upload if it can be done before ctx is canceled.
|
||||
func (p *Policy) Shutdown(ctx context.Context) error {
|
||||
if p.Logtail != nil {
|
||||
log.Printf("flushing log.\n")
|
||||
log.Printf("flushing log.")
|
||||
return p.Logtail.Shutdown(ctx)
|
||||
}
|
||||
return nil
|
||||
@@ -405,6 +432,9 @@ func newLogtailTransport(host string) *http.Transport {
|
||||
// Start with a copy of http.DefaultTransport and tweak it a bit.
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
|
||||
// We do our own zstd compression on uploads, and responses never contain any payload,
|
||||
// so don't send "Accept-Encoding: gzip" to save a few bytes on the wire, since there
|
||||
// will never be any body to decompress:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package backoff provides a back-off timer type.
|
||||
package backoff
|
||||
|
||||
import (
|
||||
@@ -12,54 +13,70 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
const MAX_BACKOFF_MSEC = 30000
|
||||
|
||||
// Backoff tracks state the history of consecutive failures and sleeps
|
||||
// an increasing amount of time, up to a provided limit.
|
||||
type Backoff struct {
|
||||
n int
|
||||
n int // number of consecutive failures
|
||||
maxBackoff time.Duration
|
||||
|
||||
// Name is the name of this backoff timer, for logging purposes.
|
||||
name string
|
||||
// logf is the function used for log messages when backing off.
|
||||
logf logger.Logf
|
||||
// NewTimer is the function that acts like time.NewTimer().
|
||||
// You can override this in unit tests.
|
||||
NewTimer func(d time.Duration) *time.Timer
|
||||
|
||||
// NewTimer is the function that acts like time.NewTimer.
|
||||
// It's for use in unit tests.
|
||||
NewTimer func(time.Duration) *time.Timer
|
||||
|
||||
// LogLongerThan sets the minimum time of a single backoff interval
|
||||
// before we mention it in the log.
|
||||
LogLongerThan time.Duration
|
||||
}
|
||||
|
||||
func NewBackoff(name string, logf logger.Logf) Backoff {
|
||||
return Backoff{
|
||||
name: name,
|
||||
logf: logf,
|
||||
NewTimer: time.NewTimer,
|
||||
// NewBackoff returns a new Backoff timer with the provided name (for logging), logger,
|
||||
// and max backoff time. By default, all failures (calls to BackOff with a non-nil err)
|
||||
// are logged unless the returned Backoff.LogLongerThan is adjusted.
|
||||
func NewBackoff(name string, logf logger.Logf, maxBackoff time.Duration) *Backoff {
|
||||
return &Backoff{
|
||||
name: name,
|
||||
logf: logf,
|
||||
maxBackoff: maxBackoff,
|
||||
NewTimer: time.NewTimer,
|
||||
}
|
||||
}
|
||||
|
||||
// Backoff sleeps an increasing amount of time if err is non-nil.
|
||||
// and the context is not a
|
||||
// It resets the backoff schedule once err is nil.
|
||||
func (b *Backoff) BackOff(ctx context.Context, err error) {
|
||||
if ctx.Err() == nil && err != nil {
|
||||
b.n++
|
||||
// n^2 backoff timer is a little smoother than the
|
||||
// common choice of 2^n.
|
||||
msec := b.n * b.n * 10
|
||||
if msec > MAX_BACKOFF_MSEC {
|
||||
msec = MAX_BACKOFF_MSEC
|
||||
}
|
||||
// Randomize the delay between 0.5-1.5 x msec, in order
|
||||
// to prevent accidental "thundering herd" problems.
|
||||
msec = rand.Intn(msec) + msec/2
|
||||
dur := time.Duration(msec) * time.Millisecond
|
||||
if dur >= b.LogLongerThan {
|
||||
b.logf("%s: backoff: %d msec\n", b.name, msec)
|
||||
}
|
||||
t := b.NewTimer(dur)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Stop()
|
||||
case <-t.C:
|
||||
}
|
||||
} else {
|
||||
// not a regular error
|
||||
if err == nil {
|
||||
// No error. Reset number of consecutive failures.
|
||||
b.n = 0
|
||||
return
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
// Fast path.
|
||||
return
|
||||
}
|
||||
|
||||
b.n++
|
||||
// n^2 backoff timer is a little smoother than the
|
||||
// common choice of 2^n.
|
||||
d := time.Duration(b.n*b.n) * 10 * time.Millisecond
|
||||
if d > b.maxBackoff {
|
||||
d = b.maxBackoff
|
||||
}
|
||||
// Randomize the delay between 0.5-1.5 x msec, in order
|
||||
// to prevent accidental "thundering herd" problems.
|
||||
d = time.Duration(float64(d) * (rand.Float64() + 0.5))
|
||||
|
||||
if d >= b.LogLongerThan {
|
||||
b.logf("%s: backoff: %d msec", b.name, d.Milliseconds())
|
||||
}
|
||||
t := b.NewTimer(d)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Stop()
|
||||
case <-t.C:
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,7 +105,7 @@ func Log(cfg Config, logf tslogger.Logf) Logger {
|
||||
sentinel: make(chan int32, 16),
|
||||
drainLogs: cfg.DrainLogs,
|
||||
timeNow: cfg.TimeNow,
|
||||
bo: backoff.NewBackoff("logtail", logf),
|
||||
bo: backoff.NewBackoff("logtail", logf, 30*time.Second),
|
||||
|
||||
shutdownStart: make(chan struct{}),
|
||||
shutdownDone: make(chan struct{}),
|
||||
@@ -133,7 +133,7 @@ type logger struct {
|
||||
drainLogs <-chan struct{} // if non-nil, external signal to attempt a drain
|
||||
sentinel chan int32
|
||||
timeNow func() time.Time
|
||||
bo backoff.Backoff
|
||||
bo *backoff.Backoff
|
||||
zstdEncoder Encoder
|
||||
uploadCancel func()
|
||||
|
||||
@@ -462,5 +462,6 @@ func (l *logger) Write(buf []byte) (int, error) {
|
||||
}
|
||||
}
|
||||
b := l.encode(buf)
|
||||
return l.send(b)
|
||||
_, err := l.send(b)
|
||||
return len(buf), err
|
||||
}
|
||||
|
||||
@@ -32,3 +32,18 @@ func TestLoggerEncodeTextAllocs(t *testing.T) {
|
||||
t.Logf("allocs = %d; want 1", int(n))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggerWriteLength(t *testing.T) {
|
||||
lg := &logger{
|
||||
timeNow: time.Now,
|
||||
buffer: NewMemoryBuffer(1024),
|
||||
}
|
||||
inBuf := []byte("some text to encode")
|
||||
n, err := lg.Write(inBuf)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if n != len(inBuf) {
|
||||
t.Errorf("logger.Write wrote %d bytes, expected %d", n, len(inBuf))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,19 @@ package interfaces
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
)
|
||||
|
||||
// LoginEndpointForProxyDetermination is the URL used for testing
|
||||
// which HTTP proxy the system should use.
|
||||
var LoginEndpointForProxyDetermination = "https://login.tailscale.com/"
|
||||
|
||||
// Tailscale returns the current machine's Tailscale interface, if any.
|
||||
// If none is found, all zero values are returned.
|
||||
// A non-nil error is only returned on a problem listing the system interfaces.
|
||||
@@ -43,7 +49,8 @@ func Tailscale() (net.IP, *net.Interface, error) {
|
||||
// maybeTailscaleInterfaceName reports whether s is an interface
|
||||
// name that might be used by Tailscale.
|
||||
func maybeTailscaleInterfaceName(s string) bool {
|
||||
return strings.HasPrefix(s, "wg") ||
|
||||
return s == "Tailscale" ||
|
||||
strings.HasPrefix(s, "wg") ||
|
||||
strings.HasPrefix(s, "ts") ||
|
||||
strings.HasPrefix(s, "tailscale") ||
|
||||
strings.HasPrefix(s, "utun")
|
||||
@@ -163,6 +170,13 @@ type State struct {
|
||||
// considered "expensive", which currently means LTE/etc
|
||||
// instead of Wifi. This field is not populated by GetState.
|
||||
IsExpensive bool
|
||||
|
||||
// DefaultRouteInterface is the interface name for the machine's default route.
|
||||
// It is not yet populated on all OSes.
|
||||
DefaultRouteInterface string
|
||||
|
||||
// HTTPProxy is the HTTP proxy to use.
|
||||
HTTPProxy string
|
||||
}
|
||||
|
||||
func (s *State) Equal(s2 *State) bool {
|
||||
@@ -175,7 +189,8 @@ func (s *State) Equal(s2 *State) bool {
|
||||
// /^tailscale/)
|
||||
func (s *State) RemoveTailscaleInterfaces() {
|
||||
for name := range s.InterfaceIPs {
|
||||
if strings.HasPrefix(name, "tailscale") { // TODO: use --tun flag value, etc; see TODO in method doc
|
||||
if name == "Tailscale" || // as it is on Windows
|
||||
strings.HasPrefix(name, "tailscale") { // TODO: use --tun flag value, etc; see TODO in method doc
|
||||
delete(s.InterfaceIPs, name)
|
||||
delete(s.InterfaceUp, name)
|
||||
}
|
||||
@@ -198,6 +213,16 @@ func GetState() (*State, error) {
|
||||
}); 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()
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,9 @@ import (
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/util/lineread"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
func init() {
|
||||
likelyHomeRouterIP = likelyHomeRouterIPDarwin
|
||||
}
|
||||
|
||||
/*
|
||||
Parse out 10.0.0.1 from:
|
||||
|
||||
@@ -31,7 +28,13 @@ default link#14 UCSI utun2
|
||||
...
|
||||
|
||||
*/
|
||||
func likelyHomeRouterIPDarwin() (ret netaddr.IP, ok bool) {
|
||||
func likelyHomeRouterIPDarwinExec() (ret netaddr.IP, ok bool) {
|
||||
if version.IsMobile() {
|
||||
// Don't try to do subprocesses on iOS. Ends up with log spam like:
|
||||
// kernel: "Sandbox: IPNExtension(86580) deny(1) process-fork"
|
||||
// This is why we have likelyHomeRouterIPDarwinSyscall.
|
||||
return ret, false
|
||||
}
|
||||
cmd := exec.Command("/usr/sbin/netstat", "-r", "-n", "-f", "inet")
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
|
||||
124
net/interfaces/interfaces_darwin_cgo.go
Normal file
124
net/interfaces/interfaces_darwin_cgo.go
Normal file
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin,cgo
|
||||
|
||||
package interfaces
|
||||
|
||||
/*
|
||||
#import "route.h"
|
||||
#import <netinet/in.h>
|
||||
#import <sys/sysctl.h>
|
||||
#import <stdlib.h>
|
||||
#import <stdio.h>
|
||||
|
||||
// privateGatewayIPFromRoute returns the private gateway ip address from rtm, if it exists.
|
||||
// Otherwise, it returns 0.
|
||||
int privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
|
||||
{
|
||||
// sockaddrs are after the message header
|
||||
struct sockaddr* dst_sa = (struct sockaddr *)(rtm + 1);
|
||||
|
||||
if((rtm->rtm_addrs & (RTA_DST|RTA_GATEWAY)) != (RTA_DST|RTA_GATEWAY))
|
||||
return 0; // missing dst or gateway addr
|
||||
if (dst_sa->sa_family != AF_INET)
|
||||
return 0; // dst not IPv4
|
||||
if ((rtm->rtm_flags & RTF_GATEWAY) == 0)
|
||||
return 0; // gateway flag not set
|
||||
|
||||
struct sockaddr_in* dst_si = (struct sockaddr_in *)dst_sa;
|
||||
if (dst_si->sin_addr.s_addr != INADDR_ANY)
|
||||
return 0; // not default route
|
||||
|
||||
#define ROUNDUP(a) ((a) > 0 ? (1 + (((a) - 1) | (sizeof(long) - 1))) : sizeof(long))
|
||||
|
||||
struct sockaddr* gateway_sa = (struct sockaddr *)((char *)dst_sa + ROUNDUP(dst_sa->sa_len));
|
||||
if (gateway_sa->sa_family != AF_INET)
|
||||
return 0; // gateway not IPv4
|
||||
|
||||
struct sockaddr_in* gateway_si= (struct sockaddr_in *)gateway_sa;
|
||||
int ip;
|
||||
ip = gateway_si->sin_addr.s_addr;
|
||||
|
||||
unsigned char a, b;
|
||||
a = (ip >> 0) & 0xff;
|
||||
b = (ip >> 8) & 0xff;
|
||||
|
||||
// Check whether ip is private, that is, whether it is
|
||||
// in one of 10.0.0.0/8, 172.16.0.0/12, or 192.168.0.0/16.
|
||||
if (a == 10)
|
||||
return ip; // matches 10.0.0.0/8
|
||||
if (a == 172 && (b >> 4) == 1)
|
||||
return ip; // matches 172.16.0.0/12
|
||||
if (a == 192 && b == 168)
|
||||
return ip; // matches 192.168.0.0/16
|
||||
|
||||
// Not a private IP.
|
||||
return 0;
|
||||
}
|
||||
|
||||
// privateGatewayIP returns the private gateway IP address, if it exists.
|
||||
// If no private gateway IP address was found, it returns 0.
|
||||
// On an error, it returns an error code in (0, 255].
|
||||
// Any private gateway IP address is > 255.
|
||||
int privateGatewayIP()
|
||||
{
|
||||
size_t needed;
|
||||
int mib[6];
|
||||
char *buf;
|
||||
|
||||
mib[0] = CTL_NET;
|
||||
mib[1] = PF_ROUTE;
|
||||
mib[2] = 0;
|
||||
mib[3] = 0;
|
||||
mib[4] = NET_RT_DUMP2;
|
||||
mib[5] = 0;
|
||||
|
||||
if (sysctl(mib, 6, NULL, &needed, NULL, 0) < 0)
|
||||
return 1; // route dump size estimation failed
|
||||
if ((buf = malloc(needed)) == 0)
|
||||
return 2; // malloc failed
|
||||
if (sysctl(mib, 6, buf, &needed, NULL, 0) < 0) {
|
||||
free(buf);
|
||||
return 3; // route dump failed
|
||||
}
|
||||
|
||||
// Loop over all routes.
|
||||
char *next, *lim;
|
||||
lim = buf + needed;
|
||||
struct rt_msghdr2 *rtm;
|
||||
for (next = buf; next < lim; next += rtm->rtm_msglen) {
|
||||
rtm = (struct rt_msghdr2 *)next;
|
||||
int ip;
|
||||
ip = privateGatewayIPFromRoute(rtm);
|
||||
if (ip) {
|
||||
free(buf);
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
free(buf);
|
||||
return 0; // no gateway found
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func init() {
|
||||
likelyHomeRouterIP = likelyHomeRouterIPDarwinSyscall
|
||||
}
|
||||
|
||||
func likelyHomeRouterIPDarwinSyscall() (ret netaddr.IP, ok bool) {
|
||||
ip := C.privateGatewayIP()
|
||||
if ip < 255 {
|
||||
return netaddr.IP{}, false
|
||||
}
|
||||
var q [4]byte
|
||||
binary.LittleEndian.PutUint32(q[:], uint32(ip))
|
||||
return netaddr.IPv4(q[0], q[1], q[2], q[3]), true
|
||||
}
|
||||
20
net/interfaces/interfaces_darwin_cgo_test.go
Normal file
20
net/interfaces/interfaces_darwin_cgo_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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 cgo,darwin
|
||||
|
||||
package interfaces
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLikelyHomeRouterIPSyscallExec(t *testing.T) {
|
||||
syscallIP, syscallOK := likelyHomeRouterIPDarwinSyscall()
|
||||
netstatIP, netstatOK := likelyHomeRouterIPDarwinExec()
|
||||
if syscallOK != netstatOK || syscallIP != netstatIP {
|
||||
t.Errorf("syscall() = %v, %v, netstat = %v, %v",
|
||||
syscallIP, syscallOK,
|
||||
netstatIP, netstatOK,
|
||||
)
|
||||
}
|
||||
}
|
||||
11
net/interfaces/interfaces_darwin_nocgo.go
Normal file
11
net/interfaces/interfaces_darwin_nocgo.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 darwin,!cgo
|
||||
|
||||
package interfaces
|
||||
|
||||
func init() {
|
||||
likelyHomeRouterIP = likelyHomeRouterIPDarwinExec
|
||||
}
|
||||
15
net/interfaces/interfaces_defaultrouteif_todo.go
Normal file
15
net/interfaces/interfaces_defaultrouteif_todo.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// 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 !linux
|
||||
|
||||
package interfaces
|
||||
|
||||
import "errors"
|
||||
|
||||
var errTODO = errors.New("TODO")
|
||||
|
||||
func DefaultRouteInterface() (string, error) {
|
||||
return "TODO", errTODO
|
||||
}
|
||||
@@ -5,8 +5,19 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/util/lineread"
|
||||
)
|
||||
|
||||
@@ -14,6 +25,8 @@ func init() {
|
||||
likelyHomeRouterIP = likelyHomeRouterIPLinux
|
||||
}
|
||||
|
||||
var procNetRouteErr syncs.AtomicBool
|
||||
|
||||
/*
|
||||
Parse 10.0.0.1 out of:
|
||||
|
||||
@@ -23,9 +36,17 @@ ens18 00000000 0100000A 0003 0 0 0 00000000
|
||||
ens18 0000000A 00000000 0001 0 0 0 0000FFFF 0 0 0
|
||||
*/
|
||||
func likelyHomeRouterIPLinux() (ret netaddr.IP, ok bool) {
|
||||
if procNetRouteErr.Get() {
|
||||
// If we failed to read /proc/net/route previously, don't keep trying.
|
||||
// But if we're on Android, go into the Android path.
|
||||
if runtime.GOOS == "android" {
|
||||
return likelyHomeRouterIPAndroid()
|
||||
}
|
||||
return ret, false
|
||||
}
|
||||
lineNum := 0
|
||||
var f []mem.RO
|
||||
lineread.File("/proc/net/route", func(line []byte) error {
|
||||
err := lineread.File("/proc/net/route", func(line []byte) error {
|
||||
lineNum++
|
||||
if lineNum == 1 {
|
||||
// Skip header line.
|
||||
@@ -55,5 +76,135 @@ func likelyHomeRouterIPLinux() (ret netaddr.IP, ok bool) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
procNetRouteErr.Set(true)
|
||||
if runtime.GOOS == "android" {
|
||||
return likelyHomeRouterIPAndroid()
|
||||
}
|
||||
log.Printf("interfaces: failed to read /proc/net/route: %v", err)
|
||||
}
|
||||
return ret, !ret.IsZero()
|
||||
}
|
||||
|
||||
// Android apps don't have permission to read /proc/net/route, at
|
||||
// least on Google devices and the Android emulator.
|
||||
func likelyHomeRouterIPAndroid() (ret netaddr.IP, ok bool) {
|
||||
cmd := exec.Command("/system/bin/ip", "route", "show", "table", "0")
|
||||
out, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Printf("interfaces: running /system/bin/ip: %v", err)
|
||||
return
|
||||
}
|
||||
// Search for line like "default via 10.0.2.2 dev radio0 table 1016 proto static mtu 1500 "
|
||||
lineread.Reader(out, func(line []byte) error {
|
||||
const pfx = "default via "
|
||||
if !mem.HasPrefix(mem.B(line), mem.S(pfx)) {
|
||||
return nil
|
||||
}
|
||||
line = line[len(pfx):]
|
||||
sp := bytes.IndexByte(line, ' ')
|
||||
if sp == -1 {
|
||||
return nil
|
||||
}
|
||||
ipb := line[:sp]
|
||||
if ip, err := netaddr.ParseIP(string(ipb)); err == nil && ip.Is4() {
|
||||
ret = ip
|
||||
log.Printf("interfaces: found Android default route %v", ip)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
cmd.Process.Kill()
|
||||
cmd.Wait()
|
||||
return ret, !ret.IsZero()
|
||||
}
|
||||
|
||||
// DefaultRouteInterface returns the name of the network interface that owns
|
||||
// the default route, not including any tailscale interfaces.
|
||||
func DefaultRouteInterface() (string, error) {
|
||||
v, err := defaultRouteInterfaceProcNet()
|
||||
if err == nil {
|
||||
return v, nil
|
||||
}
|
||||
if runtime.GOOS == "android" {
|
||||
return defaultRouteInterfaceAndroidIPRoute()
|
||||
}
|
||||
return v, err
|
||||
}
|
||||
|
||||
var zeroRouteBytes = []byte("00000000")
|
||||
|
||||
func defaultRouteInterfaceProcNet() (string, error) {
|
||||
f, err := os.Open("/proc/net/route")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
br := bufio.NewReaderSize(f, 128)
|
||||
for {
|
||||
line, err := br.ReadSlice('\n')
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !bytes.Contains(line, zeroRouteBytes) {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(string(line))
|
||||
ifc := fields[0]
|
||||
ip := fields[1]
|
||||
netmask := fields[7]
|
||||
|
||||
if strings.HasPrefix(ifc, "tailscale") ||
|
||||
strings.HasPrefix(ifc, "wg") {
|
||||
continue
|
||||
}
|
||||
if ip == "00000000" && netmask == "00000000" {
|
||||
// default route
|
||||
return ifc, nil // interface name
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("no default routes found")
|
||||
|
||||
}
|
||||
|
||||
// defaultRouteInterfaceAndroidIPRoute tries to find the machine's default route interface name
|
||||
// by parsing the "ip route" command output. We use this on Android where /proc/net/route
|
||||
// can be missing entries or have locked-down permissions.
|
||||
// See also comments in https://github.com/tailscale/tailscale/pull/666.
|
||||
func defaultRouteInterfaceAndroidIPRoute() (ifname string, err error) {
|
||||
cmd := exec.Command("/system/bin/ip", "route", "show", "table", "0")
|
||||
out, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
log.Printf("interfaces: running /system/bin/ip: %v", err)
|
||||
return "", err
|
||||
}
|
||||
// Search for line like "default via 10.0.2.2 dev radio0 table 1016 proto static mtu 1500 "
|
||||
lineread.Reader(out, func(line []byte) error {
|
||||
const pfx = "default via "
|
||||
if !mem.HasPrefix(mem.B(line), mem.S(pfx)) {
|
||||
return nil
|
||||
}
|
||||
ff := strings.Fields(string(line))
|
||||
for i, v := range ff {
|
||||
if i > 0 && ff[i-1] == "dev" && ifname == "" {
|
||||
ifname = v
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
cmd.Process.Kill()
|
||||
cmd.Wait()
|
||||
if ifname == "" {
|
||||
return "", errors.New("no default routes found")
|
||||
}
|
||||
return ifname, nil
|
||||
}
|
||||
|
||||
16
net/interfaces/interfaces_linux_test.go
Normal file
16
net/interfaces/interfaces_linux_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package interfaces
|
||||
|
||||
import "testing"
|
||||
|
||||
func BenchmarkDefaultRouteInterface(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := DefaultRouteInterface(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
257
net/interfaces/route.h
Normal file
257
net/interfaces/route.h
Normal file
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
* Copyright (c) 2000-2017 Apple Inc. All rights reserved.
|
||||
*
|
||||
* @APPLE_OSREFERENCE_LICENSE_HEADER_START@
|
||||
*
|
||||
* This file contains Original Code and/or Modifications of Original Code
|
||||
* as defined in and that are subject to the Apple Public Source License
|
||||
* Version 2.0 (the 'License'). You may not use this file except in
|
||||
* compliance with the License. The rights granted to you under the License
|
||||
* may not be used to create, or enable the creation or redistribution of,
|
||||
* unlawful or unlicensed copies of an Apple operating system, or to
|
||||
* circumvent, violate, or enable the circumvention or violation of, any
|
||||
* terms of an Apple operating system software license agreement.
|
||||
*
|
||||
* Please obtain a copy of the License at
|
||||
* http://www.opensource.apple.com/apsl/ and read it before using this file.
|
||||
*
|
||||
* The Original Code and all software distributed under the License are
|
||||
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
|
||||
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
|
||||
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
|
||||
* Please see the License for the specific language governing rights and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @APPLE_OSREFERENCE_LICENSE_HEADER_END@
|
||||
*/
|
||||
/*
|
||||
* Copyright (c) 1980, 1986, 1993
|
||||
* The Regents of the University of California. All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
* 1. Redistributions of source code must retain the above copyright
|
||||
* notice, this list of conditions and the following disclaimer.
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
* 3. All advertising materials mentioning features or use of this software
|
||||
* must display the following acknowledgement:
|
||||
* This product includes software developed by the University of
|
||||
* California, Berkeley and its contributors.
|
||||
* 4. Neither the name of the University nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software
|
||||
* without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
|
||||
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||
* SUCH DAMAGE.
|
||||
*
|
||||
* @(#)route.h 8.3 (Berkeley) 4/19/94
|
||||
* $FreeBSD: src/sys/net/route.h,v 1.36.2.1 2000/08/16 06:14:23 jayanth Exp $
|
||||
*/
|
||||
|
||||
#ifndef _NET_ROUTE_H_
|
||||
#define _NET_ROUTE_H_
|
||||
#include <sys/appleapiopts.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/socket.h>
|
||||
|
||||
/*
|
||||
* These numbers are used by reliable protocols for determining
|
||||
* retransmission behavior and are included in the routing structure.
|
||||
*/
|
||||
struct rt_metrics {
|
||||
u_int32_t rmx_locks; /* Kernel leaves these values alone */
|
||||
u_int32_t rmx_mtu; /* MTU for this path */
|
||||
u_int32_t rmx_hopcount; /* max hops expected */
|
||||
int32_t rmx_expire; /* lifetime for route, e.g. redirect */
|
||||
u_int32_t rmx_recvpipe; /* inbound delay-bandwidth product */
|
||||
u_int32_t rmx_sendpipe; /* outbound delay-bandwidth product */
|
||||
u_int32_t rmx_ssthresh; /* outbound gateway buffer limit */
|
||||
u_int32_t rmx_rtt; /* estimated round trip time */
|
||||
u_int32_t rmx_rttvar; /* estimated rtt variance */
|
||||
u_int32_t rmx_pksent; /* packets sent using this route */
|
||||
u_int32_t rmx_state; /* route state */
|
||||
u_int32_t rmx_filler[3]; /* will be used for T/TCP later */
|
||||
};
|
||||
|
||||
/*
|
||||
* rmx_rtt and rmx_rttvar are stored as microseconds;
|
||||
*/
|
||||
#define RTM_RTTUNIT 1000000 /* units for rtt, rttvar, as units per sec */
|
||||
|
||||
|
||||
|
||||
#define RTF_UP 0x1 /* route usable */
|
||||
#define RTF_GATEWAY 0x2 /* destination is a gateway */
|
||||
#define RTF_HOST 0x4 /* host entry (net otherwise) */
|
||||
#define RTF_REJECT 0x8 /* host or net unreachable */
|
||||
#define RTF_DYNAMIC 0x10 /* created dynamically (by redirect) */
|
||||
#define RTF_MODIFIED 0x20 /* modified dynamically (by redirect) */
|
||||
#define RTF_DONE 0x40 /* message confirmed */
|
||||
#define RTF_DELCLONE 0x80 /* delete cloned route */
|
||||
#define RTF_CLONING 0x100 /* generate new routes on use */
|
||||
#define RTF_XRESOLVE 0x200 /* external daemon resolves name */
|
||||
#define RTF_LLINFO 0x400 /* DEPRECATED - exists ONLY for backward
|
||||
* compatibility */
|
||||
#define RTF_LLDATA 0x400 /* used by apps to add/del L2 entries */
|
||||
#define RTF_STATIC 0x800 /* manually added */
|
||||
#define RTF_BLACKHOLE 0x1000 /* just discard pkts (during updates) */
|
||||
#define RTF_NOIFREF 0x2000 /* not eligible for RTF_IFREF */
|
||||
#define RTF_PROTO2 0x4000 /* protocol specific routing flag */
|
||||
#define RTF_PROTO1 0x8000 /* protocol specific routing flag */
|
||||
|
||||
#define RTF_PRCLONING 0x10000 /* protocol requires cloning */
|
||||
#define RTF_WASCLONED 0x20000 /* route generated through cloning */
|
||||
#define RTF_PROTO3 0x40000 /* protocol specific routing flag */
|
||||
/* 0x80000 unused */
|
||||
#define RTF_PINNED 0x100000 /* future use */
|
||||
#define RTF_LOCAL 0x200000 /* route represents a local address */
|
||||
#define RTF_BROADCAST 0x400000 /* route represents a bcast address */
|
||||
#define RTF_MULTICAST 0x800000 /* route represents a mcast address */
|
||||
#define RTF_IFSCOPE 0x1000000 /* has valid interface scope */
|
||||
#define RTF_CONDEMNED 0x2000000 /* defunct; no longer modifiable */
|
||||
#define RTF_IFREF 0x4000000 /* route holds a ref to interface */
|
||||
#define RTF_PROXY 0x8000000 /* proxying, no interface scope */
|
||||
#define RTF_ROUTER 0x10000000 /* host is a router */
|
||||
#define RTF_DEAD 0x20000000 /* Route entry is being freed */
|
||||
/* 0x40000000 and up unassigned */
|
||||
|
||||
#define RTPRF_OURS RTF_PROTO3 /* set on routes we manage */
|
||||
#define RTF_BITS \
|
||||
"\020\1UP\2GATEWAY\3HOST\4REJECT\5DYNAMIC\6MODIFIED\7DONE" \
|
||||
"\10DELCLONE\11CLONING\12XRESOLVE\13LLINFO\14STATIC\15BLACKHOLE" \
|
||||
"\16NOIFREF\17PROTO2\20PROTO1\21PRCLONING\22WASCLONED\23PROTO3" \
|
||||
"\25PINNED\26LOCAL\27BROADCAST\30MULTICAST\31IFSCOPE\32CONDEMNED" \
|
||||
"\33IFREF\34PROXY\35ROUTER"
|
||||
|
||||
#define IS_DIRECT_HOSTROUTE(rt) \
|
||||
(((rt)->rt_flags & (RTF_HOST | RTF_GATEWAY)) == RTF_HOST)
|
||||
/*
|
||||
* Routing statistics.
|
||||
*/
|
||||
struct rtstat {
|
||||
short rts_badredirect; /* bogus redirect calls */
|
||||
short rts_dynamic; /* routes created by redirects */
|
||||
short rts_newgateway; /* routes modified by redirects */
|
||||
short rts_unreach; /* lookups which failed */
|
||||
short rts_wildcard; /* lookups satisfied by a wildcard */
|
||||
short rts_badrtgwroute; /* route to gateway is not direct */
|
||||
};
|
||||
|
||||
/*
|
||||
* Structures for routing messages.
|
||||
*/
|
||||
struct rt_msghdr {
|
||||
u_short rtm_msglen; /* to skip over non-understood messages */
|
||||
u_char rtm_version; /* future binary compatibility */
|
||||
u_char rtm_type; /* message type */
|
||||
u_short rtm_index; /* index for associated ifp */
|
||||
int rtm_flags; /* flags, incl. kern & message, e.g. DONE */
|
||||
int rtm_addrs; /* bitmask identifying sockaddrs in msg */
|
||||
pid_t rtm_pid; /* identify sender */
|
||||
int rtm_seq; /* for sender to identify action */
|
||||
int rtm_errno; /* why failed */
|
||||
int rtm_use; /* from rtentry */
|
||||
u_int32_t rtm_inits; /* which metrics we are initializing */
|
||||
struct rt_metrics rtm_rmx; /* metrics themselves */
|
||||
};
|
||||
|
||||
struct rt_msghdr2 {
|
||||
u_short rtm_msglen; /* to skip over non-understood messages */
|
||||
u_char rtm_version; /* future binary compatibility */
|
||||
u_char rtm_type; /* message type */
|
||||
u_short rtm_index; /* index for associated ifp */
|
||||
int rtm_flags; /* flags, incl. kern & message, e.g. DONE */
|
||||
int rtm_addrs; /* bitmask identifying sockaddrs in msg */
|
||||
int32_t rtm_refcnt; /* reference count */
|
||||
int rtm_parentflags; /* flags of the parent route */
|
||||
int rtm_reserved; /* reserved field set to 0 */
|
||||
int rtm_use; /* from rtentry */
|
||||
u_int32_t rtm_inits; /* which metrics we are initializing */
|
||||
struct rt_metrics rtm_rmx; /* metrics themselves */
|
||||
};
|
||||
|
||||
|
||||
#define RTM_VERSION 5 /* Up the ante and ignore older versions */
|
||||
|
||||
/*
|
||||
* Message types.
|
||||
*/
|
||||
#define RTM_ADD 0x1 /* Add Route */
|
||||
#define RTM_DELETE 0x2 /* Delete Route */
|
||||
#define RTM_CHANGE 0x3 /* Change Metrics or flags */
|
||||
#define RTM_GET 0x4 /* Report Metrics */
|
||||
#define RTM_LOSING 0x5 /* RTM_LOSING is no longer generated by xnu
|
||||
* and is deprecated */
|
||||
#define RTM_REDIRECT 0x6 /* Told to use different route */
|
||||
#define RTM_MISS 0x7 /* Lookup failed on this address */
|
||||
#define RTM_LOCK 0x8 /* fix specified metrics */
|
||||
#define RTM_OLDADD 0x9 /* caused by SIOCADDRT */
|
||||
#define RTM_OLDDEL 0xa /* caused by SIOCDELRT */
|
||||
#define RTM_RESOLVE 0xb /* req to resolve dst to LL addr */
|
||||
#define RTM_NEWADDR 0xc /* address being added to iface */
|
||||
#define RTM_DELADDR 0xd /* address being removed from iface */
|
||||
#define RTM_IFINFO 0xe /* iface going up/down etc. */
|
||||
#define RTM_NEWMADDR 0xf /* mcast group membership being added to if */
|
||||
#define RTM_DELMADDR 0x10 /* mcast group membership being deleted */
|
||||
#define RTM_IFINFO2 0x12 /* */
|
||||
#define RTM_NEWMADDR2 0x13 /* */
|
||||
#define RTM_GET2 0x14 /* */
|
||||
|
||||
/*
|
||||
* Bitmask values for rtm_inits and rmx_locks.
|
||||
*/
|
||||
#define RTV_MTU 0x1 /* init or lock _mtu */
|
||||
#define RTV_HOPCOUNT 0x2 /* init or lock _hopcount */
|
||||
#define RTV_EXPIRE 0x4 /* init or lock _expire */
|
||||
#define RTV_RPIPE 0x8 /* init or lock _recvpipe */
|
||||
#define RTV_SPIPE 0x10 /* init or lock _sendpipe */
|
||||
#define RTV_SSTHRESH 0x20 /* init or lock _ssthresh */
|
||||
#define RTV_RTT 0x40 /* init or lock _rtt */
|
||||
#define RTV_RTTVAR 0x80 /* init or lock _rttvar */
|
||||
|
||||
/*
|
||||
* Bitmask values for rtm_addrs.
|
||||
*/
|
||||
#define RTA_DST 0x1 /* destination sockaddr present */
|
||||
#define RTA_GATEWAY 0x2 /* gateway sockaddr present */
|
||||
#define RTA_NETMASK 0x4 /* netmask sockaddr present */
|
||||
#define RTA_GENMASK 0x8 /* cloning mask sockaddr present */
|
||||
#define RTA_IFP 0x10 /* interface name sockaddr present */
|
||||
#define RTA_IFA 0x20 /* interface addr sockaddr present */
|
||||
#define RTA_AUTHOR 0x40 /* sockaddr for author of redirect */
|
||||
#define RTA_BRD 0x80 /* for NEWADDR, broadcast or p-p dest addr */
|
||||
|
||||
/*
|
||||
* Index offsets for sockaddr array for alternate internal encoding.
|
||||
*/
|
||||
#define RTAX_DST 0 /* destination sockaddr present */
|
||||
#define RTAX_GATEWAY 1 /* gateway sockaddr present */
|
||||
#define RTAX_NETMASK 2 /* netmask sockaddr present */
|
||||
#define RTAX_GENMASK 3 /* cloning mask sockaddr present */
|
||||
#define RTAX_IFP 4 /* interface name sockaddr present */
|
||||
#define RTAX_IFA 5 /* interface addr sockaddr present */
|
||||
#define RTAX_AUTHOR 6 /* sockaddr for author of redirect */
|
||||
#define RTAX_BRD 7 /* for NEWADDR, broadcast or p-p dest addr */
|
||||
#define RTAX_MAX 8 /* size of array to allocate */
|
||||
|
||||
struct rt_addrinfo {
|
||||
int rti_addrs;
|
||||
struct sockaddr *rti_info[RTAX_MAX];
|
||||
};
|
||||
|
||||
|
||||
#endif /* _NET_ROUTE_H_ */
|
||||
@@ -18,7 +18,9 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -36,6 +38,45 @@ import (
|
||||
"tailscale.com/types/opt"
|
||||
)
|
||||
|
||||
// Debugging and experimentation tweakables.
|
||||
var (
|
||||
debugNetcheck, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_NETCHECK"))
|
||||
)
|
||||
|
||||
// The various default timeouts for things.
|
||||
const (
|
||||
// overallProbeTimeout is the maximum amount of time netcheck will
|
||||
// spend gathering a single report.
|
||||
overallProbeTimeout = 5 * time.Second
|
||||
// stunTimeout is the maximum amount of time netcheck will spend
|
||||
// probing with STUN packets without getting a reply before
|
||||
// switching to HTTP probing, on the assumption that outbound UDP
|
||||
// is blocked.
|
||||
stunProbeTimeout = 3 * time.Second
|
||||
// hairpinCheckTimeout is the amount of time we wait for a
|
||||
// hairpinned packet to come back.
|
||||
hairpinCheckTimeout = 100 * time.Millisecond
|
||||
// defaultActiveRetransmitTime is the retransmit interval we use
|
||||
// for STUN probes when we're in steady state (not in start-up),
|
||||
// but don't have previous latency information for a DERP
|
||||
// node. This is a somewhat conservative guess because if we have
|
||||
// no data, likely the DERP node is very far away and we have no
|
||||
// data because we timed out the last time we probed it.
|
||||
defaultActiveRetransmitTime = 200 * time.Millisecond
|
||||
// defaultInitialRetransmitTime is the retransmit interval used
|
||||
// when netcheck first runs. We have no past context to work with,
|
||||
// and we want answers relatively quickly, so it's biased slightly
|
||||
// more aggressive than defaultActiveRetransmitTime. A few extra
|
||||
// packets at startup is fine.
|
||||
defaultInitialRetransmitTime = 100 * time.Millisecond
|
||||
// portMapServiceProbeTimeout is the time we wait for port mapping
|
||||
// services (UPnP, NAT-PMP, PCP) to respond before we give up and
|
||||
// decide that they're not there. Since these services are on the
|
||||
// same LAN as this machine and a single L3 hop away, we don't
|
||||
// give them much time to respond.
|
||||
portMapServiceProbeTimeout = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
type Report struct {
|
||||
UDP bool // UDP works
|
||||
IPv6 bool // IPv6 works
|
||||
@@ -130,6 +171,15 @@ type STUNConn interface {
|
||||
ReadFrom([]byte) (int, net.Addr, error)
|
||||
}
|
||||
|
||||
func (c *Client) enoughRegions() int {
|
||||
if c.Verbose {
|
||||
// Abuse verbose a bit here so netcheck can show all region latencies
|
||||
// in verbose mode.
|
||||
return 100
|
||||
}
|
||||
return 3
|
||||
}
|
||||
|
||||
func (c *Client) logf(format string, a ...interface{}) {
|
||||
if c.Logf != nil {
|
||||
c.Logf(format, a...)
|
||||
@@ -139,7 +189,7 @@ func (c *Client) logf(format string, a ...interface{}) {
|
||||
}
|
||||
|
||||
func (c *Client) vlogf(format string, a ...interface{}) {
|
||||
if c.Verbose {
|
||||
if c.Verbose || debugNetcheck {
|
||||
c.logf(format, a...)
|
||||
}
|
||||
}
|
||||
@@ -170,6 +220,8 @@ func (c *Client) MakeNextReportFull() {
|
||||
}
|
||||
|
||||
func (c *Client) ReceiveSTUNPacket(pkt []byte, src netaddr.IPPort) {
|
||||
c.vlogf("received STUN packet from %s", src)
|
||||
|
||||
c.mu.Lock()
|
||||
if c.handleHairSTUNLocked(pkt, src) {
|
||||
c.mu.Unlock()
|
||||
@@ -330,7 +382,7 @@ func makeProbePlan(dm *tailcfg.DERPMap, ifState *interfaces.State, last *Report)
|
||||
n := reg.Nodes[try%len(reg.Nodes)]
|
||||
prevLatency := last.RegionLatency[reg.RegionID] * 120 / 100
|
||||
if prevLatency == 0 {
|
||||
prevLatency = 200 * time.Millisecond
|
||||
prevLatency = defaultActiveRetransmitTime
|
||||
}
|
||||
delay := time.Duration(try) * prevLatency
|
||||
if do4 {
|
||||
@@ -353,16 +405,12 @@ func makeProbePlan(dm *tailcfg.DERPMap, ifState *interfaces.State, last *Report)
|
||||
func makeProbePlanInitial(dm *tailcfg.DERPMap, ifState *interfaces.State) (plan probePlan) {
|
||||
plan = make(probePlan)
|
||||
|
||||
// initialSTUNTimeout is only 100ms because some extra retransmits
|
||||
// when starting up is tolerable.
|
||||
const initialSTUNTimeout = 100 * time.Millisecond
|
||||
|
||||
for _, reg := range dm.Regions {
|
||||
var p4 []probe
|
||||
var p6 []probe
|
||||
for try := 0; try < 3; try++ {
|
||||
n := reg.Nodes[try%len(reg.Nodes)]
|
||||
delay := time.Duration(try) * initialSTUNTimeout
|
||||
delay := time.Duration(try) * defaultInitialRetransmitTime
|
||||
if ifState.HaveV4 && nodeMight4(n) {
|
||||
p4 = append(p4, probe{delay: delay, node: n.Name, proto: probeIPv4})
|
||||
}
|
||||
@@ -518,7 +566,7 @@ func (rs *reportState) startHairCheckLocked(dst netaddr.IPPort) {
|
||||
ua := dst.UDPAddr()
|
||||
rs.pc4Hair.WriteTo(stun.Request(rs.hairTX), ua)
|
||||
rs.c.vlogf("sent haircheck to %v", ua)
|
||||
time.AfterFunc(500*time.Millisecond, func() { close(rs.hairTimeout) })
|
||||
time.AfterFunc(hairpinCheckTimeout, func() { close(rs.hairTimeout) })
|
||||
}
|
||||
|
||||
func (rs *reportState) waitHairCheck(ctx context.Context) {
|
||||
@@ -539,6 +587,7 @@ func (rs *reportState) waitHairCheck(ctx context.Context) {
|
||||
case <-rs.gotHairSTUN:
|
||||
ret.HairPinning.Set(true)
|
||||
case <-rs.hairTimeout:
|
||||
rs.c.vlogf("hairCheck timeout")
|
||||
ret.HairPinning.Set(false)
|
||||
default:
|
||||
select {
|
||||
@@ -575,12 +624,13 @@ func (rs *reportState) addNodeLatency(node *tailcfg.DERPNode, ipp netaddr.IPPort
|
||||
ret.UDP = true
|
||||
updateLatency(ret.RegionLatency, node.RegionID, d)
|
||||
|
||||
// Once we've heard from 3 regions, start a timer to give up
|
||||
// on the other ones. The timer's duration is a function of
|
||||
// whether this is our initial full probe or an incremental
|
||||
// one. For incremental ones, wait for the duration of the
|
||||
// slowest region. For initial ones, double that.
|
||||
if len(ret.RegionLatency) == 3 {
|
||||
// Once we've heard from enough regions (3), start a timer to
|
||||
// give up on the other ones. The timer's duration is a
|
||||
// function of whether this is our initial full probe or an
|
||||
// incremental one. For incremental ones, wait for the
|
||||
// duration of the slowest region. For initial ones, double
|
||||
// that.
|
||||
if len(ret.RegionLatency) == rs.c.enoughRegions() {
|
||||
timeout := maxDurationValue(ret.RegionLatency)
|
||||
if !rs.incremental {
|
||||
timeout *= 2
|
||||
@@ -649,7 +699,7 @@ func (rs *reportState) probePortMapServices() {
|
||||
}
|
||||
defer uc.Close()
|
||||
tempPort := uc.LocalAddr().(*net.UDPAddr).Port
|
||||
uc.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
|
||||
uc.SetReadDeadline(time.Now().Add(portMapServiceProbeTimeout))
|
||||
|
||||
// Send request packets for all three protocols.
|
||||
uc.WriteTo(uPnPPacket, port1900)
|
||||
@@ -727,15 +777,10 @@ func newReport() *Report {
|
||||
//
|
||||
// It may not be called concurrently with itself.
|
||||
func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, error) {
|
||||
// Wait for STUN for 3 seconds, but then give HTTP probing
|
||||
// another 2 seconds if all UDP failed.
|
||||
const overallTimeout = 5 * time.Second
|
||||
const stunTimeout = 3 * time.Second
|
||||
|
||||
// Mask user context with ours that we guarantee to cancel so
|
||||
// we can depend on it being closed in goroutines later.
|
||||
// (User ctx might be context.Background, etc)
|
||||
ctx, cancel := context.WithTimeout(ctx, overallTimeout)
|
||||
ctx, cancel := context.WithTimeout(ctx, overallProbeTimeout)
|
||||
defer cancel()
|
||||
|
||||
if dm == nil {
|
||||
@@ -844,7 +889,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
||||
}(probeSet)
|
||||
}
|
||||
|
||||
stunTimer := time.NewTimer(stunTimeout)
|
||||
stunTimer := time.NewTimer(stunProbeTimeout)
|
||||
defer stunTimer.Stop()
|
||||
|
||||
select {
|
||||
@@ -857,7 +902,9 @@ 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")
|
||||
rs.stopTimers()
|
||||
|
||||
// Try HTTPS latency check if all STUN probes failed due to UDP presumably being blocked.
|
||||
@@ -912,7 +959,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
||||
|
||||
func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegion) (time.Duration, netaddr.IP, error) {
|
||||
var result httpstat.Result
|
||||
ctx, cancel := context.WithTimeout(httpstat.WithHTTPStat(ctx, &result), 5*time.Second)
|
||||
ctx, cancel := context.WithTimeout(httpstat.WithHTTPStat(ctx, &result), overallProbeTimeout)
|
||||
defer cancel()
|
||||
|
||||
var ip netaddr.IP
|
||||
|
||||
@@ -5,19 +5,15 @@
|
||||
package netns
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/net/interfaces"
|
||||
)
|
||||
|
||||
// tailscaleBypassMark is the mark indicating that packets originating
|
||||
@@ -43,47 +39,6 @@ func ipRuleAvailable() bool {
|
||||
return ipRuleOnce.v
|
||||
}
|
||||
|
||||
var zeroRouteBytes = []byte("00000000")
|
||||
|
||||
// defaultRouteInterface returns the name of the network interface that owns
|
||||
// the default route, not including any tailscale interfaces. We only use
|
||||
// this in SO_BINDTODEVICE mode.
|
||||
func defaultRouteInterface() (string, error) {
|
||||
f, err := os.Open("/proc/net/route")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
br := bufio.NewReaderSize(f, 128)
|
||||
for {
|
||||
line, err := br.ReadSlice('\n')
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !bytes.Contains(line, zeroRouteBytes) {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(string(line))
|
||||
ifc := fields[0]
|
||||
ip := fields[1]
|
||||
netmask := fields[7]
|
||||
|
||||
if strings.HasPrefix(ifc, "tailscale") ||
|
||||
strings.HasPrefix(ifc, "wg") {
|
||||
continue
|
||||
}
|
||||
if ip == "00000000" && netmask == "00000000" {
|
||||
// default route
|
||||
return ifc, nil // interface name
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("no default routes found")
|
||||
}
|
||||
|
||||
// ignoreErrors returns true if we should ignore setsocketopt errors in
|
||||
// this instance.
|
||||
func ignoreErrors() bool {
|
||||
@@ -133,7 +88,7 @@ func setBypassMark(fd uintptr) error {
|
||||
}
|
||||
|
||||
func bindToDevice(fd uintptr) error {
|
||||
ifc, err := defaultRouteInterface()
|
||||
ifc, err := interfaces.DefaultRouteInterface()
|
||||
if err != nil {
|
||||
// Make sure we bind to *some* interface,
|
||||
// or we could get a routing loop.
|
||||
|
||||
@@ -49,12 +49,3 @@ func TestBypassMarkInSync(t *testing.T) {
|
||||
}
|
||||
t.Errorf("tailscaleBypassMark not found in router_linux.go")
|
||||
}
|
||||
|
||||
func BenchmarkDefaultRouteInterface(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if _, err := defaultRouteInterface(); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,15 +137,15 @@ func foreachAttr(b []byte, fn func(attrType uint16, a []byte) error) error {
|
||||
}
|
||||
attrType := binary.BigEndian.Uint16(b[:2])
|
||||
attrLen := int(binary.BigEndian.Uint16(b[2:4]))
|
||||
attrLenPad := attrLen % 4
|
||||
attrLenWithPad := (attrLen + 3) &^ 3
|
||||
b = b[4:]
|
||||
if attrLen+attrLenPad > len(b) {
|
||||
if attrLenWithPad > len(b) {
|
||||
return ErrMalformedAttrs
|
||||
}
|
||||
if err := fn(attrType, b[:attrLen]); err != nil {
|
||||
return err
|
||||
}
|
||||
b = b[attrLen+attrLenPad:]
|
||||
b = b[attrLenWithPad:]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -140,6 +140,41 @@ var responseTests = []struct {
|
||||
wantAddr: net.ParseIP("2602:d1:b4cf:c100:38b2:31ff:feef:96f6"),
|
||||
wantPort: 37070,
|
||||
},
|
||||
|
||||
// Testing STUN attribute padding rules using STUN software attribute
|
||||
// with values of 1 & 3 length respectively before the XorMappedAddress attribute
|
||||
{
|
||||
name: "software-a",
|
||||
data: []byte{
|
||||
0x01, 0x01, 0x00, 0x14, 0x21, 0x12, 0xa4, 0x42,
|
||||
0xeb, 0xc2, 0xd3, 0x6e, 0xf4, 0x71, 0x21, 0x7c,
|
||||
0x4f, 0x3e, 0x30, 0x8e, 0x80, 0x22, 0x00, 0x01,
|
||||
0x61, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x08,
|
||||
0x00, 0x01, 0xce, 0x66, 0x5e, 0x12, 0xa4, 0x43,
|
||||
},
|
||||
wantTID: []byte{
|
||||
0xeb, 0xc2, 0xd3, 0x6e, 0xf4, 0x71, 0x21, 0x7c,
|
||||
0x4f, 0x3e, 0x30, 0x8e,
|
||||
},
|
||||
wantAddr: []byte{127, 0, 0, 1},
|
||||
wantPort: 61300,
|
||||
},
|
||||
{
|
||||
name: "software-abc",
|
||||
data: []byte{
|
||||
0x01, 0x01, 0x00, 0x14, 0x21, 0x12, 0xa4, 0x42,
|
||||
0xeb, 0xc2, 0xd3, 0x6e, 0xf4, 0x71, 0x21, 0x7c,
|
||||
0x4f, 0x3e, 0x30, 0x8e, 0x80, 0x22, 0x00, 0x03,
|
||||
0x61, 0x62, 0x63, 0x00, 0x00, 0x20, 0x00, 0x08,
|
||||
0x00, 0x01, 0xce, 0x66, 0x5e, 0x12, 0xa4, 0x43,
|
||||
},
|
||||
wantTID: []byte{
|
||||
0xeb, 0xc2, 0xd3, 0x6e, 0xf4, 0x71, 0x21, 0x7c,
|
||||
0x4f, 0x3e, 0x30, 0x8e,
|
||||
},
|
||||
wantAddr: []byte{127, 0, 0, 1},
|
||||
wantPort: 61300,
|
||||
},
|
||||
}
|
||||
|
||||
func TestParseResponse(t *testing.T) {
|
||||
|
||||
@@ -32,6 +32,15 @@ func CGNATRange() netaddr.IPPrefix {
|
||||
|
||||
var cgnatRange oncePrefix
|
||||
|
||||
// TailscaleServiceIP returns the listen address of services
|
||||
// provided by Tailscale itself such as the Magic DNS proxy.
|
||||
func TailscaleServiceIP() netaddr.IP {
|
||||
serviceIP.Do(func() { mustIP(&serviceIP.v, "100.100.100.100") })
|
||||
return serviceIP.v
|
||||
}
|
||||
|
||||
var serviceIP onceIP
|
||||
|
||||
// IsTailscaleIP reports whether ip is an IP address in a range that
|
||||
// Tailscale assigns from.
|
||||
func IsTailscaleIP(ip netaddr.IP) bool {
|
||||
@@ -50,3 +59,16 @@ type oncePrefix struct {
|
||||
sync.Once
|
||||
v netaddr.IPPrefix
|
||||
}
|
||||
|
||||
func mustIP(v *netaddr.IP, ip string) {
|
||||
var err error
|
||||
*v, err = netaddr.ParseIP(ip)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
type onceIP struct {
|
||||
sync.Once
|
||||
v netaddr.IP
|
||||
}
|
||||
|
||||
60
net/tshttpproxy/tshttpproxy.go
Normal file
60
net/tshttpproxy/tshttpproxy.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// 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 tshttpproxy contains Tailscale additions to httpproxy not available
|
||||
// in golang.org/x/net/http/httpproxy. Notably, it aims to support Windows better.
|
||||
package tshttpproxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
u, err := http.ProxyFromEnvironment(req)
|
||||
if u != nil && err == nil {
|
||||
return u, nil
|
||||
}
|
||||
|
||||
if sysProxyFromEnv != nil {
|
||||
u, err := sysProxyFromEnv(req)
|
||||
if u != nil && err == nil {
|
||||
return u, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var sysAuthHeader func(*url.URL) (string, error)
|
||||
|
||||
// GetAuthHeader returns the Authorization header value to send to proxy u.
|
||||
func GetAuthHeader(u *url.URL) (string, error) {
|
||||
if fake := os.Getenv("TS_DEBUG_FAKE_PROXY_AUTH"); fake != "" {
|
||||
return fake, nil
|
||||
}
|
||||
if sysAuthHeader != nil {
|
||||
return sysAuthHeader(u)
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var condSetTransportGetProxyConnectHeader func(*http.Transport)
|
||||
|
||||
// SetTarnsportGetProxyConnectHeader sets the provided Transport's
|
||||
// GetProxyConnectHeader field, if the current build of Go supports
|
||||
// it.
|
||||
//
|
||||
// See https://github.com/golang/go/issues/41048.
|
||||
func SetTransportGetProxyConnectHeader(tr *http.Transport) {
|
||||
if f := condSetTransportGetProxyConnectHeader; f != nil {
|
||||
f(tr)
|
||||
}
|
||||
}
|
||||
45
net/tshttpproxy/tshttpproxy_future.go
Normal file
45
net/tshttpproxy/tshttpproxy_future.go
Normal file
@@ -0,0 +1,45 @@
|
||||
// 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 tailscale_go
|
||||
|
||||
// We want to use https://github.com/golang/go/issues/41048 but it's only in the
|
||||
// Tailscale Go tree for now. Hence the build tag above.
|
||||
|
||||
package tshttpproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
const proxyAuthHeader = "Proxy-Authorization"
|
||||
|
||||
func init() {
|
||||
condSetTransportGetProxyConnectHeader = func(tr *http.Transport) {
|
||||
tr.GetProxyConnectHeader = func(ctx context.Context, proxyURL *url.URL, target string) (http.Header, error) {
|
||||
v, err := GetAuthHeader(proxyURL)
|
||||
if err != nil {
|
||||
log.Printf("failed to get proxy Auth header for %v; ignoring: %v", proxyURL, err)
|
||||
return nil, nil
|
||||
}
|
||||
if v == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return http.Header{proxyAuthHeader: []string{v}}, nil
|
||||
}
|
||||
tr.OnProxyConnectResponse = func(ctx context.Context, proxyURL *url.URL, connectReq *http.Request, res *http.Response) error {
|
||||
auth := connectReq.Header.Get(proxyAuthHeader)
|
||||
const truncLen = 20
|
||||
if len(auth) > truncLen {
|
||||
auth = fmt.Sprintf("%s...(%d total bytes)", auth[:truncLen], len(auth))
|
||||
}
|
||||
log.Printf("tshttpproxy: CONNECT response from %v for target %q (auth %q): %v", proxyURL, connectReq.Host, auth, res.Status)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
213
net/tshttpproxy/tshttpproxy_windows.go
Normal file
213
net/tshttpproxy/tshttpproxy_windows.go
Normal file
@@ -0,0 +1,213 @@
|
||||
// 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 tshttpproxy
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/alexbrainman/sspi/negotiate"
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
var (
|
||||
winHTTP = windows.NewLazySystemDLL("winhttp.dll")
|
||||
httpOpenProc = winHTTP.NewProc("WinHttpOpen")
|
||||
closeHandleProc = winHTTP.NewProc("WinHttpCloseHandle")
|
||||
getProxyForUrlProc = winHTTP.NewProc("WinHttpGetProxyForUrl")
|
||||
)
|
||||
|
||||
func init() {
|
||||
sysProxyFromEnv = proxyFromWinHTTPOrCache
|
||||
sysAuthHeader = sysAuthHeaderWindows
|
||||
}
|
||||
|
||||
var cachedProxy struct {
|
||||
sync.Mutex
|
||||
val *url.URL
|
||||
}
|
||||
|
||||
func proxyFromWinHTTPOrCache(req *http.Request) (*url.URL, error) {
|
||||
if req.URL == nil {
|
||||
return nil, nil
|
||||
}
|
||||
urlStr := req.URL.String()
|
||||
|
||||
ctx, cancel := context.WithTimeout(req.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
type result struct {
|
||||
proxy *url.URL
|
||||
err error
|
||||
}
|
||||
resc := make(chan result, 1)
|
||||
go func() {
|
||||
proxy, err := proxyFromWinHTTP(ctx, urlStr)
|
||||
resc <- result{proxy, err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case res := <-resc:
|
||||
err := res.err
|
||||
if err == nil {
|
||||
cachedProxy.Lock()
|
||||
defer cachedProxy.Unlock()
|
||||
if was, now := fmt.Sprint(cachedProxy.val), fmt.Sprint(res.proxy); was != now {
|
||||
log.Printf("tshttpproxy: winhttp: updating cached proxy setting from %v to %v", was, now)
|
||||
}
|
||||
cachedProxy.val = res.proxy
|
||||
return res.proxy, nil
|
||||
}
|
||||
|
||||
// See https://docs.microsoft.com/en-us/windows/win32/winhttp/error-messages
|
||||
const ERROR_WINHTTP_AUTODETECTION_FAILED = 12180
|
||||
if err == syscall.Errno(ERROR_WINHTTP_AUTODETECTION_FAILED) {
|
||||
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)
|
||||
return cachedProxy.val, nil
|
||||
}
|
||||
}
|
||||
|
||||
func proxyFromWinHTTP(ctx context.Context, urlStr string) (proxy *url.URL, err error) {
|
||||
whi, err := winHTTPOpen()
|
||||
if err != nil {
|
||||
log.Printf("winhttp: Open: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
defer whi.Close()
|
||||
|
||||
t0 := time.Now()
|
||||
v, err := whi.GetProxyForURL(urlStr)
|
||||
td := time.Since(t0).Round(time.Millisecond)
|
||||
if err := ctx.Err(); err != nil {
|
||||
log.Printf("tshttpproxy: winhttp: context canceled, ignoring GetProxyForURL(%q) after %v", urlStr, td)
|
||||
return nil, err
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if v == "" {
|
||||
return nil, nil
|
||||
}
|
||||
// Discard all but first proxy value for now.
|
||||
if i := strings.Index(v, ";"); i != -1 {
|
||||
v = v[:i]
|
||||
}
|
||||
if !strings.HasPrefix(v, "https://") {
|
||||
v = "http://" + v
|
||||
}
|
||||
return url.Parse(v)
|
||||
}
|
||||
|
||||
var userAgent = windows.StringToUTF16Ptr("Tailscale")
|
||||
|
||||
const (
|
||||
winHTTP_ACCESS_TYPE_AUTOMATIC_PROXY = 4
|
||||
winHTTP_AUTOPROXY_ALLOW_AUTOCONFIG = 0x00000100
|
||||
winHTTP_AUTOPROXY_AUTO_DETECT = 1
|
||||
winHTTP_AUTO_DETECT_TYPE_DHCP = 0x00000001
|
||||
winHTTP_AUTO_DETECT_TYPE_DNS_A = 0x00000002
|
||||
)
|
||||
|
||||
func winHTTPOpen() (winHTTPInternet, error) {
|
||||
if err := httpOpenProc.Find(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
r, _, err := httpOpenProc.Call(
|
||||
uintptr(unsafe.Pointer(userAgent)),
|
||||
winHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
|
||||
0, /* WINHTTP_NO_PROXY_NAME */
|
||||
0, /* WINHTTP_NO_PROXY_BYPASS */
|
||||
0)
|
||||
if r == 0 {
|
||||
return 0, err
|
||||
}
|
||||
return winHTTPInternet(r), nil
|
||||
}
|
||||
|
||||
type winHTTPInternet windows.Handle
|
||||
|
||||
func (hi winHTTPInternet) Close() error {
|
||||
if err := closeHandleProc.Find(); err != nil {
|
||||
return err
|
||||
}
|
||||
r, _, err := closeHandleProc.Call(uintptr(hi))
|
||||
if r == 1 {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// WINHTTP_AUTOPROXY_OPTIONS
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winhttp/ns-winhttp-winhttp_autoproxy_options
|
||||
type autoProxyOptions struct {
|
||||
DwFlags uint32
|
||||
DwAutoDetectFlags uint32
|
||||
AutoConfigUrl *uint16
|
||||
_ uintptr
|
||||
_ uint32
|
||||
FAutoLogonIfChallenged bool
|
||||
}
|
||||
|
||||
// WINHTTP_PROXY_INFO
|
||||
// https://docs.microsoft.com/en-us/windows/win32/api/winhttp/ns-winhttp-winhttp_proxy_info
|
||||
type winHTTPProxyInfo struct {
|
||||
AccessType uint16
|
||||
Proxy *uint16
|
||||
ProxyBypass *uint16
|
||||
}
|
||||
|
||||
var proxyForURLOpts = &autoProxyOptions{
|
||||
DwFlags: winHTTP_AUTOPROXY_ALLOW_AUTOCONFIG | winHTTP_AUTOPROXY_AUTO_DETECT,
|
||||
DwAutoDetectFlags: winHTTP_AUTO_DETECT_TYPE_DHCP, // | winHTTP_AUTO_DETECT_TYPE_DNS_A,
|
||||
}
|
||||
|
||||
func (hi winHTTPInternet) GetProxyForURL(urlStr string) (string, error) {
|
||||
if err := getProxyForUrlProc.Find(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var out winHTTPProxyInfo
|
||||
r, _, err := getProxyForUrlProc.Call(
|
||||
uintptr(hi),
|
||||
uintptr(unsafe.Pointer(windows.StringToUTF16Ptr(urlStr))),
|
||||
uintptr(unsafe.Pointer(proxyForURLOpts)),
|
||||
uintptr(unsafe.Pointer(&out)))
|
||||
if r == 1 {
|
||||
return windows.UTF16PtrToString(out.Proxy), nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
func sysAuthHeaderWindows(u *url.URL) (string, error) {
|
||||
spn := "HTTP/" + u.Hostname()
|
||||
creds, err := negotiate.AcquireCurrentUserCredentials()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("negotiate.AcquireCurrentUserCredentials: %w", err)
|
||||
}
|
||||
defer creds.Release()
|
||||
|
||||
secCtx, token, err := negotiate.NewClientContext(creds, spn)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("negotiate.NewClientContext: %w", err)
|
||||
}
|
||||
defer secCtx.Release()
|
||||
|
||||
return "Negotiate " + base64.StdEncoding.EncodeToString(token), nil
|
||||
}
|
||||
@@ -41,6 +41,10 @@ func parsePort(s string) int {
|
||||
return int(port)
|
||||
}
|
||||
|
||||
func isLoopbackAddr(s string) bool {
|
||||
return strings.HasPrefix(s, "127.0.0.1:") || strings.HasPrefix(s, "127.0.0.1.")
|
||||
}
|
||||
|
||||
type nothing struct{}
|
||||
|
||||
// Lowest common denominator parser for "netstat -na" format.
|
||||
@@ -74,7 +78,7 @@ func parsePortsNetstat(output string) List {
|
||||
// not interested in non-listener sockets
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(laddr, "127.0.0.1:") || strings.HasPrefix(laddr, "127.0.0.1.") {
|
||||
if isLoopbackAddr(laddr) {
|
||||
// not interested in loopback-bound listeners
|
||||
continue
|
||||
}
|
||||
@@ -85,7 +89,7 @@ func parsePortsNetstat(output string) List {
|
||||
proto = "udp"
|
||||
laddr = cols[len(cols)-2]
|
||||
raddr = cols[len(cols)-1]
|
||||
if strings.HasPrefix(laddr, "127.0.0.1:") || strings.HasPrefix(laddr, "127.0.0.1.") {
|
||||
if isLoopbackAddr(laddr) {
|
||||
// not interested in loopback-bound listeners
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -10,12 +10,15 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/syncs"
|
||||
)
|
||||
|
||||
// Reading the sockfiles on Linux is very fast, so we can do it often.
|
||||
@@ -26,13 +29,30 @@ const pollInterval = 1 * time.Second
|
||||
var sockfiles = []string{"/proc/net/tcp", "/proc/net/udp"}
|
||||
var protos = []string{"tcp", "udp"}
|
||||
|
||||
var sawProcNetPermissionErr syncs.AtomicBool
|
||||
|
||||
func listPorts() (List, error) {
|
||||
if sawProcNetPermissionErr.Get() {
|
||||
return nil, nil
|
||||
}
|
||||
l := []Port{}
|
||||
|
||||
for pi, fname := range sockfiles {
|
||||
proto := protos[pi]
|
||||
|
||||
// Android 10+ doesn't allow access to this anymore.
|
||||
// https://developer.android.com/about/versions/10/privacy/changes#proc-net-filesystem
|
||||
// Ignore it rather than have the system log about our violation.
|
||||
if runtime.GOOS == "android" && syscall.Access(fname, unix.R_OK) != nil {
|
||||
sawProcNetPermissionErr.Set(true)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
f, err := os.Open(fname)
|
||||
if os.IsPermission(err) {
|
||||
sawProcNetPermissionErr.Set(true)
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%s: %s", fname, err)
|
||||
}
|
||||
@@ -96,7 +116,18 @@ func addProcesses(pl []Port) ([]Port, error) {
|
||||
}
|
||||
|
||||
err := foreachPID(func(pid string) error {
|
||||
fdDir, err := os.Open(fmt.Sprintf("/proc/%s/fd", pid))
|
||||
fdPath := fmt.Sprintf("/proc/%s/fd", pid)
|
||||
|
||||
// Android logs a bunch of audit violations in logcat
|
||||
// if we try to open things we don't have access
|
||||
// to. So on Android only, ask if we have permission
|
||||
// rather than just trying it to determine whether we
|
||||
// have permission.
|
||||
if runtime.GOOS == "android" && syscall.Access(fdPath, unix.R_OK) != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
fdDir, err := os.Open(fdPath)
|
||||
if err != nil {
|
||||
// Can't open fd list for this pid. Maybe
|
||||
// don't have access. Ignore it.
|
||||
|
||||
@@ -95,9 +95,12 @@ func addProcesses(pl []Port) ([]Port, error) {
|
||||
if port > 0 {
|
||||
pp := ProtoPort{proto, uint16(port)}
|
||||
p := m[pp]
|
||||
if p != nil {
|
||||
switch {
|
||||
case p != nil:
|
||||
p.Process = cmd
|
||||
} else {
|
||||
case isLoopbackAddr(val):
|
||||
// ignore
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "weird: missing %v\n", pp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,10 @@ type DERPRegion struct {
|
||||
// "fra", etc.
|
||||
RegionCode string
|
||||
|
||||
// RegionName is a long English name for the region: "New York City",
|
||||
// "San Francisco", "Singapore", "Frankfurt", etc.
|
||||
RegionName string
|
||||
|
||||
// Nodes are the DERP nodes running in this region, in
|
||||
// priority order for the current client. Client TLS
|
||||
// connections should ideally only go to the first entry
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
package tailcfg
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=User,Node,Hostinfo,NetInfo -output=tailcfg_clone.go
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=User,Node,Hostinfo,NetInfo,Group,Role,Capability -output=tailcfg_clone.go
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"go4.org/mem"
|
||||
"golang.org/x/oauth2"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/structs"
|
||||
@@ -260,6 +261,7 @@ type Hostinfo struct {
|
||||
OSVersion string // operating system version, with optional distro prefix ("Debian 10.4", "Windows 10 Pro 10.0.19041")
|
||||
DeviceModel string // mobile phone model ("Pixel 3a", "iPhone 11 Pro")
|
||||
Hostname string // name of the host the client runs on
|
||||
GoArch string // 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
|
||||
@@ -441,11 +443,12 @@ type RegisterResponse struct {
|
||||
type MapRequest struct {
|
||||
Version int // current version is 4
|
||||
Compress string // "zstd" or "" (no compression)
|
||||
KeepAlive bool // server sends keep-alives
|
||||
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
|
||||
Stream bool // if true, multiple MapResponse objects are returned
|
||||
Hostinfo *Hostinfo
|
||||
|
||||
@@ -491,15 +494,53 @@ var FilterAllowAll = []FilterRule{
|
||||
},
|
||||
}
|
||||
|
||||
// DNSConfig is the DNS configuration.
|
||||
type DNSConfig struct {
|
||||
// Nameservers are the IP addresses of the nameservers to use.
|
||||
Nameservers []netaddr.IP `json:",omitempty"`
|
||||
// Domains are the search domains to use.
|
||||
Domains []string `json:",omitempty"`
|
||||
// PerDomain indicates whether it is preferred to use Nameservers
|
||||
// only for DNS queries for subdomains of Domains.
|
||||
// Some OSes and OS configurations don't support per-domain DNS configuration,
|
||||
// in which case Nameservers applies to all DNS requests regardless of PerDomain's value.
|
||||
PerDomain bool
|
||||
// Proxied indicates whether DNS requests are proxied through a tsdns.Resolver.
|
||||
// This enables Magic DNS. It is togglable independently of PerDomain.
|
||||
Proxied bool
|
||||
}
|
||||
|
||||
type MapResponse struct {
|
||||
KeepAlive bool // if set, all other fields are ignored
|
||||
KeepAlive bool `json:",omitempty"` // if set, all other fields are ignored
|
||||
|
||||
// Networking
|
||||
Node *Node
|
||||
Peers []*Node
|
||||
DNS []wgcfg.IP
|
||||
SearchPaths []string
|
||||
DERPMap *DERPMap
|
||||
Node *Node
|
||||
DERPMap *DERPMap `json:",omitempty"` // if non-empty, a change in the DERP map.
|
||||
|
||||
// Peers, if non-empty, is the complete list of peers.
|
||||
// It will be set in the first MapResponse for a long-polled request/response.
|
||||
// Subsequent responses will be delta-encoded if DeltaPeers was set in the request.
|
||||
// If Peers is non-empty, PeersChanged and PeersRemoved should
|
||||
// be ignored (and should be empty).
|
||||
// Peers is always returned sorted by Node.ID.
|
||||
Peers []*Node `json:",omitempty"`
|
||||
// PeersChanged are the Nodes (identified by their ID) that
|
||||
// have changed or been added since the past update on the
|
||||
// HTTP response. It's only set if MapRequest.DeltaPeers was true.
|
||||
// PeersChanged is always returned sorted by Node.ID.
|
||||
PeersChanged []*Node `json:",omitempty"`
|
||||
// PeersRemoved are the NodeIDs that are no longer in the peer list.
|
||||
PeersRemoved []NodeID `json:",omitempty"`
|
||||
|
||||
// DNS is the same as DNSConfig.Nameservers.
|
||||
//
|
||||
// TODO(dmytro): should be sent in DNSConfig.Nameservers once clients have updated.
|
||||
DNS []wgcfg.IP `json:",omitempty"`
|
||||
// SearchPaths are the same as DNSConfig.Domains.
|
||||
//
|
||||
// TODO(dmytro): should be sent in DNSConfig.Domains once clients have updated.
|
||||
SearchPaths []string `json:",omitempty"`
|
||||
DNSConfig DNSConfig `json:",omitempty"`
|
||||
|
||||
// ACLs
|
||||
Domain string
|
||||
@@ -530,6 +571,17 @@ type Debug struct {
|
||||
// always do its background STUN queries (see magicsock's
|
||||
// periodicReSTUN), regardless of inactivity.
|
||||
ForceBackgroundSTUN bool `json:",omitempty"`
|
||||
|
||||
// DERPRoute controls whether the DERP reverse path
|
||||
// optimization (see Issue 150) should be enabled or
|
||||
// disabled. The environment variable in magicsock is the
|
||||
// highest priority (if set), then this (if set), then the
|
||||
// binary default value.
|
||||
DERPRoute opt.Bool `json:",omitempty"`
|
||||
|
||||
// TrimWGConfig controls whether Tailscale does lazy, on-demand
|
||||
// wireguard configuration of peers.
|
||||
TrimWGConfig opt.Bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (k MachineKey) String() string { return fmt.Sprintf("mkey:%x", k[:]) }
|
||||
@@ -595,6 +647,7 @@ func (n *Node) Equal(n2 *Node) bool {
|
||||
eqCIDRs(n.Addresses, n2.Addresses) &&
|
||||
eqCIDRs(n.AllowedIPs, n2.AllowedIPs) &&
|
||||
eqStrings(n.Endpoints, n2.Endpoints) &&
|
||||
n.DERP == n2.DERP &&
|
||||
n.Hostinfo.Equal(&n2.Hostinfo) &&
|
||||
n.Created.Equal(n2.Created) &&
|
||||
eqTimePtr(n.LastSeen, n2.LastSeen) &&
|
||||
|
||||
@@ -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; DO NOT EDIT.
|
||||
// Code generated by tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Group,Role,Capability; DO NOT EDIT.
|
||||
|
||||
package tailcfg
|
||||
|
||||
@@ -73,3 +73,38 @@ func (src *NetInfo) Clone() *NetInfo {
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// Clone makes a deep copy of Group.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Group) Clone() *Group {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Group)
|
||||
*dst = *src
|
||||
dst.Members = append(src.Members[:0:0], src.Members...)
|
||||
return dst
|
||||
}
|
||||
|
||||
// Clone makes a deep copy of Role.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Role) Clone() *Role {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Role)
|
||||
*dst = *src
|
||||
dst.Capabilities = append(src.Capabilities[:0:0], src.Capabilities...)
|
||||
return dst
|
||||
}
|
||||
|
||||
// Clone makes a deep copy of Capability.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Capability) Clone() *Capability {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Capability)
|
||||
*dst = *src
|
||||
return dst
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
|
||||
func TestHostinfoEqual(t *testing.T) {
|
||||
hiHandles := []string{
|
||||
"IPNVersion", "FrontendLogID", "BackendLogID", "OS", "OSVersion",
|
||||
"DeviceModel", "Hostname", "RoutableIPs", "RequestTags", "Services",
|
||||
"DeviceModel", "Hostname", "GoArch", "RoutableIPs", "RequestTags", "Services",
|
||||
"NetInfo",
|
||||
}
|
||||
if have := fieldsOf(reflect.TypeOf(Hostinfo{})); !reflect.DeepEqual(have, hiHandles) {
|
||||
@@ -315,6 +315,11 @@ func TestNodeEqual(t *testing.T) {
|
||||
&Node{LastSeen: &now},
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Node{DERP: "foo"},
|
||||
&Node{DERP: "bar"},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
got := tt.a.Equal(tt.b)
|
||||
|
||||
@@ -7,7 +7,10 @@ package tstest
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
type testLogWriter struct {
|
||||
@@ -41,3 +44,48 @@ func (panicLogWriter) Write(b []byte) (int, error) {
|
||||
func PanicOnLog() {
|
||||
log.SetOutput(panicLogWriter{})
|
||||
}
|
||||
|
||||
// ListenFor produces a LogListener wrapping a given logf with the given logStrings
|
||||
func ListenFor(logf logger.Logf, logStrings []string) *LogListener {
|
||||
ret := LogListener{
|
||||
logf: logf,
|
||||
listenFor: logStrings,
|
||||
seen: make(map[string]bool),
|
||||
}
|
||||
for _, line := range logStrings {
|
||||
ret.seen[line] = false
|
||||
}
|
||||
return &ret
|
||||
}
|
||||
|
||||
// LogListener takes a list of log lines to listen for
|
||||
type LogListener struct {
|
||||
logf logger.Logf
|
||||
listenFor []string
|
||||
|
||||
mu sync.Mutex
|
||||
seen map[string]bool
|
||||
}
|
||||
|
||||
// Logf records and logs a given line
|
||||
func (ll *LogListener) Logf(format string, args ...interface{}) {
|
||||
ll.mu.Lock()
|
||||
if _, ok := ll.seen[format]; ok {
|
||||
ll.seen[format] = true
|
||||
}
|
||||
ll.mu.Unlock()
|
||||
ll.logf(format, args)
|
||||
}
|
||||
|
||||
// Check returns which lines haven't been logged yet
|
||||
func (ll *LogListener) Check() []string {
|
||||
ll.mu.Lock()
|
||||
defer ll.mu.Unlock()
|
||||
var notSeen []string
|
||||
for _, line := range ll.listenFor {
|
||||
if !ll.seen[line] {
|
||||
notSeen = append(notSeen, line)
|
||||
}
|
||||
}
|
||||
return notSeen
|
||||
}
|
||||
|
||||
46
tstest/log_test.go
Normal file
46
tstest/log_test.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 tstest
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestLogListener(t *testing.T) {
|
||||
var (
|
||||
l1 = "line 1: %s"
|
||||
l2 = "line 2: %s"
|
||||
l3 = "line 3: %s"
|
||||
|
||||
lineList = []string{l1, l2}
|
||||
)
|
||||
|
||||
ll := ListenFor(t.Logf, lineList)
|
||||
|
||||
if len(ll.Check()) != len(lineList) {
|
||||
t.Errorf("expected %v, got %v", lineList, ll.Check())
|
||||
}
|
||||
|
||||
ll.Logf(l3, "hi")
|
||||
|
||||
if len(ll.Check()) != len(lineList) {
|
||||
t.Errorf("expected %v, got %v", lineList, ll.Check())
|
||||
}
|
||||
|
||||
ll.Logf(l1, "hi")
|
||||
|
||||
if len(ll.Check()) != len(lineList)-1 {
|
||||
t.Errorf("expected %v, got %v", lineList, ll.Check())
|
||||
}
|
||||
|
||||
ll.Logf(l1, "bye")
|
||||
|
||||
if len(ll.Check()) != len(lineList)-1 {
|
||||
t.Errorf("expected %v, got %v", lineList, ll.Check())
|
||||
}
|
||||
|
||||
ll.Logf(l2, "hi")
|
||||
if ll.Check() != nil {
|
||||
t.Errorf("expected empty list, got ll.Check()")
|
||||
}
|
||||
}
|
||||
@@ -93,9 +93,10 @@ func mustPrefix(s string) netaddr.IPPrefix {
|
||||
// NewInternet returns a network that simulates the internet.
|
||||
func NewInternet() *Network {
|
||||
return &Network{
|
||||
Name: "internet",
|
||||
Prefix4: mustPrefix("203.0.113.0/24"), // documentation netblock that looks Internet-y
|
||||
Prefix6: mustPrefix("fc00:52::/64"),
|
||||
Name: "internet",
|
||||
// easily recognizable internett-y addresses
|
||||
Prefix4: mustPrefix("1.0.0.0/24"),
|
||||
Prefix6: mustPrefix("1111::/64"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +185,17 @@ func (n *Network) write(p *Packet) (num int, err error) {
|
||||
defer n.mu.Unlock()
|
||||
iface, ok := n.machine[p.Dst.IP]
|
||||
if !ok {
|
||||
// If the destination is within the network's authoritative
|
||||
// range, no route to host.
|
||||
if p.Dst.IP.Is4() && n.Prefix4.Contains(p.Dst.IP) {
|
||||
p.Trace("no route to %v", p.Dst.IP)
|
||||
return len(p.Payload), nil
|
||||
}
|
||||
if p.Dst.IP.Is6() && n.Prefix6.Contains(p.Dst.IP) {
|
||||
p.Trace("no route to %v", p.Dst.IP)
|
||||
return len(p.Payload), nil
|
||||
}
|
||||
|
||||
if n.defaultGW == nil {
|
||||
p.Trace("no route to %v", p.Dst.IP)
|
||||
return len(p.Payload), nil
|
||||
|
||||
@@ -7,7 +7,6 @@ package tsweb
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
type response struct {
|
||||
@@ -16,119 +15,59 @@ type response struct {
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func responseSuccess(data interface{}) *response {
|
||||
return &response{
|
||||
Status: "success",
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
// TODO: Header
|
||||
|
||||
func responseError(e string) *response {
|
||||
return &response{
|
||||
Status: "error",
|
||||
Error: e,
|
||||
}
|
||||
}
|
||||
// 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".
|
||||
type JSONHandlerFunc func(r *http.Request) (status int, data interface{}, err error)
|
||||
|
||||
func writeResponse(w http.ResponseWriter, s int, resp *response) {
|
||||
b, _ := json.Marshal(resp)
|
||||
// ServeHTTP calls the JSONHandlerFunc and automatically marshals http responses.
|
||||
//
|
||||
// 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) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(s)
|
||||
var resp *response
|
||||
status, data, err := fn(r)
|
||||
if status == 0 {
|
||||
status = http.StatusInternalServerError
|
||||
resp = &response{
|
||||
Status: "error",
|
||||
Error: "internal server error",
|
||||
}
|
||||
} else if err == nil {
|
||||
resp = &response{
|
||||
Status: "success",
|
||||
Data: data,
|
||||
}
|
||||
} else {
|
||||
if werr, ok := err.(HTTPError); ok {
|
||||
resp = &response{
|
||||
Status: "error",
|
||||
Error: werr.Msg,
|
||||
Data: data,
|
||||
}
|
||||
} else {
|
||||
resp = &response{
|
||||
Status: "error",
|
||||
Error: "internal server error",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
b, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(`{"status":"error","error":"json marshal error"}`))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(status)
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
func checkFn(t reflect.Type) {
|
||||
h := reflect.TypeOf(http.HandlerFunc(nil))
|
||||
switch t.NumIn() {
|
||||
case 2, 3:
|
||||
if !t.In(0).AssignableTo(h.In(0)) {
|
||||
panic("first argument must be http.ResponseWriter")
|
||||
}
|
||||
if !t.In(1).AssignableTo(h.In(1)) {
|
||||
panic("second argument must be *http.Request")
|
||||
}
|
||||
default:
|
||||
panic("JSONHandler: number of input parameter should be 2 or 3")
|
||||
}
|
||||
|
||||
switch t.NumOut() {
|
||||
case 1:
|
||||
if !t.Out(0).Implements(reflect.TypeOf((*error)(nil)).Elem()) {
|
||||
panic("return value must be error")
|
||||
}
|
||||
case 2:
|
||||
if !t.Out(1).Implements(reflect.TypeOf((*error)(nil)).Elem()) {
|
||||
panic("second return value must be error")
|
||||
}
|
||||
default:
|
||||
panic("JSONHandler: number of return values should be 1 or 2")
|
||||
}
|
||||
}
|
||||
|
||||
// JSONHandler wraps an HTTP handler function with a version that automatically
|
||||
// unmarshals and marshals requests and responses respectively into fn's arguments
|
||||
// and results.
|
||||
//
|
||||
// The fn parameter is a function. It must take two or three input arguments.
|
||||
// The first two arguments must be http.ResponseWriter and *http.Request.
|
||||
// The optional third argument can be of any type representing the JSON input.
|
||||
// The function's results can be either (error) or (T, error), where T is the
|
||||
// JSON-marshalled result type.
|
||||
//
|
||||
// For example:
|
||||
// fn := func(w http.ResponseWriter, r *http.Request, in *Req) (*Res, error) { ... }
|
||||
func JSONHandler(fn interface{}) http.Handler {
|
||||
v := reflect.ValueOf(fn)
|
||||
t := v.Type()
|
||||
checkFn(t)
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
wv := reflect.ValueOf(w)
|
||||
rv := reflect.ValueOf(r)
|
||||
var vs []reflect.Value
|
||||
|
||||
switch t.NumIn() {
|
||||
case 2:
|
||||
vs = v.Call([]reflect.Value{wv, rv})
|
||||
case 3:
|
||||
dv := reflect.New(t.In(2))
|
||||
err := json.NewDecoder(r.Body).Decode(dv.Interface())
|
||||
if err != nil {
|
||||
writeResponse(w, http.StatusBadRequest, responseError("bad json"))
|
||||
return
|
||||
}
|
||||
vs = v.Call([]reflect.Value{wv, rv, dv.Elem()})
|
||||
default:
|
||||
panic("JSONHandler: number of input parameter should be 2 or 3")
|
||||
}
|
||||
|
||||
var e reflect.Value
|
||||
switch len(vs) {
|
||||
case 1:
|
||||
// todo support other error types
|
||||
if vs[0].IsZero() {
|
||||
writeResponse(w, http.StatusOK, responseSuccess(nil))
|
||||
return
|
||||
}
|
||||
e = vs[0]
|
||||
case 2:
|
||||
if vs[1].IsZero() {
|
||||
if !vs[0].IsZero() {
|
||||
writeResponse(w, http.StatusOK, responseSuccess(vs[0].Interface()))
|
||||
}
|
||||
return
|
||||
}
|
||||
e = vs[1]
|
||||
default:
|
||||
panic("JSONHandler: number of return values should be 1 or 2")
|
||||
}
|
||||
|
||||
if e.Type().AssignableTo(reflect.TypeOf(HTTPError{})) {
|
||||
err := e.Interface().(HTTPError)
|
||||
writeResponse(w, err.Code, responseError(err.Error()))
|
||||
} else {
|
||||
err := e.Interface().(error)
|
||||
writeResponse(w, http.StatusBadRequest, responseError(err.Error()))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,9 +5,8 @@
|
||||
package tsweb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -26,7 +25,7 @@ type Response struct {
|
||||
}
|
||||
|
||||
func TestNewJSONHandler(t *testing.T) {
|
||||
checkStatus := func(w *httptest.ResponseRecorder, status string) *Response {
|
||||
checkStatus := func(w *httptest.ResponseRecorder, status string, code int) *Response {
|
||||
d := &Response{
|
||||
Data: &Data{},
|
||||
}
|
||||
@@ -44,6 +43,10 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
t.Fatalf("wrong status: %s %s", d.Status, status)
|
||||
}
|
||||
|
||||
if w.Code != code {
|
||||
t.Fatalf("wrong status code: %d %d", w.Code, code)
|
||||
}
|
||||
|
||||
if w.Header().Get("Content-Type") != "application/json" {
|
||||
t.Fatalf("wrong content type: %s", w.Header().Get("Content-Type"))
|
||||
}
|
||||
@@ -51,163 +54,139 @@ func TestNewJSONHandler(t *testing.T) {
|
||||
return d
|
||||
}
|
||||
|
||||
// 2 1
|
||||
h21 := JSONHandler(func(w http.ResponseWriter, r *http.Request) error {
|
||||
return nil
|
||||
h21 := JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
return http.StatusOK, nil, nil
|
||||
})
|
||||
|
||||
t.Run("2 1 simple", func(t *testing.T) {
|
||||
t.Run("200 simple", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
h21.ServeHTTP(w, r)
|
||||
checkStatus(w, "success")
|
||||
checkStatus(w, "success", http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("2 1 HTTPError", func(t *testing.T) {
|
||||
h := JSONHandler(func(w http.ResponseWriter, r *http.Request) HTTPError {
|
||||
return Error(http.StatusForbidden, "forbidden", nil)
|
||||
t.Run("403 HTTPError", func(t *testing.T) {
|
||||
h := JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
return http.StatusForbidden, nil, fmt.Errorf("forbidden")
|
||||
})
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
h.ServeHTTP(w, r)
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Fatalf("wrong code: %d %d", w.Code, http.StatusForbidden)
|
||||
}
|
||||
checkStatus(w, "error", http.StatusForbidden)
|
||||
})
|
||||
|
||||
// 2 2
|
||||
h22 := JSONHandler(func(w http.ResponseWriter, r *http.Request) (*Data, error) {
|
||||
return &Data{Name: "tailscale"}, nil
|
||||
h22 := JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
return http.StatusOK, &Data{Name: "tailscale"}, nil
|
||||
})
|
||||
t.Run("2 2 get data", func(t *testing.T) {
|
||||
|
||||
t.Run("200 get data", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
h22.ServeHTTP(w, r)
|
||||
checkStatus(w, "success")
|
||||
checkStatus(w, "success", http.StatusOK)
|
||||
})
|
||||
|
||||
// 3 1
|
||||
h31 := JSONHandler(func(w http.ResponseWriter, r *http.Request, d *Data) error {
|
||||
if d.Name == "" {
|
||||
return errors.New("name is empty")
|
||||
h31 := JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
body := new(Data)
|
||||
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
return http.StatusBadRequest, nil, err
|
||||
}
|
||||
|
||||
return nil
|
||||
if body.Name == "" {
|
||||
return http.StatusBadRequest, nil, Error(http.StatusBadGateway, "name is empty", nil)
|
||||
}
|
||||
|
||||
return http.StatusOK, nil, nil
|
||||
})
|
||||
t.Run("3 1 post data", func(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)
|
||||
checkStatus(w, "success")
|
||||
checkStatus(w, "success", http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("3 1 bad json", func(t *testing.T) {
|
||||
t.Run("400 bad json", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{`))
|
||||
h31.ServeHTTP(w, r)
|
||||
checkStatus(w, "error")
|
||||
checkStatus(w, "error", http.StatusBadRequest)
|
||||
})
|
||||
|
||||
t.Run("3 1 post data error", func(t *testing.T) {
|
||||
t.Run("400 post data error", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{}`))
|
||||
h31.ServeHTTP(w, r)
|
||||
resp := checkStatus(w, "error")
|
||||
resp := checkStatus(w, "error", http.StatusBadRequest)
|
||||
if resp.Error != "name is empty" {
|
||||
t.Fatalf("wrong error")
|
||||
}
|
||||
})
|
||||
|
||||
// 3 2
|
||||
h32 := JSONHandler(func(w http.ResponseWriter, r *http.Request, d *Data) (*Data, error) {
|
||||
if d.Price == 0 {
|
||||
return nil, errors.New("price is empty")
|
||||
h32 := JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
body := new(Data)
|
||||
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
return http.StatusBadRequest, nil, err
|
||||
}
|
||||
if body.Name == "root" {
|
||||
return http.StatusInternalServerError, nil, fmt.Errorf("invalid name")
|
||||
}
|
||||
if body.Price == 0 {
|
||||
return http.StatusBadRequest, nil, Error(http.StatusBadGateway, "price is empty", nil)
|
||||
}
|
||||
|
||||
return &Data{Price: d.Price * 2}, nil
|
||||
return http.StatusOK, &Data{Price: body.Price * 2}, nil
|
||||
})
|
||||
t.Run("3 2 post data", func(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)
|
||||
resp := checkStatus(w, "success")
|
||||
resp := checkStatus(w, "success", http.StatusOK)
|
||||
t.Log(resp.Data)
|
||||
if resp.Data.Price != 20 {
|
||||
t.Fatalf("wrong price: %d %d", resp.Data.Price, 10)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("3 2 post data error", func(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)
|
||||
resp := checkStatus(w, "error")
|
||||
resp := checkStatus(w, "error", http.StatusBadRequest)
|
||||
if resp.Error != "price is empty" {
|
||||
t.Fatalf("wrong error")
|
||||
}
|
||||
})
|
||||
|
||||
// fn check
|
||||
shouldPanic := func() {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
t.Fatalf("should panic")
|
||||
}
|
||||
t.Log(r)
|
||||
}
|
||||
|
||||
t.Run("2 0 panic", func(t *testing.T) {
|
||||
defer shouldPanic()
|
||||
JSONHandler(func(w http.ResponseWriter, r *http.Request) {})
|
||||
})
|
||||
|
||||
t.Run("2 1 panic return value", func(t *testing.T) {
|
||||
defer shouldPanic()
|
||||
JSONHandler(func(w http.ResponseWriter, r *http.Request) string {
|
||||
return ""
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("2 1 panic arguments", func(t *testing.T) {
|
||||
defer shouldPanic()
|
||||
JSONHandler(func(r *http.Request, w http.ResponseWriter) error {
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("3 1 panic arguments", func(t *testing.T) {
|
||||
defer shouldPanic()
|
||||
JSONHandler(func(name string, r *http.Request, w http.ResponseWriter) error {
|
||||
return nil
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("3 2 panic return value", func(t *testing.T) {
|
||||
defer shouldPanic()
|
||||
//lint:ignore ST1008 intentional
|
||||
JSONHandler(func(name string, r *http.Request, w http.ResponseWriter) (error, string) {
|
||||
return nil, "panic"
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("2 2 forbidden", func(t *testing.T) {
|
||||
code := http.StatusForbidden
|
||||
body := []byte("forbidden")
|
||||
h := JSONHandler(func(w http.ResponseWriter, r *http.Request) (*Data, error) {
|
||||
w.WriteHeader(code)
|
||||
w.Write(body)
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
t.Run("500 internal server error", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("GET", "/", nil)
|
||||
h.ServeHTTP(w, r)
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Fatalf("wrong code: %d %d", w.Code, code)
|
||||
r := httptest.NewRequest("POST", "/", strings.NewReader(`{"Name": "root"}`))
|
||||
h32.ServeHTTP(w, r)
|
||||
resp := checkStatus(w, "error", http.StatusInternalServerError)
|
||||
if resp.Error != "internal server error" {
|
||||
t.Fatalf("wrong error")
|
||||
}
|
||||
if !bytes.Equal(w.Body.Bytes(), []byte("forbidden")) {
|
||||
t.Fatalf("wrong body: %s %s", w.Body.Bytes(), body)
|
||||
})
|
||||
|
||||
t.Run("500 misuse", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", nil)
|
||||
JSONHandlerFunc(func(r *http.Request) (int, interface{}, error) {
|
||||
return http.StatusOK, make(chan int), nil
|
||||
}).ServeHTTP(w, r)
|
||||
resp := checkStatus(w, "error", http.StatusInternalServerError)
|
||||
if resp.Error != "json marshal error" {
|
||||
t.Fatalf("wrong error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("500 empty status code", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", "/", nil)
|
||||
JSONHandlerFunc(func(r *http.Request) (status int, data interface{}, err error) {
|
||||
return
|
||||
}).ServeHTTP(w, r)
|
||||
checkStatus(w, "error", http.StatusInternalServerError)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -28,6 +28,8 @@ func NewPrivate() Private {
|
||||
if _, err := io.ReadFull(crand.Reader, p[:]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
p[0] &= 248
|
||||
p[31] = (p[31] & 127) | 64
|
||||
return p
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ package key
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
)
|
||||
|
||||
func TestTextUnmarshal(t *testing.T) {
|
||||
@@ -22,3 +24,31 @@ func TestTextUnmarshal(t *testing.T) {
|
||||
t.Fatalf("mismatch; got %x want %x", p2, p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClamping(t *testing.T) {
|
||||
t.Run("NewPrivate", func(t *testing.T) { testClamping(t, NewPrivate) })
|
||||
|
||||
// Also test the wgcfg package, as their behavior should match.
|
||||
t.Run("wgcfg", func(t *testing.T) {
|
||||
testClamping(t, func() Private {
|
||||
k, err := wgcfg.NewPrivateKey()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return Private(k)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func testClamping(t *testing.T, newKey func() Private) {
|
||||
for i := 0; i < 100; i++ {
|
||||
k := newKey()
|
||||
if k[0]&0b111 != 0 {
|
||||
t.Fatalf("Bogus clamping in first byte: %#08b", k[0])
|
||||
return
|
||||
}
|
||||
if k[31]>>6 != 1 {
|
||||
t.Fatalf("Bogus clamping in last byte: %#08b", k[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ package version
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"rsc.io/goversion/version"
|
||||
@@ -22,22 +23,27 @@ func CmdName() string {
|
||||
if err != nil {
|
||||
return "cmd"
|
||||
}
|
||||
|
||||
// fallbackName, the lowercase basename of the executable, is what we return if
|
||||
// we can't find the Go module metadata embedded in the file.
|
||||
fallbackName := filepath.Base(strings.TrimSuffix(strings.ToLower(e), ".exe"))
|
||||
|
||||
var ret string
|
||||
v, err := version.ReadExe(e)
|
||||
if err != nil {
|
||||
ret = strings.TrimSuffix(strings.ToLower(e), ".exe")
|
||||
} else {
|
||||
// v is like:
|
||||
// "path\ttailscale.com/cmd/tailscale\nmod\ttailscale.com\t(devel)\t\ndep\tgithub.com/apenwarr/fixconsole\tv0.0.0-20191012055117-5a9f6489cc29\th1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4=\ndep\tgithub....
|
||||
for _, line := range strings.Split(v.ModuleInfo, "\n") {
|
||||
if strings.HasPrefix(line, "path\t") {
|
||||
ret = path.Base(strings.TrimPrefix(line, "path\t"))
|
||||
break
|
||||
}
|
||||
return fallbackName
|
||||
}
|
||||
// v is like:
|
||||
// "path\ttailscale.com/cmd/tailscale\nmod\ttailscale.com\t(devel)\t\ndep\tgithub.com/apenwarr/fixconsole\tv0.0.0-20191012055117-5a9f6489cc29\th1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4=\ndep\tgithub....
|
||||
for _, line := range strings.Split(v.ModuleInfo, "\n") {
|
||||
if strings.HasPrefix(line, "path\t") {
|
||||
goPkg := strings.TrimPrefix(line, "path\t") // like "tailscale.com/cmd/tailscale"
|
||||
ret = path.Base(goPkg) // goPkg is always forward slashes; use path, not filepath
|
||||
break
|
||||
}
|
||||
}
|
||||
if ret == "" {
|
||||
return "cmd"
|
||||
return fallbackName
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -5,89 +5,107 @@ set -eu
|
||||
mode=$1
|
||||
describe=$2
|
||||
|
||||
long() {
|
||||
ver="${describe#v}"
|
||||
stem="${ver%%-*}"
|
||||
case "$stem" in
|
||||
*.*.*)
|
||||
# Full SemVer, nothing to do.
|
||||
semver="${stem}"
|
||||
# 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
|
||||
semver="${stem}.0"
|
||||
;;
|
||||
*)
|
||||
echo "Unparseable version $stem" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
suffix="${ver#$stem}"
|
||||
case "$suffix" in
|
||||
-*-*)
|
||||
# Has a change count in addition to the commit hash.
|
||||
*.*)
|
||||
# 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}"
|
||||
;;
|
||||
-*)
|
||||
# Missing change count, add one.
|
||||
suffix="-0${suffix}"
|
||||
;;
|
||||
*)
|
||||
echo "Unexpected version suffix" >&2
|
||||
exit 1
|
||||
esac
|
||||
echo "${semver}${suffix}"
|
||||
}
|
||||
|
||||
short() {
|
||||
ver="$(long)"
|
||||
case "$ver" in
|
||||
*-*-*)
|
||||
echo "${ver%-*}"
|
||||
;;
|
||||
*-*)
|
||||
echo "$ver"
|
||||
;;
|
||||
*)
|
||||
echo "Long version in invalid format" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
xcode() {
|
||||
ver=$(short | sed -e 's/-/./')
|
||||
major=$(echo "$ver" | cut -f1 -d.)
|
||||
minor=$(echo "$ver" | cut -f2 -d.)
|
||||
patch=$(echo "$ver" | cut -f3 -d.)
|
||||
changecount=$(echo "$ver" | cut -f4 -d.)
|
||||
|
||||
# Apple version numbers must be major.minor.patch. We have 4 fields
|
||||
# because we need major.minor.patch for go module compatibility, and
|
||||
# changecount for automatic version numbering of unstable builds. To
|
||||
# resolve this, for Apple builds, we combine changecount into patch:
|
||||
patch=$((patch*10000 + changecount))
|
||||
|
||||
# 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-123 -> 100.98.3123
|
||||
major=$((major + 100))
|
||||
echo "VERSION_ID = $major.$minor.$patch"
|
||||
}
|
||||
|
||||
case "$mode" in
|
||||
long)
|
||||
long
|
||||
;;
|
||||
short)
|
||||
short
|
||||
;;
|
||||
xcode)
|
||||
xcode
|
||||
;;
|
||||
esac
|
||||
|
||||
@@ -15,42 +15,60 @@ 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 {
|
||||
func mkversion(t *testing.T, mode, in string) (string, bool) {
|
||||
t.Helper()
|
||||
bs, err := exec.Command("./mkversion.sh", mode, in).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Logf("mkversion.sh output: %s", string(bs))
|
||||
t.Fatalf("mkversion.sh %s %s: %v", mode, in, err)
|
||||
return "", false
|
||||
}
|
||||
return strings.TrimSpace(string(bs))
|
||||
return strings.TrimSpace(string(bs)), true
|
||||
}
|
||||
|
||||
func TestMkversion(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
ok bool
|
||||
long string
|
||||
short string
|
||||
xcode string
|
||||
}{
|
||||
{"v0.98-abcdef", "0.98.0-0-abcdef", "0.98.0-0", xcode("0.98.0", "100.98.0")},
|
||||
{"v0.98-123-abcdef", "0.98.0-123-abcdef", "0.98.0-123", xcode("0.98.123", "100.98.123")},
|
||||
{"v0.99.5-123-abcdef", "0.99.5-123-abcdef", "0.99.5-123", xcode("0.99.50123", "100.99.50123")},
|
||||
{"v0.99.5-123-abcdef", "0.99.5-123-abcdef", "0.99.5-123", xcode("0.99.50123", "100.99.50123")},
|
||||
{"v2.3-0-abcdef", "2.3.0-0-abcdef", "2.3.0-0", xcode("2.3.0", "102.3.0")},
|
||||
{"1.2.3-4-abcdef", "1.2.3-4-abcdef", "1.2.3-4", xcode("1.2.30004", "101.2.30004")},
|
||||
{"v0.98-abcdef", true, "0.98.0-abcdef", "0.98.0", xcode("0.98.0", "100.98.0")},
|
||||
{"v0.98.1-abcdef", true, "0.98.1-abcdef", "0.98.1", xcode("0.98.1", "100.98.1")},
|
||||
{"v1.1.0-37-abcdef", true, "1.1.37-abcdef", "1.1.37", xcode("1.1.37", "101.1.37")},
|
||||
{"v1.2.9-abcdef", true, "1.2.9-abcdef", "1.2.9", xcode("1.2.9", "101.2.9")},
|
||||
{"v1.2.9-0-abcdef", true, "1.2.9-abcdef", "1.2.9", xcode("1.2.9", "101.2.9")},
|
||||
{"v1.15.0-129-abcdef", true, "1.15.129-abcdef", "1.15.129", xcode("1.15.129", "101.15.129")},
|
||||
|
||||
{"v0.98-123-abcdef", true, "0.0.0-abcdef", "0.0.0", xcode("0.0.0", "100.0.0")},
|
||||
{"v1.0.0-37-abcdef", true, "0.0.0-abcdef", "0.0.0", xcode("0.0.0", "100.0.0")},
|
||||
|
||||
{"v0.99.5-0-abcdef", false, "", "", ""}, // unstable, patch not allowed
|
||||
{"v0.99.5-123-abcdef", false, "", "", ""}, // unstable, patch not allowed
|
||||
{"v1-abcdef", false, "", "", ""}, // bad semver
|
||||
{"v1.0", false, "", "", ""}, // missing suffix
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
gotlong := mkversion(t, "long", test.in)
|
||||
gotshort := mkversion(t, "short", test.in)
|
||||
gotxcode := mkversion(t, "xcode", test.in)
|
||||
if gotlong != test.long {
|
||||
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)
|
||||
}
|
||||
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 gotshort != test.short {
|
||||
if shortOK && gotshort != test.short {
|
||||
t.Errorf("mkversion.sh short %q: got %q, want %q", test.in, gotshort, test.short)
|
||||
}
|
||||
if gotxcode != test.xcode {
|
||||
if xcodeOK && gotxcode != test.xcode {
|
||||
t.Errorf("mkversion.sh xcode %q: got %q, want %q", test.in, gotxcode, test.xcode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
// Package version provides the version that the binary was built at.
|
||||
package version
|
||||
|
||||
const LONG = "date.20200727"
|
||||
const LONG = "date.20200820"
|
||||
const SHORT = LONG
|
||||
|
||||
@@ -6,11 +6,14 @@
|
||||
package filter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golang/groupcache/lru"
|
||||
"golang.org/x/time/rate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine/packet"
|
||||
)
|
||||
@@ -128,6 +131,79 @@ func maybeHexdump(flag RunFlags, b []byte) string {
|
||||
return packet.Hexdump(b) + "\n"
|
||||
}
|
||||
|
||||
// MatchesFromFilterRules parse a number of wire-format FilterRule values into
|
||||
// the Matches format.
|
||||
// If an error is returned, the Matches result is still valid, containing the rules that
|
||||
// were successfully converted.
|
||||
func MatchesFromFilterRules(pf []tailcfg.FilterRule) (Matches, error) {
|
||||
mm := make([]Match, 0, len(pf))
|
||||
var erracc error
|
||||
|
||||
for _, r := range pf {
|
||||
m := Match{}
|
||||
|
||||
for i, s := range r.SrcIPs {
|
||||
bits := 32
|
||||
if len(r.SrcBits) > i {
|
||||
bits = r.SrcBits[i]
|
||||
}
|
||||
net, err := parseIP(s, bits)
|
||||
if err != nil && erracc == nil {
|
||||
erracc = err
|
||||
continue
|
||||
}
|
||||
m.Srcs = append(m.Srcs, net)
|
||||
}
|
||||
|
||||
for _, d := range r.DstPorts {
|
||||
bits := 32
|
||||
if d.Bits != nil {
|
||||
bits = *d.Bits
|
||||
}
|
||||
net, err := parseIP(d.IP, bits)
|
||||
if err != nil && erracc == nil {
|
||||
erracc = err
|
||||
continue
|
||||
}
|
||||
m.Dsts = append(m.Dsts, NetPortRange{
|
||||
Net: net,
|
||||
Ports: PortRange{
|
||||
First: d.Ports.First,
|
||||
Last: d.Ports.Last,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
mm = append(mm, m)
|
||||
}
|
||||
return mm, erracc
|
||||
}
|
||||
|
||||
func parseIP(host string, defaultBits int) (Net, error) {
|
||||
ip := net.ParseIP(host)
|
||||
if ip != nil && ip.IsUnspecified() {
|
||||
// For clarity, reject 0.0.0.0 as an input
|
||||
return NetNone, fmt.Errorf("ports=%#v: to allow all IP addresses, use *:port, not 0.0.0.0:port", host)
|
||||
} else if ip == nil && host == "*" {
|
||||
// User explicitly requested wildcard dst ip
|
||||
return NetAny, nil
|
||||
} else {
|
||||
if ip != nil {
|
||||
ip = ip.To4()
|
||||
}
|
||||
if ip == nil || len(ip) != 4 {
|
||||
return NetNone, fmt.Errorf("ports=%#v: invalid IPv4 address", host)
|
||||
}
|
||||
if len(ip) == 4 && (defaultBits < 0 || defaultBits > 32) {
|
||||
return NetNone, fmt.Errorf("invalid CIDR size %d for host %q", defaultBits, host)
|
||||
}
|
||||
return Net{
|
||||
IP: NewIP(ip),
|
||||
Mask: Netmask(defaultBits),
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(apenwarr): use a bigger bucket for specifically TCP SYN accept logging?
|
||||
// Logging is a quick way to record every newly opened TCP connection, but
|
||||
// we have to be cautious about flooding the logs vs letting people use
|
||||
@@ -136,9 +212,13 @@ func maybeHexdump(flag RunFlags, b []byte) string {
|
||||
var acceptBucket = rate.NewLimiter(rate.Every(10*time.Second), 3)
|
||||
var dropBucket = rate.NewLimiter(rate.Every(5*time.Second), 10)
|
||||
|
||||
func (f *Filter) logRateLimit(runflags RunFlags, q *packet.ParsedPacket, r Response, why string) {
|
||||
func (f *Filter) logRateLimit(runflags RunFlags, q *packet.ParsedPacket, dir direction, r Response, why string) {
|
||||
var verdict string
|
||||
|
||||
if r == Drop && omitDropLogging(q, dir) {
|
||||
return
|
||||
}
|
||||
|
||||
if r == Drop && (runflags&LogDrops) != 0 && dropBucket.Allow() {
|
||||
verdict = "Drop"
|
||||
runflags &= HexdumpDrops
|
||||
@@ -157,26 +237,28 @@ func (f *Filter) logRateLimit(runflags RunFlags, q *packet.ParsedPacket, r Respo
|
||||
|
||||
// RunIn determines whether this node is allowed to receive q from a Tailscale peer.
|
||||
func (f *Filter) RunIn(q *packet.ParsedPacket, rf RunFlags) Response {
|
||||
r := f.pre(q, rf)
|
||||
dir := in
|
||||
r := f.pre(q, rf, dir)
|
||||
if r == Accept || r == Drop {
|
||||
// already logged
|
||||
return r
|
||||
}
|
||||
|
||||
r, why := f.runIn(q)
|
||||
f.logRateLimit(rf, q, r, why)
|
||||
f.logRateLimit(rf, q, dir, r, why)
|
||||
return r
|
||||
}
|
||||
|
||||
// RunOut determines whether this node is allowed to send q to a Tailscale peer.
|
||||
func (f *Filter) RunOut(q *packet.ParsedPacket, rf RunFlags) Response {
|
||||
r := f.pre(q, rf)
|
||||
dir := out
|
||||
r := f.pre(q, rf, dir)
|
||||
if r == Drop || r == Accept {
|
||||
// already logged
|
||||
return r
|
||||
}
|
||||
r, why := f.runOut(q)
|
||||
f.logRateLimit(rf, q, r, why)
|
||||
f.logRateLimit(rf, q, dir, r, why)
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -188,6 +270,11 @@ func (f *Filter) runIn(q *packet.ParsedPacket) (r Response, why string) {
|
||||
return Drop, "destination not allowed"
|
||||
}
|
||||
|
||||
if q.IPVersion == 6 {
|
||||
// TODO: support IPv6.
|
||||
return Drop, "no rules matched"
|
||||
}
|
||||
|
||||
switch q.IPProto {
|
||||
case packet.ICMP:
|
||||
if q.IsEchoResponse() || q.IsError() {
|
||||
@@ -247,30 +334,106 @@ func (f *Filter) runOut(q *packet.ParsedPacket) (r Response, why string) {
|
||||
return Accept, "ok out"
|
||||
}
|
||||
|
||||
func (f *Filter) pre(q *packet.ParsedPacket, rf RunFlags) Response {
|
||||
// direction is whether a packet was flowing in to this machine, or flowing out.
|
||||
type direction int
|
||||
|
||||
const (
|
||||
in direction = iota
|
||||
out
|
||||
)
|
||||
|
||||
func (d direction) String() string {
|
||||
switch d {
|
||||
case in:
|
||||
return "in"
|
||||
case out:
|
||||
return "out"
|
||||
default:
|
||||
return fmt.Sprintf("[??dir=%d]", int(d))
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Filter) pre(q *packet.ParsedPacket, rf RunFlags, dir direction) Response {
|
||||
if len(q.Buffer()) == 0 {
|
||||
// wireguard keepalive packet, always permit.
|
||||
return Accept
|
||||
}
|
||||
if len(q.Buffer()) < 20 {
|
||||
f.logRateLimit(rf, q, Drop, "too short")
|
||||
f.logRateLimit(rf, q, dir, Drop, "too short")
|
||||
return Drop
|
||||
}
|
||||
|
||||
if q.IPVersion == 6 {
|
||||
f.logRateLimit(rf, q, dir, Drop, "ipv6")
|
||||
return Drop
|
||||
}
|
||||
switch q.IPProto {
|
||||
case packet.Unknown:
|
||||
// Unknown packets are dangerous; always drop them.
|
||||
f.logRateLimit(rf, q, Drop, "unknown")
|
||||
return Drop
|
||||
case packet.IPv6:
|
||||
f.logRateLimit(rf, q, Drop, "ipv6")
|
||||
f.logRateLimit(rf, q, dir, Drop, "unknown")
|
||||
return Drop
|
||||
case packet.Fragment:
|
||||
// Fragments after the first always need to be passed through.
|
||||
// Very small fragments are considered Junk by ParsedPacket.
|
||||
f.logRateLimit(rf, q, Accept, "fragment")
|
||||
f.logRateLimit(rf, q, dir, Accept, "fragment")
|
||||
return Accept
|
||||
}
|
||||
|
||||
return noVerdict
|
||||
}
|
||||
|
||||
const (
|
||||
// ipv6AllRoutersLinkLocal is ff02::2 (All link-local routers)
|
||||
ipv6AllRoutersLinkLocal = "\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02"
|
||||
// ipv6AllMLDv2CapableRouters is ff02::16 (All MLDv2-capable routers)
|
||||
ipv6AllMLDv2CapableRouters = "\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16"
|
||||
)
|
||||
|
||||
// omitDropLogging reports whether packet p, which has already been
|
||||
// deemded a packet to Drop, should bypass the [rate-limited] logging.
|
||||
// We don't want to log scary & spammy reject warnings for packets that
|
||||
// are totally normal, like IPv6 route announcements.
|
||||
func omitDropLogging(p *packet.ParsedPacket, dir direction) bool {
|
||||
b := p.Buffer()
|
||||
switch dir {
|
||||
case out:
|
||||
switch p.IPVersion {
|
||||
case 4:
|
||||
// ParsedPacket.Decode zeros out ParsedPacket.IPProtocol for protocols
|
||||
// it doesn't know about, so parse it out ourselves if needed.
|
||||
ipProto := p.IPProto
|
||||
if ipProto == 0 && len(b) > 8 {
|
||||
ipProto = packet.IPProto(b[9])
|
||||
}
|
||||
// Omit logging about outgoing IGMP.
|
||||
if ipProto == packet.IGMP {
|
||||
return true
|
||||
}
|
||||
case 6:
|
||||
if len(b) < 40 {
|
||||
return false
|
||||
}
|
||||
src, dst := b[8:8+16], b[24:24+16]
|
||||
// Omit logging for outgoing IPv6 ICMP-v6 queries to ff02::2,
|
||||
// as sent by the OS, looking for routers.
|
||||
if p.IPProto == packet.ICMPv6 {
|
||||
if isLinkLocalV6(src) && string(dst) == ipv6AllRoutersLinkLocal {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if string(dst) == ipv6AllMLDv2CapableRouters {
|
||||
return true
|
||||
}
|
||||
// Actually, just catch all multicast.
|
||||
if dst[0] == 0xff {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isLinkLocalV6 reports whether src is in fe80::/10.
|
||||
func isLinkLocalV6(src []byte) bool {
|
||||
return len(src) == 16 && src[0] == 0xfe && src[1]>>6 == 0x80>>6
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@ package filter
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
@@ -160,6 +163,35 @@ func TestNoAllocs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIP(t *testing.T) {
|
||||
tests := []struct {
|
||||
host string
|
||||
bits int
|
||||
want Net
|
||||
wantErr string
|
||||
}{
|
||||
{"8.8.8.8", 24, Net{IP: packet.NewIP(net.ParseIP("8.8.8.8")), Mask: packet.NewIP(net.ParseIP("255.255.255.0"))}, ""},
|
||||
{"8.8.8.8", 33, Net{}, `invalid CIDR size 33 for host "8.8.8.8"`},
|
||||
{"8.8.8.8", -1, Net{}, `invalid CIDR size -1 for host "8.8.8.8"`},
|
||||
{"0.0.0.0", 24, Net{}, `ports="0.0.0.0": to allow all IP addresses, use *:port, not 0.0.0.0:port`},
|
||||
{"*", 24, NetAny, ""},
|
||||
{"fe80::1", 128, NetNone, `ports="fe80::1": invalid IPv4 address`},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got, err := parseIP(tt.host, tt.bits)
|
||||
if err != nil {
|
||||
if err.Error() == tt.wantErr {
|
||||
continue
|
||||
}
|
||||
t.Errorf("parseIP(%q, %v) error: %v; want error %q", tt.host, tt.bits, err, tt.wantErr)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("parseIP(%q, %v) = %#v; want %#v", tt.host, tt.bits, got, tt.want)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFilter(b *testing.B) {
|
||||
acl := newFilter(b.Logf)
|
||||
|
||||
@@ -219,7 +251,7 @@ func TestPreFilter(t *testing.T) {
|
||||
for _, testPacket := range packets {
|
||||
p := &ParsedPacket{}
|
||||
p.Decode(testPacket.b)
|
||||
got := f.pre(p, LogDrops|LogAccepts)
|
||||
got := f.pre(p, LogDrops|LogAccepts, in)
|
||||
if got != testPacket.want {
|
||||
t.Errorf("%q got=%v want=%v packet:\n%s", testPacket.desc, got, testPacket.want, packet.Hexdump(testPacket.b))
|
||||
}
|
||||
@@ -298,3 +330,63 @@ func rawdefault(proto packet.IPProto, trimLength int) []byte {
|
||||
port := uint16(53)
|
||||
return rawpacket(proto, ip, ip, port, port, trimLength)
|
||||
}
|
||||
|
||||
func parseHexPkt(t *testing.T, h string) *packet.ParsedPacket {
|
||||
t.Helper()
|
||||
b, err := hex.DecodeString(strings.ReplaceAll(h, " ", ""))
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read hex %q: %v", h, err)
|
||||
}
|
||||
p := new(packet.ParsedPacket)
|
||||
p.Decode(b)
|
||||
return p
|
||||
}
|
||||
|
||||
func TestOmitDropLogging(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pkt *packet.ParsedPacket
|
||||
dir direction
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "v4_tcp_out",
|
||||
pkt: &packet.ParsedPacket{IPVersion: 4, IPProto: packet.TCP},
|
||||
dir: out,
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "v6_icmp_out", // as seen on Linux
|
||||
pkt: parseHexPkt(t, "60 00 00 00 00 00 3a 00 fe800000000000000000000000000000 ff020000000000000000000000000002"),
|
||||
dir: out,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "v6_to_MLDv2_capable_routers", // as seen on Windows
|
||||
pkt: parseHexPkt(t, "60 00 00 00 00 24 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff 02 00 00 00 00 00 00 00 00 00 00 00 00 00 16 3a 00 05 02 00 00 01 00 8f 00 6e 80 00 00 00 01 04 00 00 00 ff 02 00 00 00 00 00 00 00 00 00 00 00 00 00 0c"),
|
||||
dir: out,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "v4_igmp_out", // on Windows, from https://github.com/tailscale/tailscale/issues/618
|
||||
pkt: parseHexPkt(t, "46 00 00 30 37 3a 00 00 01 02 10 0e a9 fe 53 6b e0 00 00 16 94 04 00 00 22 00 14 05 00 00 00 02 04 00 00 00 e0 00 00 fb 04 00 00 00 e0 00 00 fc"),
|
||||
dir: out,
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "v6_udp_multicast",
|
||||
pkt: parseHexPkt(t, "60 00 00 00 00 00 11 00 fe800000000000007dc6bc04499262a3 ff120000000000000000000000008384"),
|
||||
dir: out,
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := omitDropLogging(tt.pkt, tt.dir)
|
||||
if got != tt.want {
|
||||
t.Errorf("got %v; want %v\npacket: %#v\n%s", got, tt.want, tt.pkt, packet.Hexdump(tt.pkt.Buffer()))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,19 +6,23 @@ package magicsock
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/tailscale/wireguard-go/device"
|
||||
@@ -26,9 +30,11 @@ import (
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/derp/derpmap"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/stun/stuntest"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
@@ -66,11 +72,6 @@ func runDERPAndStun(t *testing.T, logf logger.Logf, l nettype.PacketListener, st
|
||||
t.Fatal(err)
|
||||
}
|
||||
d := derp.NewServer(serverPrivateKey, logf)
|
||||
if l != (nettype.Std{}) {
|
||||
// When using virtual networking, only allow DERP to forward
|
||||
// discovery traffic, not actual packets.
|
||||
d.OnlyDisco = true
|
||||
}
|
||||
|
||||
httpsrv := httptest.NewUnstartedServer(derphttp.Handler(d))
|
||||
httpsrv.Config.ErrorLog = logger.StdLogger(logf)
|
||||
@@ -172,7 +173,7 @@ func newMagicStack(t *testing.T, logf logger.Logf, l nettype.PacketListener, der
|
||||
// Wait for first endpoint update to be available
|
||||
deadline := time.Now().Add(2 * time.Second)
|
||||
for len(epCh) == 0 && time.Now().Before(deadline) {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
return &magicStack{
|
||||
@@ -185,11 +186,136 @@ func newMagicStack(t *testing.T, logf logger.Logf, l nettype.PacketListener, der
|
||||
}
|
||||
}
|
||||
|
||||
func (s *magicStack) String() string {
|
||||
pub := s.Public()
|
||||
return pub.ShortString()
|
||||
}
|
||||
|
||||
func (s *magicStack) Close() {
|
||||
s.dev.Close()
|
||||
s.conn.Close()
|
||||
}
|
||||
|
||||
func (s *magicStack) Public() key.Public {
|
||||
return key.Public(s.privateKey.Public())
|
||||
}
|
||||
|
||||
func (s *magicStack) Status() *ipnstate.Status {
|
||||
var sb ipnstate.StatusBuilder
|
||||
s.conn.UpdateStatus(&sb)
|
||||
return sb.Status()
|
||||
}
|
||||
|
||||
// IP returns the Tailscale IP address assigned to this magicStack.
|
||||
//
|
||||
// Something external needs to provide a NetworkMap and WireGuard
|
||||
// configs to the magicStack in order for it to acquire an IP
|
||||
// address. See meshStacks for one possible source of netmaps and IPs.
|
||||
func (s *magicStack) IP(t *testing.T) netaddr.IP {
|
||||
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(10 * time.Millisecond) {
|
||||
st := s.Status()
|
||||
if len(st.TailscaleIPs) > 0 {
|
||||
return st.TailscaleIPs[0]
|
||||
}
|
||||
}
|
||||
t.Fatal("timed out waiting for magicstack to get an IP assigned")
|
||||
panic("unreachable") // compiler doesn't know t.Fatal panics
|
||||
}
|
||||
|
||||
// meshStacks monitors epCh on all given ms, and plumbs network maps
|
||||
// and WireGuard configs into everyone to form a full mesh that has up
|
||||
// to date endpoint info. Think of it as an extremely stripped down
|
||||
// and purpose-built Tailscale control plane.
|
||||
//
|
||||
// meshStacks only supports disco connections, not legacy logic.
|
||||
func meshStacks(logf logger.Logf, ms []*magicStack) (cleanup func()) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
// Serialize all reconfigurations globally, just to keep things
|
||||
// simpler.
|
||||
var (
|
||||
mu sync.Mutex
|
||||
eps = make([][]string, len(ms))
|
||||
)
|
||||
|
||||
buildNetmapLocked := func(myIdx int) *controlclient.NetworkMap {
|
||||
me := ms[myIdx]
|
||||
nm := &controlclient.NetworkMap{
|
||||
PrivateKey: me.privateKey,
|
||||
NodeKey: tailcfg.NodeKey(me.privateKey.Public()),
|
||||
Addresses: []wgcfg.CIDR{{IP: wgcfg.IPv4(1, 0, 0, byte(myIdx+1)), Mask: 32}},
|
||||
}
|
||||
for i, peer := range ms {
|
||||
if i == myIdx {
|
||||
continue
|
||||
}
|
||||
addrs := []wgcfg.CIDR{{IP: wgcfg.IPv4(1, 0, 0, byte(i+1)), Mask: 32}}
|
||||
peer := &tailcfg.Node{
|
||||
ID: tailcfg.NodeID(i + 1),
|
||||
Name: fmt.Sprintf("node%d", i+1),
|
||||
Key: tailcfg.NodeKey(peer.privateKey.Public()),
|
||||
DiscoKey: peer.conn.DiscoPublicKey(),
|
||||
Addresses: addrs,
|
||||
AllowedIPs: addrs,
|
||||
Endpoints: eps[i],
|
||||
DERP: "127.3.3.40:1",
|
||||
}
|
||||
nm.Peers = append(nm.Peers, peer)
|
||||
}
|
||||
|
||||
return nm
|
||||
}
|
||||
|
||||
updateEps := func(idx int, newEps []string) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
eps[idx] = newEps
|
||||
|
||||
for i, m := range ms {
|
||||
netmap := buildNetmapLocked(i)
|
||||
m.conn.SetNetworkMap(netmap)
|
||||
peerSet := make(map[key.Public]struct{}, len(netmap.Peers))
|
||||
for _, peer := range netmap.Peers {
|
||||
peerSet[key.Public(peer.Key)] = struct{}{}
|
||||
}
|
||||
m.conn.UpdatePeers(peerSet)
|
||||
wg, err := netmap.WGCfg(logf, controlclient.AllowSingleHosts)
|
||||
if err != nil {
|
||||
// We're too far from the *testing.T to be graceful,
|
||||
// blow up. Shouldn't happen anyway.
|
||||
panic(fmt.Sprintf("failed to construct wgcfg from netmap: %v", err))
|
||||
}
|
||||
if err := m.dev.Reconfig(wg); err != nil {
|
||||
panic(fmt.Sprintf("device reconfig failed: %v", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(ms))
|
||||
for i := range ms {
|
||||
go func(myIdx int) {
|
||||
defer wg.Done()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case eps := <-ms[myIdx].epCh:
|
||||
logf("conn%d endpoints update", myIdx+1)
|
||||
updateEps(myIdx, eps)
|
||||
}
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
return func() {
|
||||
cancel()
|
||||
wg.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewConn(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
rc := tstest.NewResourceCheck()
|
||||
@@ -433,65 +559,136 @@ func makeNestable(t *testing.T) (logf logger.Logf, setT func(t *testing.T)) {
|
||||
}
|
||||
|
||||
func TestTwoDevicePing(t *testing.T) {
|
||||
t.Run("real", func(t *testing.T) {
|
||||
l, ip := nettype.Std{}, netaddr.IPv4(127, 0, 0, 1)
|
||||
l, ip := nettype.Std{}, netaddr.IPv4(127, 0, 0, 1)
|
||||
n := &devices{
|
||||
m1: l,
|
||||
m1IP: ip,
|
||||
m2: l,
|
||||
m2IP: ip,
|
||||
stun: l,
|
||||
stunIP: ip,
|
||||
}
|
||||
testTwoDevicePing(t, n)
|
||||
}
|
||||
|
||||
func TestActiveDiscovery(t *testing.T) {
|
||||
t.Run("simple_internet", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
mstun := &natlab.Machine{Name: "stun"}
|
||||
m1 := &natlab.Machine{Name: "m1"}
|
||||
m2 := &natlab.Machine{Name: "m2"}
|
||||
inet := natlab.NewInternet()
|
||||
sif := mstun.Attach("eth0", inet)
|
||||
m1if := m1.Attach("eth0", inet)
|
||||
m2if := m2.Attach("eth0", inet)
|
||||
|
||||
n := &devices{
|
||||
m1: l,
|
||||
m1IP: ip,
|
||||
m2: l,
|
||||
m2IP: ip,
|
||||
stun: l,
|
||||
stunIP: ip,
|
||||
m1: m1,
|
||||
m1IP: m1if.V4(),
|
||||
m2: m2,
|
||||
m2IP: m2if.V4(),
|
||||
stun: mstun,
|
||||
stunIP: sif.V4(),
|
||||
}
|
||||
testTwoDevicePing(t, n)
|
||||
testActiveDiscovery(t, n)
|
||||
})
|
||||
t.Run("natlab", func(t *testing.T) {
|
||||
t.Run("simple internet", func(t *testing.T) {
|
||||
mstun := &natlab.Machine{Name: "stun"}
|
||||
m1 := &natlab.Machine{Name: "m1"}
|
||||
m2 := &natlab.Machine{Name: "m2"}
|
||||
inet := natlab.NewInternet()
|
||||
sif := mstun.Attach("eth0", inet)
|
||||
m1if := m1.Attach("eth0", inet)
|
||||
m2if := m2.Attach("eth0", inet)
|
||||
|
||||
n := &devices{
|
||||
m1: m1,
|
||||
m1IP: m1if.V4(),
|
||||
m2: m2,
|
||||
m2IP: m2if.V4(),
|
||||
stun: mstun,
|
||||
stunIP: sif.V4(),
|
||||
}
|
||||
testTwoDevicePing(t, n)
|
||||
})
|
||||
t.Run("facing_easy_firewalls", func(t *testing.T) {
|
||||
mstun := &natlab.Machine{Name: "stun"}
|
||||
m1 := &natlab.Machine{
|
||||
Name: "m1",
|
||||
PacketHandler: &natlab.Firewall{},
|
||||
}
|
||||
m2 := &natlab.Machine{
|
||||
Name: "m2",
|
||||
PacketHandler: &natlab.Firewall{},
|
||||
}
|
||||
inet := natlab.NewInternet()
|
||||
sif := mstun.Attach("eth0", inet)
|
||||
m1if := m1.Attach("eth0", inet)
|
||||
m2if := m2.Attach("eth0", inet)
|
||||
|
||||
t.Run("facing firewalls", func(t *testing.T) {
|
||||
mstun := &natlab.Machine{Name: "stun"}
|
||||
m1 := &natlab.Machine{
|
||||
Name: "m1",
|
||||
PacketHandler: &natlab.Firewall{},
|
||||
}
|
||||
m2 := &natlab.Machine{
|
||||
Name: "m2",
|
||||
PacketHandler: &natlab.Firewall{},
|
||||
}
|
||||
inet := natlab.NewInternet()
|
||||
sif := mstun.Attach("eth0", inet)
|
||||
m1if := m1.Attach("eth0", inet)
|
||||
m2if := m2.Attach("eth0", inet)
|
||||
|
||||
n := &devices{
|
||||
m1: m1,
|
||||
m1IP: m1if.V4(),
|
||||
m2: m2,
|
||||
m2IP: m2if.V4(),
|
||||
stun: mstun,
|
||||
stunIP: sif.V4(),
|
||||
}
|
||||
testTwoDevicePing(t, n)
|
||||
})
|
||||
n := &devices{
|
||||
m1: m1,
|
||||
m1IP: m1if.V4(),
|
||||
m2: m2,
|
||||
m2IP: m2if.V4(),
|
||||
stun: mstun,
|
||||
stunIP: sif.V4(),
|
||||
}
|
||||
testActiveDiscovery(t, n)
|
||||
})
|
||||
|
||||
t.Run("facing_nats", func(t *testing.T) {
|
||||
mstun := &natlab.Machine{Name: "stun"}
|
||||
m1 := &natlab.Machine{
|
||||
Name: "m1",
|
||||
PacketHandler: &natlab.Firewall{},
|
||||
}
|
||||
nat1 := &natlab.Machine{
|
||||
Name: "nat1",
|
||||
}
|
||||
m2 := &natlab.Machine{
|
||||
Name: "m2",
|
||||
PacketHandler: &natlab.Firewall{},
|
||||
}
|
||||
nat2 := &natlab.Machine{
|
||||
Name: "nat2",
|
||||
}
|
||||
|
||||
inet := natlab.NewInternet()
|
||||
lan1 := &natlab.Network{
|
||||
Name: "lan1",
|
||||
Prefix4: mustPrefix("192.168.0.0/24"),
|
||||
}
|
||||
lan2 := &natlab.Network{
|
||||
Name: "lan2",
|
||||
Prefix4: mustPrefix("192.168.1.0/24"),
|
||||
}
|
||||
|
||||
sif := mstun.Attach("eth0", inet)
|
||||
nat1WAN := nat1.Attach("wan", inet)
|
||||
nat1LAN := nat1.Attach("lan1", lan1)
|
||||
nat2WAN := nat2.Attach("wan", inet)
|
||||
nat2LAN := nat2.Attach("lan2", lan2)
|
||||
m1if := m1.Attach("eth0", lan1)
|
||||
m2if := m2.Attach("eth0", lan2)
|
||||
lan1.SetDefaultGateway(nat1LAN)
|
||||
lan2.SetDefaultGateway(nat2LAN)
|
||||
|
||||
nat1.PacketHandler = &natlab.SNAT44{
|
||||
Machine: nat1,
|
||||
ExternalInterface: nat1WAN,
|
||||
Firewall: &natlab.Firewall{
|
||||
TrustedInterface: nat1LAN,
|
||||
},
|
||||
}
|
||||
nat2.PacketHandler = &natlab.SNAT44{
|
||||
Machine: nat2,
|
||||
ExternalInterface: nat2WAN,
|
||||
Firewall: &natlab.Firewall{
|
||||
TrustedInterface: nat2LAN,
|
||||
},
|
||||
}
|
||||
|
||||
n := &devices{
|
||||
m1: m1,
|
||||
m1IP: m1if.V4(),
|
||||
m2: m2,
|
||||
m2IP: m2if.V4(),
|
||||
stun: mstun,
|
||||
stunIP: sif.V4(),
|
||||
}
|
||||
testActiveDiscovery(t, n)
|
||||
})
|
||||
}
|
||||
|
||||
func mustPrefix(s string) netaddr.IPPrefix {
|
||||
pfx, err := netaddr.ParseIPPrefix(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return pfx
|
||||
}
|
||||
|
||||
type devices struct {
|
||||
@@ -505,6 +702,135 @@ type devices struct {
|
||||
stunIP netaddr.IP
|
||||
}
|
||||
|
||||
// newPinger starts continuously sending test packets from srcM to
|
||||
// dstM, until cleanup is invoked to stop it. Each ping has 1 second
|
||||
// to transit the network. It is a test failure to lose a ping.
|
||||
func newPinger(t *testing.T, logf logger.Logf, src, dst *magicStack) (cleanup func()) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
done := make(chan struct{})
|
||||
one := func() bool {
|
||||
// TODO(danderson): requiring exactly zero packet loss
|
||||
// will probably be too strict for some tests we'd like to
|
||||
// run (e.g. discovery switching to a new path on
|
||||
// failure). Figure out what kind of thing would be
|
||||
// acceptable to test instead of "every ping must
|
||||
// transit".
|
||||
pkt := tuntest.Ping(dst.IP(t).IPAddr().IP, src.IP(t).IPAddr().IP)
|
||||
select {
|
||||
case src.tun.Outbound <- pkt:
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
}
|
||||
select {
|
||||
case <-dst.tun.Inbound:
|
||||
return true
|
||||
case <-time.After(10 * time.Second):
|
||||
// Very generous timeout here because depending on
|
||||
// magicsock setup races, the first handshake might get
|
||||
// eaten by the receiving end (if wireguard-go hasn't been
|
||||
// configured quite yet), so we have to wait for at least
|
||||
// the first retransmit from wireguard before we declare
|
||||
// failure.
|
||||
t.Errorf("timed out waiting for ping to transit")
|
||||
return true
|
||||
case <-ctx.Done():
|
||||
// Try a little bit longer to consume the packet we're
|
||||
// waiting for. This is to deal with shutdown races, where
|
||||
// natlab may still be delivering a packet to us from a
|
||||
// goroutine.
|
||||
select {
|
||||
case <-dst.tun.Inbound:
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
cleanup = func() {
|
||||
cancel()
|
||||
<-done
|
||||
}
|
||||
|
||||
// Synchronously transit one ping to get things started. This is
|
||||
// nice because it means that newPinger returning means we've
|
||||
// worked through initial connectivity.
|
||||
if !one() {
|
||||
cleanup()
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
logf("sending ping stream from %s (%s) to %s (%s)", src, src.IP(t), dst, dst.IP(t))
|
||||
defer close(done)
|
||||
for one() {
|
||||
}
|
||||
}()
|
||||
|
||||
return cleanup
|
||||
}
|
||||
|
||||
// testActiveDiscovery verifies that two magicStacks tied to the given
|
||||
// devices can establish a direct p2p connection with each other. See
|
||||
// TestActiveDiscovery for the various configurations of devices that
|
||||
// get exercised.
|
||||
func testActiveDiscovery(t *testing.T, d *devices) {
|
||||
tstest.PanicOnLog()
|
||||
rc := tstest.NewResourceCheck()
|
||||
defer rc.Assert(t)
|
||||
|
||||
tlogf, setT := makeNestable(t)
|
||||
setT(t)
|
||||
|
||||
start := time.Now()
|
||||
logf := func(msg string, args ...interface{}) {
|
||||
msg = fmt.Sprintf("%s: %s", time.Since(start), msg)
|
||||
tlogf(msg, args...)
|
||||
}
|
||||
|
||||
derpMap, cleanup := runDERPAndStun(t, logf, d.stun, d.stunIP)
|
||||
defer cleanup()
|
||||
|
||||
m1 := newMagicStack(t, logger.WithPrefix(logf, "conn1: "), d.m1, derpMap)
|
||||
defer m1.Close()
|
||||
m2 := newMagicStack(t, logger.WithPrefix(logf, "conn2: "), d.m2, derpMap)
|
||||
defer m2.Close()
|
||||
|
||||
cleanup = meshStacks(logf, []*magicStack{m1, m2})
|
||||
defer cleanup()
|
||||
|
||||
m1IP := m1.IP(t)
|
||||
m2IP := m2.IP(t)
|
||||
logf("IPs: %s %s", m1IP, m2IP)
|
||||
|
||||
cleanup = newPinger(t, logf, m1, m2)
|
||||
defer cleanup()
|
||||
|
||||
// Everything is now up and running, active discovery should find
|
||||
// a direct path between our peers. Wait for it to switch away
|
||||
// from DERP.
|
||||
|
||||
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) {
|
||||
pst := m1.Status().Peer[m2.Public()]
|
||||
if pst.CurAddr != "" {
|
||||
logf("direct link %s->%s found with addr %s", m1, m2, pst.CurAddr)
|
||||
return
|
||||
}
|
||||
if now := time.Now(); now.Sub(lastLog) > time.Second {
|
||||
logf("no direct path %s->%s yet, addrs %v", m1, m2, pst.Addrs)
|
||||
lastLog = now
|
||||
}
|
||||
}
|
||||
t.Errorf("magicsock did not find a direct path from %s to %s", m1, m2)
|
||||
}
|
||||
|
||||
mustDirect(m1, m2)
|
||||
mustDirect(m2, m1)
|
||||
|
||||
logf("starting cleanup")
|
||||
}
|
||||
|
||||
func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
tstest.PanicOnLog()
|
||||
rc := tstest.NewResourceCheck()
|
||||
@@ -604,7 +930,7 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
})
|
||||
|
||||
// TODO: Remove this once the following tests are reliable.
|
||||
if os.Getenv("RUN_CURSED_TESTS") == "" {
|
||||
if run, _ := strconv.ParseBool(os.Getenv("RUN_CURSED_TESTS")); !run {
|
||||
t.Skip("skipping following tests because RUN_CURSED_TESTS is not set.")
|
||||
}
|
||||
|
||||
@@ -702,10 +1028,8 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
defer setT(outerT)
|
||||
defer func() {
|
||||
if t.Failed() || true {
|
||||
uapi1, _ := cfgs[0].ToUAPI()
|
||||
logf("cfg0: %v", uapi1)
|
||||
uapi2, _ := cfgs[1].ToUAPI()
|
||||
logf("cfg1: %v", uapi2)
|
||||
logf("cfg0: %v", stringifyConfig(cfgs[0]))
|
||||
logf("cfg1: %v", stringifyConfig(cfgs[1]))
|
||||
}
|
||||
}()
|
||||
pingSeq(t, 20, 0, true)
|
||||
@@ -943,6 +1267,7 @@ func initAddrSet(as *AddrSet) {
|
||||
func TestDiscoMessage(t *testing.T) {
|
||||
c := newConn()
|
||||
c.logf = t.Logf
|
||||
c.privateKey = key.NewPrivate()
|
||||
|
||||
peer1Pub := c.DiscoPublicKey()
|
||||
peer1Priv := c.discoPrivate
|
||||
@@ -990,3 +1315,25 @@ func TestDiscoStringLogRace(t *testing.T) {
|
||||
}()
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func stringifyConfig(cfg wgcfg.Config) string {
|
||||
j, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return string(j)
|
||||
}
|
||||
|
||||
func TestDiscoEndpointAlignment(t *testing.T) {
|
||||
var de discoEndpoint
|
||||
off := unsafe.Offsetof(de.lastRecvUnixAtomic)
|
||||
if off%8 != 0 {
|
||||
t.Fatalf("lastRecvUnixAtomic is not 8-byte aligned")
|
||||
}
|
||||
if !de.isFirstRecvActivityInAwhile() { // verify this doesn't panic on 32-bit
|
||||
t.Error("expected true")
|
||||
}
|
||||
if de.isFirstRecvActivityInAwhile() {
|
||||
t.Error("expected false on second call")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ func (m *Mon) pump() {
|
||||
default:
|
||||
}
|
||||
// Keep retrying while we're not closed.
|
||||
m.logf("error receiving from connection: %v", err)
|
||||
m.logf("error from link monitor: %v", err)
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -30,9 +30,6 @@ func newOSMon(logf logger.Logf) (osMon, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("devd dial error: %v", err)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dialing devd socket: %v", err)
|
||||
}
|
||||
return &devdConn{conn}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ func (c *nlConn) Receive() (message, error) {
|
||||
case unix.RTM_NEWADDR, unix.RTM_DELADDR:
|
||||
var rmsg rtnetlink.AddressMessage
|
||||
if err := rmsg.UnmarshalBinary(msg.Data); err != nil {
|
||||
c.logf("failed to parse type 0x%x: %v", msg.Header.Type, err)
|
||||
c.logf("failed to parse type %v: %v", msg.Header.Type, err)
|
||||
return unspecifiedMessage{}, nil
|
||||
}
|
||||
return &newAddrMessage{
|
||||
@@ -87,20 +87,41 @@ func (c *nlConn) Receive() (message, error) {
|
||||
Addr: netaddrIP(rmsg.Attributes.Local),
|
||||
Delete: msg.Header.Type == unix.RTM_DELADDR,
|
||||
}, nil
|
||||
case unix.RTM_NEWROUTE:
|
||||
case unix.RTM_NEWROUTE, unix.RTM_DELROUTE:
|
||||
typeStr := "RTM_NEWROUTE"
|
||||
if msg.Header.Type == unix.RTM_DELROUTE {
|
||||
typeStr = "RTM_DELROUTE"
|
||||
}
|
||||
var rmsg rtnetlink.RouteMessage
|
||||
if err := rmsg.UnmarshalBinary(msg.Data); err != nil {
|
||||
c.logf("RTM_NEWROUTE: failed to parse: %v", err)
|
||||
c.logf("%s: failed to parse: %v", typeStr, err)
|
||||
return unspecifiedMessage{}, nil
|
||||
}
|
||||
src := netaddrIPPrefix(rmsg.Attributes.Src, rmsg.SrcLength)
|
||||
dst := netaddrIPPrefix(rmsg.Attributes.Dst, rmsg.DstLength)
|
||||
gw := netaddrIP(rmsg.Attributes.Gateway)
|
||||
|
||||
if msg.Header.Type == unix.RTM_NEWROUTE && rmsg.Table == tsTable && rmsg.DstLength == 32 {
|
||||
// Don't log. Spammy and normal to see a bunch of these on start-up,
|
||||
// which we make ourselves.
|
||||
} else {
|
||||
c.logf("%s: src=%v, dst=%v, gw=%v, outif=%v, table=%v", typeStr,
|
||||
condNetAddrPrefix(src), condNetAddrPrefix(dst), condNetAddrIP(gw),
|
||||
rmsg.Attributes.OutIface, rmsg.Attributes.Table)
|
||||
}
|
||||
if msg.Header.Type == unix.RTM_DELROUTE {
|
||||
// Just logging it for now.
|
||||
// (Debugging https://github.com/tailscale/tailscale/issues/643)
|
||||
return unspecifiedMessage{}, nil
|
||||
}
|
||||
return &newRouteMessage{
|
||||
Table: rmsg.Table,
|
||||
Src: netaddrIP(rmsg.Attributes.Src),
|
||||
Dst: netaddrIP(rmsg.Attributes.Dst),
|
||||
Gateway: netaddrIP(rmsg.Attributes.Gateway),
|
||||
Src: src,
|
||||
Dst: dst,
|
||||
Gateway: gw,
|
||||
}, nil
|
||||
default:
|
||||
c.logf("unhandled netlink msg type 0x%x: %+v, %q", msg.Header.Type, msg.Header, msg.Data)
|
||||
c.logf("unhandled netlink msg type %+v, %q", msg.Header, msg.Data)
|
||||
return unspecifiedMessage{}, nil
|
||||
}
|
||||
}
|
||||
@@ -110,14 +131,36 @@ func netaddrIP(std net.IP) netaddr.IP {
|
||||
return ip
|
||||
}
|
||||
|
||||
// newRouteMessage is a message for a new route being added.
|
||||
type newRouteMessage struct {
|
||||
Src, Dst, Gateway netaddr.IP
|
||||
Table uint8
|
||||
func netaddrIPPrefix(std net.IP, bits uint8) netaddr.IPPrefix {
|
||||
ip, _ := netaddr.FromStdIP(std)
|
||||
return netaddr.IPPrefix{IP: ip, Bits: bits}
|
||||
}
|
||||
|
||||
func condNetAddrPrefix(ipp netaddr.IPPrefix) string {
|
||||
if ipp.IP.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return ipp.String()
|
||||
}
|
||||
|
||||
func condNetAddrIP(ip netaddr.IP) string {
|
||||
if ip.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return ip.String()
|
||||
}
|
||||
|
||||
// newRouteMessage is a message for a new route being added.
|
||||
type newRouteMessage struct {
|
||||
Src, Dst netaddr.IPPrefix
|
||||
Gateway netaddr.IP
|
||||
Table uint8
|
||||
}
|
||||
|
||||
const tsTable = 52
|
||||
|
||||
func (m *newRouteMessage) ignore() bool {
|
||||
return m.Table == 88 || tsaddr.IsTailscaleIP(m.Dst)
|
||||
return m.Table == tsTable || tsaddr.IsTailscaleIP(m.Dst.IP)
|
||||
}
|
||||
|
||||
// newAddrMessage is a message for a new address being added.
|
||||
|
||||
@@ -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,!freebsd android
|
||||
// +build !linux,!freebsd,!windows android
|
||||
|
||||
package monitor
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user