Compare commits
224 Commits
Xe/hello-v
...
crawshaw/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cf0fdb760 | ||
|
|
33bc69cf1f | ||
|
|
3a1eae5b6b | ||
|
|
1e26d4ae19 | ||
|
|
eeacf84dae | ||
|
|
41e4e02e57 | ||
|
|
9659ab81e0 | ||
|
|
12ae2d73b3 | ||
|
|
f0863346c2 | ||
|
|
35596ae5ce | ||
|
|
662fbd4a09 | ||
|
|
a4c679e646 | ||
|
|
07bf4eb685 | ||
|
|
0fb738760f | ||
|
|
e18c3a7d84 | ||
|
|
95ca86c048 | ||
|
|
93a4aa697c | ||
|
|
440effb21a | ||
|
|
0807e3e2f7 | ||
|
|
4954fbfda6 | ||
|
|
2df8adef9d | ||
|
|
25e0bb0a4e | ||
|
|
22d53fe784 | ||
|
|
016de16b2e | ||
|
|
82ab7972f4 | ||
|
|
588b70f468 | ||
|
|
018200aeba | ||
|
|
2b4bfeda1a | ||
|
|
9ea5cbf81f | ||
|
|
f26dfd054a | ||
|
|
44d9929208 | ||
|
|
0a84aaca0a | ||
|
|
1642dfdb07 | ||
|
|
bcf571ec97 | ||
|
|
7f174e84e6 | ||
|
|
5a62aa8047 | ||
|
|
7dc88e4c1e | ||
|
|
04dd6d1dae | ||
|
|
672731ac6f | ||
|
|
6521f02ff6 | ||
|
|
9f7f2af008 | ||
|
|
8432999835 | ||
|
|
81143b6d9a | ||
|
|
dad10fee9c | ||
|
|
82c4cb765c | ||
|
|
28af46fb3b | ||
|
|
b7f0e39bf2 | ||
|
|
2384c112c9 | ||
|
|
4b77eca2de | ||
|
|
79f02de55f | ||
|
|
d31eff8473 | ||
|
|
c99f260e40 | ||
|
|
e2b3d9aa5f | ||
|
|
77ec80538a | ||
|
|
9643d8b34d | ||
|
|
96dfeb2d7f | ||
|
|
85138d3183 | ||
|
|
0994a9f7c4 | ||
|
|
7e0d12e7cc | ||
|
|
1eb95c7e32 | ||
|
|
01b90df2fa | ||
|
|
90a6fb7ffe | ||
|
|
32562a82a9 | ||
|
|
0406a7436a | ||
|
|
8c0a0450d9 | ||
|
|
0a02aaf813 | ||
|
|
7b57310966 | ||
|
|
439d70dce2 | ||
|
|
d0dffe33c0 | ||
|
|
0c3e9722cc | ||
|
|
a480b1baa5 | ||
|
|
c19ed37b0f | ||
|
|
cc508be603 | ||
|
|
aa79a57f63 | ||
|
|
a217078f67 | ||
|
|
ec1b31ea83 | ||
|
|
a4fa2c5611 | ||
|
|
6fb5d4080c | ||
|
|
4145bb7148 | ||
|
|
4543e4202f | ||
|
|
6f48a8422a | ||
|
|
84aba349d9 | ||
|
|
e0f2796b43 | ||
|
|
0f90586da8 | ||
|
|
d5fd373f09 | ||
|
|
469613b4c5 | ||
|
|
27c4dd9a97 | ||
|
|
9eb65601ef | ||
|
|
6fbc9b3a98 | ||
|
|
1d7f9d5b4a | ||
|
|
d42f8b7f9a | ||
|
|
98ab533324 | ||
|
|
380a3526f6 | ||
|
|
232cfda280 | ||
|
|
ba8c6d0775 | ||
|
|
770aa71ffb | ||
|
|
44ab0acbdb | ||
|
|
d580b3f09e | ||
|
|
974be2ec5c | ||
|
|
deff20edc6 | ||
|
|
ab2a8a7493 | ||
|
|
4224b3f731 | ||
|
|
2bc518dcb2 | ||
|
|
25d2dd868b | ||
|
|
d491adbf09 | ||
|
|
c6358f2247 | ||
|
|
0a84359d2d | ||
|
|
c81814e4f8 | ||
|
|
f9f3b67f3a | ||
|
|
bdb91a20eb | ||
|
|
1bc3c03562 | ||
|
|
fa6110e47b | ||
|
|
c576fea60e | ||
|
|
0b66cfe1e0 | ||
|
|
0430c2dd12 | ||
|
|
cc99059fc2 | ||
|
|
bf0740b011 | ||
|
|
a7f12a110a | ||
|
|
d79a2f3809 | ||
|
|
ef7bac2895 | ||
|
|
79d8288f0a | ||
|
|
66480755c2 | ||
|
|
387e83c8fe | ||
|
|
fee74e7ea7 | ||
|
|
d3e56aa979 | ||
|
|
04e72f95cc | ||
|
|
c445e3d327 | ||
|
|
258d0e8d9a | ||
|
|
4c80344e27 | ||
|
|
7325b5a7ba | ||
|
|
43b30e463c | ||
|
|
bcea88da46 | ||
|
|
c8af6bc009 | ||
|
|
f45a9e291b | ||
|
|
e453c7ca57 | ||
|
|
f11cb811cc | ||
|
|
bc159dc689 | ||
|
|
c136f48b79 | ||
|
|
a4b585947d | ||
|
|
1ca3e739f7 | ||
|
|
0d0fad43ed | ||
|
|
602f92ec30 | ||
|
|
b14ea68754 | ||
|
|
affd859121 | ||
|
|
d37b3b02cd | ||
|
|
63a9adeb6c | ||
|
|
82edf94df7 | ||
|
|
a6d098c750 | ||
|
|
829eb8363a | ||
|
|
ad6edf5ecd | ||
|
|
ffa70a617d | ||
|
|
10f48087f4 | ||
|
|
061422affc | ||
|
|
524fb2c190 | ||
|
|
6756f20632 | ||
|
|
2e347d1e10 | ||
|
|
ea49b1e811 | ||
|
|
1cb0ffc3ff | ||
|
|
92cdb30b26 | ||
|
|
f858b0d25f | ||
|
|
d01c60dad5 | ||
|
|
7461dded88 | ||
|
|
8a55d463c8 | ||
|
|
8d77dfdacb | ||
|
|
b4cf837d8a | ||
|
|
c3e5903b91 | ||
|
|
15b6969a95 | ||
|
|
63ed4dd6c9 | ||
|
|
95c03d1ead | ||
|
|
471f0c470a | ||
|
|
be779b3587 | ||
|
|
f304a45481 | ||
|
|
0d0ec7853c | ||
|
|
31721759f3 | ||
|
|
b89c757817 | ||
|
|
c0cdca6d06 | ||
|
|
24fa616e73 | ||
|
|
625c413508 | ||
|
|
487c520109 | ||
|
|
793cb131f0 | ||
|
|
ac3de93d5c | ||
|
|
30a37622b4 | ||
|
|
f647e3daaf | ||
|
|
b46e337cdc | ||
|
|
9df4185c94 | ||
|
|
03c344333e | ||
|
|
e3df29d488 | ||
|
|
a038e8690c | ||
|
|
38dc6fe758 | ||
|
|
d74cddcc56 | ||
|
|
34188d93d4 | ||
|
|
14dc790137 | ||
|
|
a55a03d5ff | ||
|
|
ee6475a44d | ||
|
|
dda03a911e | ||
|
|
0eea490724 | ||
|
|
719de8f0e1 | ||
|
|
2d5db90161 | ||
|
|
e98cdbb8b6 | ||
|
|
fec9dcbda1 | ||
|
|
fe16ef6812 | ||
|
|
f68431fc02 | ||
|
|
c1ae1a3d2d | ||
|
|
99d67493be | ||
|
|
000b80de9d | ||
|
|
3fd00c4a40 | ||
|
|
517c90d7e5 | ||
|
|
daf6de4f14 | ||
|
|
ea3715e3ce | ||
|
|
360095cd34 | ||
|
|
8ee1cb6156 | ||
|
|
54d7070121 | ||
|
|
abfd73f569 | ||
|
|
2404c0ffad | ||
|
|
ebf3f2fd9f | ||
|
|
e9e4f1063d | ||
|
|
f11952ad7f | ||
|
|
c64bd587ae | ||
|
|
d038a5295d | ||
|
|
188bb14269 | ||
|
|
6e42430ad8 | ||
|
|
df5adb2e23 | ||
|
|
b83c273737 | ||
|
|
2c500cee23 |
14
Dockerfile
14
Dockerfile
@@ -49,10 +49,18 @@ RUN go mod download
|
||||
COPY . .
|
||||
|
||||
# see build_docker.sh
|
||||
ARG goflags_arg=""
|
||||
ENV GOFLAGS=$goflags_arg
|
||||
ARG VERSION_LONG=""
|
||||
ENV VERSION_LONG=$VERSION_LONG
|
||||
ARG VERSION_SHORT=""
|
||||
ENV VERSION_SHORT=$VERSION_SHORT
|
||||
ARG VERSION_GIT_HASH=""
|
||||
ENV VERSION_GIT_HASH=$VERSION_GIT_HASH
|
||||
|
||||
RUN go install -v ./cmd/...
|
||||
RUN go install -tags=xversion -ldflags="\
|
||||
-X tailscale.com/version.Long=$VERSION_LONG \
|
||||
-X tailscale.com/version.Short=$VERSION_SHORT \
|
||||
-X tailscale.com/version.GitCommit=$VERSION_GIT_HASH" \
|
||||
-v ./cmd/...
|
||||
|
||||
FROM alpine:3.11
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2
|
||||
|
||||
8
Makefile
8
Makefile
@@ -12,7 +12,13 @@ depaware:
|
||||
go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscaled
|
||||
go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscale
|
||||
|
||||
check: staticcheck vet depaware
|
||||
buildwindows:
|
||||
GOOS=windows GOARCH=amd64 go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
|
||||
|
||||
build386:
|
||||
GOOS=linux GOARCH=386 go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
|
||||
|
||||
check: staticcheck vet depaware buildwindows build386
|
||||
|
||||
staticcheck:
|
||||
go run honnef.co/go/tools/cmd/staticcheck -- $$(go list ./... | grep -v tempfork)
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.5.0
|
||||
1.7.0
|
||||
|
||||
@@ -27,5 +27,8 @@ set -eu
|
||||
|
||||
eval $(./version/version.sh)
|
||||
|
||||
GOFLAGS='-tags xversion -ldflags '"-X tailscale.com/version.Long=${VERSION_LONG} -X tailscale.com/version.Short=${VERSION_SHORT} -X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}"
|
||||
docker build --build-arg goflags_arg="'""${GOFLAGS}""'" -t tailscale:tailscale .
|
||||
docker build \
|
||||
--build-arg VERSION_LONG=$VERSION_LONG \
|
||||
--build-arg VERSION_SHORT=$VERSION_SHORT \
|
||||
--build-arg VERSION_GIT_HASH=$VERSION_GIT_HASH \
|
||||
-t tailscale:tailscale .
|
||||
|
||||
139
client/tailscale/tailscale.go
Normal file
139
client/tailscale/tailscale.go
Normal file
@@ -0,0 +1,139 @@
|
||||
// Copyright (c) 2021 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 tailscale contains Tailscale client code.
|
||||
package tailscale
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
// TailscaledSocket is the tailscaled Unix socket.
|
||||
var TailscaledSocket = paths.DefaultTailscaledSocket()
|
||||
|
||||
// tsClient does HTTP requests to the local Tailscale daemon.
|
||||
var tsClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
if addr != "local-tailscaled.sock:80" {
|
||||
return nil, fmt.Errorf("unexpected URL address %q", addr)
|
||||
}
|
||||
if TailscaledSocket == paths.DefaultTailscaledSocket() {
|
||||
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
|
||||
// a TCP server on a random port, find the random port. For HTTP connections,
|
||||
// we don't send the token. It gets added in an HTTP Basic-Auth header.
|
||||
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
|
||||
}
|
||||
}
|
||||
return safesocket.Connect(TailscaledSocket, 41112)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// DoLocalRequest makes an HTTP request to the local machine's Tailscale daemon.
|
||||
//
|
||||
// URLs are of the form http://local-tailscaled.sock/localapi/v0/whois?ip=1.2.3.4.
|
||||
//
|
||||
// The hostname must be "local-tailscaled.sock", even though it
|
||||
// doesn't actually do any DNS lookup. The actual means of connecting to and
|
||||
// authenticating to the local Tailscale daemon vary by platform.
|
||||
//
|
||||
// DoLocalRequest may mutate the request to add Authorization headers.
|
||||
func DoLocalRequest(req *http.Request) (*http.Response, error) {
|
||||
if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
||||
req.SetBasicAuth("", token)
|
||||
}
|
||||
return tsClient.Do(req)
|
||||
}
|
||||
|
||||
// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port.
|
||||
func WhoIs(ctx context.Context, remoteAddr string) (*tailcfg.WhoIsResponse, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := DoLocalRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
slurp, _ := ioutil.ReadAll(res.Body)
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("HTTP %s: %s", res.Status, slurp)
|
||||
}
|
||||
r := new(tailcfg.WhoIsResponse)
|
||||
if err := json.Unmarshal(slurp, r); err != nil {
|
||||
if max := 200; len(slurp) > max {
|
||||
slurp = slurp[:max]
|
||||
}
|
||||
return nil, fmt.Errorf("failed to parse JSON WhoIsResponse from %q", slurp)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// Goroutines returns a dump of the Tailscale daemon's current goroutines.
|
||||
func Goroutines(ctx context.Context) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/goroutines", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := DoLocalRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("HTTP %s: %s", res.Status, body)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// Status returns the Tailscale daemon's status.
|
||||
func Status(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return status(ctx, "")
|
||||
}
|
||||
|
||||
// StatusWithPeers returns the Tailscale daemon's status, without the peer info.
|
||||
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return status(ctx, "?peers=false")
|
||||
}
|
||||
|
||||
func status(ctx context.Context, queryString string) (*ipnstate.Status, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/status"+queryString, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := DoLocalRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
return nil, fmt.Errorf("HTTP %s: %s", res.Status, body)
|
||||
}
|
||||
st := new(ipnstate.Status)
|
||||
if err := json.NewDecoder(res.Body).Decode(st); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
69
cmd/derper/bootstrap_dns.go
Normal file
69
cmd/derper/bootstrap_dns.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2021 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 main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"expvar"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
dnsMu sync.Mutex
|
||||
dnsCache = map[string][]net.IP{}
|
||||
)
|
||||
|
||||
var bootstrapDNSRequests = expvar.NewInt("counter_bootstrap_dns_requests")
|
||||
|
||||
func refreshBootstrapDNSLoop() {
|
||||
if *bootstrapDNS == "" {
|
||||
return
|
||||
}
|
||||
for {
|
||||
refreshBootstrapDNS()
|
||||
time.Sleep(10 * time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
func refreshBootstrapDNS() {
|
||||
if *bootstrapDNS == "" {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
names := strings.Split(*bootstrapDNS, ",")
|
||||
var r net.Resolver
|
||||
for _, name := range names {
|
||||
addrs, err := r.LookupIP(ctx, "ip", name)
|
||||
if err != nil {
|
||||
log.Printf("bootstrap DNS lookup %q: %v", name, err)
|
||||
continue
|
||||
}
|
||||
dnsMu.Lock()
|
||||
dnsCache[name] = addrs
|
||||
dnsMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func handleBootstrapDNS(w http.ResponseWriter, r *http.Request) {
|
||||
bootstrapDNSRequests.Add(1)
|
||||
dnsMu.Lock()
|
||||
j, err := json.MarshalIndent(dnsCache, "", "\t")
|
||||
dnsMu.Unlock()
|
||||
if err != nil {
|
||||
log.Printf("bootstrap DNS JSON: %v", err)
|
||||
http.Error(w, "JSON marshal error", 500)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(j)
|
||||
}
|
||||
@@ -48,6 +48,7 @@ var (
|
||||
runSTUN = flag.Bool("stun", false, "also run a STUN server")
|
||||
meshPSKFile = flag.String("mesh-psk-file", defaultMeshPSKFile(), "if non-empty, path to file containing the mesh pre-shared key file. It should contain some hex string; whitespace is trimmed.")
|
||||
meshWith = flag.String("mesh-with", "", "optional comma-separated list of hostnames to mesh with; the server's own hostname can be in the list")
|
||||
bootstrapDNS = flag.String("bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns")
|
||||
)
|
||||
|
||||
type config struct {
|
||||
@@ -145,6 +146,8 @@ func main() {
|
||||
// Create our own mux so we don't expose /debug/ stuff to the world.
|
||||
mux := tsweb.NewMux(debugHandler(s))
|
||||
mux.Handle("/derp", derphttp.Handler(s))
|
||||
go refreshBootstrapDNSLoop()
|
||||
mux.HandleFunc("/bootstrap-dns", handleBootstrapDNS)
|
||||
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
@@ -153,7 +156,7 @@ func main() {
|
||||
<p>
|
||||
This is a
|
||||
<a href="https://tailscale.com/">Tailscale</a>
|
||||
<a href="https://godoc.org/tailscale.com/derp">DERP</a>
|
||||
<a href="https://pkg.go.dev/tailscale.com/derp">DERP</a>
|
||||
server.
|
||||
</p>
|
||||
`)
|
||||
|
||||
@@ -7,20 +7,17 @@ package main // import "tailscale.com/cmd/hello"
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
@@ -30,10 +27,13 @@ var (
|
||||
testIP = flag.String("test-ip", "", "if non-empty, look up IP and exit before running a server")
|
||||
)
|
||||
|
||||
//go:embed hello.tmpl.html
|
||||
var embeddedTemplate string
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *testIP != "" {
|
||||
res, err := whoIs(*testIP)
|
||||
res, err := tailscale.WhoIs(context.Background(), *testIP)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -42,8 +42,18 @@ func main() {
|
||||
e.Encode(res)
|
||||
return
|
||||
}
|
||||
if !devMode() {
|
||||
tmpl = template.Must(template.New("home").Parse(slurpHTML()))
|
||||
if devMode() {
|
||||
// Parse it optimistically
|
||||
var err error
|
||||
tmpl, err = template.New("home").Parse(embeddedTemplate)
|
||||
if err != nil {
|
||||
log.Printf("ignoring template error in dev mode: %v", err)
|
||||
}
|
||||
} else {
|
||||
if embeddedTemplate == "" {
|
||||
log.Fatalf("embeddedTemplate is empty; must be build with Go 1.16+")
|
||||
}
|
||||
tmpl = template.Must(template.New("home").Parse(embeddedTemplate))
|
||||
}
|
||||
|
||||
http.HandleFunc("/", root)
|
||||
@@ -69,24 +79,24 @@ func main() {
|
||||
log.Fatal(<-errc)
|
||||
}
|
||||
|
||||
func slurpHTML() string {
|
||||
slurp, err := ioutil.ReadFile("hello.tmpl.html")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return string(slurp)
|
||||
}
|
||||
|
||||
func devMode() bool { return *httpsAddr == "" && *httpAddr != "" }
|
||||
|
||||
func getTmpl() (*template.Template, error) {
|
||||
if devMode() {
|
||||
return template.New("home").Parse(slurpHTML())
|
||||
tmplData, err := ioutil.ReadFile("hello.tmpl.html")
|
||||
if os.IsNotExist(err) {
|
||||
log.Printf("using baked-in template in dev mode; can't find hello.tmpl.html in current directory")
|
||||
return tmpl, nil
|
||||
}
|
||||
return template.New("home").Parse(string(tmplData))
|
||||
}
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
var tmpl *template.Template // not used in dev mode, initialized by main after flag parse
|
||||
// tmpl is the template used in prod mode.
|
||||
// In dev mode it's only used if the template file doesn't exist on disk.
|
||||
// It's initialized by main after flag parsing.
|
||||
var tmpl *template.Template
|
||||
|
||||
type tmplData struct {
|
||||
DisplayName string // "Foo Barberson"
|
||||
@@ -97,6 +107,23 @@ type tmplData struct {
|
||||
IP string // "100.2.3.4"
|
||||
}
|
||||
|
||||
func tailscaleIP(who *tailcfg.WhoIsResponse) string {
|
||||
if who == nil {
|
||||
return ""
|
||||
}
|
||||
for _, nodeIP := range who.Node.Addresses {
|
||||
if nodeIP.IP.Is4() && nodeIP.IsSingleIP() {
|
||||
return nodeIP.IP.String()
|
||||
}
|
||||
}
|
||||
for _, nodeIP := range who.Node.Addresses {
|
||||
if nodeIP.IsSingleIP() {
|
||||
return nodeIP.IP.String()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func root(w http.ResponseWriter, r *http.Request) {
|
||||
if r.TLS == nil && *httpsAddr != "" {
|
||||
host := r.Host
|
||||
@@ -110,11 +137,6 @@ func root(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
http.Error(w, "no remote addr", 500)
|
||||
return
|
||||
}
|
||||
tmpl, err := getTmpl()
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
@@ -122,7 +144,7 @@ func root(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
who, err := whoIs(ip)
|
||||
who, err := tailscale.WhoIs(r.Context(), r.RemoteAddr)
|
||||
var data tmplData
|
||||
if err != nil {
|
||||
if devMode() {
|
||||
@@ -136,7 +158,7 @@ func root(w http.ResponseWriter, r *http.Request) {
|
||||
IP: "100.1.2.3",
|
||||
}
|
||||
} else {
|
||||
log.Printf("whois(%q) error: %v", ip, err)
|
||||
log.Printf("whois(%q) error: %v", r.RemoteAddr, err)
|
||||
http.Error(w, "Your Tailscale works, but we failed to look you up.", 500)
|
||||
return
|
||||
}
|
||||
@@ -147,7 +169,7 @@ func root(w http.ResponseWriter, r *http.Request) {
|
||||
ProfilePicURL: who.UserProfile.ProfilePicURL,
|
||||
MachineName: firstLabel(who.Node.ComputedName),
|
||||
MachineOS: who.Node.Hostinfo.OS,
|
||||
IP: ip,
|
||||
IP: tailscaleIP(who),
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
@@ -161,48 +183,3 @@ func firstLabel(s string) string {
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// tsSockClient does HTTP requests to the local Tailscale daemon.
|
||||
// The hostname in the HTTP request is ignored.
|
||||
var tsSockClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
|
||||
// a TCP server on a random port, find the random port. For HTTP connections,
|
||||
// we don't send the token. It gets added in an HTTP Basic-Auth header.
|
||||
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
|
||||
}
|
||||
return safesocket.ConnectDefault()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func whoIs(ip string) (*tailcfg.WhoIsResponse, error) {
|
||||
ctx := context.Background()
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/whois?ip="+url.QueryEscape(ip), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
||||
req.SetBasicAuth("", token)
|
||||
}
|
||||
res, err := tsSockClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
slurp, _ := ioutil.ReadAll(res.Body)
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("HTTP %s: %s", res.Status, slurp)
|
||||
}
|
||||
r := new(tailcfg.WhoIsResponse)
|
||||
if err := json.Unmarshal(slurp, r); err != nil {
|
||||
if max := 200; len(slurp) > max {
|
||||
slurp = slurp[:max]
|
||||
}
|
||||
return nil, fmt.Errorf("failed to parse JSON WhoIsResponse from %q", slurp)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
@@ -16,8 +17,10 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
@@ -50,9 +53,11 @@ func Run(args []string) error {
|
||||
|
||||
rootCmd := &ffcli.Command{
|
||||
Name: "tailscale",
|
||||
ShortUsage: "tailscale subcommand [flags]",
|
||||
ShortUsage: "tailscale [flags] <subcommand> [command flags]",
|
||||
ShortHelp: "The easiest, most secure way to use WireGuard.",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
For help on subcommands, add --help after: "tailscale status --help".
|
||||
|
||||
This CLI is still under active development. Commands and flags will
|
||||
change in the future.
|
||||
`),
|
||||
@@ -60,18 +65,32 @@ change in the future.
|
||||
upCmd,
|
||||
downCmd,
|
||||
netcheckCmd,
|
||||
ipCmd,
|
||||
statusCmd,
|
||||
pingCmd,
|
||||
versionCmd,
|
||||
webCmd,
|
||||
pushCmd,
|
||||
},
|
||||
FlagSet: rootfs,
|
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||
FlagSet: rootfs,
|
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||
UsageFunc: usageFunc,
|
||||
}
|
||||
for _, c := range rootCmd.Subcommands {
|
||||
c.UsageFunc = usageFunc
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
tailscale.TailscaledSocket = rootArgs.socket
|
||||
|
||||
err := rootCmd.Run(context.Background())
|
||||
if err == flag.ErrHelp {
|
||||
return nil
|
||||
@@ -129,3 +148,81 @@ 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
|
||||
}
|
||||
|
||||
func usageFunc(c *ffcli.Command) string {
|
||||
var b strings.Builder
|
||||
|
||||
fmt.Fprintf(&b, "USAGE\n")
|
||||
if c.ShortUsage != "" {
|
||||
fmt.Fprintf(&b, " %s\n", c.ShortUsage)
|
||||
} else {
|
||||
fmt.Fprintf(&b, " %s\n", c.Name)
|
||||
}
|
||||
fmt.Fprintf(&b, "\n")
|
||||
|
||||
if c.LongHelp != "" {
|
||||
fmt.Fprintf(&b, "%s\n\n", c.LongHelp)
|
||||
}
|
||||
|
||||
if len(c.Subcommands) > 0 {
|
||||
fmt.Fprintf(&b, "SUBCOMMANDS\n")
|
||||
tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
|
||||
for _, subcommand := range c.Subcommands {
|
||||
fmt.Fprintf(tw, " %s\t%s\n", subcommand.Name, subcommand.ShortHelp)
|
||||
}
|
||||
tw.Flush()
|
||||
fmt.Fprintf(&b, "\n")
|
||||
}
|
||||
|
||||
if countFlags(c.FlagSet) > 0 {
|
||||
fmt.Fprintf(&b, "FLAGS\n")
|
||||
tw := tabwriter.NewWriter(&b, 0, 2, 2, ' ', 0)
|
||||
c.FlagSet.VisitAll(func(f *flag.Flag) {
|
||||
var s string
|
||||
name, usage := flag.UnquoteUsage(f)
|
||||
if isBoolFlag(f) {
|
||||
s = fmt.Sprintf(" --%s, --%s=false", f.Name, f.Name)
|
||||
} else {
|
||||
s = fmt.Sprintf(" --%s", f.Name) // Two spaces before --; see next two comments.
|
||||
if len(name) > 0 {
|
||||
s += " " + name
|
||||
}
|
||||
}
|
||||
// Four spaces before the tab triggers good alignment
|
||||
// for both 4- and 8-space tab stops.
|
||||
s += "\n \t"
|
||||
s += strings.ReplaceAll(usage, "\n", "\n \t")
|
||||
|
||||
if f.DefValue != "" {
|
||||
s += fmt.Sprintf(" (default %s)", f.DefValue)
|
||||
}
|
||||
|
||||
fmt.Fprintln(&b, s)
|
||||
})
|
||||
tw.Flush()
|
||||
fmt.Fprintf(&b, "\n")
|
||||
}
|
||||
|
||||
return strings.TrimSpace(b.String())
|
||||
}
|
||||
|
||||
func isBoolFlag(f *flag.Flag) bool {
|
||||
bf, ok := f.Value.(interface {
|
||||
IsBoolFlag() bool
|
||||
})
|
||||
return ok && bf.IsBoolFlag()
|
||||
}
|
||||
|
||||
func countFlags(fs *flag.FlagSet) (n int) {
|
||||
fs.VisitAll(func(*flag.Flag) { n++ })
|
||||
return n
|
||||
}
|
||||
|
||||
43
cmd/tailscale/cli/debug.go
Normal file
43
cmd/tailscale/cli/debug.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2021 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"
|
||||
"os"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
var debugCmd = &ffcli.Command{
|
||||
Name: "debug",
|
||||
Exec: runDebug,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("debug", flag.ExitOnError)
|
||||
fs.BoolVar(&debugArgs.goroutines, "daemon-goroutines", false, "If true, dump the tailscaled daemon's goroutines")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var debugArgs struct {
|
||||
goroutines bool
|
||||
}
|
||||
|
||||
func runDebug(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("unknown arguments")
|
||||
}
|
||||
if debugArgs.goroutines {
|
||||
goroutines, err := tailscale.Goroutines(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
os.Stdout.Write(goroutines)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -6,10 +6,12 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
)
|
||||
|
||||
@@ -26,6 +28,16 @@ func runDown(ctx context.Context, args []string) error {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error fetching current status: %w", err)
|
||||
}
|
||||
if st.BackendState == "Stopped" {
|
||||
log.Printf("already stopped")
|
||||
return nil
|
||||
}
|
||||
log.Printf("was in state %q", st.BackendState)
|
||||
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
@@ -38,17 +50,6 @@ func runDown(ctx context.Context, args []string) error {
|
||||
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 {
|
||||
@@ -58,7 +59,6 @@ func runDown(ctx context.Context, args []string) error {
|
||||
}
|
||||
})
|
||||
|
||||
bc.RequestStatus()
|
||||
bc.SetWantRunning(false)
|
||||
pump(ctx, bc, c)
|
||||
|
||||
|
||||
69
cmd/tailscale/cli/ip.go
Normal file
69
cmd/tailscale/cli/ip.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// 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"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
var ipCmd = &ffcli.Command{
|
||||
Name: "ip",
|
||||
ShortUsage: "ip [-4] [-6]",
|
||||
ShortHelp: "Show this machine's current Tailscale IP address(es)",
|
||||
Exec: runIP,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("ip", flag.ExitOnError)
|
||||
fs.BoolVar(&ipArgs.want4, "4", false, "only print IPv4 address")
|
||||
fs.BoolVar(&ipArgs.want6, "6", false, "only print IPv6 address")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var ipArgs struct {
|
||||
want4 bool
|
||||
want6 bool
|
||||
}
|
||||
|
||||
func runIP(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("unknown arguments")
|
||||
}
|
||||
v4, v6 := ipArgs.want4, ipArgs.want6
|
||||
if v4 && v6 {
|
||||
return errors.New("tailscale up -4 and -6 are mutually exclusive")
|
||||
}
|
||||
if !v4 && !v6 {
|
||||
v4, v6 = true, true
|
||||
}
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(st.TailscaleIPs) == 0 {
|
||||
return fmt.Errorf("no current Tailscale IPs; state: %v", st.BackendState)
|
||||
}
|
||||
match := false
|
||||
for _, ip := range st.TailscaleIPs {
|
||||
if ip.Is4() && v4 || ip.Is6() && v6 {
|
||||
match = true
|
||||
fmt.Println(ip)
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
if ipArgs.want4 {
|
||||
return errors.New("no Tailscale IPv4 address")
|
||||
}
|
||||
if ipArgs.want6 {
|
||||
return errors.New("no Tailscale IPv6 address")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/derp/derpmap"
|
||||
"tailscale.com/net/netcheck"
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
@@ -45,6 +46,7 @@ var netcheckArgs struct {
|
||||
func runNetcheck(ctx context.Context, args []string) error {
|
||||
c := &netcheck.Client{
|
||||
UDPBindAddr: os.Getenv("TS_DEBUG_NETCHECK_UDP_BIND"),
|
||||
PortMapper: portmapper.NewClient(logger.WithPrefix(log.Printf, "portmap: ")),
|
||||
}
|
||||
if netcheckArgs.verbose {
|
||||
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
)
|
||||
@@ -47,6 +48,7 @@ relay node.
|
||||
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.BoolVar(&pingArgs.tsmp, "tsmp", false, "do a TSMP-level ping (through IP + wireguard, but not involving host OS stack)")
|
||||
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
|
||||
@@ -57,6 +59,7 @@ var pingArgs struct {
|
||||
num int
|
||||
untilDirect bool
|
||||
verbose bool
|
||||
tsmp bool
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
@@ -69,7 +72,6 @@ func runPing(ctx context.Context, args []string) error {
|
||||
}
|
||||
var ip string
|
||||
prc := make(chan *ipnstate.PingResult, 1)
|
||||
stc := make(chan *ipnstate.Status, 1)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
@@ -77,46 +79,15 @@ func runPing(ctx context.Context, args []string) error {
|
||||
if pr := n.PingResult; pr != nil && pr.IP == ip {
|
||||
prc <- pr
|
||||
}
|
||||
if n.Status != nil {
|
||||
stc <- n.Status
|
||||
}
|
||||
})
|
||||
go pump(ctx, bc, c)
|
||||
|
||||
hostOrIP := args[0]
|
||||
|
||||
// If the argument is an IP address, use it directly without any resolution.
|
||||
if net.ParseIP(hostOrIP) != nil {
|
||||
ip = hostOrIP
|
||||
ip, err := tailscaleIPFromArg(ctx, hostOrIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Otherwise, try to resolve it first from the network peer list.
|
||||
if ip == "" {
|
||||
bc.RequestStatus()
|
||||
select {
|
||||
case st := <-stc:
|
||||
for _, ps := range st.Peer {
|
||||
if hostOrIP == dnsOrQuoteHostname(st, ps) || hostOrIP == ps.DNSName {
|
||||
ip = ps.TailAddr
|
||||
break
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, use DNS.
|
||||
if ip == "" {
|
||||
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)
|
||||
}
|
||||
@@ -125,7 +96,7 @@ func runPing(ctx context.Context, args []string) error {
|
||||
anyPong := false
|
||||
for {
|
||||
n++
|
||||
bc.Ping(ip)
|
||||
bc.Ping(ip, pingArgs.tsmp)
|
||||
timer := time.NewTimer(pingArgs.timeout)
|
||||
select {
|
||||
case <-timer.C:
|
||||
@@ -140,8 +111,20 @@ func runPing(ctx context.Context, args []string) error {
|
||||
if pr.DERPRegionID != 0 {
|
||||
via = fmt.Sprintf("DERP(%s)", pr.DERPRegionCode)
|
||||
}
|
||||
if pingArgs.tsmp {
|
||||
// TODO(bradfitz): populate the rest of ipnstate.PingResult for TSMP queries?
|
||||
// For now just say it came via TSMP.
|
||||
via = "TSMP"
|
||||
}
|
||||
anyPong = true
|
||||
fmt.Printf("pong from %s (%s) via %v in %v\n", pr.NodeName, pr.NodeIP, via, latency)
|
||||
extra := ""
|
||||
if pr.PeerAPIPort != 0 {
|
||||
extra = fmt.Sprintf(", %d", pr.PeerAPIPort)
|
||||
}
|
||||
fmt.Printf("pong from %s (%s%s) via %v in %v\n", pr.NodeName, pr.NodeIP, extra, via, latency)
|
||||
if pingArgs.tsmp {
|
||||
return nil
|
||||
}
|
||||
if pr.Endpoint != "" && pingArgs.untilDirect {
|
||||
return nil
|
||||
}
|
||||
@@ -157,3 +140,31 @@ func runPing(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func tailscaleIPFromArg(ctx context.Context, hostOrIP string) (ip string, err error) {
|
||||
// If the argument is an IP address, use it directly without any resolution.
|
||||
if net.ParseIP(hostOrIP) != nil {
|
||||
return hostOrIP, nil
|
||||
}
|
||||
|
||||
// Otherwise, try to resolve it first from the network peer list.
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, ps := range st.Peer {
|
||||
if hostOrIP == dnsOrQuoteHostname(st, ps) || hostOrIP == ps.DNSName {
|
||||
return ps.TailAddr, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, use DNS.
|
||||
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 {
|
||||
return addrs[0], nil
|
||||
}
|
||||
}
|
||||
|
||||
173
cmd/tailscale/cli/push.go
Normal file
173
cmd/tailscale/cli/push.go
Normal file
@@ -0,0 +1,173 @@
|
||||
// Copyright (c) 2021 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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
)
|
||||
|
||||
var pushCmd = &ffcli.Command{
|
||||
Name: "push",
|
||||
ShortUsage: "push [--flags] <hostname-or-IP> <file>",
|
||||
ShortHelp: "Push a file to a host",
|
||||
Exec: runPush,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("push", flag.ExitOnError)
|
||||
fs.StringVar(&pushArgs.name, "name", "", "alternate filename to use, especially useful when <file> is \"-\" (stdin)")
|
||||
fs.BoolVar(&pushArgs.verbose, "verbose", false, "verbose output")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var pushArgs struct {
|
||||
name string
|
||||
verbose bool
|
||||
}
|
||||
|
||||
func runPush(ctx context.Context, args []string) error {
|
||||
if len(args) != 2 || args[0] == "" {
|
||||
return errors.New("usage: push <hostname-or-IP> <file>")
|
||||
}
|
||||
var ip string
|
||||
|
||||
hostOrIP, fileArg := args[0], args[1]
|
||||
ip, err := tailscaleIPFromArg(ctx, hostOrIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peerAPIPort, err := discoverPeerAPIPort(ctx, ip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var fileContents io.Reader
|
||||
var name = pushArgs.name
|
||||
if fileArg == "-" {
|
||||
fileContents = os.Stdin
|
||||
if name == "" {
|
||||
name, fileContents, err = pickStdinFilename()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
f, err := os.Open(fileArg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
fileContents = f
|
||||
if name == "" {
|
||||
name = fileArg
|
||||
}
|
||||
}
|
||||
|
||||
dstURL := "http://" + net.JoinHostPort(ip, fmt.Sprint(peerAPIPort)) + "/v0/put/" + url.PathEscape(name)
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", dstURL, fileContents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pushArgs.verbose {
|
||||
log.Printf("sending to %v ...", dstURL)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode == 200 {
|
||||
return nil
|
||||
}
|
||||
io.Copy(os.Stdout, res.Body)
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
|
||||
func discoverPeerAPIPort(ctx context.Context, ip string) (port uint16, err error) {
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
prc := make(chan *ipnstate.PingResult, 2)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if pr := n.PingResult; pr != nil && pr.IP == ip {
|
||||
prc <- pr
|
||||
}
|
||||
})
|
||||
go pump(ctx, bc, c)
|
||||
|
||||
ticker := time.NewTicker(time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
discoPings := 0
|
||||
timer := time.NewTimer(10 * time.Second)
|
||||
defer timer.Stop()
|
||||
|
||||
sendPings := func() {
|
||||
bc.Ping(ip, false)
|
||||
bc.Ping(ip, true)
|
||||
}
|
||||
sendPings()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
sendPings()
|
||||
case <-timer.C:
|
||||
return 0, fmt.Errorf("timeout contacting %v; it offline?", ip)
|
||||
case pr := <-prc:
|
||||
if p := pr.PeerAPIPort; p != 0 {
|
||||
return p, nil
|
||||
}
|
||||
discoPings++
|
||||
if discoPings == 3 {
|
||||
return 0, fmt.Errorf("%v is online, but seems to be running an old Tailscale version", ip)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return 0, ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const maxSniff = 4 << 20
|
||||
|
||||
func ext(b []byte) string {
|
||||
if len(b) < maxSniff && utf8.Valid(b) {
|
||||
return ".txt"
|
||||
}
|
||||
if exts, _ := mime.ExtensionsByType(http.DetectContentType(b)); len(exts) > 0 {
|
||||
return exts[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// pickStdinFilename reads a bit of stdin to return a good filename
|
||||
// for its contents. The returned Reader is the concatenation of the
|
||||
// read and unread bits.
|
||||
func pickStdinFilename() (name string, r io.Reader, err error) {
|
||||
sniff, err := io.ReadAll(io.LimitReader(os.Stdin, maxSniff))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return "stdin" + ext(sniff), io.MultiReader(bytes.NewReader(sniff), os.Stdin), nil
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -19,6 +18,7 @@ import (
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/toqueteos/webbrowser"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/interfaces"
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
|
||||
var statusCmd = &ffcli.Command{
|
||||
Name: "status",
|
||||
ShortUsage: "status [-active] [-web] [-json]",
|
||||
ShortUsage: "status [--active] [--web] [--json]",
|
||||
ShortHelp: "Show state of tailscaled and its connections",
|
||||
Exec: runStatus,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
@@ -37,7 +37,7 @@ var statusCmd = &ffcli.Command{
|
||||
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.BoolVar(&statusArgs.peers, "peers", true, "show status of peers")
|
||||
fs.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address; use port 0 for automatic")
|
||||
fs.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address for web mode; use port 0 for automatic")
|
||||
fs.BoolVar(&statusArgs.browser, "browser", true, "Open a browser in web mode")
|
||||
return fs
|
||||
})(),
|
||||
@@ -54,42 +54,7 @@ var statusArgs struct {
|
||||
}
|
||||
|
||||
func runStatus(ctx context.Context, args []string) error {
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
bc.AllowVersionSkew = true
|
||||
|
||||
ch := make(chan *ipnstate.Status, 1)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if n.Status != nil {
|
||||
select {
|
||||
case ch <- n.Status:
|
||||
default:
|
||||
// A status update from somebody else's request.
|
||||
// Ignoring this matters mostly for "tailscale status -web"
|
||||
// mode, otherwise the channel send would block forever
|
||||
// and pump would stop reading from tailscaled, which
|
||||
// previously caused tailscaled to block (while holding
|
||||
// a mutex), backing up unrelated clients.
|
||||
// See https://github.com/tailscale/tailscale/issues/1234
|
||||
}
|
||||
}
|
||||
})
|
||||
go pump(ctx, bc, c)
|
||||
|
||||
getStatus := func() (*ipnstate.Status, error) {
|
||||
bc.RequestStatus()
|
||||
select {
|
||||
case st := <-ch:
|
||||
return st, nil
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
st, err := getStatus()
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -127,7 +92,7 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
st, err := getStatus()
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
|
||||
@@ -14,16 +14,17 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
@@ -48,11 +49,12 @@ specify any flags, options are reset to their default.
|
||||
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale IP of the exit node for internet traffic")
|
||||
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.advertiseTags, "advertise-tags", "", "ACL tags to request (comma-separated, e.g. \"tag:eng,tag:montreal,tag: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")
|
||||
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" || isBSD(runtime.GOOS) {
|
||||
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\")")
|
||||
upf.BoolVar(&upArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
|
||||
}
|
||||
if runtime.GOOS == "linux" {
|
||||
upf.BoolVar(&upArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
|
||||
@@ -71,19 +73,20 @@ func defaultNetfilterMode() string {
|
||||
}
|
||||
|
||||
var upArgs struct {
|
||||
server string
|
||||
acceptRoutes bool
|
||||
acceptDNS bool
|
||||
singleRoutes bool
|
||||
exitNodeIP string
|
||||
shieldsUp bool
|
||||
forceReauth bool
|
||||
advertiseRoutes string
|
||||
advertiseTags string
|
||||
snat bool
|
||||
netfilterMode string
|
||||
authKey string
|
||||
hostname string
|
||||
server string
|
||||
acceptRoutes bool
|
||||
acceptDNS bool
|
||||
singleRoutes bool
|
||||
exitNodeIP string
|
||||
shieldsUp bool
|
||||
forceReauth bool
|
||||
advertiseRoutes string
|
||||
advertiseDefaultRoute bool
|
||||
advertiseTags string
|
||||
snat bool
|
||||
netfilterMode string
|
||||
authKey string
|
||||
hostname string
|
||||
}
|
||||
|
||||
func isBSD(s string) bool {
|
||||
@@ -94,14 +97,14 @@ func warnf(format string, args ...interface{}) {
|
||||
fmt.Printf("Warning: "+format+"\n", args...)
|
||||
}
|
||||
|
||||
// checkIPForwarding prints warnings if IP forwarding is not
|
||||
// checkIPForwarding prints warnings on linux 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) || version.OS() == "macOS" {
|
||||
} else if isBSD(runtime.GOOS) {
|
||||
key = "net.inet.ip.forwarding"
|
||||
} else {
|
||||
return
|
||||
@@ -148,7 +151,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
var routes []netaddr.IPPrefix
|
||||
routeMap := map[netaddr.IPPrefix]bool{}
|
||||
var default4, default6 bool
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
|
||||
@@ -165,15 +168,34 @@ func runUp(ctx context.Context, args []string) error {
|
||||
} else if ipp == ipv6default {
|
||||
default6 = true
|
||||
}
|
||||
routes = append(routes, ipp)
|
||||
routeMap[ipp] = true
|
||||
}
|
||||
if default4 && !default6 {
|
||||
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv4default, ipv6default)
|
||||
} else if default6 && !default4 {
|
||||
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv6default, ipv4default)
|
||||
}
|
||||
checkIPForwarding()
|
||||
}
|
||||
if upArgs.advertiseDefaultRoute {
|
||||
routeMap[netaddr.MustParseIPPrefix("0.0.0.0/0")] = true
|
||||
routeMap[netaddr.MustParseIPPrefix("::/0")] = true
|
||||
}
|
||||
if len(routeMap) > 0 {
|
||||
checkIPForwarding()
|
||||
if isBSD(runtime.GOOS) {
|
||||
warnf("Subnet routing and exit nodes only work with additional manual configuration on %v, and is not currently officially supported.", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
routes := make([]netaddr.IPPrefix, 0, len(routeMap))
|
||||
for r := range routeMap {
|
||||
routes = append(routes, r)
|
||||
}
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
if routes[i].Bits != routes[j].Bits {
|
||||
return routes[i].Bits < routes[j].Bits
|
||||
}
|
||||
return routes[i].IP.Less(routes[j].IP)
|
||||
})
|
||||
|
||||
var exitNodeIP netaddr.IP
|
||||
if upArgs.exitNodeIP != "" {
|
||||
@@ -231,6 +253,18 @@ func runUp(ctx context.Context, args []string) error {
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
if !prefs.ExitNodeIP.IsZero() {
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
fatalf("can't fetch status from tailscaled: %v", err)
|
||||
}
|
||||
for _, ip := range st.TailscaleIPs {
|
||||
if prefs.ExitNodeIP == ip {
|
||||
fatalf("cannot use %s as the exit node as it is a local IP address to this machine, did you mean --advertise-exit-node?", ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var printed bool
|
||||
var loginOnce sync.Once
|
||||
startLoginInteractive := func() { loginOnce.Do(func() { bc.StartLoginInteractive() }) }
|
||||
@@ -282,7 +316,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
// supports server mode, though, the transition to StateStore
|
||||
// is only half complete. Only server mode uses it, and the
|
||||
// Windows service (~tailscaled) is the one that computes the
|
||||
// StateKey based on the connection idenity. So for now, just
|
||||
// StateKey based on the connection identity. So for now, just
|
||||
// do as the Windows GUI's always done:
|
||||
if runtime.GOOS == "windows" {
|
||||
// The Windows service will set this as needed based
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"log"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -42,29 +42,10 @@ func runVersion(ctx context.Context, args []string) error {
|
||||
|
||||
fmt.Printf("Client: %s\n", version.String())
|
||||
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
bc.AllowVersionSkew = true
|
||||
|
||||
done := make(chan struct{})
|
||||
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if n.Status != nil {
|
||||
fmt.Printf("Daemon: %s\n", n.Version)
|
||||
close(done)
|
||||
}
|
||||
})
|
||||
go pump(ctx, bc, c)
|
||||
|
||||
bc.RequestStatus()
|
||||
select {
|
||||
case <-done:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
st, err := tailscale.StatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Daemon: %s\n", st.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
212
cmd/tailscale/cli/web.go
Normal file
212
cmd/tailscale/cli/web.go
Normal file
@@ -0,0 +1,212 @@
|
||||
// Copyright (c) 2021 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 (
|
||||
"bytes"
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/cgi"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
//go:embed web.html
|
||||
var webHTML string
|
||||
|
||||
var tmpl = template.Must(template.New("html").Parse(webHTML))
|
||||
|
||||
type tmplData struct {
|
||||
SynologyUser string
|
||||
Status string
|
||||
DeviceName string
|
||||
IP string
|
||||
}
|
||||
|
||||
var webCmd = &ffcli.Command{
|
||||
Name: "web",
|
||||
ShortUsage: "web [flags]",
|
||||
ShortHelp: "Run a web server for controlling Tailscale",
|
||||
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
webf := flag.NewFlagSet("web", flag.ExitOnError)
|
||||
webf.StringVar(&webArgs.listen, "listen", "localhost:8088", "listen address; use port 0 for automatic")
|
||||
webf.BoolVar(&webArgs.cgi, "cgi", false, "run as CGI script")
|
||||
return webf
|
||||
})(),
|
||||
Exec: runWeb,
|
||||
}
|
||||
|
||||
var webArgs struct {
|
||||
listen string
|
||||
cgi bool
|
||||
}
|
||||
|
||||
func runWeb(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
if webArgs.cgi {
|
||||
return cgi.Serve(http.HandlerFunc(webHandler))
|
||||
}
|
||||
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(webHandler))
|
||||
}
|
||||
|
||||
func auth() (string, error) {
|
||||
if distro.Get() == distro.Synology {
|
||||
cmd := exec.Command("/usr/syno/synoman/webman/modules/authenticate.cgi")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("auth: %v: %s", err, out)
|
||||
}
|
||||
return string(out), nil
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func synoTokenRedirect(w http.ResponseWriter, r *http.Request) bool {
|
||||
if distro.Get() != distro.Synology {
|
||||
return false
|
||||
}
|
||||
if r.Header.Get("X-Syno-Token") != "" {
|
||||
return false
|
||||
}
|
||||
if r.URL.Query().Get("SynoToken") != "" {
|
||||
return false
|
||||
}
|
||||
if r.Method == "POST" && r.FormValue("SynoToken") != "" {
|
||||
return false
|
||||
}
|
||||
// We need a SynoToken for authenticate.cgi.
|
||||
// So we tell the client to get one.
|
||||
serverURL := r.URL.Scheme + "://" + r.URL.Host
|
||||
fmt.Fprintf(w, synoTokenRedirectHTML, serverURL)
|
||||
return true
|
||||
}
|
||||
|
||||
const synoTokenRedirectHTML = `<html><body>
|
||||
Redirecting with session token...
|
||||
<script>
|
||||
var serverURL = %q;
|
||||
var req = new XMLHttpRequest();
|
||||
req.overrideMimeType("application/json");
|
||||
req.open("GET", serverURL + "/webman/login.cgi", true);
|
||||
req.onload = function() {
|
||||
var jsonResponse = JSON.parse(req.responseText);
|
||||
var token = jsonResponse["SynoToken"];
|
||||
document.location.href = serverURL + "/webman/3rdparty/Tailscale/?SynoToken=" + token;
|
||||
};
|
||||
req.send(null);
|
||||
</script>
|
||||
</body></html>
|
||||
`
|
||||
|
||||
func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if synoTokenRedirect(w, r) {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := auth()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == "POST" {
|
||||
type mi map[string]interface{}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
url, err := tailscaleUp(r.Context())
|
||||
if err != nil {
|
||||
json.NewEncoder(w).Encode(mi{"error": err})
|
||||
return
|
||||
}
|
||||
json.NewEncoder(w).Encode(mi{"url": url})
|
||||
return
|
||||
}
|
||||
|
||||
st, err := tailscale.Status(r.Context())
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
|
||||
data := tmplData{
|
||||
SynologyUser: user,
|
||||
Status: st.BackendState,
|
||||
DeviceName: st.Self.DNSName,
|
||||
}
|
||||
if len(st.TailscaleIPs) != 0 {
|
||||
data.IP = st.TailscaleIPs[0].String()
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := tmpl.Execute(buf, data); err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
w.Write(buf.Bytes())
|
||||
}
|
||||
|
||||
// TODO(crawshaw): some of this is very similar to the code in 'tailscale up', can we share anything?
|
||||
func tailscaleUp(ctx context.Context) (authURL string, retErr error) {
|
||||
prefs := ipn.NewPrefs()
|
||||
prefs.ControlURL = "https://login.tailscale.com"
|
||||
prefs.WantRunning = true
|
||||
prefs.CorpDNS = true
|
||||
prefs.AllowSingleHosts = true
|
||||
prefs.ForceDaemon = (runtime.GOOS == "windows")
|
||||
|
||||
if distro.Get() == distro.Synology {
|
||||
prefs.NetfilterMode = preftype.NetfilterOff
|
||||
}
|
||||
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
bc.SetPrefs(prefs)
|
||||
|
||||
opts := ipn.Options{
|
||||
StateKey: ipn.GlobalDaemonStateKey,
|
||||
Notify: func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
msg := *n.ErrMessage
|
||||
if msg == ipn.ErrMsgPermissionDenied {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
msg += " (Tailscale service in use by other user?)"
|
||||
default:
|
||||
msg += " (try 'sudo tailscale up [...]')"
|
||||
}
|
||||
}
|
||||
retErr = fmt.Errorf("backend error: %v", msg)
|
||||
cancel()
|
||||
} else if url := n.BrowseToURL; url != nil {
|
||||
authURL = *url
|
||||
cancel()
|
||||
}
|
||||
},
|
||||
}
|
||||
bc.Start(opts)
|
||||
bc.StartLoginInteractive()
|
||||
pump(ctx, bc, c)
|
||||
|
||||
if authURL == "" && retErr == nil {
|
||||
return "", fmt.Errorf("login failed with no backend error message")
|
||||
}
|
||||
return authURL, retErr
|
||||
}
|
||||
47
cmd/tailscale/cli/web.html
Normal file
47
cmd/tailscale/cli/web.html
Normal file
@@ -0,0 +1,47 @@
|
||||
<!doctype html>
|
||||
<html><title>Tailscale Client</title><body>
|
||||
<h1>Tailscale</h1>
|
||||
<div style="float:right;">{{.SynologyUser}}</div>
|
||||
<table>
|
||||
<tr><th>Status:</th><td>{{.Status}}</td></tr>
|
||||
<tr><th>Device Name:</th><td>{{.DeviceName}}</td></tr>
|
||||
<tr><th>Tailscale IP:</th><td>{{.IP}}</td></tr>
|
||||
</table>
|
||||
|
||||
<p><input id="login" type="button" value="Log in…"></p>
|
||||
|
||||
<script>
|
||||
login.onclick = function() {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const token = urlParams.get("SynoToken");
|
||||
|
||||
var params = new URLSearchParams("up=true");
|
||||
if (token) {
|
||||
params.set("SynoToken", token)
|
||||
}
|
||||
|
||||
var req = new XMLHttpRequest();
|
||||
const url = [location.protocol, '//', location.host, location.pathname, "?", params.toString()].join('');
|
||||
req.overrideMimeType("application/json");
|
||||
req.open("POST", url, true);
|
||||
req.onload = function() {
|
||||
var jsonResponse = JSON.parse(req.responseText);
|
||||
const err = jsonResponse["error"];
|
||||
if (err) {
|
||||
document.body.innerText = err;
|
||||
return
|
||||
}
|
||||
var url = jsonResponse["url"];
|
||||
console.log("jsonResponse: ", jsonResponse);
|
||||
if (url) {
|
||||
document.location.href = url;
|
||||
} else {
|
||||
//location.reload();
|
||||
}
|
||||
};
|
||||
req.send(null);
|
||||
}
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -2,8 +2,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscale
|
||||
W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole
|
||||
github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli
|
||||
github.com/peterbourgon/ff/v2/ffcli from tailscale.com/cmd/tailscale/cli
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
@@ -15,6 +13,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn
|
||||
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
|
||||
@@ -29,16 +28,18 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/filter
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/tsaddr from tailscale.com/net/interfaces
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/types/empty from tailscale.com/ipn
|
||||
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
||||
tailscale.com/types/key from tailscale.com/derp+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/netmap from tailscale.com/ipn
|
||||
@@ -50,7 +51,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/types/wgkey from tailscale.com/types/netmap+
|
||||
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
||||
W tailscale.com/util/endian from tailscale.com/net/netns
|
||||
tailscale.com/util/lineread from tailscale.com/net/interfaces
|
||||
L tailscale.com/util/lineread from tailscale.com/net/interfaces
|
||||
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/wgengine/filter from tailscale.com/types/netmap
|
||||
@@ -65,21 +66,18 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/net/context/ctxhttp from golang.org/x/oauth2/internal
|
||||
golang.org/x/net/dns/dnsmessage from net
|
||||
golang.org/x/net/http/httpguts from net/http
|
||||
golang.org/x/net/http/httpguts from net/http+
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/oauth2 from tailscale.com/ipn+
|
||||
golang.org/x/oauth2/internal from golang.org/x/oauth2
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp
|
||||
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from tailscale.com/net/netns+
|
||||
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
|
||||
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
@@ -117,6 +115,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
debug/elf from rsc.io/goversion/version
|
||||
debug/macho from rsc.io/goversion/version
|
||||
debug/pe from rsc.io/goversion/version
|
||||
embed from tailscale.com/cmd/tailscale/cli
|
||||
encoding from encoding/json
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base64 from encoding/json+
|
||||
@@ -132,20 +131,22 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
hash/adler32 from compress/zlib
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/maphash from go4.org/mem
|
||||
html from tailscale.com/ipn/ipnstate
|
||||
html from tailscale.com/ipn/ipnstate+
|
||||
html/template from tailscale.com/cmd/tailscale/cli
|
||||
io from bufio+
|
||||
io/fs from crypto/rand+
|
||||
io/ioutil from golang.org/x/oauth2/internal+
|
||||
io/ioutil from golang.org/x/sys/cpu+
|
||||
log from expvar+
|
||||
math from compress/flate+
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
math/rand from math/big+
|
||||
mime from golang.org/x/oauth2/internal+
|
||||
mime from mime/multipart+
|
||||
mime/multipart from net/http
|
||||
mime/quotedprintable from mime/multipart
|
||||
net from crypto/tls+
|
||||
net/http from expvar+
|
||||
net/http/cgi from tailscale.com/cmd/tailscale/cli
|
||||
net/http/httptrace from github.com/tcnksm/go-httpstat+
|
||||
net/http/internal from net/http
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
@@ -156,7 +157,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
path from debug/dwarf+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from rsc.io/goversion/version
|
||||
regexp from rsc.io/goversion/version+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from golang.org/x/sync/singleflight
|
||||
sort from compress/flate+
|
||||
@@ -165,7 +166,9 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
sync from compress/flate+
|
||||
sync/atomic from context+
|
||||
syscall from crypto/rand+
|
||||
text/tabwriter from github.com/peterbourgon/ff/v2/ffcli
|
||||
text/tabwriter from github.com/peterbourgon/ff/v2/ffcli+
|
||||
text/template from html/template
|
||||
text/template/parse from html/template+
|
||||
time from compress/gzip+
|
||||
unicode from bytes+
|
||||
unicode/utf16 from encoding/asn1+
|
||||
|
||||
@@ -8,20 +8,19 @@ package main // import "tailscale.com/cmd/tailscale"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/apenwarr/fixconsole"
|
||||
"tailscale.com/cmd/tailscale/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := fixconsole.FixConsoleIfNeeded()
|
||||
if err != nil {
|
||||
log.Printf("fixConsoleOutput: %v\n", err)
|
||||
args := os.Args[1:]
|
||||
if name, _ := os.Executable(); strings.HasSuffix(filepath.Base(name), ".cgi") {
|
||||
args = []string{"web", "-cgi"}
|
||||
}
|
||||
|
||||
if err := cli.Run(os.Args[1:]); err != nil {
|
||||
if err := cli.Run(args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
@@ -60,24 +60,24 @@ func debugMode(args []string) error {
|
||||
}
|
||||
|
||||
func runMonitor(ctx context.Context) error {
|
||||
dump := func() {
|
||||
st, err := interfaces.GetState()
|
||||
if err != nil {
|
||||
log.Printf("error getting state: %v", err)
|
||||
return
|
||||
}
|
||||
dump := func(st *interfaces.State) {
|
||||
j, _ := json.MarshalIndent(st, "", " ")
|
||||
os.Stderr.Write(j)
|
||||
}
|
||||
mon, err := monitor.New(log.Printf, func() {
|
||||
log.Printf("Link monitor fired. State:")
|
||||
dump()
|
||||
})
|
||||
mon, err := monitor.New(log.Printf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mon.RegisterChangeCallback(func(changed bool, st *interfaces.State) {
|
||||
if !changed {
|
||||
log.Printf("Link monitor fired; no change")
|
||||
return
|
||||
}
|
||||
log.Printf("Link monitor fired. New state:")
|
||||
dump(st)
|
||||
})
|
||||
log.Printf("Starting link change monitor; initial state:")
|
||||
dump()
|
||||
dump(mon.InterfaceState())
|
||||
mon.Start()
|
||||
log.Printf("Started link change monitor; waiting...")
|
||||
select {}
|
||||
|
||||
@@ -3,11 +3,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
|
||||
LW github.com/go-multierror/multierror from tailscale.com/wgengine/router
|
||||
W 💣 github.com/github/certstore from tailscale.com/control/controlclient
|
||||
github.com/go-multierror/multierror from tailscale.com/wgengine/router+
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/wgengine/router/dns
|
||||
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
|
||||
github.com/google/btree from inet.af/netstack/tcpip/header+
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
|
||||
@@ -18,6 +20,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
|
||||
W github.com/pkg/errors from github.com/github/certstore
|
||||
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
|
||||
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
|
||||
@@ -33,43 +36,44 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 go4.org/mem from tailscale.com/control/controlclient+
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire
|
||||
gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
|
||||
gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/rand from gvisor.dev/gvisor/pkg/tcpip/network/hash+
|
||||
💣 gvisor.dev/gvisor/pkg/sleep from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/tcpip+
|
||||
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
|
||||
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/linewriter+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/buffer from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/hash/jenkins from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header from gvisor.dev/gvisor/pkg/tcpip/link/channel+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header/parse from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/link/channel from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/fragmentation from gvisor.dev/gvisor/pkg/tcpip/network/ipv4
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/hash from gvisor.dev/gvisor/pkg/tcpip/network/ipv4
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/ip from gvisor.dev/gvisor/pkg/tcpip/network/ipv4
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/ipv4 from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/ports from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
gvisor.dev/gvisor/pkg/tcpip/stack from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/icmp from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/packet from gvisor.dev/gvisor/pkg/tcpip/transport/raw
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/raw from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/transport/tcp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/tcpconntrack from gvisor.dev/gvisor/pkg/tcpip/stack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/udp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/tcpip+
|
||||
inet.af/netaddr from tailscale.com/control/controlclient+
|
||||
💣 inet.af/netstack/gohacks from inet.af/netstack/state/wire+
|
||||
inet.af/netstack/linewriter from inet.af/netstack/log
|
||||
inet.af/netstack/log from inet.af/netstack/state+
|
||||
inet.af/netstack/rand from inet.af/netstack/tcpip/network/hash+
|
||||
💣 inet.af/netstack/sleep from inet.af/netstack/tcpip/transport/tcp
|
||||
💣 inet.af/netstack/state from inet.af/netstack/tcpip+
|
||||
inet.af/netstack/state/wire from inet.af/netstack/state
|
||||
💣 inet.af/netstack/sync from inet.af/netstack/linewriter+
|
||||
💣 inet.af/netstack/tcpip from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
|
||||
💣 inet.af/netstack/tcpip/buffer from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/tcpip/hash/jenkins from inet.af/netstack/tcpip/stack+
|
||||
inet.af/netstack/tcpip/header from inet.af/netstack/tcpip/header/parse+
|
||||
inet.af/netstack/tcpip/header/parse from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/link/channel from tailscale.com/wgengine/netstack
|
||||
inet.af/netstack/tcpip/network/hash from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/network/internal/fragmentation from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/network/internal/ip from inet.af/netstack/tcpip/network/ipv4+
|
||||
inet.af/netstack/tcpip/network/ipv4 from tailscale.com/wgengine/netstack
|
||||
inet.af/netstack/tcpip/network/ipv6 from tailscale.com/wgengine/netstack
|
||||
inet.af/netstack/tcpip/ports from inet.af/netstack/tcpip/stack+
|
||||
inet.af/netstack/tcpip/seqnum from inet.af/netstack/tcpip/header+
|
||||
💣 inet.af/netstack/tcpip/stack from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/tcpip/transport/icmp from tailscale.com/wgengine/netstack
|
||||
inet.af/netstack/tcpip/transport/packet from inet.af/netstack/tcpip/transport/raw
|
||||
inet.af/netstack/tcpip/transport/raw from inet.af/netstack/tcpip/transport/icmp+
|
||||
💣 inet.af/netstack/tcpip/transport/tcp from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/tcpip/transport/tcpconntrack from inet.af/netstack/tcpip/stack
|
||||
inet.af/netstack/tcpip/transport/udp from inet.af/netstack/tcpip/adapters/gonet+
|
||||
inet.af/netstack/waiter from inet.af/netstack/tcpip+
|
||||
inet.af/peercred from tailscale.com/ipn/ipnserver
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp+
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
|
||||
tailscale.com/derp/derpmap from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/derp/derpmap from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/disco from tailscale.com/derp+
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/internal/deepprint from tailscale.com/ipn/ipnlocal+
|
||||
@@ -86,54 +90,60 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
|
||||
tailscale.com/logtail/filch from tailscale.com/logpolicy
|
||||
tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/net/dns from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/dnsfallback from tailscale.com/control/controlclient
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/net/netns from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/net/packet from tailscale.com/wgengine+
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn/ipnlocal+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/safesocket from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/control/controlclient+
|
||||
W 💣 tailscale.com/tempfork/wireguard-windows/firewall from tailscale.com/cmd/tailscaled
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/empty from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
||||
tailscale.com/types/key from tailscale.com/derp+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/opt from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/pad32 from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/persist from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/preftype from tailscale.com/ipn+
|
||||
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/dnsname from tailscale.com/wgengine/tsdns+
|
||||
tailscale.com/util/dnsname from tailscale.com/ipn/ipnstate+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
L tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/winutil from tailscale.com/logpolicy+
|
||||
tailscale.com/version from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/version/distro from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/wgengine/monitor from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/monitor from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/router/dns from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/tsdns from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/tstun from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/wgengine/wglog from tailscale.com/wgengine
|
||||
@@ -151,7 +161,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/context/ctxhttp from golang.org/x/oauth2/internal
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from net/http
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
@@ -161,8 +170,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/oauth2 from tailscale.com/control/controlclient+
|
||||
golang.org/x/oauth2/internal from golang.org/x/oauth2
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp
|
||||
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
@@ -181,7 +188,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from internal/profile+
|
||||
compress/zlib from debug/elf+
|
||||
container/heap from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
container/heap from inet.af/netstack/tcpip/transport/tcp
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdsa+
|
||||
@@ -222,7 +229,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
hash from compress/zlib+
|
||||
hash/adler32 from compress/zlib
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from tailscale.com/wgengine/magicsock
|
||||
hash/fnv from tailscale.com/wgengine/magicsock+
|
||||
hash/maphash from go4.org/mem
|
||||
html from net/http/pprof+
|
||||
io from bufio+
|
||||
@@ -233,7 +240,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
math/rand from github.com/mdlayher/netlink+
|
||||
mime from golang.org/x/oauth2/internal+
|
||||
mime from mime/multipart+
|
||||
mime/multipart from net/http
|
||||
mime/quotedprintable from mime/multipart
|
||||
net from crypto/tls+
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -93,6 +94,9 @@ func installSystemDaemonDarwin(args []string) (err error) {
|
||||
}()
|
||||
|
||||
// Copy ourselves to /usr/local/bin/tailscaled.
|
||||
if err := os.MkdirAll(filepath.Dir(targetBin), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find our own executable path: %w", err)
|
||||
|
||||
@@ -11,9 +11,11 @@ package main // import "tailscale.com/cmd/tailscaled"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
@@ -21,17 +23,25 @@ import (
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/go-multierror/multierror"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/net/socks5"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/types/flagtype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
@@ -56,19 +66,25 @@ func defaultTunName() string {
|
||||
// "utun" is recognized by wireguard-go/tun/tun_darwin.go
|
||||
// as a magic value that uses/creates any free number.
|
||||
return "utun"
|
||||
case "linux":
|
||||
if distro.Get() == distro.Synology {
|
||||
// Try TUN, but fall back to userspace networking if needed.
|
||||
// See https://github.com/tailscale/tailscale-synology/issues/35
|
||||
return "tailscale0,userspace-networking"
|
||||
}
|
||||
}
|
||||
return "tailscale0"
|
||||
}
|
||||
|
||||
var args struct {
|
||||
cleanup bool
|
||||
fake bool
|
||||
debug string
|
||||
tunname string
|
||||
tunname string // tun name, "userspace-networking", or comma-separated list thereof
|
||||
port uint16
|
||||
statepath string
|
||||
socketpath string
|
||||
verbose int
|
||||
socksAddr string // listen address for SOCKS5 server
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -94,9 +110,9 @@ func main() {
|
||||
printVersion := false
|
||||
flag.IntVar(&args.verbose, "verbose", 0, "log verbosity level; 0 is default, 1 or higher are increasingly verbose")
|
||||
flag.BoolVar(&args.cleanup, "cleanup", false, "clean up system state and exit")
|
||||
flag.BoolVar(&args.fake, "fake", false, "use userspace fake tunnel+routing instead of kernel TUN interface")
|
||||
flag.StringVar(&args.debug, "debug", "", "listen address ([ip]:port) of optional debug server")
|
||||
flag.StringVar(&args.tunname, "tun", defaultTunName(), "tunnel interface name")
|
||||
flag.StringVar(&args.socksAddr, "socks5-server", "", `optional [ip]:port to run a SOCK5 server (e.g. "localhost:1080")`)
|
||||
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
|
||||
flag.Var(flagtype.PortValue(&args.port, magicsock.DefaultPort), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
||||
flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "path of state file")
|
||||
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
|
||||
@@ -131,9 +147,9 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if runtime.GOOS == "darwin" && os.Getuid() != 0 {
|
||||
if runtime.GOOS == "darwin" && os.Getuid() != 0 && !strings.Contains(args.tunname, "userspace-networking") {
|
||||
log.SetFlags(0)
|
||||
log.Fatalf("tailscaled requires root; use sudo tailscaled")
|
||||
log.Fatalf("tailscaled requires root; use sudo tailscaled (or use --tun=userspace-networking)")
|
||||
}
|
||||
|
||||
if args.socketpath == "" && runtime.GOOS != "windows" {
|
||||
@@ -190,20 +206,72 @@ func run() error {
|
||||
go runDebugServer(debugMux, args.debug)
|
||||
}
|
||||
|
||||
var e wgengine.Engine
|
||||
if args.fake {
|
||||
var impl wgengine.FakeImplFunc
|
||||
if args.tunname == "userspace-networking" {
|
||||
impl = netstack.Impl
|
||||
}
|
||||
e, err = wgengine.NewFakeUserspaceEngine(logf, 0, impl)
|
||||
} else {
|
||||
e, err = wgengine.NewUserspaceEngine(logf, args.tunname, args.port)
|
||||
linkMon, err := monitor.New(logf)
|
||||
if err != nil {
|
||||
log.Fatalf("creating link monitor: %v", err)
|
||||
}
|
||||
pol.Logtail.SetLinkMonitor(linkMon)
|
||||
|
||||
var socksListener net.Listener
|
||||
if args.socksAddr != "" {
|
||||
var err error
|
||||
socksListener, err = net.Listen("tcp", args.socksAddr)
|
||||
if err != nil {
|
||||
log.Fatalf("SOCKS5 listener: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
e, useNetstack, err := createEngine(logf, linkMon)
|
||||
if err != nil {
|
||||
logf("wgengine.New: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
var ns *netstack.Impl
|
||||
if useNetstack {
|
||||
tunDev, magicConn, ok := e.(wgengine.InternalsGetter).GetInternals()
|
||||
if !ok {
|
||||
log.Fatalf("%T is not a wgengine.InternalsGetter", e)
|
||||
}
|
||||
ns, err = netstack.Create(logf, tunDev, e, magicConn)
|
||||
if err != nil {
|
||||
log.Fatalf("netstack.Create: %v", err)
|
||||
}
|
||||
if err := ns.Start(); err != nil {
|
||||
log.Fatalf("failed to start netstack: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if socksListener != nil {
|
||||
srv := &socks5.Server{
|
||||
Logf: logger.WithPrefix(logf, "socks5: "),
|
||||
}
|
||||
if useNetstack {
|
||||
srv.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return ns.DialContextTCP(ctx, addr)
|
||||
}
|
||||
} else {
|
||||
var mu sync.Mutex
|
||||
var dns netstack.DNSMap
|
||||
e.AddNetworkMapCallback(func(nm *netmap.NetworkMap) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
dns = netstack.DNSMapFromNetworkMap(nm)
|
||||
})
|
||||
srv.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
ipp, err := dns.Resolve(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, network, ipp.String())
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
log.Fatalf("SOCKS5 server exited: %v", srv.Serve(socksListener))
|
||||
}()
|
||||
}
|
||||
|
||||
e = wgengine.NewWatchdog(e)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -244,6 +312,50 @@ func run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func createEngine(logf logger.Logf, linkMon *monitor.Mon) (e wgengine.Engine, isUserspace bool, err error) {
|
||||
if args.tunname == "" {
|
||||
return nil, false, errors.New("no --tun value specified")
|
||||
}
|
||||
var errs []error
|
||||
for _, name := range strings.Split(args.tunname, ",") {
|
||||
logf("wgengine.NewUserspaceEngine(tun %q) ...", name)
|
||||
e, isUserspace, err = tryEngine(logf, linkMon, name)
|
||||
if err == nil {
|
||||
return e, isUserspace, nil
|
||||
}
|
||||
logf("wgengine.NewUserspaceEngine(tun %q) error: %v", name, err)
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return nil, false, multierror.New(errs)
|
||||
}
|
||||
|
||||
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.Engine, isUserspace bool, err error) {
|
||||
conf := wgengine.Config{
|
||||
ListenPort: args.port,
|
||||
LinkMonitor: linkMon,
|
||||
}
|
||||
isUserspace = name == "userspace-networking"
|
||||
if !isUserspace {
|
||||
dev, err := tstun.New(logf, name)
|
||||
if err != nil {
|
||||
tstun.Diagnose(logf, name)
|
||||
return nil, false, err
|
||||
}
|
||||
conf.Tun = dev
|
||||
r, err := router.New(logf, dev)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
return nil, false, err
|
||||
}
|
||||
conf.Router = r
|
||||
}
|
||||
e, err = wgengine.NewUserspaceEngine(logf, conf)
|
||||
if err != nil {
|
||||
return nil, isUserspace, err
|
||||
}
|
||||
return e, isUserspace, nil
|
||||
}
|
||||
|
||||
func newDebugMux() *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/debug/pprof/", pprof.Index)
|
||||
|
||||
@@ -21,16 +21,21 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/tempfork/wireguard-windows/firewall"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
const serviceName = "Tailscale"
|
||||
@@ -83,6 +88,10 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
|
||||
}
|
||||
|
||||
func beWindowsSubprocess() bool {
|
||||
if beFirewallKillswitch() {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(os.Args) != 3 || os.Args[1] != "/subproc" {
|
||||
return false
|
||||
}
|
||||
@@ -108,16 +117,69 @@ func beWindowsSubprocess() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func beFirewallKillswitch() bool {
|
||||
if len(os.Args) != 3 || os.Args[1] != "/firewall" {
|
||||
return false
|
||||
}
|
||||
|
||||
log.SetFlags(0)
|
||||
log.Printf("killswitch subprocess starting, tailscale GUID is %s", os.Args[2])
|
||||
|
||||
go func() {
|
||||
b := make([]byte, 16)
|
||||
for {
|
||||
_, err := os.Stdin.Read(b)
|
||||
if err != nil {
|
||||
log.Fatalf("parent process died or requested exit, exiting (%v)", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
guid, err := windows.GUIDFromString(os.Args[2])
|
||||
if err != nil {
|
||||
log.Fatalf("invalid GUID %q: %v", os.Args[2], err)
|
||||
}
|
||||
|
||||
luid, err := winipcfg.LUIDFromGUID(&guid)
|
||||
if err != nil {
|
||||
log.Fatalf("no interface with GUID %q", guid)
|
||||
}
|
||||
|
||||
noProtection := false
|
||||
var dnsIPs []net.IP // unused in called code.
|
||||
start := time.Now()
|
||||
firewall.EnableFirewall(uint64(luid), noProtection, dnsIPs)
|
||||
log.Printf("killswitch enabled, took %s", time.Since(start))
|
||||
|
||||
// Block until the monitor goroutine shuts us down.
|
||||
select {}
|
||||
}
|
||||
|
||||
func startIPNServer(ctx context.Context, logid string) error {
|
||||
var logf logger.Logf = log.Printf
|
||||
var eng wgengine.Engine
|
||||
var err error
|
||||
|
||||
getEngine := func() (wgengine.Engine, error) {
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, "Tailscale", 41641)
|
||||
dev, err := tstun.New(logf, "Tailscale")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r, err := router.New(logf, dev)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
return nil, err
|
||||
}
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
Tun: dev,
|
||||
Router: r,
|
||||
ListenPort: 41641,
|
||||
})
|
||||
if err != nil {
|
||||
r.Close()
|
||||
dev.Close()
|
||||
return nil, err
|
||||
}
|
||||
return wgengine.NewWatchdog(eng), nil
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -102,10 +101,10 @@ func (s Status) String() string {
|
||||
|
||||
type LoginGoal struct {
|
||||
_ structs.Incomparable
|
||||
wantLoggedIn bool // true if we *want* to be logged in
|
||||
token *oauth2.Token // oauth token to use when logging in
|
||||
flags LoginFlags // flags to use when logging in
|
||||
url string // auth url that needs to be visited
|
||||
wantLoggedIn bool // true if we *want* to be logged in
|
||||
token *tailcfg.Oauth2Token // oauth token to use when logging in
|
||||
flags LoginFlags // flags to use when logging in
|
||||
url string // auth url that needs to be visited
|
||||
}
|
||||
|
||||
// Client connects to a tailcontrol server for a node.
|
||||
@@ -179,8 +178,11 @@ func NewNoStart(opts Options) (*Client, error) {
|
||||
|
||||
}
|
||||
|
||||
func (c *Client) onHealthChange(key string, err error) {
|
||||
c.logf("controlclient: restarting map request for %q health change to new state: %v", key, err)
|
||||
func (c *Client) onHealthChange(sys health.Subsystem, err error) {
|
||||
if sys == health.SysOverall {
|
||||
return
|
||||
}
|
||||
c.logf("controlclient: restarting map request for %q health change to new state: %v", sys, err)
|
||||
c.cancelMapSafely()
|
||||
}
|
||||
|
||||
@@ -520,8 +522,10 @@ func (c *Client) mapRoutine() {
|
||||
c.mu.Lock()
|
||||
c.inPollNetMap = false
|
||||
c.mu.Unlock()
|
||||
health.SetInPollNetMap(false)
|
||||
|
||||
err := c.direct.PollNetMap(ctx, -1, func(nm *netmap.NetworkMap) {
|
||||
health.SetInPollNetMap(true)
|
||||
c.mu.Lock()
|
||||
|
||||
select {
|
||||
@@ -554,6 +558,7 @@ func (c *Client) mapRoutine() {
|
||||
}
|
||||
})
|
||||
|
||||
health.SetInPollNetMap(false)
|
||||
c.mu.Lock()
|
||||
c.synced = false
|
||||
c.inPollNetMap = false
|
||||
@@ -599,7 +604,6 @@ func (c *Client) SetHostinfo(hi *tailcfg.Hostinfo) {
|
||||
// No changes. Don't log.
|
||||
return
|
||||
}
|
||||
c.logf("Hostinfo: %v", hi)
|
||||
|
||||
// Send new Hostinfo to server
|
||||
c.sendNewMapRequest()
|
||||
@@ -663,7 +667,7 @@ func (c *Client) sendStatus(who string, err error, url string, nm *netmap.Networ
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) Login(t *oauth2.Token, flags LoginFlags) {
|
||||
func (c *Client) Login(t *tailcfg.Oauth2Token, flags LoginFlags) {
|
||||
c.logf("client.Login(%v, %v)", t != nil, flags)
|
||||
|
||||
c.mu.Lock()
|
||||
|
||||
69
control/controlclient/debug.go
Normal file
69
control/controlclient/debug.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2021 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 (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func dumpGoroutinesToURL(c *http.Client, targetURL string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
zbuf := new(bytes.Buffer)
|
||||
zw := gzip.NewWriter(zbuf)
|
||||
zw.Write(scrubbedGoroutineDump())
|
||||
zw.Close()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", targetURL, zbuf)
|
||||
if err != nil {
|
||||
log.Printf("dumpGoroutinesToURL: %v", err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Encoding", "gzip")
|
||||
t0 := time.Now()
|
||||
_, err = c.Do(req)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err != nil {
|
||||
log.Printf("dumpGoroutinesToURL error: %v to %v (after %v)", err, targetURL, d)
|
||||
} else {
|
||||
log.Printf("dumpGoroutinesToURL complete to %v (after %v)", targetURL, d)
|
||||
}
|
||||
}
|
||||
|
||||
var reHexArgs = regexp.MustCompile(`\b0x[0-9a-f]+\b`)
|
||||
|
||||
// scrubbedGoroutineDump returns the list of all current goroutines, but with the actual
|
||||
// values of arguments scrubbed out, lest it contain some private key material.
|
||||
func scrubbedGoroutineDump() []byte {
|
||||
buf := make([]byte, 1<<20)
|
||||
buf = buf[:runtime.Stack(buf, true)]
|
||||
|
||||
saw := map[string][]byte{} // "0x123" => "v1%3" (unique value 1 and its value mod 8)
|
||||
return reHexArgs.ReplaceAllFunc(buf, func(in []byte) []byte {
|
||||
if string(in) == "0x0" {
|
||||
return in
|
||||
}
|
||||
if v, ok := saw[string(in)]; ok {
|
||||
return v
|
||||
}
|
||||
u64, err := strconv.ParseUint(string(in[2:]), 16, 64)
|
||||
if err != nil {
|
||||
return []byte("??")
|
||||
}
|
||||
v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8))
|
||||
saw[string(in)] = v
|
||||
return v
|
||||
})
|
||||
}
|
||||
11
control/controlclient/debug_test.go
Normal file
11
control/controlclient/debug_test.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2021 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 "testing"
|
||||
|
||||
func TestScrubbedGoroutineDump(t *testing.T) {
|
||||
t.Logf("Got:\n%s\n", scrubbedGoroutineDump())
|
||||
}
|
||||
@@ -31,11 +31,12 @@ import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/oauth2"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/log/logheap"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
@@ -48,6 +49,7 @@ import (
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
// Direct is the client that connects to a tailcontrol server for a node.
|
||||
@@ -59,6 +61,7 @@ type Direct struct {
|
||||
newDecompressor func() (Decompressor, error)
|
||||
keepAlive bool
|
||||
logf logger.Logf
|
||||
linkMon *monitor.Mon // or nil
|
||||
discoPubKey tailcfg.DiscoKey
|
||||
machinePrivKey wgkey.Private
|
||||
debugFlags []string
|
||||
@@ -90,6 +93,7 @@ type Options struct {
|
||||
Logf logger.Logf
|
||||
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
|
||||
DebugFlags []string // debug settings to send to control
|
||||
LinkMonitor *monitor.Mon // optional link monitor
|
||||
|
||||
// KeepSharerAndUserSplit controls whether the client
|
||||
// understands Node.Sharer. If false, the Sharer is mapped to the User.
|
||||
@@ -126,16 +130,18 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
httpc := opts.HTTPTestClient
|
||||
if httpc == nil {
|
||||
dnsCache := &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward, // use default cache's forwarder
|
||||
UseLastGood: true,
|
||||
Forward: dnscache.Get().Forward, // use default cache's forwarder
|
||||
UseLastGood: true,
|
||||
LookupIPFallback: dnsfallback.Lookup,
|
||||
}
|
||||
dialer := netns.NewDialer()
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
tr.DialContext = dnscache.Dialer(dialer.DialContext, dnsCache)
|
||||
tr.ForceAttemptHTTP2 = true
|
||||
tr.TLSClientConfig = tlsdial.Config(serverURL.Host, tr.TLSClientConfig)
|
||||
tr.DialContext = dnscache.Dialer(dialer.DialContext, dnsCache)
|
||||
tr.DialTLSContext = dnscache.TLSDialer(dialer.DialContext, dnsCache, tr.TLSClientConfig)
|
||||
tr.ForceAttemptHTTP2 = true
|
||||
httpc = &http.Client{Transport: tr}
|
||||
}
|
||||
|
||||
@@ -152,6 +158,7 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
discoPubKey: opts.DiscoPublicKey,
|
||||
debugFlags: opts.DebugFlags,
|
||||
keepSharerAndUserSplit: opts.KeepSharerAndUserSplit,
|
||||
linkMon: opts.LinkMonitor,
|
||||
}
|
||||
if opts.Hostinfo == nil {
|
||||
c.SetHostinfo(NewHostinfo())
|
||||
@@ -258,7 +265,7 @@ func (c *Direct) TryLogout(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Direct) TryLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags) (url string, err error) {
|
||||
func (c *Direct) TryLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags LoginFlags) (url string, err error) {
|
||||
c.logf("direct.TryLogin(token=%v, flags=%v)", t != nil, flags)
|
||||
return c.doLoginOrRegen(ctx, t, flags, false, "")
|
||||
}
|
||||
@@ -268,7 +275,7 @@ func (c *Direct) WaitLoginURL(ctx context.Context, url string) (newUrl string, e
|
||||
return c.doLoginOrRegen(ctx, nil, LoginDefault, false, url)
|
||||
}
|
||||
|
||||
func (c *Direct) doLoginOrRegen(ctx context.Context, t *oauth2.Token, flags LoginFlags, regen bool, url string) (newUrl string, err error) {
|
||||
func (c *Direct) doLoginOrRegen(ctx context.Context, t *tailcfg.Oauth2Token, flags LoginFlags, regen bool, url string) (newUrl string, err error) {
|
||||
mustregen, url, err := c.doLogin(ctx, t, flags, regen, url)
|
||||
if err != nil {
|
||||
return url, err
|
||||
@@ -280,7 +287,7 @@ func (c *Direct) doLoginOrRegen(ctx context.Context, t *oauth2.Token, flags Logi
|
||||
return url, err
|
||||
}
|
||||
|
||||
func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags, regen bool, url string) (mustregen bool, newurl string, err error) {
|
||||
func (c *Direct) doLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags LoginFlags, regen bool, url string) (mustregen bool, newurl string, err error) {
|
||||
c.mu.Lock()
|
||||
persist := c.persist
|
||||
tryingNewKey := c.tryingNewKey
|
||||
@@ -344,12 +351,14 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
err = errors.New("hostinfo: BackendLogID missing")
|
||||
return regen, url, err
|
||||
}
|
||||
now := time.Now().Round(time.Second)
|
||||
request := tailcfg.RegisterRequest{
|
||||
Version: 1,
|
||||
OldNodeKey: tailcfg.NodeKey(oldNodeKey),
|
||||
NodeKey: tailcfg.NodeKey(tryingNewKey.Public()),
|
||||
Hostinfo: hostinfo,
|
||||
Followup: url,
|
||||
Timestamp: &now,
|
||||
}
|
||||
c.logf("RegisterReq: onode=%v node=%v fup=%v",
|
||||
request.OldNodeKey.ShortString(),
|
||||
@@ -358,6 +367,20 @@ func (c *Direct) doLogin(ctx context.Context, t *oauth2.Token, flags LoginFlags,
|
||||
request.Auth.Provider = persist.Provider
|
||||
request.Auth.LoginName = persist.LoginName
|
||||
request.Auth.AuthKey = authKey
|
||||
err = signRegisterRequest(&request, c.serverURL, c.serverKey, c.machinePrivKey.Public())
|
||||
if err != nil {
|
||||
// If signing failed, clear all related fields
|
||||
request.SignatureType = tailcfg.SignatureNone
|
||||
request.Timestamp = nil
|
||||
request.DeviceCert = nil
|
||||
request.Signature = nil
|
||||
|
||||
// Don't log the common error types. Signatures are not usually enabled,
|
||||
// so these are expected.
|
||||
if err != errCertificateNotConfigured && err != errNoCertStore {
|
||||
c.logf("RegisterReq sign error: %v", err)
|
||||
}
|
||||
}
|
||||
bodyData, err := encode(request, &serverKey, &c.machinePrivKey)
|
||||
if err != nil {
|
||||
return regen, url, err
|
||||
@@ -527,7 +550,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
vlogf = c.logf
|
||||
}
|
||||
|
||||
request := tailcfg.MapRequest{
|
||||
request := &tailcfg.MapRequest{
|
||||
Version: tailcfg.CurrentMapRequestVersion,
|
||||
KeepAlive: c.keepAlive,
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
@@ -539,12 +562,15 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
OmitPeers: cb == nil,
|
||||
}
|
||||
var extraDebugFlags []string
|
||||
if hostinfo != nil && ipForwardingBroken(hostinfo.RoutableIPs) {
|
||||
if hostinfo != nil && c.linkMon != nil && ipForwardingBroken(hostinfo.RoutableIPs, c.linkMon.InterfaceState()) {
|
||||
extraDebugFlags = append(extraDebugFlags, "warn-ip-forwarding-off")
|
||||
}
|
||||
if health.RouterHealth() != nil {
|
||||
extraDebugFlags = append(extraDebugFlags, "warn-router-unhealthy")
|
||||
}
|
||||
if health.NetworkCategoryHealth() != nil {
|
||||
extraDebugFlags = append(extraDebugFlags, "warn-network-category-unhealthy")
|
||||
}
|
||||
if len(extraDebugFlags) > 0 {
|
||||
old := request.DebugFlags
|
||||
request.DebugFlags = append(old[:len(old):len(old)], extraDebugFlags...)
|
||||
@@ -596,6 +622,8 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
health.NoteMapRequestHeard(request)
|
||||
|
||||
if cb == nil {
|
||||
io.Copy(ioutil.Discard, res.Body)
|
||||
return nil
|
||||
@@ -669,6 +697,14 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
return err
|
||||
}
|
||||
|
||||
if allowStream {
|
||||
health.GotStreamedMapResponse()
|
||||
}
|
||||
|
||||
if pr := resp.PingRequest; pr != nil {
|
||||
go answerPing(c.logf, c.httpc, pr)
|
||||
}
|
||||
|
||||
if resp.KeepAlive {
|
||||
vlogf("netmap: got keep-alive")
|
||||
} else {
|
||||
@@ -699,6 +735,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
if resp.Debug.LogHeapPprof {
|
||||
go logheap.LogHeap(resp.Debug.LogHeapURL)
|
||||
}
|
||||
if resp.Debug.GoroutineDumpURL != "" {
|
||||
go dumpGoroutinesToURL(c.httpc, resp.Debug.GoroutineDumpURL)
|
||||
}
|
||||
setControlAtomic(&controlUseDERPRoute, resp.Debug.DERPRoute)
|
||||
setControlAtomic(&controlTrimWGConfig, resp.Debug.TrimWGConfig)
|
||||
}
|
||||
@@ -911,7 +950,7 @@ func encode(v interface{}, serverKey *wgkey.Key, mkey *wgkey.Private) ([]byte, e
|
||||
return nil, err
|
||||
}
|
||||
if debugMap {
|
||||
if _, ok := v.(tailcfg.MapRequest); ok {
|
||||
if _, ok := v.(*tailcfg.MapRequest); ok {
|
||||
log.Printf("MapRequest: %s", b)
|
||||
}
|
||||
}
|
||||
@@ -1127,7 +1166,7 @@ func TrimWGConfig() opt.Bool {
|
||||
// and will definitely not work for the routes provided.
|
||||
//
|
||||
// It should not return false positives.
|
||||
func ipForwardingBroken(routes []netaddr.IPPrefix) bool {
|
||||
func ipForwardingBroken(routes []netaddr.IPPrefix, state *interfaces.State) bool {
|
||||
if len(routes) == 0 {
|
||||
// Nothing to route, so no need to warn.
|
||||
return false
|
||||
@@ -1141,8 +1180,21 @@ func ipForwardingBroken(routes []netaddr.IPPrefix) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
localIPs := map[netaddr.IP]bool{}
|
||||
for _, addrs := range state.InterfaceIPs {
|
||||
for _, pfx := range addrs {
|
||||
localIPs[pfx.IP] = true
|
||||
}
|
||||
}
|
||||
|
||||
v4Routes, v6Routes := false, false
|
||||
for _, r := range routes {
|
||||
// It's possible to advertise a route to one of the local
|
||||
// machine's local IPs. IP forwarding isn't required for this
|
||||
// to work, so we shouldn't warn for such exports.
|
||||
if r.IsSingleIP() && localIPs[r.IP] {
|
||||
continue
|
||||
}
|
||||
if r.IP.Is4() {
|
||||
v4Routes = true
|
||||
} else {
|
||||
@@ -1194,3 +1246,29 @@ func ipForwardingBroken(routes []netaddr.IPPrefix) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func answerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest) {
|
||||
if pr.URL == "" {
|
||||
logf("invalid PingRequest with no URL")
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "HEAD", pr.URL, nil)
|
||||
if err != nil {
|
||||
logf("http.NewRequestWithContext(%q): %v", pr.URL, err)
|
||||
return
|
||||
}
|
||||
if pr.Log {
|
||||
logf("answerPing: sending ping to %v ...", pr.URL)
|
||||
}
|
||||
t0 := time.Now()
|
||||
_, err = c.Do(req)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err != nil {
|
||||
logf("answerPing error: %v to %v (after %v)", err, pr.URL, d)
|
||||
} else if pr.Log {
|
||||
logf("answerPing complete to %v (after %v)", pr.URL, d)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ package controlclient
|
||||
import (
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
@@ -14,7 +15,12 @@ func init() {
|
||||
osVersion = osVersionWindows
|
||||
}
|
||||
|
||||
var winVerCache atomic.Value // of string
|
||||
|
||||
func osVersionWindows() string {
|
||||
if s, ok := winVerCache.Load().(string); ok {
|
||||
return s
|
||||
}
|
||||
cmd := exec.Command("cmd", "/c", "ver")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
out, _ := cmd.Output() // "\nMicrosoft Windows [Version 10.0.19041.388]\n\n"
|
||||
@@ -26,5 +32,8 @@ func osVersionWindows() string {
|
||||
if sp := strings.Index(s, " "); sp != -1 {
|
||||
s = s[sp+1:]
|
||||
}
|
||||
if s != "" {
|
||||
winVerCache.Store(s)
|
||||
}
|
||||
return s // "10.0.19041.388", ideally
|
||||
}
|
||||
|
||||
31
control/controlclient/sign.go
Normal file
31
control/controlclient/sign.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// 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 (
|
||||
"crypto"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
var (
|
||||
errNoCertStore = errors.New("no certificate store")
|
||||
errCertificateNotConfigured = errors.New("no certificate subject configured")
|
||||
)
|
||||
|
||||
// HashRegisterRequest generates the hash required sign or verify a
|
||||
// tailcfg.RegisterRequest with tailcfg.SignatureV1.
|
||||
func HashRegisterRequest(ts time.Time, serverURL string, deviceCert []byte, serverPubKey, machinePubKey wgkey.Key) []byte {
|
||||
h := crypto.SHA256.New()
|
||||
|
||||
// hash.Hash.Write never returns an error, so we don't check for one here.
|
||||
fmt.Fprintf(h, "%s%s%s%s%s",
|
||||
ts.UTC().Format(time.RFC3339), serverURL, deviceCert, serverPubKey, machinePubKey)
|
||||
|
||||
return h.Sum(nil)
|
||||
}
|
||||
160
control/controlclient/sign_supported.go
Normal file
160
control/controlclient/sign_supported.go
Normal file
@@ -0,0 +1,160 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build windows,cgo
|
||||
|
||||
// darwin,cgo is also supported by certstore but machineCertificateSubject will
|
||||
// need to be loaded by a different mechanism, so this is not currently enabled
|
||||
// on darwin.
|
||||
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/github/certstore"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/util/winutil"
|
||||
)
|
||||
|
||||
var getMachineCertificateSubjectOnce struct {
|
||||
sync.Once
|
||||
v string // Subject of machine certificate to search for
|
||||
}
|
||||
|
||||
// getMachineCertificateSubject returns the exact name of a Subject that needs
|
||||
// to be present in an identity's certificate chain to sign a RegisterRequest,
|
||||
// formatted as per pkix.Name.String(). The Subject may be that of the identity
|
||||
// itself, an intermediate CA or the root CA.
|
||||
//
|
||||
// If getMachineCertificateSubject() returns "" then no lookup will occur and
|
||||
// each RegisterRequest will be unsigned.
|
||||
//
|
||||
// Example: "CN=Tailscale Inc Test Root CA,OU=Tailscale Inc Test Certificate Authority,O=Tailscale Inc,ST=ON,C=CA"
|
||||
func getMachineCertificateSubject() string {
|
||||
getMachineCertificateSubjectOnce.Do(func() {
|
||||
getMachineCertificateSubjectOnce.v = winutil.GetRegString("MachineCertificateSubject", "")
|
||||
})
|
||||
|
||||
return getMachineCertificateSubjectOnce.v
|
||||
}
|
||||
|
||||
var (
|
||||
errNoMatch = errors.New("no matching certificate")
|
||||
errBadRequest = errors.New("malformed request")
|
||||
)
|
||||
|
||||
// findIdentity locates an identity from the Windows or Darwin certificate
|
||||
// store. It returns the first certificate with a matching Subject anywhere in
|
||||
// its certificate chain, so it is possible to search for the leaf certificate,
|
||||
// intermediate CA or root CA. If err is nil then the returned identity will
|
||||
// never be nil (if no identity is found, the error errNoMatch will be
|
||||
// returned). If an identity is returned then its certificate chain is also
|
||||
// returned.
|
||||
func findIdentity(subject string, st certstore.Store) (certstore.Identity, []*x509.Certificate, error) {
|
||||
ids, err := st.Identities()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var selected certstore.Identity
|
||||
var chain []*x509.Certificate
|
||||
|
||||
for _, id := range ids {
|
||||
chain, err = id.CertificateChain()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if chain[0].PublicKeyAlgorithm != x509.RSA {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, c := range chain {
|
||||
if c.Subject.String() == subject {
|
||||
selected = id
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, id := range ids {
|
||||
if id != selected {
|
||||
id.Close()
|
||||
}
|
||||
}
|
||||
|
||||
if selected == nil {
|
||||
return nil, nil, errNoMatch
|
||||
}
|
||||
|
||||
return selected, chain, nil
|
||||
}
|
||||
|
||||
// signRegisterRequest looks for a suitable machine identity from the local
|
||||
// system certificate store, and if one is found, signs the RegisterRequest
|
||||
// using that identity's public key. In addition to the signature, the full
|
||||
// certificate chain is included so that the control server can validate the
|
||||
// certificate from a copy of the root CA's certificate.
|
||||
func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey wgkey.Key) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("signRegisterRequest: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if req.Timestamp == nil {
|
||||
return errBadRequest
|
||||
}
|
||||
|
||||
machineCertificateSubject := getMachineCertificateSubject()
|
||||
if machineCertificateSubject == "" {
|
||||
return errCertificateNotConfigured
|
||||
}
|
||||
|
||||
st, err := certstore.Open(certstore.System)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open cert store: %w", err)
|
||||
}
|
||||
defer st.Close()
|
||||
|
||||
id, chain, err := findIdentity(machineCertificateSubject, st)
|
||||
if err != nil {
|
||||
return fmt.Errorf("find identity: %w", err)
|
||||
}
|
||||
defer id.Close()
|
||||
|
||||
signer, err := id.Signer()
|
||||
if err != nil {
|
||||
return fmt.Errorf("create signer: %w", err)
|
||||
}
|
||||
|
||||
cl := 0
|
||||
for _, c := range chain {
|
||||
cl += len(c.Raw)
|
||||
}
|
||||
req.DeviceCert = make([]byte, 0, cl)
|
||||
for _, c := range chain {
|
||||
req.DeviceCert = append(req.DeviceCert, c.Raw...)
|
||||
}
|
||||
|
||||
h := HashRegisterRequest(req.Timestamp.UTC(), serverURL, req.DeviceCert, serverPubKey, machinePubKey)
|
||||
|
||||
req.Signature, err = signer.Sign(nil, h, &rsa.PSSOptions{
|
||||
SaltLength: rsa.PSSSaltLengthEqualsHash,
|
||||
Hash: crypto.SHA256,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("sign: %w", err)
|
||||
}
|
||||
req.SignatureType = tailcfg.SignatureV1
|
||||
|
||||
return nil
|
||||
}
|
||||
17
control/controlclient/sign_unsupported.go
Normal file
17
control/controlclient/sign_unsupported.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !windows !cgo
|
||||
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
// signRegisterRequest on non-supported platforms always returns errNoCertStore.
|
||||
func signRegisterRequest(req *tailcfg.RegisterRequest, serverURL string, serverPubKey, machinePubKey wgkey.Key) error {
|
||||
return errNoCertStore
|
||||
}
|
||||
@@ -59,7 +59,8 @@ Login:
|
||||
* server sends frameServerInfo
|
||||
|
||||
Steady state:
|
||||
* server occasionally sends frameKeepAlive
|
||||
* server occasionally sends frameKeepAlive (or framePing)
|
||||
* client responds to any framePing with a framePong
|
||||
* client sends frameSendPacket
|
||||
* server then sends frameRecvPacket to recipient
|
||||
*/
|
||||
@@ -97,6 +98,9 @@ const (
|
||||
// connection. (To be used for cluster load balancing
|
||||
// purposes, when clients end up on a non-ideal node)
|
||||
frameClosePeer = frameType(0x11) // 32B pub key of peer to close.
|
||||
|
||||
framePing = frameType(0x12) // 8 byte ping payload, to be echoed back in framePong
|
||||
framePong = frameType(0x13) // 8 byte payload, the contents of the ping being replied to
|
||||
)
|
||||
|
||||
var bin = binary.BigEndian
|
||||
|
||||
@@ -21,13 +21,14 @@ 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
|
||||
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
|
||||
canAckPings bool
|
||||
|
||||
wmu sync.Mutex // hold while writing to bw
|
||||
bw *bufio.Writer
|
||||
@@ -48,8 +49,9 @@ func (f clientOptFunc) update(o *clientOpt) { f(o) }
|
||||
|
||||
// clientOpt are the options passed to newClient.
|
||||
type clientOpt struct {
|
||||
MeshKey string
|
||||
ServerPub key.Public
|
||||
MeshKey string
|
||||
ServerPub key.Public
|
||||
CanAckPings bool
|
||||
}
|
||||
|
||||
// MeshKey returns a ClientOpt to pass to the DERP server during connect to get
|
||||
@@ -64,6 +66,12 @@ func ServerPublicKey(key key.Public) ClientOpt {
|
||||
return clientOptFunc(func(o *clientOpt) { o.ServerPub = key })
|
||||
}
|
||||
|
||||
// CanAckPings returns a ClientOpt to set whether it advertises to the
|
||||
// server that it's capable of acknowledging ping requests.
|
||||
func CanAckPings(v bool) ClientOpt {
|
||||
return clientOptFunc(func(o *clientOpt) { o.CanAckPings = v })
|
||||
}
|
||||
|
||||
func NewClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opts ...ClientOpt) (*Client, error) {
|
||||
var opt clientOpt
|
||||
for _, o := range opts {
|
||||
@@ -77,13 +85,14 @@ func NewClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logg
|
||||
|
||||
func newClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opt clientOpt) (*Client, error) {
|
||||
c := &Client{
|
||||
privateKey: privateKey,
|
||||
publicKey: privateKey.Public(),
|
||||
logf: logf,
|
||||
nc: nc,
|
||||
br: brw.Reader,
|
||||
bw: brw.Writer,
|
||||
meshKey: opt.MeshKey,
|
||||
privateKey: privateKey,
|
||||
publicKey: privateKey.Public(),
|
||||
logf: logf,
|
||||
nc: nc,
|
||||
br: brw.Reader,
|
||||
bw: brw.Writer,
|
||||
meshKey: opt.MeshKey,
|
||||
canAckPings: opt.CanAckPings,
|
||||
}
|
||||
if opt.ServerPub.IsZero() {
|
||||
if err := c.recvServerKey(); err != nil {
|
||||
@@ -147,6 +156,10 @@ type clientInfo struct {
|
||||
// connection list & forward packets. It's empty for regular
|
||||
// users.
|
||||
MeshKey string `json:"meshKey,omitempty"`
|
||||
|
||||
// CanAckPings is whether the client declares it's able to ack
|
||||
// pings.
|
||||
CanAckPings bool
|
||||
}
|
||||
|
||||
func (c *Client) sendClientKey() error {
|
||||
@@ -155,8 +168,9 @@ func (c *Client) sendClientKey() error {
|
||||
return err
|
||||
}
|
||||
msg, err := json.Marshal(clientInfo{
|
||||
Version: ProtocolVersion,
|
||||
MeshKey: c.meshKey,
|
||||
Version: ProtocolVersion,
|
||||
MeshKey: c.meshKey,
|
||||
CanAckPings: c.canAckPings,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -238,6 +252,18 @@ func (c *Client) ForwardPacket(srcKey, dstKey key.Public, pkt []byte) (err error
|
||||
|
||||
func (c *Client) writeTimeoutFired() { c.nc.Close() }
|
||||
|
||||
func (c *Client) SendPong(data [8]byte) error {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
if err := writeFrameHeader(c.bw, framePong, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(data[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.bw.Flush()
|
||||
}
|
||||
|
||||
// NotePreferred sends a packet that tells the server whether this
|
||||
// client is the user's preferred server. This is only used in the
|
||||
// server for stats.
|
||||
@@ -319,6 +345,19 @@ type ServerInfoMessage struct{}
|
||||
|
||||
func (ServerInfoMessage) msg() {}
|
||||
|
||||
// PingMessage is a request from a client or server to reply to the
|
||||
// other side with a PongMessage with the given payload.
|
||||
type PingMessage [8]byte
|
||||
|
||||
func (PingMessage) msg() {}
|
||||
|
||||
// KeepAliveMessage is a one-way empty message from server to client, just to
|
||||
// keep the connection alive. It's like a PingMessage, but doesn't solicit
|
||||
// a reply from the client.
|
||||
type KeepAliveMessage struct{}
|
||||
|
||||
func (KeepAliveMessage) msg() {}
|
||||
|
||||
// Recv reads a message from the DERP server.
|
||||
//
|
||||
// The returned message may alias memory owned by the Client; it
|
||||
@@ -397,9 +436,9 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
// 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.
|
||||
continue
|
||||
// A one-way keep-alive message that doesn't require an acknowledgement.
|
||||
// This predated framePing/framePong.
|
||||
return KeepAliveMessage{}, nil
|
||||
case framePeerGone:
|
||||
if n < keyLen {
|
||||
c.logf("[unexpected] dropping short peerGone frame from DERP server")
|
||||
@@ -427,6 +466,15 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
copy(rp.Source[:], b[:keyLen])
|
||||
rp.Data = b[keyLen:n]
|
||||
return rp, nil
|
||||
|
||||
case framePing:
|
||||
var pm PingMessage
|
||||
if n < 8 {
|
||||
c.logf("[unexpected] dropping short ping frame")
|
||||
continue
|
||||
}
|
||||
copy(pm[:], b[:])
|
||||
return pm, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package derp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"crypto/x509"
|
||||
@@ -791,6 +792,63 @@ func TestMetaCert(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type dummyNetConn struct {
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (dummyNetConn) SetReadDeadline(time.Time) error { return nil }
|
||||
|
||||
func TestClientRecv(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
want interface{}
|
||||
}{
|
||||
{
|
||||
name: "ping",
|
||||
input: []byte{
|
||||
byte(framePing), 0, 0, 0, 8,
|
||||
1, 2, 3, 4, 5, 6, 7, 8,
|
||||
},
|
||||
want: PingMessage{1, 2, 3, 4, 5, 6, 7, 8},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Client{
|
||||
nc: dummyNetConn{},
|
||||
br: bufio.NewReader(bytes.NewReader(tt.input)),
|
||||
logf: t.Logf,
|
||||
}
|
||||
got, err := c.Recv()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("got %#v; want %#v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientSendPong(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
c := &Client{
|
||||
bw: bufio.NewWriter(&buf),
|
||||
}
|
||||
if err := c.SendPong([8]byte{1, 2, 3, 4, 5, 6, 7, 8}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []byte{
|
||||
byte(framePong), 0, 0, 0, 8,
|
||||
1, 2, 3, 4, 5, 6, 7, 8,
|
||||
}
|
||||
if !bytes.Equal(buf.Bytes(), want) {
|
||||
t.Errorf("unexpected output\nwrote: % 02x\n want: % 02x", buf.Bytes(), want)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
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) })
|
||||
|
||||
@@ -63,6 +63,7 @@ type Client struct {
|
||||
|
||||
mu sync.Mutex
|
||||
preferred bool
|
||||
canAckPings bool
|
||||
closed bool
|
||||
netConn io.Closer
|
||||
client *derp.Client
|
||||
@@ -333,7 +334,11 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
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), derp.ServerPublicKey(serverPub))
|
||||
derpClient, err = derp.NewClient(c.privateKey, httpConn, brw, c.logf,
|
||||
derp.MeshKey(c.MeshKey),
|
||||
derp.ServerPublicKey(serverPub),
|
||||
derp.CanAckPings(c.canAckPings),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -642,6 +647,38 @@ func (c *Client) ForwardPacket(from, to key.Public, b []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// SendPong sends a reply to a ping, with the ping's provided
|
||||
// challenge/identifier data.
|
||||
//
|
||||
// Unlike other send methods, SendPong makes no attempt to connect or
|
||||
// reconnect to the peer. It's best effort. If there's a connection
|
||||
// problem, the server will choose to hang up on us if we're not
|
||||
// replying.
|
||||
func (c *Client) SendPong(data [8]byte) error {
|
||||
c.mu.Lock()
|
||||
if c.closed {
|
||||
c.mu.Unlock()
|
||||
return ErrClientClosed
|
||||
}
|
||||
if c.client == nil {
|
||||
c.mu.Unlock()
|
||||
return errors.New("not connected")
|
||||
}
|
||||
dc := c.client
|
||||
c.mu.Unlock()
|
||||
|
||||
return dc.SendPong(data)
|
||||
}
|
||||
|
||||
// SetCanAckPings sets whether this client will reply to ping requests from the server.
|
||||
//
|
||||
// This only affects future connections.
|
||||
func (c *Client) SetCanAckPings(v bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.canAckPings = v
|
||||
}
|
||||
|
||||
// NotePreferred notes whether this Client is the caller's preferred
|
||||
// (home) DERP node. It's only used for stats.
|
||||
func (c *Client) NotePreferred(v bool) {
|
||||
|
||||
32
go.mod
32
go.mod
@@ -5,41 +5,43 @@ go 1.16
|
||||
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
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/github/certstore v0.1.0
|
||||
github.com/gliderlabs/ssh v0.2.2
|
||||
github.com/go-multierror/multierror v1.0.2
|
||||
github.com/go-ole/go-ole v1.2.4
|
||||
github.com/godbus/dbus/v5 v5.0.3
|
||||
github.com/golang/protobuf v1.4.2 // indirect
|
||||
github.com/google/go-cmp v0.5.4
|
||||
github.com/goreleaser/nfpm v1.1.10
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b
|
||||
github.com/klauspost/compress v1.10.10
|
||||
github.com/kr/pty v1.1.8
|
||||
github.com/mdlayher/netlink v1.2.0
|
||||
github.com/mdlayher/netlink v1.3.2
|
||||
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a
|
||||
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/pkg/errors v0.9.1 // indirect
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210210202228-3cc76ed5f222
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210327173134-f6a42a1646a0
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
|
||||
golang.org/x/sys v0.0.0-20210216224549-f992740a1bac
|
||||
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
|
||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8
|
||||
gvisor.dev/gvisor v0.0.0-20210111185822-3ff3110fcdd6
|
||||
gopkg.in/yaml.v2 v2.2.8 // indirect
|
||||
honnef.co/go/tools v0.1.0
|
||||
inet.af/netaddr v0.0.0-20210105212526-648fbc18a69d
|
||||
inet.af/peercred v0.0.0-20210216231719-993aa01eacaa
|
||||
inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44
|
||||
inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22
|
||||
inet.af/peercred v0.0.0-20210302202138-56e694897155
|
||||
rsc.io/goversion v1.2.0
|
||||
)
|
||||
|
||||
replace github.com/github/certstore => github.com/cyolosecurity/certstore v0.0.0-20200922073901-ece7f1d353c2
|
||||
|
||||
418
go.sum
418
go.sum
@@ -1,34 +1,7 @@
|
||||
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/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
|
||||
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
||||
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
|
||||
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
|
||||
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.20200908182639-5b44b70ab3ab/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
|
||||
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
||||
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
@@ -36,214 +9,95 @@ github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5 h1:P5U+E4x5OkVEK
|
||||
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=
|
||||
github.com/apenwarr/fixconsole v0.0.0-20191012055117-5a9f6489cc29/go.mod h1:JYWahgHer+Z2xbsgHPtaDYVWzeHDminu+YIBWkxpCAY=
|
||||
github.com/apenwarr/w32 v0.0.0-20190407065021-aa00fece76ab h1:CMGzRRCjnD50RjUFSArBLuCxiDvdp7b8YPAcikBEQ+k=
|
||||
github.com/apenwarr/w32 v0.0.0-20190407065021-aa00fece76ab/go.mod h1:nfFtvHn2Hgs9G1u0/J6LHQv//EksNC+7G8vXmd1VTJ8=
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
|
||||
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/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/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=
|
||||
github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=
|
||||
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-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE=
|
||||
github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE=
|
||||
github.com/containerd/containerd v1.3.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a/go.mod h1:W0qIOTD7mp2He++YVq+kgfXezRYqzP1uDuMVH1bITDY=
|
||||
github.com/containerd/fifo v0.0.0-20191213151349-ff969a566b00/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=
|
||||
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/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=
|
||||
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/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
||||
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/cyolosecurity/certstore v0.0.0-20200922073901-ece7f1d353c2 h1:TGPWAij+nY2FB7TlyUTqTmYvXJon/AZAfRMYc/76K80=
|
||||
github.com/cyolosecurity/certstore v0.0.0-20200922073901-ece7f1d353c2/go.mod h1:Sgb3YVYOB2iCO06NJ6We5gjXe7uxxM3zPYoEXjuTKno=
|
||||
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/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
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/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||
github.com/dpjacques/clockwork v0.1.1-0.20200827220843-c1f524b839be/go.mod h1:D8mP2A8vVT2GkXqPorSBmhnshhkFBYgzhA90KmJt25Y=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20201127111758-49e582c6c23d/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
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/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
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/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
||||
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
|
||||
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-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
|
||||
github.com/go-multierror/multierror v1.0.2 h1:AwsKbEXkmf49ajdFJgcFXqSG0aLo0HEyAE9zk9JguJo=
|
||||
github.com/go-multierror/multierror v1.0.2/go.mod h1:U7SZR/D9jHgt2nkSj8XcbCWdmVM2igraCHQ3HC1HiKY=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
|
||||
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
|
||||
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
|
||||
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
|
||||
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/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=
|
||||
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
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-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
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/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 v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
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.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
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/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
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/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3-0.20201020212313-ab46b8bd0abd/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
|
||||
github.com/google/go-cmp v0.5.4/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/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
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/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1/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/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
|
||||
github.com/goreleaser/nfpm v1.1.10 h1:0nwzKUJTcygNxTzVKq2Dh9wpVP1W2biUH6SNKmoxR3w=
|
||||
github.com/goreleaser/nfpm v1.1.10/go.mod h1:oOcoGRVwvKIODz57NUfiRwFWGfn00NXdgnn6MrYtO5k=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
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.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
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/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA=
|
||||
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391 h1:Dqu/4JhMV1vpXHDjzQCuDCEsjNi0xfuSmQlMOyqayKA=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
|
||||
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
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/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b h1:c3NTyLNozICy8B4mlMXemD3z/gXgQzVXZS/HqT+i3do=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=
|
||||
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/konsorten/go-windows-terminal-sequences v1.0.3/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/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.4-0.20190131011033-7dc38fb350b1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lxn/walk v0.0.0-20201110160827-18ea5e372cdb/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20201111105847-2a20daff6a55/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0=
|
||||
github.com/mattn/go-zglob v0.0.1 h1:xsEx/XUoVlI6yXjqBK062zYhRTZltCNmYPx6v+8DNaY=
|
||||
github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
|
||||
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY=
|
||||
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
|
||||
github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0=
|
||||
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
|
||||
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
|
||||
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
|
||||
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
|
||||
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
|
||||
github.com/mdlayher/netlink v1.2.0 h1:zPolhRjfuabdf8ofZsl56eoU+92cvSlAn13lw4veCZ0=
|
||||
github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=
|
||||
github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
|
||||
github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
|
||||
github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys=
|
||||
github.com/mdlayher/netlink v1.3.2 h1:fMZOU2/M7PRMzGM3br5l1N2fu6bPSHtRytmQ338a9iA=
|
||||
github.com/mdlayher/netlink v1.3.2/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
|
||||
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a h1:wMv2mvcHRH4jqIxaVL5t6gSq1hjPiaWH7TOcA0Z+uNo=
|
||||
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a/go.mod h1:HtjVsQfsrBm1GDcDTUFn4ZXhftxTwO/hxrvEiRc61U4=
|
||||
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
|
||||
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mohae/deepcopy v0.0.0-20170308212314-bb9b5e7adda9/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.5.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/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
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/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/runtime-spec v1.0.2/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/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/peterbourgon/ff/v2 v2.0.0 h1:lx0oYI5qr/FU1xnpNhQ+EZM04gKgn46jyYvGEEqBBbY=
|
||||
github.com/peterbourgon/ff/v2 v2.0.0/go.mod h1:xjwr+t+SjWm4L46fcj/D+Ap+6ME7+HqFzaP22pP5Ggk=
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 h1:+/+DxvQaYifJ+grD4klzrS5y+KJXldn/2YTl5JG+vZ8=
|
||||
@@ -251,220 +105,121 @@ github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnI
|
||||
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 v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b h1:+gCnWOZV8Z/8jehJ2CdqB47Z3S+SREmQcuXkRFLNsiI=
|
||||
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
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 v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
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/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 h1:lK99QQdH3yBWY6aGilF+IRlQIdmhzLrsEmF6JgN+Ryw=
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210210202228-3cc76ed5f222 h1:VzTS7LIwCH8jlxwrZguU0TsCLV/MDOunoNIDJdFajyM=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210210202228-3cc76ed5f222/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210324165952-2963b66bc23a h1:tQ7Y0ALSe5109GMFB7TVtfNBsVcAuM422hVSJrXWMTE=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210324165952-2963b66bc23a/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210327173134-f6a42a1646a0 h1:7KFBvUmm3TW/K+bAN22D7M6xSSoY/39s+PajaNBGrLw=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210327173134-f6a42a1646a0/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924=
|
||||
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 v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/vishvananda/netlink v1.0.1-0.20190930145447-2ec5bdc52b86/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/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.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/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.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go4.org/intern v0.0.0-20210101010959-7cab76ca296a h1:28p852HIWWaOS019DYK/A3yTmpm1HJaUce63pvll4C8=
|
||||
go4.org/intern v0.0.0-20210101010959-7cab76ca296a/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
|
||||
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg=
|
||||
go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174 h1:vSug/WNOi2+4jrKdivxayTN/zd8EA1UrStjpWvvo1jk=
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/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-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
|
||||
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
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-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/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 h1:gzMM0EjIYiRmJI3+jBdFuoynZlpxa2JQZsolKu09BXo=
|
||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
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-20191004110552-13f9640d40b9/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/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b h1:iFwSg7t5GZmB/Q5TjiEAsdoLDrdJRC1RiF2WhuV29Qw=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
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-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-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-20190813064441-fde4db37ae7a/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-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/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-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201117222635-ba5294a509c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210105210732-16f7687f5001/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210216224549-f992740a1bac h1:9glrpwtNjBYgRpb67AZJKHfzj1stG/8BL5H7In2oTC4=
|
||||
golang.org/x/sys v0.0.0-20210216224549-f992740a1bac/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e h1:XNp2Flc/1eWQGk5BLzqTAN7fQIwIbfyVTuVxXxZh73M=
|
||||
golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b h1:a0ErnNnPKmhDyIXQvdZr+Lq8dc8xpMeqkF8y5PgQU4Q=
|
||||
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
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/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-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/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/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-20181011042414-1f849cf54d09/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-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-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-20200609164405-eb789aa7ce50/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20201021000207-d49c4edd7d96/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58 h1:1Bs6RVeBFtLZ8Yi1Hk07DiOqzvwLD/4hln4iahvFlag=
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -474,85 +229,26 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1N
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20201111175144-60b3766b89b9 h1:qowcZ56hhpeoESmWzI4Exhx4Y78TpCyXUJur4/c0CoE=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20201111175144-60b3766b89b9/go.mod h1:LMeNfjlcPZTrBC1juwgbQyA4Zy2XVcsrdO/fIJxwyuA=
|
||||
golang.zx2c4.com/wireguard v0.0.20201118/go.mod h1:Dz+cq5bnrai9EpgYj4GDof/+qaGzbRWbeaAOs1bUYa0=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8 h1:nlXPqGA98n+qcq1pwZ28KjM5EsFQvamKS00A+VUeVjs=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8/go.mod h1:psva4yDnAHLuh7lUzOK7J7bLYxNFfo0iKWz+mi9gzkA=
|
||||
google.golang.org/api v0.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/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/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/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.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.1-0.20201020201750-d3470999428b h1:jEdfCm+8YTWSYgU4L7Nq0jjU+q9RxIhi0cXLTY+Ih3A=
|
||||
google.golang.org/protobuf v1.25.1-0.20201020201750-d3470999428b/go.mod h1:hFxJC2f0epmp1elRCiEGJTKAWbwxZ2nvqZdHl3FQXCY=
|
||||
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/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
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/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-20210111185822-3ff3110fcdd6 h1:H5EvGkFG+pgAAbZMV8Me3Gy+HUYdaDcGXKWWixZ0EE8=
|
||||
gvisor.dev/gvisor v0.0.0-20210111185822-3ff3110fcdd6/go.mod h1:5DEMKRjYDiM24fvDUWPjBpABm9ROMcv/kEcox3fHtm0=
|
||||
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.1.0 h1:AWNL1W1i7f0wNZ8VwOKNJ0sliKvOF/adn0EHenfUh+c=
|
||||
honnef.co/go/tools v0.1.0/go.mod h1:XtegFAyX/PfluP4921rXU5IkjkqBCDnUq4W8VCIoKvM=
|
||||
inet.af/netaddr v0.0.0-20210105212526-648fbc18a69d h1:6f0242aW/6x2enQBOSKgDS8KQNw6Tp7IVR8eG3x0Jc8=
|
||||
inet.af/netaddr v0.0.0-20210105212526-648fbc18a69d/go.mod h1:jPZo7Jy4nke2cCgISa4fKJKa5T7+EO8k5fWwWghzneg=
|
||||
inet.af/peercred v0.0.0-20210216231719-993aa01eacaa h1:6qseJO2iNDHl+MLL2BkO5oURJR4A9pLmRz11Yf7KdGM=
|
||||
inet.af/peercred v0.0.0-20210216231719-993aa01eacaa/go.mod h1:VZeNdG7cRIUqKl9DWoFX86AHyfYwdb4RextAw1CAEO4=
|
||||
k8s.io/api v0.16.13/go.mod h1:QWu8UWSTiuQZMMeYjwLs6ILu5O74qKSJ0c+4vrchDxs=
|
||||
k8s.io/apimachinery v0.16.13/go.mod h1:4HMHS3mDHtVttspuuhrJ1GGr/0S9B6iWYWZ57KnnZqQ=
|
||||
k8s.io/apimachinery v0.16.14-rc.0/go.mod h1:4HMHS3mDHtVttspuuhrJ1GGr/0S9B6iWYWZ57KnnZqQ=
|
||||
k8s.io/client-go v0.16.13/go.mod h1:UKvVT4cajC2iN7DCjLgT0KVY/cbY6DGdUCyRiIfws5M=
|
||||
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
|
||||
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
|
||||
k8s.io/kube-openapi v0.0.0-20200410163147-594e756bea31/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
|
||||
k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44 h1:p7fX77zWzZMuNdJUhniBsmN1OvFOrW9SOtvgnzqUZX4=
|
||||
inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44/go.mod h1:I2i9ONCXRZDnG1+7O8fSuYzjcPxHQXrIfzD/IkR87x4=
|
||||
inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22 h1:DNtszwGa6w76qlIr+PbPEnlBJdiRV8SaxeigOy0q1gg=
|
||||
inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22/go.mod h1:GVx+5OZtbG4TVOW5ilmyRZAZXr1cNwfqUEkTOtWK0PM=
|
||||
inet.af/peercred v0.0.0-20210302202138-56e694897155 h1:KojYNEYqDkZ2O3LdyTstR1l13L3ePKTIEM2h7ONkfkE=
|
||||
inet.af/peercred v0.0.0-20210302202138-56e694897155/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU=
|
||||
rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w=
|
||||
rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=
|
||||
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
|
||||
223
health/health.go
223
health/health.go
@@ -7,13 +7,52 @@
|
||||
package health
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-multierror/multierror"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
m = map[string]error{} // error key => err (or nil for no error)
|
||||
watchers = map[*watchHandle]func(string, error){} // opt func to run if error state changes
|
||||
// mu guards everything in this var block.
|
||||
mu sync.Mutex
|
||||
|
||||
sysErr = map[Subsystem]error{} // error key => err (or nil for no error)
|
||||
watchers = map[*watchHandle]func(Subsystem, error){} // opt func to run if error state changes
|
||||
timer *time.Timer
|
||||
|
||||
inMapPoll bool
|
||||
inMapPollSince time.Time
|
||||
lastMapPollEndedAt time.Time
|
||||
lastStreamedMapResponse time.Time
|
||||
derpHomeRegion int
|
||||
derpRegionConnected = map[int]bool{}
|
||||
derpRegionLastFrame = map[int]time.Time{}
|
||||
lastMapRequestHeard time.Time // time we got a 200 from control for a MapRequest
|
||||
ipnState string
|
||||
ipnWantRunning bool
|
||||
anyInterfaceUp = true // until told otherwise
|
||||
)
|
||||
|
||||
// Subsystem is the name of a subsystem whose health can be monitored.
|
||||
type Subsystem string
|
||||
|
||||
const (
|
||||
// SysOverall is the name representing the overall health of
|
||||
// the system, rather than one particular subsystem.
|
||||
SysOverall = Subsystem("overall")
|
||||
|
||||
// SysRouter is the name the wgengine/router subsystem.
|
||||
SysRouter = Subsystem("router")
|
||||
|
||||
// SysNetworkCategory is the name of the subsystem that sets
|
||||
// the Windows network adapter's "category" (public, private, domain).
|
||||
// If it's unhealthy, the Windows firewall rules won't match.
|
||||
SysNetworkCategory = Subsystem("network-category")
|
||||
)
|
||||
|
||||
type watchHandle byte
|
||||
@@ -22,37 +61,55 @@ type watchHandle byte
|
||||
// error changes state either to unhealthy or from unhealthy. It is
|
||||
// not called on transition from unknown to healthy. It must be non-nil
|
||||
// and is run in its own goroutine. The returned func unregisters it.
|
||||
func RegisterWatcher(cb func(errKey string, err error)) (unregister func()) {
|
||||
func RegisterWatcher(cb func(key Subsystem, err error)) (unregister func()) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
handle := new(watchHandle)
|
||||
watchers[handle] = cb
|
||||
if timer == nil {
|
||||
timer = time.AfterFunc(time.Minute, timerSelfCheck)
|
||||
}
|
||||
return func() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
delete(watchers, handle)
|
||||
if len(watchers) == 0 && timer != nil {
|
||||
timer.Stop()
|
||||
timer = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetRouter sets the state of the wgengine/router.Router.
|
||||
func SetRouterHealth(err error) { set("router", err) }
|
||||
func SetRouterHealth(err error) { set(SysRouter, err) }
|
||||
|
||||
// RouterHealth returns the wgengine/router.Router error state.
|
||||
func RouterHealth() error { return get("router") }
|
||||
func RouterHealth() error { return get(SysRouter) }
|
||||
|
||||
func get(key string) error {
|
||||
// SetNetworkCategoryHealth sets the state of setting the network adaptor's category.
|
||||
// This only applies on Windows.
|
||||
func SetNetworkCategoryHealth(err error) { set(SysNetworkCategory, err) }
|
||||
|
||||
func NetworkCategoryHealth() error { return get(SysNetworkCategory) }
|
||||
|
||||
func get(key Subsystem) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return m[key]
|
||||
return sysErr[key]
|
||||
}
|
||||
|
||||
func set(key string, err error) {
|
||||
func set(key Subsystem, err error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
old, ok := m[key]
|
||||
setLocked(key, err)
|
||||
}
|
||||
|
||||
func setLocked(key Subsystem, err error) {
|
||||
old, ok := sysErr[key]
|
||||
if !ok && err == nil {
|
||||
// Initial happy path.
|
||||
m[key] = nil
|
||||
sysErr[key] = nil
|
||||
selfCheckLocked()
|
||||
return
|
||||
}
|
||||
if ok && (old == nil) == (err == nil) {
|
||||
@@ -60,12 +117,152 @@ func set(key string, err error) {
|
||||
// don't run callbacks, but exact error might've
|
||||
// changed, so note it.
|
||||
if err != nil {
|
||||
m[key] = err
|
||||
sysErr[key] = err
|
||||
}
|
||||
return
|
||||
}
|
||||
m[key] = err
|
||||
sysErr[key] = err
|
||||
selfCheckLocked()
|
||||
for _, cb := range watchers {
|
||||
go cb(key, err)
|
||||
}
|
||||
}
|
||||
|
||||
// GotStreamedMapResponse notes that we got a tailcfg.MapResponse
|
||||
// message in streaming mode, even if it's just a keep-alive message.
|
||||
func GotStreamedMapResponse() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
lastStreamedMapResponse = time.Now()
|
||||
selfCheckLocked()
|
||||
}
|
||||
|
||||
// SetInPollNetMap records that we're in
|
||||
func SetInPollNetMap(v bool) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if v == inMapPoll {
|
||||
return
|
||||
}
|
||||
inMapPoll = v
|
||||
if v {
|
||||
inMapPollSince = time.Now()
|
||||
} else {
|
||||
lastMapPollEndedAt = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// SetMagicSockDERPHome notes what magicsock's view of its home DERP is.
|
||||
func SetMagicSockDERPHome(region int) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
derpHomeRegion = region
|
||||
selfCheckLocked()
|
||||
}
|
||||
|
||||
// NoteMapRequestHeard notes whenever we successfully sent a map request
|
||||
// to control for which we received a 200 response.
|
||||
func NoteMapRequestHeard(mr *tailcfg.MapRequest) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
// TODO: extract mr.HostInfo.NetInfo.PreferredDERP, compare
|
||||
// against SetMagicSockDERPHome and
|
||||
// SetDERPRegionConnectedState
|
||||
|
||||
lastMapRequestHeard = time.Now()
|
||||
selfCheckLocked()
|
||||
}
|
||||
|
||||
func SetDERPRegionConnectedState(region int, connected bool) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
derpRegionConnected[region] = connected
|
||||
selfCheckLocked()
|
||||
}
|
||||
|
||||
func NoteDERPRegionReceivedFrame(region int) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
derpRegionLastFrame[region] = time.Now()
|
||||
selfCheckLocked()
|
||||
}
|
||||
|
||||
// state is an ipn.State.String() value: "Running", "Stopped", "NeedsLogin", etc.
|
||||
func SetIPNState(state string, wantRunning bool) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
ipnState = state
|
||||
ipnWantRunning = wantRunning
|
||||
selfCheckLocked()
|
||||
}
|
||||
|
||||
// SetAnyInterfaceUp sets whether any network interface is up.
|
||||
func SetAnyInterfaceUp(up bool) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
anyInterfaceUp = up
|
||||
selfCheckLocked()
|
||||
}
|
||||
|
||||
func timerSelfCheck() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
selfCheckLocked()
|
||||
if timer != nil {
|
||||
timer.Reset(time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
func selfCheckLocked() {
|
||||
if ipnState == "" {
|
||||
// Don't check yet.
|
||||
return
|
||||
}
|
||||
setLocked(SysOverall, overallErrorLocked())
|
||||
}
|
||||
|
||||
func overallErrorLocked() error {
|
||||
if !anyInterfaceUp {
|
||||
return errors.New("network down")
|
||||
}
|
||||
if ipnState != "Running" || !ipnWantRunning {
|
||||
return fmt.Errorf("state=%v, wantRunning=%v", ipnState, ipnWantRunning)
|
||||
}
|
||||
now := time.Now()
|
||||
if !inMapPoll && (lastMapPollEndedAt.IsZero() || now.Sub(lastMapPollEndedAt) > 10*time.Second) {
|
||||
return errors.New("not in map poll")
|
||||
}
|
||||
const tooIdle = 2*time.Minute + 5*time.Second
|
||||
if d := now.Sub(lastStreamedMapResponse).Round(time.Second); d > tooIdle {
|
||||
return fmt.Errorf("no map response in %v", d)
|
||||
}
|
||||
rid := derpHomeRegion
|
||||
if rid == 0 {
|
||||
return errors.New("no DERP home")
|
||||
}
|
||||
if !derpRegionConnected[rid] {
|
||||
return fmt.Errorf("not connected to home DERP region %v", rid)
|
||||
}
|
||||
if d := now.Sub(derpRegionLastFrame[rid]).Round(time.Second); d > tooIdle {
|
||||
return fmt.Errorf("haven't heard from home DERP region %v in %v", rid, d)
|
||||
}
|
||||
|
||||
// TODO: use
|
||||
_ = inMapPollSince
|
||||
_ = lastMapPollEndedAt
|
||||
_ = lastStreamedMapResponse
|
||||
_ = lastMapRequestHeard
|
||||
|
||||
var errs []error
|
||||
for sys, err := range sysErr {
|
||||
if err == nil || sys == SysOverall {
|
||||
continue
|
||||
}
|
||||
errs = append(errs, fmt.Errorf("%v: %w", sys, err))
|
||||
}
|
||||
sort.Slice(errs, func(i, j int) bool {
|
||||
// Not super efficient (stringifying these in a sort), but probably max 2 or 3 items.
|
||||
return errs[i].Error() < errs[j].Error()
|
||||
})
|
||||
return multierror.New(errs)
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/router/dns"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/empty"
|
||||
@@ -28,7 +27,7 @@ const (
|
||||
Running
|
||||
)
|
||||
|
||||
// GoogleIDToken Type is the oauth2.Token.TokenType for the Google
|
||||
// GoogleIDToken Type is the tailcfg.Oauth2Token.TokenType for the Google
|
||||
// ID tokens used by the Android client.
|
||||
const GoogleIDTokenType = "ts_android_google_login"
|
||||
|
||||
@@ -65,7 +64,6 @@ type Notify struct {
|
||||
Prefs *Prefs // preferences were changed
|
||||
NetMap *netmap.NetworkMap // new netmap received
|
||||
Engine *EngineStatus // wireguard engine stats
|
||||
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
|
||||
@@ -143,7 +141,7 @@ type Backend interface {
|
||||
// eventually.
|
||||
StartLoginInteractive()
|
||||
// Login logs in with an OAuth2 token.
|
||||
Login(token *oauth2.Token)
|
||||
Login(token *tailcfg.Oauth2Token)
|
||||
// Logout terminates the current login session and stops the
|
||||
// wireguard engine.
|
||||
Logout()
|
||||
@@ -159,9 +157,6 @@ type Backend interface {
|
||||
// counts. Connection events are emitted automatically without
|
||||
// polling.
|
||||
RequestEngineStatus()
|
||||
// RequestStatus requests that a full Status update
|
||||
// notification is sent.
|
||||
RequestStatus()
|
||||
// FakeExpireAfter pretends that the current key is going to
|
||||
// expire after duration x. This is useful for testing GUIs to
|
||||
// make sure they react properly with keys that are going to
|
||||
@@ -170,5 +165,5 @@ type Backend interface {
|
||||
// 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)
|
||||
Ping(ip string, useTSMP bool)
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
|
||||
@@ -46,7 +46,7 @@ func (b *FakeBackend) StartLoginInteractive() {
|
||||
b.login()
|
||||
}
|
||||
|
||||
func (b *FakeBackend) Login(token *oauth2.Token) {
|
||||
func (b *FakeBackend) Login(token *tailcfg.Oauth2Token) {
|
||||
b.login()
|
||||
}
|
||||
|
||||
@@ -87,14 +87,10 @@ func (b *FakeBackend) RequestEngineStatus() {
|
||||
b.notify(Notify{Engine: &EngineStatus{}})
|
||||
}
|
||||
|
||||
func (b *FakeBackend) RequestStatus() {
|
||||
b.notify(Notify{Status: &ipnstate.Status{}})
|
||||
}
|
||||
|
||||
func (b *FakeBackend) FakeExpireAfter(x time.Duration) {
|
||||
b.notify(Notify{NetMap: &netmap.NetworkMap{}})
|
||||
}
|
||||
|
||||
func (b *FakeBackend) Ping(ip string) {
|
||||
func (b *FakeBackend) Ping(ip string, useTSMP bool) {
|
||||
b.notify(Notify{PingResult: &ipnstate.PingResult{}})
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
@@ -155,7 +155,7 @@ func (h *Handle) StartLoginInteractive() {
|
||||
h.b.StartLoginInteractive()
|
||||
}
|
||||
|
||||
func (h *Handle) Login(token *oauth2.Token) {
|
||||
func (h *Handle) Login(token *tailcfg.Oauth2Token) {
|
||||
h.b.Login(token)
|
||||
}
|
||||
|
||||
@@ -167,10 +167,6 @@ func (h *Handle) RequestEngineStatus() {
|
||||
h.b.RequestEngineStatus()
|
||||
}
|
||||
|
||||
func (h *Handle) RequestStatus() {
|
||||
h.b.RequestStatus()
|
||||
}
|
||||
|
||||
func (h *Handle) FakeExpireAfter(x time.Duration) {
|
||||
h.b.FakeExpireAfter(x)
|
||||
}
|
||||
|
||||
@@ -9,21 +9,26 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/internal/deepprint"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn/policy"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/portlist"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/empty"
|
||||
@@ -37,8 +42,6 @@ import (
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/router/dns"
|
||||
"tailscale.com/wgengine/tsdns"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
"tailscale.com/wgengine/wgcfg/nmcfg"
|
||||
)
|
||||
@@ -64,19 +67,21 @@ func getControlDebugFlags() []string {
|
||||
// state machine generates events back out to zero or more components.
|
||||
type LocalBackend struct {
|
||||
// Elements that are thread-safe or constant after construction.
|
||||
ctx context.Context // canceled by Close
|
||||
ctxCancel context.CancelFunc // cancels ctx
|
||||
logf logger.Logf // general logging
|
||||
keyLogf logger.Logf // for printing list of peers on change
|
||||
statsLogf logger.Logf // for printing peers stats on change
|
||||
e wgengine.Engine
|
||||
store ipn.StateStore
|
||||
backendLogID string
|
||||
portpoll *portlist.Poller // may be nil
|
||||
portpollOnce sync.Once // guards starting readPoller
|
||||
gotPortPollRes chan struct{} // closed upon first readPoller result
|
||||
serverURL string // tailcontrol URL
|
||||
newDecompressor func() (controlclient.Decompressor, error)
|
||||
ctx context.Context // canceled by Close
|
||||
ctxCancel context.CancelFunc // cancels ctx
|
||||
logf logger.Logf // general logging
|
||||
keyLogf logger.Logf // for printing list of peers on change
|
||||
statsLogf logger.Logf // for printing peers stats on change
|
||||
e wgengine.Engine
|
||||
store ipn.StateStore
|
||||
backendLogID string
|
||||
unregisterLinkMon func()
|
||||
unregisterHealthWatch func()
|
||||
portpoll *portlist.Poller // may be nil
|
||||
portpollOnce sync.Once // guards starting readPoller
|
||||
gotPortPollRes chan struct{} // closed upon first readPoller result
|
||||
serverURL string // tailcontrol URL
|
||||
newDecompressor func() (controlclient.Decompressor, error)
|
||||
|
||||
filterHash string
|
||||
|
||||
@@ -93,15 +98,16 @@ type LocalBackend struct {
|
||||
// hostinfo is mutated in-place while mu is held.
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
// netMap is not mutated in-place once set.
|
||||
netMap *netmap.NetworkMap
|
||||
nodeByAddr map[netaddr.IP]*tailcfg.Node
|
||||
activeLogin string // last logged LoginName from netMap
|
||||
engineStatus ipn.EngineStatus
|
||||
endpoints []string
|
||||
blocked bool
|
||||
authURL string
|
||||
interact bool
|
||||
prevIfState *interfaces.State
|
||||
netMap *netmap.NetworkMap
|
||||
nodeByAddr map[netaddr.IP]*tailcfg.Node
|
||||
activeLogin string // last logged LoginName from netMap
|
||||
engineStatus ipn.EngineStatus
|
||||
endpoints []string
|
||||
blocked bool
|
||||
authURL string
|
||||
interact bool
|
||||
prevIfState *interfaces.State
|
||||
peerAPIListeners []*peerAPIListener
|
||||
|
||||
// statusLock must be held before calling statusChanged.Wait() or
|
||||
// statusChanged.Broadcast().
|
||||
@@ -116,8 +122,8 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge
|
||||
panic("ipn.NewLocalBackend: wgengine must not be nil")
|
||||
}
|
||||
|
||||
// Default filter blocks everything, until Start() is called.
|
||||
e.SetFilter(filter.NewAllowNone(logf))
|
||||
// Default filter blocks everything and logs nothing, until Start() is called.
|
||||
e.SetFilter(filter.NewAllowNone(logf, &netaddr.IPSet{}))
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
portpoll, err := portlist.NewPoller()
|
||||
@@ -138,14 +144,33 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge
|
||||
portpoll: portpoll,
|
||||
gotPortPollRes: make(chan struct{}),
|
||||
}
|
||||
e.SetLinkChangeCallback(b.linkChange)
|
||||
b.statusChanged = sync.NewCond(&b.statusLock)
|
||||
|
||||
linkMon := e.GetLinkMonitor()
|
||||
b.prevIfState = linkMon.InterfaceState()
|
||||
// Call our linkChange code once with the current state, and
|
||||
// then also whenever it changes:
|
||||
b.linkChange(false, linkMon.InterfaceState())
|
||||
b.unregisterLinkMon = linkMon.RegisterChangeCallback(b.linkChange)
|
||||
|
||||
b.unregisterHealthWatch = health.RegisterWatcher(b.onHealthChange)
|
||||
|
||||
wiredPeerAPIPort := false
|
||||
if ig, ok := e.(wgengine.InternalsGetter); ok {
|
||||
if tunWrap, _, ok := ig.GetInternals(); ok {
|
||||
tunWrap.PeerAPIPort = b.getPeerAPIPortForTSMPPing
|
||||
wiredPeerAPIPort = true
|
||||
}
|
||||
}
|
||||
if !wiredPeerAPIPort {
|
||||
b.logf("[unexpected] failed to wire up peer API port for engine %T", e)
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// linkChange is called (in a new goroutine) by wgengine when its link monitor
|
||||
// detects a network change.
|
||||
// linkChange is our link monitor callback, called whenever the network changes.
|
||||
// major is whether ifst is different than earlier.
|
||||
func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
@@ -169,6 +194,18 @@ func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
|
||||
go b.authReconfig()
|
||||
}
|
||||
}
|
||||
|
||||
// If the local network configuration has changed, our filter may
|
||||
// need updating to tweak default routes.
|
||||
b.updateFilter(b.netMap, b.prefs)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) onHealthChange(sys health.Subsystem, err error) {
|
||||
if err == nil {
|
||||
b.logf("health(%q): ok", sys)
|
||||
} else {
|
||||
b.logf("health(%q): error: %v", sys, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown halts the backend and all its sub-components. The backend
|
||||
@@ -178,6 +215,8 @@ func (b *LocalBackend) Shutdown() {
|
||||
cli := b.c
|
||||
b.mu.Unlock()
|
||||
|
||||
b.unregisterLinkMon()
|
||||
b.unregisterHealthWatch()
|
||||
if cli != nil {
|
||||
cli.Shutdown()
|
||||
}
|
||||
@@ -194,62 +233,104 @@ func (b *LocalBackend) Status() *ipnstate.Status {
|
||||
return sb.Status()
|
||||
}
|
||||
|
||||
// StatusWithoutPeers is like Status but omits any details
|
||||
// of peers.
|
||||
func (b *LocalBackend) StatusWithoutPeers() *ipnstate.Status {
|
||||
sb := new(ipnstate.StatusBuilder)
|
||||
b.updateStatus(sb, nil)
|
||||
return sb.Status()
|
||||
}
|
||||
|
||||
// UpdateStatus implements ipnstate.StatusUpdater.
|
||||
func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
b.e.UpdateStatus(sb)
|
||||
b.updateStatus(sb, b.populatePeerStatusLocked)
|
||||
}
|
||||
|
||||
// updateStatus populates sb with status.
|
||||
//
|
||||
// extraLocked, if non-nil, is called while b.mu is still held.
|
||||
func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func(*ipnstate.StatusBuilder)) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
sb.SetBackendState(b.state.String())
|
||||
|
||||
sb.MutateStatus(func(s *ipnstate.Status) {
|
||||
s.Version = version.Long
|
||||
s.BackendState = b.state.String()
|
||||
s.AuthURL = b.authURL
|
||||
if b.netMap != nil {
|
||||
s.MagicDNSSuffix = b.netMap.MagicDNSSuffix()
|
||||
}
|
||||
})
|
||||
sb.MutateSelfStatus(func(ss *ipnstate.PeerStatus) {
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
ss.PeerAPIURL = append(ss.PeerAPIURL, pln.urlStr)
|
||||
}
|
||||
})
|
||||
// TODO: hostinfo, and its networkinfo
|
||||
// TODO: EngineStatus copy (and deprecate it?)
|
||||
if b.netMap != nil {
|
||||
sb.SetMagicDNSSuffix(b.netMap.MagicDNSSuffix())
|
||||
for id, up := range b.netMap.UserProfiles {
|
||||
sb.AddUser(id, up)
|
||||
}
|
||||
for _, p := range b.netMap.Peers {
|
||||
var lastSeen time.Time
|
||||
if p.LastSeen != nil {
|
||||
lastSeen = *p.LastSeen
|
||||
}
|
||||
var tailAddr string
|
||||
for _, addr := range p.Addresses {
|
||||
// The peer struct currently only allows a single
|
||||
// Tailscale IP address. For compatibility with the
|
||||
// old display, make sure it's the IPv4 address.
|
||||
if addr.IP.Is4() && addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.IP) {
|
||||
tailAddr = addr.IP.String()
|
||||
break
|
||||
}
|
||||
}
|
||||
sb.AddPeer(key.Public(p.Key), &ipnstate.PeerStatus{
|
||||
InNetworkMap: true,
|
||||
UserID: p.User,
|
||||
TailAddr: tailAddr,
|
||||
HostName: p.Hostinfo.Hostname,
|
||||
DNSName: p.Name,
|
||||
OS: p.Hostinfo.OS,
|
||||
KeepAlive: p.KeepAlive,
|
||||
Created: p.Created,
|
||||
LastSeen: lastSeen,
|
||||
ShareeNode: p.Hostinfo.ShareeNode,
|
||||
ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID,
|
||||
})
|
||||
}
|
||||
|
||||
if extraLocked != nil {
|
||||
extraLocked(sb)
|
||||
}
|
||||
}
|
||||
|
||||
// WhoIs reports the node and user who owns the node with the given IP.
|
||||
func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
|
||||
if b.netMap == nil {
|
||||
return
|
||||
}
|
||||
for id, up := range b.netMap.UserProfiles {
|
||||
sb.AddUser(id, up)
|
||||
}
|
||||
for _, p := range b.netMap.Peers {
|
||||
var lastSeen time.Time
|
||||
if p.LastSeen != nil {
|
||||
lastSeen = *p.LastSeen
|
||||
}
|
||||
var tailAddr string
|
||||
for _, addr := range p.Addresses {
|
||||
// The peer struct currently only allows a single
|
||||
// Tailscale IP address. For compatibility with the
|
||||
// old display, make sure it's the IPv4 address.
|
||||
if addr.IP.Is4() && addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.IP) {
|
||||
tailAddr = addr.IP.String()
|
||||
break
|
||||
}
|
||||
}
|
||||
sb.AddPeer(key.Public(p.Key), &ipnstate.PeerStatus{
|
||||
InNetworkMap: true,
|
||||
UserID: p.User,
|
||||
TailAddr: tailAddr,
|
||||
HostName: p.Hostinfo.Hostname,
|
||||
DNSName: p.Name,
|
||||
OS: p.Hostinfo.OS,
|
||||
KeepAlive: p.KeepAlive,
|
||||
Created: p.Created,
|
||||
LastSeen: lastSeen,
|
||||
ShareeNode: p.Hostinfo.ShareeNode,
|
||||
ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// WhoIs reports the node and user who owns the node with the given IP:port.
|
||||
// If the IP address is a Tailscale IP, the provided port may be 0.
|
||||
// If ok == true, n and u are valid.
|
||||
func (b *LocalBackend) WhoIs(ip netaddr.IP) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool) {
|
||||
func (b *LocalBackend) WhoIs(ipp netaddr.IPPort) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
n, ok = b.nodeByAddr[ip]
|
||||
n, ok = b.nodeByAddr[ipp.IP]
|
||||
if !ok {
|
||||
return nil, u, false
|
||||
var ip netaddr.IP
|
||||
if ipp.Port != 0 {
|
||||
ip, ok = b.e.WhoIsIPPort(ipp)
|
||||
}
|
||||
if !ok {
|
||||
return nil, u, false
|
||||
}
|
||||
n, ok = b.nodeByAddr[ip]
|
||||
if !ok {
|
||||
return nil, u, false
|
||||
}
|
||||
}
|
||||
u, ok = b.netMap.UserProfiles[n.User]
|
||||
if !ok {
|
||||
@@ -310,7 +391,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
prefsChanged = true
|
||||
}
|
||||
if st.NetMap != nil {
|
||||
if b.keepOneExitNodeLocked(st.NetMap) {
|
||||
if b.findExitNodeIDLocked(st.NetMap) {
|
||||
prefsChanged = true
|
||||
}
|
||||
b.setNetMapLocked(st.NetMap)
|
||||
@@ -371,51 +452,35 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
b.authReconfig()
|
||||
}
|
||||
|
||||
// keepOneExitNodeLocked edits nm to retain only the default
|
||||
// routes provided by the exit node specified in b.prefs. It returns
|
||||
// whether prefs was mutated as part of the process, due to an exit
|
||||
// node IP being converted into a node ID.
|
||||
func (b *LocalBackend) keepOneExitNodeLocked(nm *netmap.NetworkMap) (prefsChanged bool) {
|
||||
// findExitNodeIDLocked updates b.prefs to reference an exit node by ID,
|
||||
// rather than by IP. It returns whether prefs was mutated.
|
||||
func (b *LocalBackend) findExitNodeIDLocked(nm *netmap.NetworkMap) (prefsChanged bool) {
|
||||
// If we have a desired IP on file, try to find the corresponding
|
||||
// node.
|
||||
if !b.prefs.ExitNodeIP.IsZero() {
|
||||
// IP takes precedence over ID, so if both are set, clear ID.
|
||||
if b.prefs.ExitNodeID != "" {
|
||||
b.prefs.ExitNodeID = ""
|
||||
prefsChanged = true
|
||||
}
|
||||
|
||||
peerLoop:
|
||||
for _, peer := range nm.Peers {
|
||||
for _, addr := range peer.Addresses {
|
||||
if !addr.IsSingleIP() || addr.IP != b.prefs.ExitNodeIP {
|
||||
continue
|
||||
}
|
||||
// Found the node being referenced, upgrade prefs to
|
||||
// reference it directly for next time.
|
||||
b.prefs.ExitNodeID = peer.StableID
|
||||
b.prefs.ExitNodeIP = netaddr.IP{}
|
||||
prefsChanged = true
|
||||
break peerLoop
|
||||
}
|
||||
}
|
||||
if b.prefs.ExitNodeIP.IsZero() {
|
||||
return false
|
||||
}
|
||||
|
||||
// IP takes precedence over ID, so if both are set, clear ID.
|
||||
if b.prefs.ExitNodeID != "" {
|
||||
b.prefs.ExitNodeID = ""
|
||||
prefsChanged = true
|
||||
}
|
||||
|
||||
// At this point, we have a node ID if the requested node is in
|
||||
// the netmap. If not, the ID will be empty, and we'll strip out
|
||||
// all default routes.
|
||||
for _, peer := range nm.Peers {
|
||||
out := peer.AllowedIPs[:0]
|
||||
for _, allowedIP := range peer.AllowedIPs {
|
||||
if allowedIP.Bits == 0 && peer.StableID != b.prefs.ExitNodeID {
|
||||
for _, addr := range peer.Addresses {
|
||||
if !addr.IsSingleIP() || addr.IP != b.prefs.ExitNodeIP {
|
||||
continue
|
||||
}
|
||||
out = append(out, allowedIP)
|
||||
// Found the node being referenced, upgrade prefs to
|
||||
// reference it directly for next time.
|
||||
b.prefs.ExitNodeID = peer.StableID
|
||||
b.prefs.ExitNodeIP = netaddr.IP{}
|
||||
return true
|
||||
}
|
||||
peer.AllowedIPs = out
|
||||
}
|
||||
|
||||
return prefsChanged
|
||||
return false
|
||||
}
|
||||
|
||||
// setWgengineStatus is the callback by the wireguard engine whenever it posts a new status.
|
||||
@@ -559,6 +624,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
HTTPTestClient: opts.HTTPTestClient,
|
||||
DiscoPublicKey: discoPublic,
|
||||
DebugFlags: controlDebugFlags,
|
||||
LinkMonitor: b.e.GetLinkMonitor(),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -608,40 +674,126 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
|
||||
haveNetmap = netMap != nil
|
||||
addrs []netaddr.IPPrefix
|
||||
packetFilter []filter.Match
|
||||
advRoutes []netaddr.IPPrefix
|
||||
localNetsB netaddr.IPSetBuilder
|
||||
logNetsB netaddr.IPSetBuilder
|
||||
shieldsUp = prefs == nil || prefs.ShieldsUp // Be conservative when not ready
|
||||
)
|
||||
// Log traffic for Tailscale IPs.
|
||||
logNetsB.AddPrefix(tsaddr.CGNATRange())
|
||||
logNetsB.AddPrefix(tsaddr.TailscaleULARange())
|
||||
logNetsB.RemovePrefix(tsaddr.ChromeOSVMRange())
|
||||
if haveNetmap {
|
||||
addrs = netMap.Addresses
|
||||
for _, p := range addrs {
|
||||
localNetsB.AddPrefix(p)
|
||||
}
|
||||
packetFilter = netMap.PacketFilter
|
||||
}
|
||||
if prefs != nil {
|
||||
advRoutes = prefs.AdvertiseRoutes
|
||||
for _, r := range prefs.AdvertiseRoutes {
|
||||
if r.Bits == 0 {
|
||||
// When offering a default route to the world, we
|
||||
// filter out locally reachable LANs, so that the
|
||||
// default route effectively appears to be a "guest
|
||||
// wifi": you get internet access, but to additionally
|
||||
// get LAN access the LAN(s) need to be offered
|
||||
// explicitly as well.
|
||||
s, err := shrinkDefaultRoute(r)
|
||||
if err != nil {
|
||||
b.logf("computing default route filter: %v", err)
|
||||
continue
|
||||
}
|
||||
localNetsB.AddSet(s)
|
||||
} else {
|
||||
localNetsB.AddPrefix(r)
|
||||
// When advertising a non-default route, we assume
|
||||
// this is a corporate subnet that should be present
|
||||
// in the audit logs.
|
||||
logNetsB.AddPrefix(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
localNets := localNetsB.IPSet()
|
||||
logNets := logNetsB.IPSet()
|
||||
|
||||
changed := deepprint.UpdateHash(&b.filterHash, haveNetmap, addrs, packetFilter, advRoutes, shieldsUp)
|
||||
changed := deepprint.UpdateHash(&b.filterHash, haveNetmap, addrs, packetFilter, localNets.Ranges(), logNets.Ranges(), shieldsUp)
|
||||
if !changed {
|
||||
return
|
||||
}
|
||||
|
||||
if !haveNetmap {
|
||||
b.logf("netmap packet filter: (not ready yet)")
|
||||
b.e.SetFilter(filter.NewAllowNone(b.logf))
|
||||
b.e.SetFilter(filter.NewAllowNone(b.logf, logNets))
|
||||
return
|
||||
}
|
||||
|
||||
localNets := unmapIPPrefixes(netMap.Addresses, advRoutes)
|
||||
|
||||
oldFilter := b.e.GetFilter()
|
||||
if shieldsUp {
|
||||
b.logf("netmap packet filter: (shields up)")
|
||||
b.e.SetFilter(filter.NewShieldsUpFilter(localNets, oldFilter, b.logf))
|
||||
b.e.SetFilter(filter.NewShieldsUpFilter(localNets, logNets, oldFilter, b.logf))
|
||||
} else {
|
||||
b.logf("netmap packet filter: %v", packetFilter)
|
||||
b.e.SetFilter(filter.New(packetFilter, localNets, oldFilter, b.logf))
|
||||
b.e.SetFilter(filter.New(packetFilter, localNets, logNets, oldFilter, b.logf))
|
||||
}
|
||||
}
|
||||
|
||||
var removeFromDefaultRoute = []netaddr.IPPrefix{
|
||||
// RFC1918 LAN ranges
|
||||
netaddr.MustParseIPPrefix("192.168.0.0/16"),
|
||||
netaddr.MustParseIPPrefix("172.16.0.0/12"),
|
||||
netaddr.MustParseIPPrefix("10.0.0.0/8"),
|
||||
// IPv4 link-local
|
||||
netaddr.MustParseIPPrefix("169.254.0.0/16"),
|
||||
// IPv4 multicast
|
||||
netaddr.MustParseIPPrefix("224.0.0.0/4"),
|
||||
// Tailscale IPv4 range
|
||||
tsaddr.CGNATRange(),
|
||||
// IPv6 Link-local addresses
|
||||
netaddr.MustParseIPPrefix("fe80::/10"),
|
||||
// IPv6 multicast
|
||||
netaddr.MustParseIPPrefix("ff00::/8"),
|
||||
// Tailscale IPv6 range
|
||||
tsaddr.TailscaleULARange(),
|
||||
}
|
||||
|
||||
// shrinkDefaultRoute returns an IPSet representing the IPs in route,
|
||||
// minus those in removeFromDefaultRoute and local interface subnets.
|
||||
func shrinkDefaultRoute(route netaddr.IPPrefix) (*netaddr.IPSet, error) {
|
||||
var b netaddr.IPSetBuilder
|
||||
b.AddPrefix(route)
|
||||
var hostIPs []netaddr.IP
|
||||
err := interfaces.ForeachInterfaceAddress(func(_ interfaces.Interface, pfx netaddr.IPPrefix) {
|
||||
if tsaddr.IsTailscaleIP(pfx.IP) {
|
||||
return
|
||||
}
|
||||
if pfx.IsSingleIP() {
|
||||
return
|
||||
}
|
||||
hostIPs = append(hostIPs, pfx.IP)
|
||||
b.RemovePrefix(pfx)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Having removed all the LAN subnets, re-add the hosts's own
|
||||
// IPs. It's fine for clients to connect to an exit node's public
|
||||
// IP address, just not the attached subnet.
|
||||
//
|
||||
// Truly forbidden subnets (in removeFromDefaultRoute) will still
|
||||
// be stripped back out by the next step.
|
||||
for _, ip := range hostIPs {
|
||||
if route.Contains(ip) {
|
||||
b.Add(ip)
|
||||
}
|
||||
}
|
||||
|
||||
for _, pfx := range removeFromDefaultRoute {
|
||||
b.RemovePrefix(pfx)
|
||||
}
|
||||
return b.IPSet(), nil
|
||||
}
|
||||
|
||||
// dnsCIDRsEqual determines whether two CIDR lists are equal
|
||||
// for DNS map construction purposes (that is, only the first entry counts).
|
||||
func dnsCIDRsEqual(newAddr, oldAddr []netaddr.IPPrefix) bool {
|
||||
@@ -710,8 +862,8 @@ func (b *LocalBackend) updateDNSMap(netMap *netmap.NetworkMap) {
|
||||
}
|
||||
set(netMap.Name, netMap.Addresses)
|
||||
|
||||
dnsMap := tsdns.NewMap(nameToIP, magicDNSRootDomains(netMap))
|
||||
// map diff will be logged in tsdns.Resolver.SetMap.
|
||||
dnsMap := dns.NewMap(nameToIP, magicDNSRootDomains(netMap))
|
||||
// map diff will be logged in dns.Resolver.SetMap.
|
||||
b.e.SetDNSMap(dnsMap)
|
||||
}
|
||||
|
||||
@@ -992,7 +1144,7 @@ func (b *LocalBackend) getEngineStatus() ipn.EngineStatus {
|
||||
}
|
||||
|
||||
// Login implements Backend.
|
||||
func (b *LocalBackend) Login(token *oauth2.Token) {
|
||||
func (b *LocalBackend) Login(token *tailcfg.Oauth2Token) {
|
||||
b.mu.Lock()
|
||||
b.assertClientLocked()
|
||||
c := b.c
|
||||
@@ -1043,13 +1195,13 @@ func (b *LocalBackend) FakeExpireAfter(x time.Duration) {
|
||||
b.send(ipn.Notify{NetMap: b.netMap})
|
||||
}
|
||||
|
||||
func (b *LocalBackend) Ping(ipStr string) {
|
||||
func (b *LocalBackend) Ping(ipStr string, useTSMP bool) {
|
||||
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.e.Ping(ip, useTSMP, func(pr *ipnstate.PingResult) {
|
||||
b.send(ipn.Notify{PingResult: pr})
|
||||
})
|
||||
}
|
||||
@@ -1189,25 +1341,58 @@ func (b *LocalBackend) SetPrefs(newp *ipn.Prefs) {
|
||||
b.send(ipn.Notify{Prefs: newp})
|
||||
}
|
||||
|
||||
func (b *LocalBackend) getPeerAPIPortForTSMPPing(ip netaddr.IP) (port uint16, ok bool) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
if pln.ip.BitLen() == ip.BitLen() {
|
||||
return uint16(pln.Port()), true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (b *LocalBackend) peerAPIServicesLocked() (ret []tailcfg.Service) {
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
proto := tailcfg.ServiceProto("peerapi4")
|
||||
if pln.ip.Is6() {
|
||||
proto = "peerapi6"
|
||||
}
|
||||
ret = append(ret, tailcfg.Service{
|
||||
Proto: proto,
|
||||
Port: uint16(pln.Port()),
|
||||
})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// doSetHostinfoFilterServices calls SetHostinfo on the controlclient,
|
||||
// possibly after mangling the given hostinfo.
|
||||
//
|
||||
// TODO(danderson): we shouldn't be mangling hostinfo here after
|
||||
// painstakingly constructing it in twelvety other places.
|
||||
func (b *LocalBackend) doSetHostinfoFilterServices(hi *tailcfg.Hostinfo) {
|
||||
hi2 := *hi
|
||||
b.mu.Lock()
|
||||
cc := b.c
|
||||
if cc == nil {
|
||||
// Control client isn't up yet.
|
||||
b.mu.Unlock()
|
||||
return
|
||||
}
|
||||
peerAPIServices := b.peerAPIServicesLocked()
|
||||
b.mu.Unlock()
|
||||
|
||||
// Make a shallow copy of hostinfo so we can mutate
|
||||
// at the Service field.
|
||||
hi2 := *hi // shallow copy
|
||||
if !b.shouldUploadServices() {
|
||||
hi2.Services = []tailcfg.Service{}
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
cli := b.c
|
||||
b.mu.Unlock()
|
||||
|
||||
// b.c might not be started yet
|
||||
if cli != nil {
|
||||
cli.SetHostinfo(&hi2)
|
||||
}
|
||||
// Don't mutate hi.Service's underlying array. Append to
|
||||
// the slice with no free capacity.
|
||||
c := len(hi2.Services)
|
||||
hi2.Services = append(hi2.Services[:c:c], peerAPIServices...)
|
||||
cc.SetHostinfo(&hi2)
|
||||
}
|
||||
|
||||
// NetMap returns the latest cached network map received from
|
||||
@@ -1268,7 +1453,7 @@ func (b *LocalBackend) authReconfig() {
|
||||
}
|
||||
}
|
||||
|
||||
cfg, err := nmcfg.WGCfg(nm, b.logf, flags)
|
||||
cfg, err := nmcfg.WGCfg(nm, b.logf, flags, uc.ExitNodeID)
|
||||
if err != nil {
|
||||
b.logf("wgcfg: %v", err)
|
||||
return
|
||||
@@ -1296,6 +1481,69 @@ func (b *LocalBackend) authReconfig() {
|
||||
return
|
||||
}
|
||||
b.logf("[v1] authReconfig: ra=%v dns=%v 0x%02x: %v", uc.RouteAll, uc.CorpDNS, flags, err)
|
||||
|
||||
b.initPeerAPIListener()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) initPeerAPIListener() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
pln.ln.Close()
|
||||
}
|
||||
b.peerAPIListeners = nil
|
||||
|
||||
selfNode := b.netMap.SelfNode
|
||||
if len(b.netMap.Addresses) == 0 || selfNode == nil {
|
||||
return
|
||||
}
|
||||
|
||||
stateFile := paths.DefaultTailscaledStateFile()
|
||||
if stateFile == "" {
|
||||
b.logf("peerapi disabled; no state directory")
|
||||
return
|
||||
}
|
||||
baseDir := fmt.Sprintf("%s-uid-%d",
|
||||
strings.ReplaceAll(b.activeLogin, "@", "-"),
|
||||
selfNode.User)
|
||||
dir := filepath.Join(filepath.Dir(stateFile), "files", baseDir)
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
b.logf("peerapi disabled; error making directory: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
var tunName string
|
||||
if ge, ok := b.e.(wgengine.InternalsGetter); ok {
|
||||
if tunWrap, _, ok := ge.GetInternals(); ok {
|
||||
tunName, _ = tunWrap.Name()
|
||||
}
|
||||
}
|
||||
|
||||
ps := &peerAPIServer{
|
||||
b: b,
|
||||
rootDir: dir,
|
||||
tunName: tunName,
|
||||
selfNode: selfNode,
|
||||
}
|
||||
|
||||
for _, a := range b.netMap.Addresses {
|
||||
ln, err := ps.listen(a.IP, b.prevIfState)
|
||||
if err != nil {
|
||||
b.logf("[unexpected] peerAPI listen(%q) error: %v", a.IP, err)
|
||||
continue
|
||||
}
|
||||
pln := &peerAPIListener{
|
||||
ps: ps,
|
||||
ip: a.IP,
|
||||
ln: ln,
|
||||
lb: b,
|
||||
}
|
||||
pln.urlStr = "http://" + net.JoinHostPort(a.IP.String(), strconv.Itoa(pln.Port()))
|
||||
|
||||
go pln.serve()
|
||||
b.peerAPIListeners = append(b.peerAPIListeners, pln)
|
||||
}
|
||||
}
|
||||
|
||||
// magicDNSRootDomains returns the subset of nm.DNS.Domains that are the search domains for MagicDNS.
|
||||
@@ -1312,6 +1560,41 @@ var (
|
||||
ipv6Default = netaddr.MustParseIPPrefix("::/0")
|
||||
)
|
||||
|
||||
// peerRoutes returns the routerConfig.Routes to access peers.
|
||||
// If there are over cgnatThreshold CGNAT routes, one big CGNAT route
|
||||
// is used instead.
|
||||
func peerRoutes(peers []wgcfg.Peer, cgnatThreshold int) (routes []netaddr.IPPrefix) {
|
||||
tsULA := tsaddr.TailscaleULARange()
|
||||
cgNAT := tsaddr.CGNATRange()
|
||||
var didULA bool
|
||||
var cgNATIPs []netaddr.IPPrefix
|
||||
for _, peer := range peers {
|
||||
for _, aip := range peer.AllowedIPs {
|
||||
aip = unmapIPPrefix(aip)
|
||||
// Only add the Tailscale IPv6 ULA once, if we see anybody using part of it.
|
||||
if aip.IP.Is6() && aip.IsSingleIP() && tsULA.Contains(aip.IP) {
|
||||
if !didULA {
|
||||
didULA = true
|
||||
routes = append(routes, tsULA)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if aip.IsSingleIP() && cgNAT.Contains(aip.IP) {
|
||||
cgNATIPs = append(cgNATIPs, aip)
|
||||
} else {
|
||||
routes = append(routes, aip)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(cgNATIPs) > cgnatThreshold {
|
||||
// Probably the hello server. Just append one big route.
|
||||
routes = append(routes, cgNAT)
|
||||
} else {
|
||||
routes = append(routes, cgNATIPs...)
|
||||
}
|
||||
return routes
|
||||
}
|
||||
|
||||
// routerConfig produces a router.Config from a wireguard config and IPN prefs.
|
||||
func routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router.Config {
|
||||
rs := &router.Config{
|
||||
@@ -1319,10 +1602,7 @@ func routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router.Config {
|
||||
SubnetRoutes: unmapIPPrefixes(prefs.AdvertiseRoutes),
|
||||
SNATSubnetRoutes: !prefs.NoSNAT,
|
||||
NetfilterMode: prefs.NetfilterMode,
|
||||
}
|
||||
|
||||
for _, peer := range cfg.Peers {
|
||||
rs.Routes = append(rs.Routes, unmapIPPrefixes(peer.AllowedIPs)...)
|
||||
Routes: peerRoutes(cfg.Peers, 10_000),
|
||||
}
|
||||
|
||||
// Sanity check: we expect the control server to program both a v4
|
||||
@@ -1359,10 +1639,14 @@ func routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router.Config {
|
||||
return rs
|
||||
}
|
||||
|
||||
func unmapIPPrefix(ipp netaddr.IPPrefix) netaddr.IPPrefix {
|
||||
return netaddr.IPPrefix{IP: ipp.IP.Unmap(), Bits: ipp.Bits}
|
||||
}
|
||||
|
||||
func unmapIPPrefixes(ippsList ...[]netaddr.IPPrefix) (ret []netaddr.IPPrefix) {
|
||||
for _, ipps := range ippsList {
|
||||
for _, ipp := range ipps {
|
||||
ret = append(ret, netaddr.IPPrefix{IP: ipp.IP.Unmap(), Bits: ipp.Bits})
|
||||
ret = append(ret, unmapIPPrefix(ipp))
|
||||
}
|
||||
}
|
||||
return ret
|
||||
@@ -1405,6 +1689,7 @@ func (b *LocalBackend) enterState(newState ipn.State) {
|
||||
}
|
||||
b.logf("Switching ipn state %v -> %v (WantRunning=%v)",
|
||||
state, newState, prefs.WantRunning)
|
||||
health.SetIPNState(newState.String(), prefs.WantRunning)
|
||||
if notify != nil {
|
||||
b.send(ipn.Notify{State: &newState})
|
||||
}
|
||||
@@ -1494,12 +1779,6 @@ func (b *LocalBackend) RequestEngineStatus() {
|
||||
b.e.RequestStatus()
|
||||
}
|
||||
|
||||
// RequestStatus implements Backend.
|
||||
func (b *LocalBackend) RequestStatus() {
|
||||
st := b.Status()
|
||||
b.send(ipn.Notify{Status: st})
|
||||
}
|
||||
|
||||
// stateMachine updates the state machine state based on other things
|
||||
// that have happened. It is invoked from the various callbacks that
|
||||
// feed events into LocalBackend.
|
||||
|
||||
@@ -5,11 +5,15 @@
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
func TestNetworkMapCompare(t *testing.T) {
|
||||
@@ -118,3 +122,172 @@ func TestNetworkMapCompare(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func inRemove(ip netaddr.IP) bool {
|
||||
for _, pfx := range removeFromDefaultRoute {
|
||||
if pfx.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestShrinkDefaultRoute(t *testing.T) {
|
||||
tests := []struct {
|
||||
route string
|
||||
in []string
|
||||
out []string
|
||||
localIPFn func(netaddr.IP) bool // true if this machine's local IP address should be "in" after shrinking.
|
||||
}{
|
||||
{
|
||||
route: "0.0.0.0/0",
|
||||
in: []string{"1.2.3.4", "25.0.0.1"},
|
||||
out: []string{
|
||||
"10.0.0.1",
|
||||
"10.255.255.255",
|
||||
"192.168.0.1",
|
||||
"192.168.255.255",
|
||||
"172.16.0.1",
|
||||
"172.31.255.255",
|
||||
"100.101.102.103",
|
||||
"224.0.0.1",
|
||||
"169.254.169.254",
|
||||
// Some random IPv6 stuff that shouldn't be in a v4
|
||||
// default route.
|
||||
"fe80::",
|
||||
"2601::1",
|
||||
},
|
||||
localIPFn: func(ip netaddr.IP) bool { return !inRemove(ip) && ip.Is4() },
|
||||
},
|
||||
{
|
||||
route: "::/0",
|
||||
in: []string{"::1", "2601::1"},
|
||||
out: []string{
|
||||
"fe80::1",
|
||||
"ff00::1",
|
||||
tsaddr.TailscaleULARange().IP.String(),
|
||||
},
|
||||
localIPFn: func(ip netaddr.IP) bool { return !inRemove(ip) && ip.Is6() },
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
def := netaddr.MustParseIPPrefix(test.route)
|
||||
got, err := shrinkDefaultRoute(def)
|
||||
if err != nil {
|
||||
t.Fatalf("shrinkDefaultRoute(%q): %v", test.route, err)
|
||||
}
|
||||
for _, ip := range test.in {
|
||||
if !got.Contains(netaddr.MustParseIP(ip)) {
|
||||
t.Errorf("shrink(%q).Contains(%v) = false, want true", test.route, ip)
|
||||
}
|
||||
}
|
||||
for _, ip := range test.out {
|
||||
if got.Contains(netaddr.MustParseIP(ip)) {
|
||||
t.Errorf("shrink(%q).Contains(%v) = true, want false", test.route, ip)
|
||||
}
|
||||
}
|
||||
ips, _, err := interfaces.LocalAddresses()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, ip := range ips {
|
||||
want := test.localIPFn(ip)
|
||||
if gotContains := got.Contains(ip); gotContains != want {
|
||||
t.Errorf("shrink(%q).Contains(%v) = %v, want %v", test.route, ip, gotContains, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerRoutes(t *testing.T) {
|
||||
pp := netaddr.MustParseIPPrefix
|
||||
tests := []struct {
|
||||
name string
|
||||
peers []wgcfg.Peer
|
||||
want []netaddr.IPPrefix
|
||||
}{
|
||||
{
|
||||
name: "small_v4",
|
||||
peers: []wgcfg.Peer{
|
||||
{
|
||||
AllowedIPs: []netaddr.IPPrefix{
|
||||
pp("100.101.102.103/32"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netaddr.IPPrefix{
|
||||
pp("100.101.102.103/32"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "big_v4",
|
||||
peers: []wgcfg.Peer{
|
||||
{
|
||||
AllowedIPs: []netaddr.IPPrefix{
|
||||
pp("100.101.102.103/32"),
|
||||
pp("100.101.102.104/32"),
|
||||
pp("100.101.102.105/32"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netaddr.IPPrefix{
|
||||
pp("100.64.0.0/10"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "has_1_v6",
|
||||
peers: []wgcfg.Peer{
|
||||
{
|
||||
AllowedIPs: []netaddr.IPPrefix{
|
||||
pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b240/128"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netaddr.IPPrefix{
|
||||
pp("fd7a:115c:a1e0::/48"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "has_2_v6",
|
||||
peers: []wgcfg.Peer{
|
||||
{
|
||||
AllowedIPs: []netaddr.IPPrefix{
|
||||
pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b240/128"),
|
||||
pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b241/128"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netaddr.IPPrefix{
|
||||
pp("fd7a:115c:a1e0::/48"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "big_v4_big_v6",
|
||||
peers: []wgcfg.Peer{
|
||||
{
|
||||
AllowedIPs: []netaddr.IPPrefix{
|
||||
pp("100.101.102.103/32"),
|
||||
pp("100.101.102.104/32"),
|
||||
pp("100.101.102.105/32"),
|
||||
pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b240/128"),
|
||||
pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b241/128"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netaddr.IPPrefix{
|
||||
pp("fd7a:115c:a1e0::/48"),
|
||||
pp("100.64.0.0/10"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := peerRoutes(tt.peers, 2)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("got = %v; want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ func TestLocalLogLines(t *testing.T) {
|
||||
|
||||
// set up a LocalBackend, super bare bones. No functional data.
|
||||
store := &ipn.MemoryStore{}
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logListen.Logf, 0, nil)
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logListen.Logf, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
243
ipn/ipnlocal/peerapi.go
Normal file
243
ipn/ipnlocal/peerapi.go
Normal file
@@ -0,0 +1,243 @@
|
||||
// Copyright (c) 2021 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 ipnlocal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
"html"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
var initListenConfig func(*net.ListenConfig, netaddr.IP, *interfaces.State, string) error
|
||||
|
||||
type peerAPIServer struct {
|
||||
b *LocalBackend
|
||||
rootDir string
|
||||
tunName string
|
||||
selfNode *tailcfg.Node
|
||||
}
|
||||
|
||||
func (s *peerAPIServer) listen(ip netaddr.IP, ifState *interfaces.State) (ln net.Listener, err error) {
|
||||
ipStr := ip.String()
|
||||
|
||||
var lc net.ListenConfig
|
||||
if initListenConfig != nil {
|
||||
// On iOS/macOS, this sets the lc.Control hook to
|
||||
// setsockopt the interface index to bind to, to get
|
||||
// out of the network sandbox.
|
||||
if err := initListenConfig(&lc, ip, ifState, s.tunName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
|
||||
ipStr = ""
|
||||
}
|
||||
}
|
||||
|
||||
tcp4or6 := "tcp4"
|
||||
if ip.Is6() {
|
||||
tcp4or6 = "tcp6"
|
||||
}
|
||||
|
||||
// Make a best effort to pick a deterministic port number for
|
||||
// the ip The lower three bytes are the same for IPv4 and IPv6
|
||||
// Tailscale addresses (at least currently), so we'll usually
|
||||
// get the same port number on both address families for
|
||||
// dev/debugging purposes, which is nice. But it's not so
|
||||
// deterministic that people will bake this into clients.
|
||||
// We try a few times just in case something's already
|
||||
// listening on that port (on all interfaces, probably).
|
||||
for try := uint8(0); try < 5; try++ {
|
||||
a16 := ip.As16()
|
||||
hashData := a16[len(a16)-3:]
|
||||
hashData[0] += try
|
||||
tryPort := (32 << 10) | uint16(crc32.ChecksumIEEE(hashData))
|
||||
ln, err = lc.Listen(context.Background(), tcp4or6, net.JoinHostPort(ipStr, strconv.Itoa(int(tryPort))))
|
||||
if err == nil {
|
||||
return ln, nil
|
||||
}
|
||||
}
|
||||
// Fall back to random ephemeral port.
|
||||
return lc.Listen(context.Background(), tcp4or6, net.JoinHostPort(ipStr, "0"))
|
||||
}
|
||||
|
||||
type peerAPIListener struct {
|
||||
ps *peerAPIServer
|
||||
ip netaddr.IP
|
||||
ln net.Listener
|
||||
lb *LocalBackend
|
||||
urlStr string
|
||||
}
|
||||
|
||||
func (pln *peerAPIListener) Port() int {
|
||||
ta, ok := pln.ln.Addr().(*net.TCPAddr)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return ta.Port
|
||||
}
|
||||
|
||||
func (pln *peerAPIListener) serve() {
|
||||
defer pln.ln.Close()
|
||||
logf := pln.lb.logf
|
||||
for {
|
||||
c, err := pln.ln.Accept()
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
logf("peerapi.Accept: %v", err)
|
||||
return
|
||||
}
|
||||
ta, ok := c.RemoteAddr().(*net.TCPAddr)
|
||||
if !ok {
|
||||
c.Close()
|
||||
logf("peerapi: unexpected RemoteAddr %#v", c.RemoteAddr())
|
||||
continue
|
||||
}
|
||||
ipp, ok := netaddr.FromStdAddr(ta.IP, ta.Port, "")
|
||||
if !ok {
|
||||
logf("peerapi: bogus TCPAddr %#v", ta)
|
||||
c.Close()
|
||||
continue
|
||||
}
|
||||
peerNode, peerUser, ok := pln.lb.WhoIs(ipp)
|
||||
if !ok {
|
||||
logf("peerapi: unknown peer %v", ipp)
|
||||
c.Close()
|
||||
continue
|
||||
}
|
||||
h := &peerAPIHandler{
|
||||
ps: pln.ps,
|
||||
isSelf: pln.ps.selfNode.User == peerNode.User,
|
||||
remoteAddr: ipp,
|
||||
peerNode: peerNode,
|
||||
peerUser: peerUser,
|
||||
lb: pln.lb,
|
||||
}
|
||||
httpServer := &http.Server{
|
||||
Handler: h,
|
||||
}
|
||||
go httpServer.Serve(&oneConnListener{Listener: pln.ln, conn: c})
|
||||
}
|
||||
}
|
||||
|
||||
type oneConnListener struct {
|
||||
net.Listener
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func (l *oneConnListener) Accept() (c net.Conn, err error) {
|
||||
c = l.conn
|
||||
if c == nil {
|
||||
err = io.EOF
|
||||
return
|
||||
}
|
||||
err = nil
|
||||
l.conn = nil
|
||||
return
|
||||
}
|
||||
|
||||
func (l *oneConnListener) Close() error { return nil }
|
||||
|
||||
// peerAPIHandler serves the Peer API for a source specific client.
|
||||
type peerAPIHandler struct {
|
||||
ps *peerAPIServer
|
||||
remoteAddr netaddr.IPPort
|
||||
isSelf bool // whether peerNode is owned by same user as this node
|
||||
peerNode *tailcfg.Node // peerNode is who's making the request
|
||||
peerUser tailcfg.UserProfile // profile of peerNode
|
||||
lb *LocalBackend
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) logf(format string, a ...interface{}) {
|
||||
h.ps.b.logf("peerapi: "+format, a...)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.URL.Path, "/v0/put/") {
|
||||
h.put(w, r)
|
||||
return
|
||||
}
|
||||
who := h.peerUser.DisplayName
|
||||
fmt.Fprintf(w, `<html>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<body>
|
||||
<h1>Hello, %s (%v)</h1>
|
||||
This is my Tailscale device. Your device is %v.
|
||||
`, html.EscapeString(who), h.remoteAddr.IP, html.EscapeString(h.peerNode.ComputedName))
|
||||
|
||||
if h.isSelf {
|
||||
fmt.Fprintf(w, "<p>You are the owner of this node.\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) put(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.isSelf {
|
||||
http.Error(w, "not owner", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "PUT" {
|
||||
http.Error(w, "not method PUT", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if h.ps.rootDir == "" {
|
||||
http.Error(w, "no rootdir", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
name := path.Base(r.URL.Path)
|
||||
if name == "." || name == "/" {
|
||||
http.Error(w, "bad filename", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
fileBase := strings.ReplaceAll(url.PathEscape(name), ":", "%3a")
|
||||
dstFile := filepath.Join(h.ps.rootDir, fileBase)
|
||||
f, err := os.Create(dstFile)
|
||||
if err != nil {
|
||||
h.logf("put Create error: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var success bool
|
||||
defer func() {
|
||||
if !success {
|
||||
os.Remove(dstFile)
|
||||
}
|
||||
}()
|
||||
n, err := io.Copy(f, r.Body)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
h.logf("put Copy error: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
h.logf("put Close error: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
h.logf("put(%q): %d bytes from %v/%v", name, n, h.remoteAddr.IP, h.peerNode.ComputedName)
|
||||
|
||||
// TODO: set modtime
|
||||
// TODO: some real response
|
||||
success = true
|
||||
io.WriteString(w, "{}\n")
|
||||
}
|
||||
54
ipn/ipnlocal/peerapi_macios_ext.go
Normal file
54
ipn/ipnlocal/peerapi_macios_ext.go
Normal file
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2021 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,redo ios,redo
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
)
|
||||
|
||||
func init() {
|
||||
initListenConfig = initListenConfigNetworkExtension
|
||||
}
|
||||
|
||||
// initListenConfigNetworkExtension configures nc for listening on IP
|
||||
// through the iOS/macOS Network/System Extension (Packet Tunnel
|
||||
// Provider) sandbox.
|
||||
func initListenConfigNetworkExtension(nc *net.ListenConfig, ip netaddr.IP, st *interfaces.State, tunIfName string) error {
|
||||
tunIf, ok := st.Interface[tunIfName]
|
||||
if !ok {
|
||||
return fmt.Errorf("no interface with name %q", tunIfName)
|
||||
}
|
||||
nc.Control = func(network, address string, c syscall.RawConn) error {
|
||||
var sockErr error
|
||||
err := c.Control(func(fd uintptr) {
|
||||
|
||||
v6 := strings.Contains(address, "]:") || strings.HasSuffix(network, "6") // hacky test for v6
|
||||
proto := unix.IPPROTO_IP
|
||||
opt := unix.IP_BOUND_IF
|
||||
if v6 {
|
||||
proto = unix.IPPROTO_IPV6
|
||||
opt = unix.IPV6_BOUND_IF
|
||||
}
|
||||
|
||||
sockErr = unix.SetsockoptInt(int(fd), proto, opt, tunIf.Index)
|
||||
log.Printf("peerapi: bind(%q, %q) on index %v = %v", network, address, tunIf.Index, sockErr)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sockErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -116,23 +117,33 @@ type server struct {
|
||||
disconnectSub map[chan<- struct{}]struct{} // keys are subscribers of disconnects
|
||||
}
|
||||
|
||||
// connIdentity represents the owner of a localhost TCP connection.
|
||||
// connIdentity represents the owner of a localhost TCP or unix socket connection.
|
||||
type connIdentity struct {
|
||||
Unknown bool
|
||||
Pid int
|
||||
UserID string
|
||||
User *user.User
|
||||
IsUnixSock bool
|
||||
Conn net.Conn
|
||||
NotWindows bool // runtime.GOOS != "windows"
|
||||
|
||||
// Fields used when NotWindows:
|
||||
IsUnixSock bool // Conn is a *net.UnixConn
|
||||
Creds *peercred.Creds // or nil
|
||||
|
||||
// Used on Windows:
|
||||
// TODO(bradfitz): merge these into the peercreds package and
|
||||
// use that for all.
|
||||
Pid int
|
||||
UserID string
|
||||
User *user.User
|
||||
}
|
||||
|
||||
// getConnIdentity returns the localhost TCP connection's identity information
|
||||
// (pid, userid, user). If it's not Windows (for now), it returns a nil error
|
||||
// and a ConnIdentity with Unknown set true. It's only an error if we expected
|
||||
// and a ConnIdentity with NotWindows set true. It's only an error if we expected
|
||||
// to be able to map it and couldn't.
|
||||
func (s *server) getConnIdentity(c net.Conn) (ci connIdentity, err error) {
|
||||
ci = connIdentity{Conn: c}
|
||||
if runtime.GOOS != "windows" { // for now; TODO: expand to other OSes
|
||||
ci = connIdentity{Unknown: true}
|
||||
ci.NotWindows = true
|
||||
_, ci.IsUnixSock = c.(*net.UnixConn)
|
||||
ci.Creds, _ = peercred.Get(c)
|
||||
return ci, nil
|
||||
}
|
||||
la, err := netaddr.ParseIPPort(c.LocalAddr().String())
|
||||
@@ -285,7 +296,7 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
defer s.removeAndCloseConn(c)
|
||||
logf("[v1] incoming control connection")
|
||||
|
||||
if isReadonlyConn(c, logf) {
|
||||
if isReadonlyConn(ci, logf) {
|
||||
ctx = ipn.ReadonlyContextOf(ctx)
|
||||
}
|
||||
|
||||
@@ -311,11 +322,19 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
}
|
||||
}
|
||||
|
||||
func isReadonlyConn(c net.Conn, logf logger.Logf) bool {
|
||||
func isReadonlyConn(ci connIdentity, logf logger.Logf) bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows doesn't need/use this mechanism, at least yet. It
|
||||
// has a different last-user-wins auth model.
|
||||
return false
|
||||
}
|
||||
const ro = true
|
||||
const rw = false
|
||||
creds, err := peercred.Get(c)
|
||||
if err != nil {
|
||||
if !safesocket.PlatformUsesPeerCreds() {
|
||||
return rw
|
||||
}
|
||||
creds := ci.Creds
|
||||
if creds == nil {
|
||||
logf("connection from unknown peer; read-only")
|
||||
return ro
|
||||
}
|
||||
@@ -328,6 +347,10 @@ func isReadonlyConn(c net.Conn, logf logger.Logf) bool {
|
||||
logf("connection from userid %v; root has access", uid)
|
||||
return rw
|
||||
}
|
||||
if selfUID := os.Getuid(); selfUID != 0 && uid == strconv.Itoa(selfUID) {
|
||||
logf("connection from userid %v; connection from non-root user matching daemon has access", uid)
|
||||
return rw
|
||||
}
|
||||
var adminGroupID string
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
@@ -407,6 +430,25 @@ func (s *server) checkConnIdentityLocked(ci connIdentity) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// localAPIPermissions returns the permissions for the given identity accessing
|
||||
// the Tailscale local daemon API.
|
||||
//
|
||||
// s.mu must not be held.
|
||||
func (s *server) localAPIPermissions(ci connIdentity) (read, write bool) {
|
||||
if runtime.GOOS == "windows" {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.checkConnIdentityLocked(ci) == nil {
|
||||
return true, true
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
if ci.IsUnixSock {
|
||||
return true, !isReadonlyConn(ci, logger.Discard)
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
// registerDisconnectSub adds ch as a subscribe to connection disconnect
|
||||
// events. If add is false, the subscriber is removed.
|
||||
func (s *server) registerDisconnectSub(ch chan<- struct{}, add bool) {
|
||||
@@ -523,7 +565,7 @@ func (s *server) setServerModeUserLocked() {
|
||||
s.logf("ipnserver: [unexpected] now in server mode, but no connected client")
|
||||
return
|
||||
}
|
||||
if ci.Unknown {
|
||||
if ci.NotWindows {
|
||||
return
|
||||
}
|
||||
if ci.User != nil {
|
||||
@@ -701,9 +743,6 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
opts.DebugMux.HandleFunc("/debug/ipn", func(w http.ResponseWriter, r *http.Request) {
|
||||
serveHTMLStatus(w, b)
|
||||
})
|
||||
h := localapi.NewHandler(b)
|
||||
h.PermitRead = true
|
||||
opts.DebugMux.Handle("/localapi/", h)
|
||||
}
|
||||
|
||||
server.b = b
|
||||
@@ -943,15 +982,15 @@ func (psc *protoSwitchConn) Close() error {
|
||||
}
|
||||
|
||||
func (s *server) localhostHandler(ci connIdentity) http.Handler {
|
||||
lah := localapi.NewHandler(s.b)
|
||||
lah.PermitRead, lah.PermitWrite = s.localAPIPermissions(ci)
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if ci.IsUnixSock && strings.HasPrefix(r.URL.Path, "/localapi/") {
|
||||
h := localapi.NewHandler(s.b)
|
||||
h.PermitRead = true
|
||||
h.PermitWrite = false // TODO: flesh out connIdentity on more platforms then set this
|
||||
h.ServeHTTP(w, r)
|
||||
if strings.HasPrefix(r.URL.Path, "/localapi/") {
|
||||
lah.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if ci.Unknown {
|
||||
if ci.NotWindows {
|
||||
io.WriteString(w, "<html><title>Tailscale</title><body><h1>Tailscale</h1>This is the local Tailscale daemon.")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestRunMultipleAccepts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
eng, err := wgengine.NewFakeUserspaceEngine(logf, 0, nil)
|
||||
eng, err := wgengine.NewFakeUserspaceEngine(logf, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,15 @@ import (
|
||||
|
||||
// Status represents the entire state of the IPN network.
|
||||
type Status struct {
|
||||
// Version is the daemon's long version (see version.Long).
|
||||
Version string
|
||||
|
||||
// BackendState is an ipn.State string value:
|
||||
// "NoState", "NeedsLogin", "NeedsMachineAuth", "Stopped",
|
||||
// "Starting", "Running".
|
||||
BackendState string
|
||||
|
||||
AuthURL string // current URL provided by control to authorize client
|
||||
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
||||
Self *PeerStatus
|
||||
|
||||
@@ -79,6 +87,8 @@ type PeerStatus struct {
|
||||
KeepAlive bool
|
||||
ExitNode bool // true if this is the currently selected exit node.
|
||||
|
||||
PeerAPIURL []string
|
||||
|
||||
// ShareeNode indicates this node exists in the netmap because
|
||||
// it's owned by a shared-to user and that node might connect
|
||||
// to us. These nodes should be hidden by "tailscale status"
|
||||
@@ -104,16 +114,16 @@ type StatusBuilder struct {
|
||||
st Status
|
||||
}
|
||||
|
||||
func (sb *StatusBuilder) SetBackendState(v string) {
|
||||
// MutateStatus calls f with the status to mutate.
|
||||
//
|
||||
// It may not assume other fields of status are already populated, and
|
||||
// may not retain or write to the Status after f returns.
|
||||
//
|
||||
// MutateStatus acquires a lock so f must not call back into sb.
|
||||
func (sb *StatusBuilder) MutateStatus(f func(*Status)) {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
sb.st.BackendState = v
|
||||
}
|
||||
|
||||
func (sb *StatusBuilder) SetMagicDNSSuffix(v string) {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
sb.st.MagicDNSSuffix = v
|
||||
f(&sb.st)
|
||||
}
|
||||
|
||||
func (sb *StatusBuilder) Status() *Status {
|
||||
@@ -123,11 +133,19 @@ func (sb *StatusBuilder) Status() *Status {
|
||||
return &sb.st
|
||||
}
|
||||
|
||||
// SetSelfStatus sets the status of the local machine.
|
||||
func (sb *StatusBuilder) SetSelfStatus(ss *PeerStatus) {
|
||||
// MutateSelfStatus calls f with the PeerStatus of our own node to mutate.
|
||||
//
|
||||
// It may not assume other fields of status are already populated, and
|
||||
// may not retain or write to the Status after f returns.
|
||||
//
|
||||
// MutateStatus acquires a lock so f must not call back into sb.
|
||||
func (sb *StatusBuilder) MutateSelfStatus(f func(*PeerStatus)) {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
sb.st.Self = ss
|
||||
if sb.st.Self == nil {
|
||||
sb.st.Self = new(PeerStatus)
|
||||
}
|
||||
f(sb.st.Self)
|
||||
}
|
||||
|
||||
// AddUser adds a user profile to the status.
|
||||
@@ -387,10 +405,23 @@ type PingResult struct {
|
||||
Err string
|
||||
LatencySeconds float64
|
||||
|
||||
Endpoint string // ip:port if direct UDP was used
|
||||
// Endpoint is the ip:port if direct UDP was used.
|
||||
// It is not currently set for TSMP pings.
|
||||
Endpoint string
|
||||
|
||||
DERPRegionID int // non-zero if DERP was used
|
||||
DERPRegionCode string // three-letter airport/region code if DERP was used
|
||||
// DERPRegionID is non-zero DERP region ID if DERP was used.
|
||||
// It is not currently set for TSMP pings.
|
||||
DERPRegionID int
|
||||
|
||||
// DERPRegionCode is the three-letter region code
|
||||
// corresponding to DERPRegionID.
|
||||
// It is not currently set for TSMP pings.
|
||||
DERPRegionCode string
|
||||
|
||||
// PeerAPIPort is set by TSMP ping responses for peers that
|
||||
// are running a peerapi server. This is the port they're
|
||||
// running the server on.
|
||||
PeerAPIPort uint16 `json:",omitempty"`
|
||||
|
||||
// TODO(bradfitz): details like whether port mapping was used on either side? (Once supported)
|
||||
}
|
||||
|
||||
@@ -9,9 +9,12 @@ import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
@@ -53,6 +56,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/localapi/v0/whois":
|
||||
h.serveWhoIs(w, r)
|
||||
case "/localapi/v0/goroutines":
|
||||
h.serveGoroutines(w, r)
|
||||
case "/localapi/v0/status":
|
||||
h.serveStatus(w, r)
|
||||
default:
|
||||
io.WriteString(w, "tailscaled\n")
|
||||
}
|
||||
@@ -64,21 +71,21 @@ func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
b := h.b
|
||||
var ip netaddr.IP
|
||||
if v := r.FormValue("ip"); v != "" {
|
||||
var ipp netaddr.IPPort
|
||||
if v := r.FormValue("addr"); v != "" {
|
||||
var err error
|
||||
ip, err = netaddr.ParseIP(r.FormValue("ip"))
|
||||
ipp, err = netaddr.ParseIPPort(v)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid 'ip' parameter", 400)
|
||||
http.Error(w, "invalid 'addr' parameter", 400)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "missing 'ip' parameter", 400)
|
||||
http.Error(w, "missing 'addr' parameter", 400)
|
||||
return
|
||||
}
|
||||
n, u, ok := b.WhoIs(ip)
|
||||
n, u, ok := b.WhoIs(ipp)
|
||||
if !ok {
|
||||
http.Error(w, "no match for IP", 404)
|
||||
http.Error(w, "no match for IP:port", 404)
|
||||
return
|
||||
}
|
||||
res := &tailcfg.WhoIsResponse{
|
||||
@@ -93,3 +100,44 @@ func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(j)
|
||||
}
|
||||
|
||||
func (h *Handler) serveGoroutines(w http.ResponseWriter, r *http.Request) {
|
||||
// Require write access out of paranoia that the goroutine dump
|
||||
// (at least its arguments) might contain something sensitive.
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "goroutine dump access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
buf := make([]byte, 2<<20)
|
||||
buf = buf[:runtime.Stack(buf, true)]
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write(buf)
|
||||
}
|
||||
|
||||
func (h *Handler) serveStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitRead {
|
||||
http.Error(w, "status access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
var st *ipnstate.Status
|
||||
if defBool(r.FormValue("peers"), true) {
|
||||
st = h.b.Status()
|
||||
} else {
|
||||
st = h.b.StatusWithoutPeers()
|
||||
}
|
||||
e := json.NewEncoder(w)
|
||||
e.SetIndent("", "\t")
|
||||
e.Encode(st)
|
||||
}
|
||||
|
||||
func defBool(a string, def bool) bool {
|
||||
if a == "" {
|
||||
return def
|
||||
}
|
||||
v, err := strconv.ParseBool(a)
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/version"
|
||||
@@ -56,7 +56,8 @@ type FakeExpireAfterArgs struct {
|
||||
}
|
||||
|
||||
type PingArgs struct {
|
||||
IP string
|
||||
IP string
|
||||
UseTSMP bool
|
||||
}
|
||||
|
||||
// Command is a command message that is JSON encoded and sent by a
|
||||
@@ -76,7 +77,7 @@ type Command struct {
|
||||
Quit *NoArgs
|
||||
Start *StartArgs
|
||||
StartLoginInteractive *NoArgs
|
||||
Login *oauth2.Token
|
||||
Login *tailcfg.Oauth2Token
|
||||
Logout *NoArgs
|
||||
SetPrefs *SetPrefsArgs
|
||||
SetWantRunning *bool
|
||||
@@ -173,11 +174,8 @@ func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error {
|
||||
if c := cmd.RequestEngineStatus; c != nil {
|
||||
bs.b.RequestEngineStatus()
|
||||
return nil
|
||||
} else if c := cmd.RequestStatus; c != nil {
|
||||
bs.b.RequestStatus()
|
||||
return nil
|
||||
} else if c := cmd.Ping; c != nil {
|
||||
bs.b.Ping(c.IP)
|
||||
bs.b.Ping(c.IP, c.UseTSMP)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -299,7 +297,7 @@ func (bc *BackendClient) StartLoginInteractive() {
|
||||
bc.send(Command{StartLoginInteractive: &NoArgs{}})
|
||||
}
|
||||
|
||||
func (bc *BackendClient) Login(token *oauth2.Token) {
|
||||
func (bc *BackendClient) Login(token *tailcfg.Oauth2Token) {
|
||||
bc.send(Command{Login: token})
|
||||
}
|
||||
|
||||
@@ -323,8 +321,11 @@ 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) Ping(ip string, useTSMP bool) {
|
||||
bc.send(Command{Ping: &PingArgs{
|
||||
IP: ip,
|
||||
UseTSMP: useTSMP,
|
||||
}})
|
||||
}
|
||||
|
||||
func (bc *BackendClient) SetWantRunning(v bool) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
@@ -176,7 +176,7 @@ func TestClientServer(t *testing.T) {
|
||||
h.Logout()
|
||||
flushUntil(NeedsLogin)
|
||||
|
||||
h.Login(&oauth2.Token{
|
||||
h.Login(&tailcfg.Oauth2Token{
|
||||
AccessToken: "google_id_token",
|
||||
TokenType: GoogleIDTokenType,
|
||||
})
|
||||
|
||||
@@ -168,6 +168,11 @@ func (p *Prefs) pretty(goos string) string {
|
||||
if p.ShieldsUp {
|
||||
sb.WriteString("shields=true ")
|
||||
}
|
||||
if !p.ExitNodeIP.IsZero() {
|
||||
fmt.Fprintf(&sb, "exit=%v ", p.ExitNodeIP)
|
||||
} else if !p.ExitNodeID.IsZero() {
|
||||
fmt.Fprintf(&sb, "exit=%v ", p.ExitNodeID)
|
||||
}
|
||||
if len(p.AdvertiseRoutes) > 0 || goos == "linux" {
|
||||
fmt.Fprintf(&sb, "routes=%v ", p.AdvertiseRoutes)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
@@ -376,6 +377,20 @@ func TestPrefsPretty(t *testing.T) {
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist{lm=, o=, n=[B1VKl] u=""}}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
ExitNodeIP: netaddr.MustParseIP("1.2.3.4"),
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false exit=1.2.3.4 routes=[] nf=off Persist=nil}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
ExitNodeID: tailcfg.StableNodeID("myNodeABC"),
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false exit=myNodeABC routes=[] nf=off Persist=nil}`,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
got := tt.p.pretty(tt.os)
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/term"
|
||||
@@ -37,9 +38,29 @@ import (
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/racebuild"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var getLogTargetOnce struct {
|
||||
sync.Once
|
||||
v string // URL of logs server, or empty for default
|
||||
}
|
||||
|
||||
func getLogTarget() string {
|
||||
getLogTargetOnce.Do(func() {
|
||||
if val, ok := os.LookupEnv("TS_LOG_TARGET"); ok {
|
||||
getLogTargetOnce.v = val
|
||||
} else {
|
||||
if runtime.GOOS == "windows" {
|
||||
getLogTargetOnce.v = winutil.GetRegString("LogTarget", "")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return getLogTargetOnce.v
|
||||
}
|
||||
|
||||
// Config represents an instance of logs in a collection.
|
||||
type Config struct {
|
||||
Collection string
|
||||
@@ -400,7 +421,7 @@ func New(collection string) *Policy {
|
||||
HTTPC: &http.Client{Transport: newLogtailTransport(logtail.DefaultHost)},
|
||||
}
|
||||
|
||||
if val, ok := os.LookupEnv("TS_LOG_TARGET"); ok {
|
||||
if val := getLogTarget(); val != "" {
|
||||
log.Println("You have enabled a non-default log target. Doing without being told to by Tailscale staff or your network administrator will make getting support difficult.")
|
||||
c.BaseURL = val
|
||||
u, _ := url.Parse(val)
|
||||
|
||||
@@ -18,7 +18,9 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/interfaces"
|
||||
tslogger "tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
// DefaultHost is the default host name to upload logs to when
|
||||
@@ -106,6 +108,7 @@ type Logger struct {
|
||||
url string
|
||||
lowMem bool
|
||||
skipClientTime bool
|
||||
linkMonitor *monitor.Mon
|
||||
buffer Buffer
|
||||
sent chan struct{} // signal to speed up drain
|
||||
drainLogs <-chan struct{} // if non-nil, external signal to attempt a drain
|
||||
@@ -128,6 +131,14 @@ func (l *Logger) SetVerbosityLevel(level int) {
|
||||
l.stderrLevel = level
|
||||
}
|
||||
|
||||
// SetLinkMonitor sets the optional the link monitor.
|
||||
//
|
||||
// It should not be changed concurrently with log writes and should
|
||||
// only be set once.
|
||||
func (l *Logger) SetLinkMonitor(lm *monitor.Mon) {
|
||||
l.linkMonitor = lm
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the logger while completing any
|
||||
// remaining uploads.
|
||||
//
|
||||
@@ -277,6 +288,11 @@ func (l *Logger) uploading(ctx context.Context) {
|
||||
}
|
||||
uploaded, err := l.upload(ctx, body, origlen)
|
||||
if err != nil {
|
||||
if !l.internetUp() {
|
||||
fmt.Fprintf(l.stderr, "logtail: internet down; waiting\n")
|
||||
l.awaitInternetUp(ctx)
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(l.stderr, "logtail: upload: %v\n", err)
|
||||
}
|
||||
l.bo.BackOff(ctx, err)
|
||||
@@ -293,6 +309,34 @@ func (l *Logger) uploading(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) internetUp() bool {
|
||||
if l.linkMonitor == nil {
|
||||
// No way to tell, so assume it is.
|
||||
return true
|
||||
}
|
||||
return l.linkMonitor.InterfaceState().AnyInterfaceUp()
|
||||
}
|
||||
|
||||
func (l *Logger) awaitInternetUp(ctx context.Context) {
|
||||
upc := make(chan bool, 1)
|
||||
defer l.linkMonitor.RegisterChangeCallback(func(changed bool, st *interfaces.State) {
|
||||
if st.AnyInterfaceUp() {
|
||||
select {
|
||||
case upc <- true:
|
||||
default:
|
||||
}
|
||||
}
|
||||
})()
|
||||
if l.internetUp() {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-upc:
|
||||
fmt.Fprintf(l.stderr, "logtail: internet back up\n")
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
|
||||
// upload uploads body to the log server.
|
||||
// origlen indicates the pre-compression body length.
|
||||
// origlen of -1 indicates that the body is not compressed.
|
||||
|
||||
@@ -22,8 +22,8 @@ type Config struct {
|
||||
// Note that Nameservers may still be applied to all queries
|
||||
// if the manager does not support per-domain settings.
|
||||
PerDomain bool
|
||||
// Proxied indicates whether DNS requests are proxied through a tsdns.Resolver.
|
||||
// This enables Magic DNS.
|
||||
// Proxied indicates whether DNS requests are proxied through a dns.Resolver.
|
||||
// This enables MagicDNS.
|
||||
Proxied bool
|
||||
}
|
||||
|
||||
19
net/dns/flush_windows.go
Normal file
19
net/dns/flush_windows.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2021 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 dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// Flush clears the local resolver cache.
|
||||
func Flush() error {
|
||||
out, err := exec.Command("ipconfig", "/flushdns").CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%v (output: %s)", err, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tsdns
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -163,6 +163,14 @@ func (f *forwarder) Close() {
|
||||
f.wg.Wait()
|
||||
}
|
||||
|
||||
func (f *forwarder) rebindFromNetworkChange() {
|
||||
for _, c := range f.conns {
|
||||
c.mu.Lock()
|
||||
c.reconnectLocked()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (f *forwarder) setUpstreams(upstreams []net.Addr) {
|
||||
f.mu.Lock()
|
||||
f.upstreams = upstreams
|
||||
@@ -308,7 +316,7 @@ func (c *fwdConn) send(packet []byte, dst net.Addr) {
|
||||
var b *backoff.Backoff // lazily initialized, since it is not needed in the common case
|
||||
backOff := func(err error) {
|
||||
if b == nil {
|
||||
b = backoff.NewBackoff("tsdns-fwdConn-send", c.logf, 30*time.Second)
|
||||
b = backoff.NewBackoff("dns-fwdConn-send", c.logf, 30*time.Second)
|
||||
}
|
||||
b.BackOff(context.Background(), err)
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tsdns
|
||||
package dns
|
||||
|
||||
import (
|
||||
"sort"
|
||||
@@ -92,6 +92,9 @@ func (m *Map) PrettyDiffFrom(old *Map) string {
|
||||
}
|
||||
|
||||
buf := new(strings.Builder)
|
||||
space := func() bool {
|
||||
return buf.Len() < (1 << 10)
|
||||
}
|
||||
|
||||
for len(oldNames) > 0 && len(newNames) > 0 {
|
||||
var name string
|
||||
@@ -109,6 +112,9 @@ func (m *Map) PrettyDiffFrom(old *Map) string {
|
||||
oldNames = oldNames[1:]
|
||||
newNames = newNames[1:]
|
||||
}
|
||||
if !space() {
|
||||
continue
|
||||
}
|
||||
|
||||
ipOld, inOld := oldNameToIP[name]
|
||||
ipNew, inNew := newNameToIP[name]
|
||||
@@ -128,6 +134,9 @@ func (m *Map) PrettyDiffFrom(old *Map) string {
|
||||
}
|
||||
|
||||
for _, name := range oldNames {
|
||||
if !space() {
|
||||
break
|
||||
}
|
||||
if _, ok := newNameToIP[name]; !ok {
|
||||
buf.WriteByte('-')
|
||||
printSingleNameIP(buf, name, oldNameToIP[name])
|
||||
@@ -135,11 +144,17 @@ func (m *Map) PrettyDiffFrom(old *Map) string {
|
||||
}
|
||||
|
||||
for _, name := range newNames {
|
||||
if !space() {
|
||||
break
|
||||
}
|
||||
if _, ok := oldNameToIP[name]; !ok {
|
||||
buf.WriteByte('+')
|
||||
printSingleNameIP(buf, name, newNameToIP[name])
|
||||
}
|
||||
}
|
||||
if !space() {
|
||||
buf.WriteString("... [truncated]\n")
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
@@ -2,9 +2,11 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tsdns
|
||||
package dns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
@@ -135,4 +137,20 @@ func TestPrettyDiffFrom(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("truncated", func(t *testing.T) {
|
||||
small := NewMap(nil, nil)
|
||||
m := map[string]netaddr.IP{}
|
||||
for i := 0; i < 5000; i++ {
|
||||
m[fmt.Sprintf("host%d.ipn.dev.", i)] = netaddr.IPv4(100, 64, 1, 1)
|
||||
}
|
||||
veryBig := NewMap(m, nil)
|
||||
diff := veryBig.PrettyDiffFrom(small)
|
||||
if len(diff) > 3<<10 {
|
||||
t.Errorf("pretty diff too large: %d bytes", len(diff))
|
||||
}
|
||||
if !strings.Contains(diff, "truncated") {
|
||||
t.Errorf("big diff not truncated")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tsdns
|
||||
package dns
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
// +build !darwin,!windows
|
||||
|
||||
package tsdns
|
||||
package dns
|
||||
|
||||
func networkIsDown(err error) bool { return false }
|
||||
func networkIsUnreachable(err error) bool { return false }
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tsdns
|
||||
package dns
|
||||
|
||||
import (
|
||||
"net"
|
||||
@@ -2,9 +2,9 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package tsdns provides a Resolver capable of resolving
|
||||
// Package dns provides a Resolver capable of resolving
|
||||
// domains on a Tailscale network.
|
||||
package tsdns
|
||||
package dns
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
@@ -16,8 +16,10 @@ import (
|
||||
|
||||
dns "golang.org/x/net/dns/dnsmessage"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
// maxResponseBytes is the maximum size of a response from a Resolver.
|
||||
@@ -58,7 +60,9 @@ type Packet struct {
|
||||
// If it is asked to resolve a domain that is not of that form,
|
||||
// it delegates to upstream nameservers if any are set.
|
||||
type Resolver struct {
|
||||
logf logger.Logf
|
||||
logf logger.Logf
|
||||
linkMon *monitor.Mon // or nil
|
||||
unregLinkMon func() // or nil
|
||||
// forwarder forwards requests to upstream nameservers.
|
||||
forwarder *forwarder
|
||||
|
||||
@@ -86,13 +90,18 @@ type ResolverConfig struct {
|
||||
// Forward determines whether the resolver will forward packets to
|
||||
// nameservers set with SetUpstreams if the domain name is not of a Tailscale node.
|
||||
Forward bool
|
||||
// LinkMonitor optionally provides a link monitor to use to rebind
|
||||
// connections on link changes.
|
||||
// If nil, rebinds are not performend.
|
||||
LinkMonitor *monitor.Mon
|
||||
}
|
||||
|
||||
// NewResolver constructs a resolver associated with the given root domain.
|
||||
// The root domain must be in canonical form (with a trailing period).
|
||||
func NewResolver(config ResolverConfig) *Resolver {
|
||||
r := &Resolver{
|
||||
logf: logger.WithPrefix(config.Logf, "tsdns: "),
|
||||
logf: logger.WithPrefix(config.Logf, "dns: "),
|
||||
linkMon: config.LinkMonitor,
|
||||
queue: make(chan Packet, queueSize),
|
||||
responses: make(chan Packet),
|
||||
errors: make(chan error),
|
||||
@@ -102,6 +111,9 @@ func NewResolver(config ResolverConfig) *Resolver {
|
||||
if config.Forward {
|
||||
r.forwarder = newForwarder(r.logf, r.responses)
|
||||
}
|
||||
if r.linkMon != nil {
|
||||
r.unregLinkMon = r.linkMon.RegisterChangeCallback(r.onLinkMonitorChange)
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
@@ -130,6 +142,10 @@ func (r *Resolver) Close() {
|
||||
}
|
||||
close(r.closed)
|
||||
|
||||
if r.unregLinkMon != nil {
|
||||
r.unregLinkMon()
|
||||
}
|
||||
|
||||
if r.forwarder != nil {
|
||||
r.forwarder.Close()
|
||||
}
|
||||
@@ -137,6 +153,15 @@ func (r *Resolver) Close() {
|
||||
r.wg.Wait()
|
||||
}
|
||||
|
||||
func (r *Resolver) onLinkMonitorChange(changed bool, state *interfaces.State) {
|
||||
if !changed {
|
||||
return
|
||||
}
|
||||
if r.forwarder != nil {
|
||||
r.forwarder.rebindFromNetworkChange()
|
||||
}
|
||||
}
|
||||
|
||||
// SetMap sets the resolver's DNS map, taking ownership of it.
|
||||
func (r *Resolver) SetMap(m *Map) {
|
||||
r.mu.Lock()
|
||||
@@ -194,6 +219,11 @@ func (r *Resolver) Resolve(domain string, tp dns.Type) (netaddr.IP, dns.RCode, e
|
||||
return netaddr.IP{}, dns.RCodeServerFailure, errMapNotSet
|
||||
}
|
||||
|
||||
// Reject .onion domains per RFC 7686.
|
||||
if dnsname.HasSuffix(domain, ".onion") {
|
||||
return netaddr.IP{}, dns.RCodeNameError, nil
|
||||
}
|
||||
|
||||
anyHasSuffix := false
|
||||
for _, suffix := range dnsMap.rootDomains {
|
||||
if dnsname.HasSuffix(domain, suffix) {
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tsdns
|
||||
package dns
|
||||
|
||||
import (
|
||||
"log"
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tsdns
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -219,6 +219,7 @@ func TestResolve(t *testing.T) {
|
||||
{"mx-ipv6", "test2.ipn.dev.", dns.TypeMX, netaddr.IP{}, dns.RCodeSuccess},
|
||||
{"mx-nxdomain", "test3.ipn.dev.", dns.TypeMX, netaddr.IP{}, dns.RCodeNameError},
|
||||
{"ns-nxdomain", "test3.ipn.dev.", dns.TypeNS, netaddr.IP{}, dns.RCodeNameError},
|
||||
{"onion-domain", "footest.onion.", dns.TypeA, netaddr.IP{}, dns.RCodeNameError},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -8,6 +8,8 @@ package dnscache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
@@ -18,6 +20,7 @@ import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/singleflight"
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
var single = &Resolver{
|
||||
@@ -55,6 +58,10 @@ type Resolver struct {
|
||||
// If nil, net.DefaultResolver is used.
|
||||
Forward *net.Resolver
|
||||
|
||||
// LookupIPFallback optionally provides a backup DNS mechanism
|
||||
// to use if Forward returns an error or no results.
|
||||
LookupIPFallback func(ctx context.Context, host string) ([]netaddr.IP, error)
|
||||
|
||||
// TTL is how long to keep entries cached
|
||||
//
|
||||
// If zero, a default (currently 10 minutes) is used.
|
||||
@@ -198,6 +205,18 @@ func (r *Resolver) lookupIP(host string) (ip, ip6 net.IP, err error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), r.lookupTimeoutForHost(host))
|
||||
defer cancel()
|
||||
ips, err := r.fwd().LookupIPAddr(ctx, host)
|
||||
if (err != nil || len(ips) == 0) && r.LookupIPFallback != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
var fips []netaddr.IP
|
||||
fips, err = r.LookupIPFallback(ctx, host)
|
||||
if err == nil {
|
||||
ips = nil
|
||||
for _, fip := range fips {
|
||||
ips = append(ips, *fip.IPAddr())
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -269,13 +288,33 @@ type DialContextFunc func(ctx context.Context, network, address string) (net.Con
|
||||
|
||||
// Dialer returns a wrapped DialContext func that uses the provided dnsCache.
|
||||
func Dialer(fwd DialContextFunc, dnsCache *Resolver) DialContextFunc {
|
||||
return func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return func(ctx context.Context, network, address string) (retConn net.Conn, ret error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
// Bogus. But just let the real dialer return an error rather than
|
||||
// inventing a similar one.
|
||||
return fwd(ctx, network, address)
|
||||
}
|
||||
defer func() {
|
||||
// On any failure, assume our DNS is wrong and try our fallback, if any.
|
||||
if ret == nil || dnsCache.LookupIPFallback == nil {
|
||||
return
|
||||
}
|
||||
ips, err := dnsCache.LookupIPFallback(ctx, host)
|
||||
if err != nil {
|
||||
// Return with original error
|
||||
return
|
||||
}
|
||||
for _, ip := range ips {
|
||||
dst := net.JoinHostPort(ip.String(), port)
|
||||
if c, err := fwd(ctx, network, dst); err == nil {
|
||||
retConn = c
|
||||
ret = nil
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ip, ip6, err := dnsCache.LookupIP(ctx, host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve %q: %w", host, err)
|
||||
@@ -300,3 +339,62 @@ func Dialer(fwd DialContextFunc, dnsCache *Resolver) DialContextFunc {
|
||||
return fwd(ctx, network, dst)
|
||||
}
|
||||
}
|
||||
|
||||
var errTLSHandshakeTimeout = errors.New("timeout doing TLS handshake")
|
||||
|
||||
// TLSDialer is like Dialer but returns a func suitable for using with net/http.Transport.DialTLSContext.
|
||||
// It returns a *tls.Conn type on success.
|
||||
// On TLS cert validation failure, it can invoke a backup DNS resolution strategy.
|
||||
func TLSDialer(fwd DialContextFunc, dnsCache *Resolver, tlsConfigBase *tls.Config) DialContextFunc {
|
||||
tcpDialer := Dialer(fwd, dnsCache)
|
||||
return func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tcpConn, err := tcpDialer(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := cloneTLSConfig(tlsConfigBase)
|
||||
if cfg.ServerName == "" {
|
||||
cfg.ServerName = host
|
||||
}
|
||||
tlsConn := tls.Client(tcpConn, cfg)
|
||||
|
||||
errc := make(chan error, 2)
|
||||
handshakeCtx, handshakeTimeoutCancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer handshakeTimeoutCancel()
|
||||
done := make(chan bool)
|
||||
defer close(done)
|
||||
go func() {
|
||||
select {
|
||||
case <-done:
|
||||
case <-handshakeCtx.Done():
|
||||
errc <- errTLSHandshakeTimeout
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
err := tlsConn.Handshake()
|
||||
handshakeTimeoutCancel()
|
||||
errc <- err
|
||||
}()
|
||||
if err := <-errc; err != nil {
|
||||
tcpConn.Close()
|
||||
// TODO: if err != errTLSHandshakeTimeout,
|
||||
// assume it might be some captive portal or
|
||||
// otherwise incorrect DNS and try the backup
|
||||
// DNS mechanism.
|
||||
return nil, err
|
||||
}
|
||||
return tlsConn, nil
|
||||
}
|
||||
}
|
||||
|
||||
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
|
||||
if cfg == nil {
|
||||
return &tls.Config{}
|
||||
}
|
||||
return cfg.Clone()
|
||||
}
|
||||
|
||||
117
net/dnsfallback/dnsfallback.go
Normal file
117
net/dnsfallback/dnsfallback.go
Normal file
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2021 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 dnsfallback contains a DNS fallback mechanism
|
||||
// for starting up Tailscale when the system DNS is broken or otherwise unavailable.
|
||||
package dnsfallback
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/derp/derpmap"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
)
|
||||
|
||||
func Lookup(ctx context.Context, host string) ([]netaddr.IP, error) {
|
||||
type nameIP struct {
|
||||
dnsName string
|
||||
ip netaddr.IP
|
||||
}
|
||||
|
||||
dm := derpmap.Prod()
|
||||
var cands4, cands6 []nameIP
|
||||
for _, dr := range dm.Regions {
|
||||
for _, n := range dr.Nodes {
|
||||
if ip, err := netaddr.ParseIP(n.IPv4); err == nil {
|
||||
cands4 = append(cands4, nameIP{n.HostName, ip})
|
||||
}
|
||||
if ip, err := netaddr.ParseIP(n.IPv6); err == nil {
|
||||
cands6 = append(cands6, nameIP{n.HostName, ip})
|
||||
}
|
||||
}
|
||||
}
|
||||
rand.Shuffle(len(cands4), func(i, j int) { cands4[i], cands4[j] = cands4[j], cands4[i] })
|
||||
rand.Shuffle(len(cands6), func(i, j int) { cands6[i], cands6[j] = cands6[j], cands6[i] })
|
||||
|
||||
const maxCands = 6
|
||||
var cands []nameIP // up to maxCands alternating v4/v6 as long as we have both
|
||||
for (len(cands4) > 0 || len(cands6) > 0) && len(cands) < maxCands {
|
||||
if len(cands4) > 0 {
|
||||
cands = append(cands, cands4[0])
|
||||
cands4 = cands4[1:]
|
||||
}
|
||||
if len(cands6) > 0 {
|
||||
cands = append(cands, cands6[0])
|
||||
cands6 = cands6[1:]
|
||||
}
|
||||
}
|
||||
if len(cands) == 0 {
|
||||
return nil, fmt.Errorf("no DNS fallback options for %q", host)
|
||||
}
|
||||
for _, cand := range cands {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("trying bootstrapDNS(%q, %q) for %q ...", cand.dnsName, cand.ip, host)
|
||||
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
dm, err := bootstrapDNSMap(ctx, cand.dnsName, cand.ip, host)
|
||||
if err != nil {
|
||||
log.Printf("bootstrapDNS(%q, %q) for %q error: %v", cand.dnsName, cand.ip, host, err)
|
||||
continue
|
||||
}
|
||||
if ips := dm[host]; len(ips) > 0 {
|
||||
log.Printf("bootstrapDNS(%q, %q) for %q = %v", cand.dnsName, cand.ip, host, ips)
|
||||
return ips, nil
|
||||
}
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("no DNS fallback candidates remain for %q", host)
|
||||
}
|
||||
|
||||
// serverName and serverIP of are, say, "derpN.tailscale.com".
|
||||
// queryName is the name being sought (e.g. "login.tailscale.com"), passed as hint.
|
||||
func bootstrapDNSMap(ctx context.Context, serverName string, serverIP netaddr.IP, queryName string) (dnsMap, error) {
|
||||
dialer := netns.NewDialer()
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
tr.DialContext = func(ctx context.Context, netw, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, "tcp", net.JoinHostPort(serverIP.String(), "443"))
|
||||
}
|
||||
c := &http.Client{Transport: tr}
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://"+serverName+"/bootstrap-dns?q="+url.QueryEscape(queryName), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dm := make(dnsMap)
|
||||
res, err := c.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
return nil, errors.New(res.Status)
|
||||
}
|
||||
if err := json.NewDecoder(res.Body).Decode(&dm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dm, nil
|
||||
}
|
||||
|
||||
// dnsMap is the JSON type returned by the DERP /bootstrap-dns handler:
|
||||
// https://derp10.tailscale.com/bootstrap-dns
|
||||
type dnsMap map[string][]netaddr.IP
|
||||
@@ -15,16 +15,18 @@ import (
|
||||
"fmt"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/ipproto"
|
||||
)
|
||||
|
||||
// Tuple is a 4-tuple of source and destination IP and port.
|
||||
// Tuple is a 5-tuple of proto, source and destination IP and port.
|
||||
type Tuple struct {
|
||||
Src netaddr.IPPort
|
||||
Dst netaddr.IPPort
|
||||
Proto ipproto.Proto
|
||||
Src netaddr.IPPort
|
||||
Dst netaddr.IPPort
|
||||
}
|
||||
|
||||
func (t Tuple) String() string {
|
||||
return fmt.Sprintf("(%v => %v)", t.Src, t.Dst)
|
||||
return fmt.Sprintf("(%v %v => %v)", t.Proto, t.Src, t.Dst)
|
||||
}
|
||||
|
||||
// Cache is an LRU cache keyed by Tuple.
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -78,7 +78,7 @@ func isProblematicInterface(nif *net.Interface) bool {
|
||||
|
||||
// LocalAddresses returns the machine's IP addresses, separated by
|
||||
// whether they're loopback addresses.
|
||||
func LocalAddresses() (regular, loopback []string, err error) {
|
||||
func LocalAddresses() (regular, loopback []netaddr.IP, err error) {
|
||||
// TODO(crawshaw): don't serve interface addresses that we are routing
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
@@ -113,20 +113,26 @@ func LocalAddresses() (regular, loopback []string, err error) {
|
||||
if tsaddr.IsTailscaleIP(ip) {
|
||||
continue
|
||||
}
|
||||
if linkLocalIPv4.Contains(ip) {
|
||||
if ip.IsLinkLocalUnicast() {
|
||||
continue
|
||||
}
|
||||
if ip.IsLoopback() || ifcIsLoopback {
|
||||
loopback = append(loopback, ip.String())
|
||||
loopback = append(loopback, ip)
|
||||
} else {
|
||||
regular = append(regular, ip.String())
|
||||
regular = append(regular, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sortIPs(regular)
|
||||
sortIPs(loopback)
|
||||
return regular, loopback, nil
|
||||
}
|
||||
|
||||
func sortIPs(s []netaddr.IP) {
|
||||
sort.Slice(s, func(i, j int) bool { return s[i].Less(s[j]) })
|
||||
}
|
||||
|
||||
// Interface is a wrapper around Go's net.Interface with some extra methods.
|
||||
type Interface struct {
|
||||
*net.Interface
|
||||
@@ -135,8 +141,10 @@ type Interface struct {
|
||||
func (i Interface) IsLoopback() bool { return isLoopback(i.Interface) }
|
||||
func (i Interface) IsUp() bool { return isUp(i.Interface) }
|
||||
|
||||
// ForeachInterfaceAddress calls fn for each interface's address on the machine.
|
||||
func ForeachInterfaceAddress(fn func(Interface, netaddr.IP)) error {
|
||||
// ForeachInterfaceAddress calls fn for each interface's address on
|
||||
// the machine. The IPPrefix's IP is the IP address assigned to the
|
||||
// interface, and Bits are the subnet mask.
|
||||
func ForeachInterfaceAddress(fn func(Interface, netaddr.IPPrefix)) error {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -150,8 +158,8 @@ func ForeachInterfaceAddress(fn func(Interface, netaddr.IP)) error {
|
||||
for _, a := range addrs {
|
||||
switch v := a.(type) {
|
||||
case *net.IPNet:
|
||||
if ip, ok := netaddr.FromStdIP(v.IP); ok {
|
||||
fn(Interface{iface}, ip)
|
||||
if pfx, ok := netaddr.FromStdIPNet(v); ok {
|
||||
fn(Interface{iface}, pfx)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,12 +167,47 @@ func ForeachInterfaceAddress(fn func(Interface, netaddr.IP)) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForeachInterface calls fn for each interface on the machine, with
|
||||
// all its addresses. The IPPrefix's IP is the IP address assigned to
|
||||
// the interface, and Bits are the subnet mask.
|
||||
func ForeachInterface(fn func(Interface, []netaddr.IPPrefix)) error {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range ifaces {
|
||||
iface := &ifaces[i]
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var pfxs []netaddr.IPPrefix
|
||||
for _, a := range addrs {
|
||||
switch v := a.(type) {
|
||||
case *net.IPNet:
|
||||
if pfx, ok := netaddr.FromStdIPNet(v); ok {
|
||||
pfxs = append(pfxs, pfx)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Slice(pfxs, func(i, j int) bool {
|
||||
return pfxs[i].IP.Less(pfxs[j].IP)
|
||||
})
|
||||
fn(Interface{iface}, pfxs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// State is intended to store the state of the machine's network interfaces,
|
||||
// routing table, and other network configuration.
|
||||
// For now it's pretty basic.
|
||||
type State struct {
|
||||
InterfaceIPs map[string][]netaddr.IP
|
||||
InterfaceUp map[string]bool
|
||||
// InterfaceIPs maps from an interface name to the IP addresses
|
||||
// configured on that interface. Each address is represented as an
|
||||
// IPPrefix, where the IP is the interface IP address and Bits is
|
||||
// the subnet mask.
|
||||
InterfaceIPs map[string][]netaddr.IPPrefix
|
||||
Interface map[string]Interface
|
||||
|
||||
// HaveV6Global is whether this machine has an IPv6 global address
|
||||
// on some non-Tailscale interface that's up.
|
||||
@@ -195,14 +238,14 @@ type State struct {
|
||||
func (s *State) String() string {
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "interfaces.State{defaultRoute=%v ifs={", s.DefaultRouteInterface)
|
||||
ifs := make([]string, 0, len(s.InterfaceUp))
|
||||
for k := range s.InterfaceUp {
|
||||
ifs := make([]string, 0, len(s.Interface))
|
||||
for k := range s.Interface {
|
||||
if anyInterestingIP(s.InterfaceIPs[k]) {
|
||||
ifs = append(ifs, k)
|
||||
}
|
||||
}
|
||||
sort.Slice(ifs, func(i, j int) bool {
|
||||
upi, upj := s.InterfaceUp[ifs[i]], s.InterfaceUp[ifs[j]]
|
||||
upi, upj := s.Interface[ifs[i]].IsUp(), s.Interface[ifs[j]].IsUp()
|
||||
if upi != upj {
|
||||
// Up sorts before down.
|
||||
return upi
|
||||
@@ -213,17 +256,17 @@ func (s *State) String() string {
|
||||
if i > 0 {
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
if s.InterfaceUp[ifName] {
|
||||
if s.Interface[ifName].IsUp() {
|
||||
fmt.Fprintf(&sb, "%s:[", ifName)
|
||||
needSpace := false
|
||||
for _, ip := range s.InterfaceIPs[ifName] {
|
||||
if !isInterestingIP(ip) {
|
||||
for _, pfx := range s.InterfaceIPs[ifName] {
|
||||
if !isInterestingIP(pfx.IP) {
|
||||
continue
|
||||
}
|
||||
if needSpace {
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
fmt.Fprintf(&sb, "%s", ip)
|
||||
fmt.Fprintf(&sb, "%s", pfx)
|
||||
needSpace = true
|
||||
}
|
||||
sb.WriteString("]")
|
||||
@@ -246,10 +289,71 @@ func (s *State) String() string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (s *State) Equal(s2 *State) bool {
|
||||
return reflect.DeepEqual(s, s2)
|
||||
// EqualFiltered reports whether s and s2 are equal,
|
||||
// considering only interfaces in s for which filter returns true.
|
||||
func (s *State) EqualFiltered(s2 *State, filter func(i Interface, ips []netaddr.IPPrefix) bool) bool {
|
||||
if s == nil && s2 == nil {
|
||||
return true
|
||||
}
|
||||
if s == nil || s2 == nil {
|
||||
return false
|
||||
}
|
||||
if s.HaveV6Global != s2.HaveV6Global ||
|
||||
s.HaveV4 != s2.HaveV4 ||
|
||||
s.IsExpensive != s2.IsExpensive ||
|
||||
s.DefaultRouteInterface != s2.DefaultRouteInterface ||
|
||||
s.HTTPProxy != s2.HTTPProxy ||
|
||||
s.PAC != s2.PAC {
|
||||
return false
|
||||
}
|
||||
for iname, i := range s.Interface {
|
||||
ips := s.InterfaceIPs[iname]
|
||||
if !filter(i, ips) {
|
||||
continue
|
||||
}
|
||||
i2, ok := s2.Interface[iname]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
ips2, ok := s2.InterfaceIPs[iname]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if !interfacesEqual(i, i2) || !prefixesEqual(ips, ips2) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func interfacesEqual(a, b Interface) bool {
|
||||
return a.Index == b.Index &&
|
||||
a.MTU == b.MTU &&
|
||||
a.Name == b.Name &&
|
||||
a.Flags == b.Flags &&
|
||||
bytes.Equal([]byte(a.HardwareAddr), []byte(b.HardwareAddr))
|
||||
}
|
||||
|
||||
func prefixesEqual(a, b []netaddr.IPPrefix) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i, v := range a {
|
||||
if b[i] != v {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// FilterInteresting reports whether i is an interesting non-Tailscale interface.
|
||||
func FilterInteresting(i Interface, ips []netaddr.IPPrefix) bool {
|
||||
return !isTailscaleInterface(i.Name, ips) && anyInterestingIP(ips)
|
||||
}
|
||||
|
||||
// FilterAll always returns true, to use EqualFiltered against all interfaces.
|
||||
func FilterAll(i Interface, ips []netaddr.IPPrefix) bool { return true }
|
||||
|
||||
func (s *State) HasPAC() bool { return s != nil && s.PAC != "" }
|
||||
|
||||
// AnyInterfaceUp reports whether any interface seems like it has Internet access.
|
||||
@@ -257,20 +361,25 @@ func (s *State) AnyInterfaceUp() bool {
|
||||
return s != nil && (s.HaveV4 || s.HaveV6Global)
|
||||
}
|
||||
|
||||
// RemoveTailscaleInterfaces modifes s to remove any interfaces that
|
||||
// are owned by this process. (TODO: make this true; currently it
|
||||
// makes the Linux-only assumption that the interface is named
|
||||
// /^tailscale/)
|
||||
func (s *State) RemoveTailscaleInterfaces() {
|
||||
for name := range s.InterfaceIPs {
|
||||
if isTailscaleInterfaceName(name) {
|
||||
delete(s.InterfaceIPs, name)
|
||||
delete(s.InterfaceUp, name)
|
||||
func hasTailscaleIP(pfxs []netaddr.IPPrefix) bool {
|
||||
for _, pfx := range pfxs {
|
||||
if tsaddr.IsTailscaleIP(pfx.IP) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isTailscaleInterfaceName(name string) bool {
|
||||
func isTailscaleInterface(name string, ips []netaddr.IPPrefix) bool {
|
||||
if runtime.GOOS == "darwin" && strings.HasPrefix(name, "utun") && hasTailscaleIP(ips) {
|
||||
// On macOS in the sandboxed app (at least as of
|
||||
// 2021-02-25), we often see two utun devices
|
||||
// (e.g. utun4 and utun7) with the same IPv4 and IPv6
|
||||
// addresses. Just remove all utun devices with
|
||||
// Tailscale IPs until we know what's happening with
|
||||
// macOS NetworkExtensions and utun devices.
|
||||
return true
|
||||
}
|
||||
return name == "Tailscale" || // as it is on Windows
|
||||
strings.HasPrefix(name, "tailscale") // TODO: use --tun flag value, etc; see TODO in method doc
|
||||
}
|
||||
@@ -283,20 +392,27 @@ var getPAC func() string
|
||||
// It does not set the returned State.IsExpensive. The caller can populate that.
|
||||
func GetState() (*State, error) {
|
||||
s := &State{
|
||||
InterfaceIPs: make(map[string][]netaddr.IP),
|
||||
InterfaceUp: make(map[string]bool),
|
||||
InterfaceIPs: make(map[string][]netaddr.IPPrefix),
|
||||
Interface: make(map[string]Interface),
|
||||
}
|
||||
if err := ForeachInterfaceAddress(func(ni Interface, ip netaddr.IP) {
|
||||
if err := ForeachInterface(func(ni Interface, pfxs []netaddr.IPPrefix) {
|
||||
ifUp := ni.IsUp()
|
||||
s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], ip)
|
||||
s.InterfaceUp[ni.Name] = ifUp
|
||||
if ifUp && !ip.IsLoopback() && !ip.IsLinkLocalUnicast() && !isTailscaleInterfaceName(ni.Name) {
|
||||
s.HaveV6Global = s.HaveV6Global || isGlobalV6(ip)
|
||||
s.HaveV4 = s.HaveV4 || ip.Is4()
|
||||
s.Interface[ni.Name] = ni
|
||||
s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], pfxs...)
|
||||
if !ifUp || isTailscaleInterface(ni.Name, pfxs) {
|
||||
return
|
||||
}
|
||||
for _, pfx := range pfxs {
|
||||
if pfx.IP.IsLoopback() || pfx.IP.IsLinkLocalUnicast() {
|
||||
continue
|
||||
}
|
||||
s.HaveV6Global = s.HaveV6Global || isGlobalV6(pfx.IP)
|
||||
s.HaveV4 = s.HaveV4 || pfx.IP.Is4()
|
||||
}
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.DefaultRouteInterface, _ = DefaultRouteInterface()
|
||||
|
||||
if s.AnyInterfaceUp() {
|
||||
@@ -326,7 +442,8 @@ func HTTPOfListener(ln net.Listener) string {
|
||||
|
||||
var goodIP string
|
||||
var privateIP string
|
||||
ForeachInterfaceAddress(func(i Interface, ip netaddr.IP) {
|
||||
ForeachInterfaceAddress(func(i Interface, pfx netaddr.IPPrefix) {
|
||||
ip := pfx.IP
|
||||
if isPrivateIP(ip) {
|
||||
if privateIP == "" {
|
||||
privateIP = ip.String()
|
||||
@@ -362,7 +479,8 @@ func LikelyHomeRouterIP() (gateway, myIP netaddr.IP, ok bool) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
ForeachInterfaceAddress(func(i Interface, ip netaddr.IP) {
|
||||
ForeachInterfaceAddress(func(i Interface, pfx netaddr.IPPrefix) {
|
||||
ip := pfx.IP
|
||||
if !i.IsUp() || ip.IsZero() || !myIP.IsZero() {
|
||||
return
|
||||
}
|
||||
@@ -394,19 +512,18 @@ func mustCIDR(s string) netaddr.IPPrefix {
|
||||
}
|
||||
|
||||
var (
|
||||
private1 = mustCIDR("10.0.0.0/8")
|
||||
private2 = mustCIDR("172.16.0.0/12")
|
||||
private3 = mustCIDR("192.168.0.0/16")
|
||||
privatev4s = []netaddr.IPPrefix{private1, private2, private3}
|
||||
linkLocalIPv4 = mustCIDR("169.254.0.0/16")
|
||||
v6Global1 = mustCIDR("2000::/3")
|
||||
private1 = mustCIDR("10.0.0.0/8")
|
||||
private2 = mustCIDR("172.16.0.0/12")
|
||||
private3 = mustCIDR("192.168.0.0/16")
|
||||
privatev4s = []netaddr.IPPrefix{private1, private2, private3}
|
||||
v6Global1 = mustCIDR("2000::/3")
|
||||
)
|
||||
|
||||
// anyInterestingIP reports ips contains any IP that matches
|
||||
// anyInterestingIP reports whether pfxs contains any IP that matches
|
||||
// isInterestingIP.
|
||||
func anyInterestingIP(ips []netaddr.IP) bool {
|
||||
for _, ip := range ips {
|
||||
if isInterestingIP(ip) {
|
||||
func anyInterestingIP(pfxs []netaddr.IPPrefix) bool {
|
||||
for _, pfx := range pfxs {
|
||||
if isInterestingIP(pfx.IP) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,69 +6,119 @@ package interfaces
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os/exec"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"go4.org/mem"
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/util/lineread"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
/*
|
||||
Parse out 10.0.0.1 from:
|
||||
|
||||
$ netstat -r -n -f inet
|
||||
Routing tables
|
||||
|
||||
Internet:
|
||||
Destination Gateway Flags Netif Expire
|
||||
default 10.0.0.1 UGSc en0
|
||||
default link#14 UCSI utun2
|
||||
10/16 link#4 UCS en0 !
|
||||
10.0.0.1/32 link#4 UCS en0 !
|
||||
...
|
||||
|
||||
*/
|
||||
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()
|
||||
func DefaultRouteInterface() (string, error) {
|
||||
idx, err := DefaultRouteInterfaceIndex()
|
||||
if err != nil {
|
||||
return
|
||||
return "", err
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return
|
||||
iface, err := net.InterfaceByIndex(idx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer cmd.Wait()
|
||||
|
||||
var f []mem.RO
|
||||
lineread.Reader(stdout, func(lineb []byte) error {
|
||||
line := mem.B(lineb)
|
||||
if !mem.Contains(line, mem.S("default")) {
|
||||
return nil
|
||||
}
|
||||
f = mem.AppendFields(f[:0], line)
|
||||
if len(f) < 3 || !f[0].EqualString("default") {
|
||||
return nil
|
||||
}
|
||||
ipm, flagsm := f[1], f[2]
|
||||
if !mem.Contains(flagsm, mem.S("G")) {
|
||||
return nil
|
||||
}
|
||||
ip, err := netaddr.ParseIP(string(mem.Append(nil, ipm)))
|
||||
if err == nil && isPrivateIP(ip) {
|
||||
ret = ip
|
||||
// We've found what we're looking for.
|
||||
return errStopReadingNetstatTable
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return ret, !ret.IsZero()
|
||||
return iface.Name, nil
|
||||
}
|
||||
|
||||
var errStopReadingNetstatTable = errors.New("found private gateway")
|
||||
func DefaultRouteInterfaceIndex() (int, error) {
|
||||
// $ netstat -nr
|
||||
// Routing tables
|
||||
// Internet:
|
||||
// Destination Gateway Flags Netif Expire
|
||||
// default 10.0.0.1 UGSc en0 <-- want this one
|
||||
// default 10.0.0.1 UGScI en1
|
||||
|
||||
// From man netstat:
|
||||
// U RTF_UP Route usable
|
||||
// G RTF_GATEWAY Destination requires forwarding by intermediary
|
||||
// S RTF_STATIC Manually added
|
||||
// c RTF_PRCLONING Protocol-specified generate new routes on use
|
||||
// I RTF_IFSCOPE Route is associated with an interface scope
|
||||
|
||||
rib, err := route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route.FetchRIB: %w", err)
|
||||
}
|
||||
msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route.ParseRIB: %w", err)
|
||||
}
|
||||
indexSeen := map[int]int{} // index => count
|
||||
for _, m := range msgs {
|
||||
rm, ok := m.(*route.RouteMessage)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
const RTF_GATEWAY = 0x2
|
||||
const RTF_IFSCOPE = 0x1000000
|
||||
if rm.Flags&RTF_GATEWAY == 0 {
|
||||
continue
|
||||
}
|
||||
if rm.Flags&RTF_IFSCOPE != 0 {
|
||||
continue
|
||||
}
|
||||
indexSeen[rm.Index]++
|
||||
}
|
||||
if len(indexSeen) == 0 {
|
||||
return 0, errors.New("no gateway index found")
|
||||
}
|
||||
if len(indexSeen) == 1 {
|
||||
for idx := range indexSeen {
|
||||
return idx, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("ambiguous gateway interfaces found: %v", indexSeen)
|
||||
}
|
||||
|
||||
func init() {
|
||||
likelyHomeRouterIP = likelyHomeRouterIPDarwinFetchRIB
|
||||
}
|
||||
|
||||
func likelyHomeRouterIPDarwinFetchRIB() (ret netaddr.IP, ok bool) {
|
||||
rib, err := route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0)
|
||||
if err != nil {
|
||||
log.Printf("routerIP/FetchRIB: %v", err)
|
||||
return ret, false
|
||||
}
|
||||
msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
|
||||
if err != nil {
|
||||
log.Printf("routerIP/ParseRIB: %v", err)
|
||||
return ret, false
|
||||
}
|
||||
for _, m := range msgs {
|
||||
rm, ok := m.(*route.RouteMessage)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
const RTF_GATEWAY = 0x2
|
||||
const RTF_IFSCOPE = 0x1000000
|
||||
if rm.Flags&RTF_GATEWAY == 0 {
|
||||
continue
|
||||
}
|
||||
if rm.Flags&RTF_IFSCOPE != 0 {
|
||||
continue
|
||||
}
|
||||
if len(rm.Addrs) > unix.RTAX_GATEWAY {
|
||||
dst4, ok := rm.Addrs[unix.RTAX_DST].(*route.Inet4Addr)
|
||||
if !ok || dst4.IP != ([4]byte{0, 0, 0, 0}) {
|
||||
// Expect 0.0.0.0 as DST field.
|
||||
continue
|
||||
}
|
||||
gw, ok := rm.Addrs[unix.RTAX_GATEWAY].(*route.Inet4Addr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
return netaddr.IPv4(gw.IP[0], gw.IP[1], gw.IP[2], gw.IP[3]), true
|
||||
}
|
||||
}
|
||||
|
||||
return ret, false
|
||||
}
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build 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.
|
||||
uint32_t 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;
|
||||
uint32_t 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.
|
||||
uint32_t 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;
|
||||
uint32_t 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
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin,!cgo
|
||||
|
||||
package interfaces
|
||||
|
||||
func init() {
|
||||
likelyHomeRouterIP = likelyHomeRouterIPDarwinExec
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin,!redo,!ios
|
||||
// (Exclude redo, because we don't want this code in the App Store
|
||||
// version's sandbox, where it won't work, and also don't want it on
|
||||
// iOS. This is just for utun-using non-sandboxed cmd/tailscaled on macOS.
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/route"
|
||||
)
|
||||
|
||||
func DefaultRouteInterface() (string, error) {
|
||||
idx, err := DefaultRouteInterfaceIndex()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
iface, err := net.InterfaceByIndex(idx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return iface.Name, nil
|
||||
}
|
||||
|
||||
func DefaultRouteInterfaceIndex() (int, error) {
|
||||
// $ netstat -nr
|
||||
// Routing tables
|
||||
// Internet:
|
||||
// Destination Gateway Flags Netif Expire
|
||||
// default 10.0.0.1 UGSc en0 <-- want this one
|
||||
// default 10.0.0.1 UGScI en1
|
||||
|
||||
// From man netstat:
|
||||
// U RTF_UP Route usable
|
||||
// G RTF_GATEWAY Destination requires forwarding by intermediary
|
||||
// S RTF_STATIC Manually added
|
||||
// c RTF_PRCLONING Protocol-specified generate new routes on use
|
||||
// I RTF_IFSCOPE Route is associated with an interface scope
|
||||
|
||||
rib, err := route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route.FetchRIB: %w", err)
|
||||
}
|
||||
msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route.ParseRIB: %w", err)
|
||||
}
|
||||
indexSeen := map[int]int{} // index => count
|
||||
for _, m := range msgs {
|
||||
rm, ok := m.(*route.RouteMessage)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
const RTF_GATEWAY = 0x2
|
||||
const RTF_IFSCOPE = 0x1000000
|
||||
if rm.Flags&RTF_GATEWAY == 0 {
|
||||
continue
|
||||
}
|
||||
if rm.Flags&RTF_IFSCOPE != 0 {
|
||||
continue
|
||||
}
|
||||
indexSeen[rm.Index]++
|
||||
}
|
||||
if len(indexSeen) == 0 {
|
||||
return 0, errors.New("no gateway index found")
|
||||
}
|
||||
if len(indexSeen) == 1 {
|
||||
for idx := range indexSeen {
|
||||
return idx, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("ambiguous gateway interfaces found: %v", indexSeen)
|
||||
}
|
||||
86
net/interfaces/interfaces_darwin_test.go
Normal file
86
net/interfaces/interfaces_darwin_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// 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 (
|
||||
"errors"
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/util/lineread"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
func TestLikelyHomeRouterIPSyscallExec(t *testing.T) {
|
||||
syscallIP, syscallOK := likelyHomeRouterIPDarwinFetchRIB()
|
||||
netstatIP, netstatOK := likelyHomeRouterIPDarwinExec()
|
||||
if syscallOK != netstatOK || syscallIP != netstatIP {
|
||||
t.Errorf("syscall() = %v, %v, netstat = %v, %v",
|
||||
syscallIP, syscallOK,
|
||||
netstatIP, netstatOK,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Parse out 10.0.0.1 from:
|
||||
|
||||
$ netstat -r -n -f inet
|
||||
Routing tables
|
||||
|
||||
Internet:
|
||||
Destination Gateway Flags Netif Expire
|
||||
default 10.0.0.1 UGSc en0
|
||||
default link#14 UCSI utun2
|
||||
10/16 link#4 UCS en0 !
|
||||
10.0.0.1/32 link#4 UCS en0 !
|
||||
...
|
||||
|
||||
*/
|
||||
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 {
|
||||
return
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return
|
||||
}
|
||||
defer cmd.Wait()
|
||||
|
||||
var f []mem.RO
|
||||
lineread.Reader(stdout, func(lineb []byte) error {
|
||||
line := mem.B(lineb)
|
||||
if !mem.Contains(line, mem.S("default")) {
|
||||
return nil
|
||||
}
|
||||
f = mem.AppendFields(f[:0], line)
|
||||
if len(f) < 3 || !f[0].EqualString("default") {
|
||||
return nil
|
||||
}
|
||||
ipm, flagsm := f[1], f[2]
|
||||
if !mem.Contains(flagsm, mem.S("G")) {
|
||||
return nil
|
||||
}
|
||||
ip, err := netaddr.ParseIP(string(mem.Append(nil, ipm)))
|
||||
if err == nil && isPrivateIP(ip) {
|
||||
ret = ip
|
||||
// We've found what we're looking for.
|
||||
return errStopReadingNetstatTable
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return ret, !ret.IsZero()
|
||||
}
|
||||
|
||||
var errStopReadingNetstatTable = errors.New("found private gateway")
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !linux,!windows,!darwin darwin,redo
|
||||
// +build !linux,!windows,!darwin
|
||||
|
||||
package interfaces
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -13,7 +14,11 @@ func TestGetState(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Got: %#v", st)
|
||||
j, err := json.MarshalIndent(st, "", "\t")
|
||||
if err != nil {
|
||||
t.Errorf("JSON: %v", err)
|
||||
}
|
||||
t.Logf("Got: %s", j)
|
||||
t.Logf("As string: %s", st)
|
||||
|
||||
st2, err := GetState()
|
||||
@@ -21,14 +26,13 @@ func TestGetState(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !st.Equal(st2) {
|
||||
if !st.EqualFiltered(st2, FilterAll) {
|
||||
// let's assume nobody was changing the system network interfaces between
|
||||
// the two GetState calls.
|
||||
t.Fatal("two States back-to-back were not equal")
|
||||
}
|
||||
|
||||
st.RemoveTailscaleInterfaces()
|
||||
t.Logf("As string without Tailscale:\n\t%s", st)
|
||||
t.Logf("As string:\n\t%s", st)
|
||||
}
|
||||
|
||||
func TestLikelyHomeRouterIP(t *testing.T) {
|
||||
|
||||
@@ -7,17 +7,19 @@ package interfaces
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"go4.org/mem"
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tsconst"
|
||||
"tailscale.com/util/lineread"
|
||||
)
|
||||
|
||||
const (
|
||||
fallbackInterfaceMetric = uint32(0) // Used if we cannot get the actual interface metric
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -25,58 +27,76 @@ func init() {
|
||||
getPAC = getPACWindows
|
||||
}
|
||||
|
||||
/*
|
||||
Parse out 10.0.0.1 from:
|
||||
|
||||
Z:\>route print -4
|
||||
===========================================================================
|
||||
Interface List
|
||||
15...aa 15 48 ff 1c 72 ......Red Hat VirtIO Ethernet Adapter
|
||||
5...........................Tailscale Tunnel
|
||||
1...........................Software Loopback Interface 1
|
||||
===========================================================================
|
||||
|
||||
IPv4 Route Table
|
||||
===========================================================================
|
||||
Active Routes:
|
||||
Network Destination Netmask Gateway Interface Metric
|
||||
0.0.0.0 0.0.0.0 10.0.0.1 10.0.28.63 5
|
||||
10.0.0.0 255.255.0.0 On-link 10.0.28.63 261
|
||||
10.0.28.63 255.255.255.255 On-link 10.0.28.63 261
|
||||
10.0.42.0 255.255.255.0 100.103.42.106 100.103.42.106 5
|
||||
10.0.255.255 255.255.255.255 On-link 10.0.28.63 261
|
||||
34.193.248.174 255.255.255.255 100.103.42.106 100.103.42.106 5
|
||||
|
||||
*/
|
||||
func likelyHomeRouterIPWindows() (ret netaddr.IP, ok bool) {
|
||||
cmd := exec.Command("route", "print", "-4")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
rs, err := winipcfg.GetIPForwardTable2(windows.AF_INET)
|
||||
if err != nil {
|
||||
log.Printf("routerIP/GetIPForwardTable2 error: %v", err)
|
||||
return
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return
|
||||
}
|
||||
defer cmd.Wait()
|
||||
|
||||
var f []mem.RO
|
||||
lineread.Reader(stdout, func(lineb []byte) error {
|
||||
line := mem.B(lineb)
|
||||
if !mem.Contains(line, mem.S("0.0.0.0")) {
|
||||
return nil
|
||||
var ifaceMetricCache map[winipcfg.LUID]uint32
|
||||
|
||||
getIfaceMetric := func(luid winipcfg.LUID) (metric uint32) {
|
||||
if ifaceMetricCache == nil {
|
||||
ifaceMetricCache = make(map[winipcfg.LUID]uint32)
|
||||
} else if m, ok := ifaceMetricCache[luid]; ok {
|
||||
return m
|
||||
}
|
||||
f = mem.AppendFields(f[:0], line)
|
||||
if len(f) < 3 || !f[0].EqualString("0.0.0.0") || !f[1].EqualString("0.0.0.0") {
|
||||
return nil
|
||||
|
||||
if iface, err := luid.IPInterface(windows.AF_INET); err == nil {
|
||||
metric = iface.Metric
|
||||
} else {
|
||||
log.Printf("routerIP/luid.IPInterface error: %v", err)
|
||||
metric = fallbackInterfaceMetric
|
||||
}
|
||||
ipm := f[2]
|
||||
ip, err := netaddr.ParseIP(string(mem.Append(nil, ipm)))
|
||||
if err == nil && isPrivateIP(ip) {
|
||||
|
||||
ifaceMetricCache[luid] = metric
|
||||
return
|
||||
}
|
||||
|
||||
unspec := net.IPv4(0, 0, 0, 0)
|
||||
var best *winipcfg.MibIPforwardRow2 // best (lowest metric) found so far, or nil
|
||||
|
||||
for i := range rs {
|
||||
r := &rs[i]
|
||||
if r.Loopback || r.DestinationPrefix.PrefixLength != 0 || !r.DestinationPrefix.Prefix.IP().Equal(unspec) {
|
||||
// Not a default route, so skip
|
||||
continue
|
||||
}
|
||||
|
||||
ip, ok := netaddr.FromStdIP(r.NextHop.IP())
|
||||
if !ok {
|
||||
// Not a valid gateway, so skip (won't happen though)
|
||||
continue
|
||||
}
|
||||
|
||||
if best == nil {
|
||||
best = r
|
||||
ret = ip
|
||||
continue
|
||||
}
|
||||
|
||||
// We can get here only if there are multiple default gateways defined (rare case),
|
||||
// in which case we need to calculate the effective metric.
|
||||
// Effective metric is sum of interface metric and route metric offset
|
||||
if ifaceMetricCache == nil {
|
||||
// If we're here it means that previous route still isn't updated, so update it
|
||||
best.Metric += getIfaceMetric(best.InterfaceLUID)
|
||||
}
|
||||
r.Metric += getIfaceMetric(r.InterfaceLUID)
|
||||
|
||||
if best.Metric > r.Metric || best.Metric == r.Metric && ret.Compare(ip) > 0 {
|
||||
// Pick the route with lower metric, or lower IP if metrics are equal
|
||||
best = r
|
||||
ret = ip
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if !ret.IsZero() && !isPrivateIP(ret) {
|
||||
// Default route has a non-private gateway
|
||||
return netaddr.IP{}, false
|
||||
}
|
||||
|
||||
return ret, !ret.IsZero()
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,7 @@ package netcheck
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -25,11 +23,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/tcnksm/go-httpstat"
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -68,12 +66,6 @@ const (
|
||||
// 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 {
|
||||
@@ -160,6 +152,10 @@ type Client struct {
|
||||
// It defaults to ":0".
|
||||
UDPBindAddr string
|
||||
|
||||
// PortMapper, if non-nil, is used for portmap queries.
|
||||
// If nil, portmap discovery is not done.
|
||||
PortMapper *portmapper.Client // lazily initialized on first use
|
||||
|
||||
mu sync.Mutex // guards following
|
||||
nextFull bool // do a full region scan, even if last != nil
|
||||
prev map[time.Time]*Report // some previous reports
|
||||
@@ -219,8 +215,8 @@ func (c *Client) handleHairSTUNLocked(pkt []byte, src netaddr.IPPort) bool {
|
||||
// (non-incremental) probe of all DERP regions.
|
||||
func (c *Client) MakeNextReportFull() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.nextFull = true
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) ReceiveSTUNPacket(pkt []byte, src netaddr.IPPort) {
|
||||
@@ -309,6 +305,9 @@ type probePlan map[string][]probe
|
||||
func sortRegions(dm *tailcfg.DERPMap, last *Report) (prev []*tailcfg.DERPRegion) {
|
||||
prev = make([]*tailcfg.DERPRegion, 0, len(dm.Regions))
|
||||
for _, reg := range dm.Regions {
|
||||
if reg.Avoid {
|
||||
continue
|
||||
}
|
||||
prev = append(prev, reg)
|
||||
}
|
||||
sort.Slice(prev, func(i, j int) bool {
|
||||
@@ -363,7 +362,7 @@ func makeProbePlan(dm *tailcfg.DERPMap, ifState *interfaces.State, last *Report)
|
||||
tries = 2
|
||||
} else if hadBoth {
|
||||
// For dual stack machines, make the 3rd & slower nodes alternate
|
||||
// breetween
|
||||
// beetween.
|
||||
if ri%2 == 0 {
|
||||
do4, do6 = true, false
|
||||
} else {
|
||||
@@ -374,6 +373,12 @@ func makeProbePlan(dm *tailcfg.DERPMap, ifState *interfaces.State, last *Report)
|
||||
do6 = false
|
||||
}
|
||||
|
||||
if reg.RegionID == last.PreferredDERP {
|
||||
// But if we already had a DERP home, try extra hard to
|
||||
// make sure it's there so we don't flip flop around.
|
||||
tries = 4
|
||||
}
|
||||
|
||||
for try := 0; try < tries; try++ {
|
||||
if len(reg.Nodes) == 0 {
|
||||
// Shouldn't be possible.
|
||||
@@ -388,6 +393,9 @@ func makeProbePlan(dm *tailcfg.DERPMap, ifState *interfaces.State, last *Report)
|
||||
prevLatency = defaultActiveRetransmitTime
|
||||
}
|
||||
delay := time.Duration(try) * prevLatency
|
||||
if try > 1 {
|
||||
delay += time.Duration(try) * 50 * time.Millisecond
|
||||
}
|
||||
if do4 {
|
||||
p4 = append(p4, probe{delay: delay, node: n.Name, proto: probeIPv4})
|
||||
}
|
||||
@@ -680,104 +688,20 @@ func (rs *reportState) setOptBool(b *opt.Bool, v bool) {
|
||||
|
||||
func (rs *reportState) probePortMapServices() {
|
||||
defer rs.waitPortMap.Done()
|
||||
gw, myIP, ok := interfaces.LikelyHomeRouterIP()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
rs.setOptBool(&rs.report.UPnP, false)
|
||||
rs.setOptBool(&rs.report.PMP, false)
|
||||
rs.setOptBool(&rs.report.PCP, false)
|
||||
|
||||
port1900 := netaddr.IPPort{IP: gw, Port: 1900}.UDPAddr()
|
||||
port5351 := netaddr.IPPort{IP: gw, Port: 5351}.UDPAddr()
|
||||
|
||||
rs.c.logf("[v1] probePortMapServices: me %v -> gw %v", myIP, gw)
|
||||
|
||||
// Create a UDP4 socket used just for querying for UPnP, NAT-PMP, and PCP.
|
||||
uc, err := netns.Listener().ListenPacket(context.Background(), "udp4", ":0")
|
||||
res, err := rs.c.PortMapper.Probe(context.Background())
|
||||
if err != nil {
|
||||
rs.c.logf("probePortMapServices: %v", err)
|
||||
return
|
||||
}
|
||||
defer uc.Close()
|
||||
tempPort := uc.LocalAddr().(*net.UDPAddr).Port
|
||||
uc.SetReadDeadline(time.Now().Add(portMapServiceProbeTimeout))
|
||||
|
||||
// Send request packets for all three protocols.
|
||||
uc.WriteTo(uPnPPacket, port1900)
|
||||
uc.WriteTo(pmpPacket, port5351)
|
||||
uc.WriteTo(pcpPacket(myIP, tempPort, false), port5351)
|
||||
|
||||
res := make([]byte, 1500)
|
||||
sentPCPDelete := false
|
||||
for {
|
||||
n, addr, err := uc.ReadFrom(res)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
switch addr.(*net.UDPAddr).Port {
|
||||
case 1900:
|
||||
if mem.Contains(mem.B(res[:n]), mem.S(":InternetGatewayDevice:")) {
|
||||
rs.setOptBool(&rs.report.UPnP, true)
|
||||
}
|
||||
case 5351:
|
||||
if n == 12 && res[0] == 0x00 { // right length and version 0
|
||||
rs.setOptBool(&rs.report.PMP, true)
|
||||
}
|
||||
if n == 60 && res[0] == 0x02 { // right length and version 2
|
||||
rs.setOptBool(&rs.report.PCP, true)
|
||||
|
||||
if !sentPCPDelete {
|
||||
sentPCPDelete = true
|
||||
// And now delete the mapping.
|
||||
// (PCP is the only protocol of the three that requires
|
||||
// we cause a side effect to detect whether it's present,
|
||||
// so we need to redo that side effect now.)
|
||||
uc.WriteTo(pcpPacket(myIP, tempPort, true), port5351)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var pmpPacket = []byte{0, 0} // version 0, opcode 0 = "Public address request"
|
||||
|
||||
var uPnPPacket = []byte("M-SEARCH * HTTP/1.1\r\n" +
|
||||
"HOST: 239.255.255.250:1900\r\n" +
|
||||
"ST: ssdp:all\r\n" +
|
||||
"MAN: \"ssdp:discover\"\r\n" +
|
||||
"MX: 2\r\n\r\n")
|
||||
|
||||
var v4unspec, _ = netaddr.ParseIP("0.0.0.0")
|
||||
|
||||
// pcpPacket generates a PCP packet with a MAP opcode.
|
||||
func pcpPacket(myIP netaddr.IP, mapToLocalPort int, delete bool) []byte {
|
||||
const udpProtoNumber = 17
|
||||
lifetimeSeconds := uint32(1)
|
||||
if delete {
|
||||
lifetimeSeconds = 0
|
||||
}
|
||||
const opMap = 1
|
||||
|
||||
// 24 byte header + 36 byte map opcode
|
||||
pkt := make([]byte, (32+32+128)/8+(96+8+24+16+16+128)/8)
|
||||
|
||||
// The header (https://tools.ietf.org/html/rfc6887#section-7.1)
|
||||
pkt[0] = 2 // version
|
||||
pkt[1] = opMap
|
||||
binary.BigEndian.PutUint32(pkt[4:8], lifetimeSeconds)
|
||||
myIP16 := myIP.As16()
|
||||
copy(pkt[8:], myIP16[:])
|
||||
|
||||
// The map opcode body (https://tools.ietf.org/html/rfc6887#section-11.1)
|
||||
mapOp := pkt[24:]
|
||||
rand.Read(mapOp[:12]) // 96 bit mappping nonce
|
||||
mapOp[12] = udpProtoNumber
|
||||
binary.BigEndian.PutUint16(mapOp[16:], uint16(mapToLocalPort))
|
||||
v4unspec16 := v4unspec.As16()
|
||||
copy(mapOp[20:], v4unspec16[:])
|
||||
return pkt
|
||||
rs.setOptBool(&rs.report.UPnP, res.UPnP)
|
||||
rs.setOptBool(&rs.report.PMP, res.PMP)
|
||||
rs.setOptBool(&rs.report.PCP, res.PCP)
|
||||
}
|
||||
|
||||
func newReport() *Report {
|
||||
@@ -854,7 +778,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
||||
}
|
||||
defer rs.pc4Hair.Close()
|
||||
|
||||
if !c.SkipExternalNetwork {
|
||||
if !c.SkipExternalNetwork && c.PortMapper != nil {
|
||||
rs.waitPortMap.Add(1)
|
||||
go rs.probePortMapServices()
|
||||
}
|
||||
@@ -927,7 +851,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
||||
|
||||
rs.waitHairCheck(ctx)
|
||||
c.vlogf("hairCheck done")
|
||||
if !c.SkipExternalNetwork {
|
||||
if !c.SkipExternalNetwork && c.PortMapper != nil {
|
||||
rs.waitPortMap.Wait()
|
||||
c.vlogf("portMap done")
|
||||
}
|
||||
|
||||
@@ -410,6 +410,35 @@ func TestMakeProbePlan(t *testing.T) {
|
||||
"region-5-v6": []probe{p("5a", 6), p("5b", 6, 100*ms), p("5c", 6, 200*ms)},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "try_harder_for_preferred_derp",
|
||||
dm: basicMap,
|
||||
have6if: true,
|
||||
last: &Report{
|
||||
RegionLatency: map[int]time.Duration{
|
||||
1: 10 * time.Millisecond,
|
||||
2: 20 * time.Millisecond,
|
||||
3: 30 * time.Millisecond,
|
||||
4: 40 * time.Millisecond,
|
||||
},
|
||||
RegionV4Latency: map[int]time.Duration{
|
||||
1: 10 * time.Millisecond,
|
||||
2: 20 * time.Millisecond,
|
||||
},
|
||||
RegionV6Latency: map[int]time.Duration{
|
||||
3: 30 * time.Millisecond,
|
||||
4: 40 * time.Millisecond,
|
||||
},
|
||||
PreferredDERP: 1,
|
||||
},
|
||||
want: probePlan{
|
||||
"region-1-v4": []probe{p("1a", 4), p("1a", 4, 12*ms), p("1a", 4, 124*ms), p("1a", 4, 186*ms)},
|
||||
"region-1-v6": []probe{p("1a", 6), p("1a", 6, 12*ms), p("1a", 6, 124*ms), p("1a", 6, 186*ms)},
|
||||
"region-2-v4": []probe{p("2a", 4), p("2b", 4, 24*ms)},
|
||||
"region-2-v6": []probe{p("2a", 6), p("2b", 6, 24*ms)},
|
||||
"region-3-v4": []probe{p("3a", 4)},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
)
|
||||
|
||||
const tcpHeaderLength = 20
|
||||
const sctpHeaderLength = 12
|
||||
|
||||
// maxPacketLength is the largest length that all headers support.
|
||||
// IPv4 headers using uint16 for this forces an upper bound of 64KB.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user