Compare commits
246 Commits
simenghe/i
...
danderson/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c0a1375eb | ||
|
|
a35c3ba221 | ||
|
|
83906abc5e | ||
|
|
ae9b3f38d6 | ||
|
|
baf8854f9a | ||
|
|
3606e68721 | ||
|
|
4aab083cae | ||
|
|
b49d9bc74d | ||
|
|
1925fb584e | ||
|
|
88bd796622 | ||
|
|
ac0353e982 | ||
|
|
780e65a613 | ||
|
|
37053801bb | ||
|
|
51976ab3a2 | ||
|
|
246fa67e56 | ||
|
|
6990a314f5 | ||
|
|
3ac731dda1 | ||
|
|
71b375c502 | ||
|
|
0ac2130590 | ||
|
|
c1aa5a2e33 | ||
|
|
f35b8c3ead | ||
|
|
fab296536c | ||
|
|
6731f934a6 | ||
|
|
47045265b9 | ||
|
|
4ff0757d44 | ||
|
|
1dd2552032 | ||
|
|
36ffd509de | ||
|
|
edb338f542 | ||
|
|
faa891c1f2 | ||
|
|
8269a23758 | ||
|
|
bf8556ab86 | ||
|
|
6ef734e493 | ||
|
|
adf696172d | ||
|
|
af30897f0d | ||
|
|
1f006025c2 | ||
|
|
fcca374fa7 | ||
|
|
cd426eaf4c | ||
|
|
9f62cc665e | ||
|
|
5c383bdf5d | ||
|
|
56db3e2548 | ||
|
|
6f8c8c771b | ||
|
|
b7ae529ecc | ||
|
|
e199e407d2 | ||
|
|
d5e1abd0c4 | ||
|
|
57b794c338 | ||
|
|
4c8b5fdec4 | ||
|
|
a666b546fb | ||
|
|
0c038b477f | ||
|
|
278e7de9c9 | ||
|
|
93284209bc | ||
|
|
8ab44b339e | ||
|
|
6da6d47a83 | ||
|
|
a24cee0d67 | ||
|
|
d2aa144dcc | ||
|
|
25e060a841 | ||
|
|
833200da6f | ||
|
|
e804ab29fd | ||
|
|
b2eea1ee00 | ||
|
|
39610aeb09 | ||
|
|
98d557dd24 | ||
|
|
3e7ff5ff98 | ||
|
|
954867fef5 | ||
|
|
c992504375 | ||
|
|
1bca722824 | ||
|
|
00b4c2331b | ||
|
|
9547669787 | ||
|
|
b5a41ff381 | ||
|
|
ec9f3f4cc0 | ||
|
|
c68a12afe9 | ||
|
|
d2d55bd63c | ||
|
|
c6740da624 | ||
|
|
7c7eb8094b | ||
|
|
5aba620fb9 | ||
|
|
b9bd7dbc5d | ||
|
|
26b6fe7f02 | ||
|
|
3700cf9ea4 | ||
|
|
5f45d8f8e6 | ||
|
|
a4e19f2233 | ||
|
|
bdb93c5942 | ||
|
|
26c1183941 | ||
|
|
0796c53404 | ||
|
|
8bdf878832 | ||
|
|
360223fccb | ||
|
|
4d19db7c9f | ||
|
|
e6d4ab2dd6 | ||
|
|
98d36ee18d | ||
|
|
85304d7392 | ||
|
|
777b711d96 | ||
|
|
5c98b1b8d0 | ||
|
|
eee6b85b9b | ||
|
|
a5da4ed981 | ||
|
|
a729070252 | ||
|
|
fd7b738e5b | ||
|
|
fdc081c291 | ||
|
|
f013960d87 | ||
|
|
f3c96df162 | ||
|
|
0858673f1f | ||
|
|
9d0c86b6ec | ||
|
|
1db9032ff5 | ||
|
|
260b85458c | ||
|
|
54e33b511a | ||
|
|
eab80e3877 | ||
|
|
24ee0ed3c3 | ||
|
|
31ea073a73 | ||
|
|
d8fbce7eef | ||
|
|
01d4dd331d | ||
|
|
be921d1a95 | ||
|
|
0373ba36f3 | ||
|
|
1606ef5219 | ||
|
|
3e039daf95 | ||
|
|
7298e777d4 | ||
|
|
5a7ff2b231 | ||
|
|
b622c60ed0 | ||
|
|
effee49e45 | ||
|
|
3ff8a55fa7 | ||
|
|
d37451bac6 | ||
|
|
e422e9f4c9 | ||
|
|
0554b64452 | ||
|
|
9da4181606 | ||
|
|
f6e833748b | ||
|
|
8a3d52e882 | ||
|
|
c2202cc27c | ||
|
|
142670b8c2 | ||
|
|
881bb8bcdc | ||
|
|
b6179b9e83 | ||
|
|
2d35737a7a | ||
|
|
c179b9b535 | ||
|
|
690ade4ee1 | ||
|
|
f414a9cc01 | ||
|
|
1034b17bc7 | ||
|
|
965dccd4fc | ||
|
|
7b9f02fcb1 | ||
|
|
d8d9036dbb | ||
|
|
1b14e1d6bd | ||
|
|
bf7ad05230 | ||
|
|
68df379a7d | ||
|
|
aaf2df7ab1 | ||
|
|
dde8e28f00 | ||
|
|
c17d743886 | ||
|
|
281d503626 | ||
|
|
dfa5e38fad | ||
|
|
e299300b48 | ||
|
|
7428ecfebd | ||
|
|
5c266bdb73 | ||
|
|
3377089583 | ||
|
|
53a2f63658 | ||
|
|
e94ec448a7 | ||
|
|
064b916b1a | ||
|
|
d145c594ad | ||
|
|
7b295f3d21 | ||
|
|
4a2c3e2a0a | ||
|
|
1986d071c3 | ||
|
|
60f34c70a2 | ||
|
|
8db26a2261 | ||
|
|
cecfc14875 | ||
|
|
2968893add | ||
|
|
95a9adbb97 | ||
|
|
3daf27eaad | ||
|
|
74eee4de1c | ||
|
|
d666bd8533 | ||
|
|
23ad028414 | ||
|
|
3a4201e773 | ||
|
|
a5fb8e0731 | ||
|
|
ecac74bb65 | ||
|
|
e4fecfe31d | ||
|
|
0aa77ba80f | ||
|
|
ed8587f90d | ||
|
|
24db1a3c9b | ||
|
|
130c5e727b | ||
|
|
f80193fa4c | ||
|
|
81cdd2f26c | ||
|
|
9a0c8bdd20 | ||
|
|
a909d37a59 | ||
|
|
e74d37d30f | ||
|
|
b6d70203d3 | ||
|
|
7f7a81e5ae | ||
|
|
87244eda3f | ||
|
|
787939a60c | ||
|
|
84a6dcd9a9 | ||
|
|
4dbbd0aa4a | ||
|
|
0ec9040c5e | ||
|
|
4b1f2ae382 | ||
|
|
41d06bdf86 | ||
|
|
c179580599 | ||
|
|
e41193ec4d | ||
|
|
ec4d721572 | ||
|
|
e2eaae8224 | ||
|
|
2ba36c294b | ||
|
|
14135dd935 | ||
|
|
6eecf3c9d1 | ||
|
|
798b0da470 | ||
|
|
391207bbcf | ||
|
|
171ec9f8f4 | ||
|
|
1bb6abc604 | ||
|
|
236eb4d04d | ||
|
|
4f89fe17a2 | ||
|
|
3ebe16558c | ||
|
|
09e81b8ba1 | ||
|
|
b67a3007d5 | ||
|
|
9d4eddcef8 | ||
|
|
ee71c966fd | ||
|
|
e28bc49e5f | ||
|
|
d2480fd508 | ||
|
|
1896bf99d9 | ||
|
|
d3697053c9 | ||
|
|
0ae2d2b3ab | ||
|
|
61622b18fa | ||
|
|
98ad7f279c | ||
|
|
4f4dae32dd | ||
|
|
01e159b610 | ||
|
|
fb06ad19e7 | ||
|
|
d349a3231e | ||
|
|
664edbe566 | ||
|
|
5b845631ce | ||
|
|
ceb8c2b34e | ||
|
|
52972679e6 | ||
|
|
4c684fcf8c | ||
|
|
652bbc9aa0 | ||
|
|
1cedd944cf | ||
|
|
ede8ec1e20 | ||
|
|
1f2a877c61 | ||
|
|
61e8fd4698 | ||
|
|
d976a84d7e | ||
|
|
05da2691a5 | ||
|
|
87481282eb | ||
|
|
4c0494185b | ||
|
|
c84d7baf98 | ||
|
|
d98829583a | ||
|
|
67158549ab | ||
|
|
36492ace9d | ||
|
|
1072397375 | ||
|
|
1b1c85d069 | ||
|
|
3d049a9d71 | ||
|
|
46896a9311 | ||
|
|
22cac33fe7 | ||
|
|
7e7c4c1bbe | ||
|
|
92077ae78c | ||
|
|
afbd35482d | ||
|
|
33cacb5284 | ||
|
|
b90f149f5e | ||
|
|
7a08c159e6 | ||
|
|
254fc7885b | ||
|
|
dc78be12c5 | ||
|
|
a19eea965f | ||
|
|
440566c5d2 | ||
|
|
1e83b97498 |
34
.github/workflows/go_generate.yml
vendored
Normal file
34
.github/workflows/go_generate.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: go generate
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "release-branch/*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: check 'go generate' is clean
|
||||
run: |
|
||||
mkdir gentools
|
||||
go build -o gentools/stringer golang.org/x/tools/cmd/stringer
|
||||
PATH="$PATH:$(pwd)/gentools" go generate ./...
|
||||
echo
|
||||
echo
|
||||
git diff --name-only --exit-code || (echo "The files above need updating. Please run 'go generate'."; exit 1)
|
||||
20
.github/workflows/staticcheck.yml
vendored
20
.github/workflows/staticcheck.yml
vendored
@@ -31,16 +31,28 @@ jobs:
|
||||
run: "staticcheck -version"
|
||||
|
||||
- name: Run staticcheck (linux/amd64)
|
||||
run: "GOOS=linux GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
env:
|
||||
GOOS: linux
|
||||
GOARCH: amd64
|
||||
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- name: Run staticcheck (darwin/amd64)
|
||||
run: "GOOS=darwin GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
env:
|
||||
GOOS: darwin
|
||||
GOARCH: amd64
|
||||
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- name: Run staticcheck (windows/amd64)
|
||||
run: "GOOS=windows GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
env:
|
||||
GOOS: windows
|
||||
GOARCH: amd64
|
||||
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- name: Run staticcheck (windows/386)
|
||||
run: "GOOS=windows GOARCH=386 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
env:
|
||||
GOOS: windows
|
||||
GOARCH: "386"
|
||||
run: "staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
name: "integration-vms"
|
||||
|
||||
on:
|
||||
# # NOTE(Xe): uncomment this region when testing the test
|
||||
# pull_request:
|
||||
# branches: [ main ]
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
paths:
|
||||
- "tstest/integration/vms/**"
|
||||
release:
|
||||
types: [ created ]
|
||||
|
||||
|
||||
@@ -42,8 +42,7 @@ FROM golang:1.16-alpine AS build-env
|
||||
|
||||
WORKDIR /go/src/tailscale
|
||||
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
@@ -62,6 +61,6 @@ RUN go install -tags=xversion -ldflags="\
|
||||
-X tailscale.com/version.GitCommit=$VERSION_GIT_HASH" \
|
||||
-v ./cmd/...
|
||||
|
||||
FROM alpine:3.11
|
||||
FROM alpine:3.14
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2
|
||||
COPY --from=build-env /go/bin/* /usr/local/bin/
|
||||
|
||||
5
Makefile
5
Makefile
@@ -18,7 +18,10 @@ buildwindows:
|
||||
build386:
|
||||
GOOS=linux GOARCH=386 go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
|
||||
|
||||
check: staticcheck vet depaware buildwindows build386
|
||||
buildlinuxarm:
|
||||
GOOS=linux GOARCH=arm go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
|
||||
|
||||
check: staticcheck vet depaware buildwindows build386 buildlinuxarm
|
||||
|
||||
staticcheck:
|
||||
go run honnef.co/go/tools/cmd/staticcheck -- $$(go list ./... | grep -v tempfork)
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.11.0
|
||||
1.15.0
|
||||
|
||||
47
api.md
47
api.md
@@ -18,6 +18,7 @@ Currently based on {some authentication method}. Visit the [admin panel](https:/
|
||||
- [GET tailnet ACL](#tailnet-acl-get)
|
||||
- [POST tailnet ACL](#tailnet-acl-post): set ACL for a tailnet
|
||||
- [POST tailnet ACL preview](#tailnet-acl-preview-post): preview rule matches on an ACL for a resource
|
||||
- [POST tailnet ACL validate](#tailnet-acl-validate-post): run validation tests against the tailnet's existing ACL
|
||||
- [Devices](#tailnet-devices)
|
||||
- [GET tailnet devices](#tailnet-devices-get)
|
||||
- [DNS](#tailnet-dns)
|
||||
@@ -473,7 +474,7 @@ Determines what rules match for a user on an ACL without saving the ACL to the s
|
||||
###### Query Parameters
|
||||
`type` - can be 'user' or 'ipport'
|
||||
`previewFor` - if type=user, a user's email. If type=ipport, a IP address + port like "10.0.0.1:80".
|
||||
The provided ACL is queried with this paramater to determine which rules match.
|
||||
The provided ACL is queried with this parameter to determine which rules match.
|
||||
|
||||
###### POST Body
|
||||
ACL JSON or HuJSON (see https://tailscale.com/kb/1018/acls)
|
||||
@@ -510,6 +511,50 @@ Response:
|
||||
{"matches":[{"users":["*"],"ports":["*:*"],"lineNumber":19}],"user":"user1@example.com"}
|
||||
```
|
||||
|
||||
<a name=tailnet-acl-validate-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/acl/validate` - run validation tests against the tailnet's active ACL
|
||||
|
||||
Runs the provided ACL tests against the tailnet's existing ACL. This endpoint does not modify the ACL in any way.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
|
||||
The POST body should be a JSON formatted array of ACL Tests.
|
||||
|
||||
See https://tailscale.com/kb/1018/acls for more information on the format of ACL tests.
|
||||
|
||||
##### Example
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/acl/validate
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl/validate' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '
|
||||
{
|
||||
[
|
||||
{"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
Response:
|
||||
If all the tests pass, the response will be empty, with an http status code of 200.
|
||||
|
||||
Failed test error response:
|
||||
A 400 http status code and the errors in the response body.
|
||||
```
|
||||
{
|
||||
"message":"test(s) failed",
|
||||
"data":[
|
||||
{
|
||||
"user":"user1@example.com",
|
||||
"errors":["address \"2.2.2.2:22\": want: Drop, got: Accept"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-devices></a>
|
||||
|
||||
### Devices
|
||||
|
||||
@@ -8,6 +8,7 @@ package tailscale
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -16,15 +17,19 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
// TailscaledSocket is the tailscaled Unix socket.
|
||||
@@ -91,6 +96,9 @@ func send(ctx context.Context, method, path string, wantStatus int, body io.Read
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if server := res.Header.Get("Tailscale-Version"); server != version.Long {
|
||||
fmt.Fprintf(os.Stderr, "Warning: client version %q != tailscaled server version %q\n", version.Long, server)
|
||||
}
|
||||
slurp, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -293,3 +301,69 @@ func CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
|
||||
}
|
||||
return &derpMap, nil
|
||||
}
|
||||
|
||||
// CertPair returns a cert and private key for the provided DNS domain.
|
||||
//
|
||||
// It returns a cached certificate from disk if it's still valid.
|
||||
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
|
||||
res, err := send(ctx, "GET", "/localapi/v0/cert/"+domain+"?type=pair", 200, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
// with ?type=pair, the response PEM is first the one private
|
||||
// key PEM block, then the cert PEM blocks.
|
||||
i := mem.Index(mem.B(res), mem.S("--\n--"))
|
||||
if i == -1 {
|
||||
return nil, nil, fmt.Errorf("unexpected output: no delimiter")
|
||||
}
|
||||
i += len("--\n")
|
||||
keyPEM, certPEM = res[:i], res[i:]
|
||||
if mem.Contains(mem.B(certPEM), mem.S(" PRIVATE KEY-----")) {
|
||||
return nil, nil, fmt.Errorf("unexpected output: key in cert")
|
||||
}
|
||||
return certPEM, keyPEM, nil
|
||||
}
|
||||
|
||||
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
|
||||
//
|
||||
// It returns a cached certificate from disk if it's still valid.
|
||||
//
|
||||
// It's the right signature to use as the value of
|
||||
// tls.Config.GetCertificate.
|
||||
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if hi == nil || hi.ServerName == "" {
|
||||
return nil, errors.New("no SNI ServerName")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
name := hi.ServerName
|
||||
if !strings.Contains(name, ".") {
|
||||
if v, ok := ExpandSNIName(ctx, name); ok {
|
||||
name = v
|
||||
}
|
||||
}
|
||||
certPEM, keyPEM, err := CertPair(ctx, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cert, nil
|
||||
}
|
||||
|
||||
// ExpandSNIName expands bare label name into the the most likely actual TLS cert name.
|
||||
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
|
||||
st, err := StatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
for _, d := range st.CertDomains {
|
||||
if len(d) > len(name)+1 && strings.HasPrefix(d, name) && d[len(name)] == '.' {
|
||||
return d, true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
77
cmd/addlicense/main.go
Normal file
77
cmd/addlicense/main.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// 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.
|
||||
|
||||
// Program addlicense adds a license header to a file.
|
||||
// It is intended for use with 'go generate',
|
||||
// so it has a slightly weird usage.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
var (
|
||||
year = flag.Int("year", 0, "copyright year")
|
||||
file = flag.String("file", "", "file to modify")
|
||||
)
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stderr, `
|
||||
usage: addlicense -year YEAR -file FILE <subcommand args...>
|
||||
`[1:])
|
||||
|
||||
flag.PrintDefaults()
|
||||
fmt.Fprintf(os.Stderr, `
|
||||
addlicense adds a Tailscale license to the beginning of file,
|
||||
using year as the copyright year.
|
||||
|
||||
It is intended for use with 'go generate', so it also runs a subcommand,
|
||||
which presumably creates the file.
|
||||
|
||||
Sample usage:
|
||||
|
||||
addlicense -year 2021 -file pull_strings.go stringer -type=pull
|
||||
`[1:])
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
if len(flag.Args()) == 0 {
|
||||
flag.Usage()
|
||||
}
|
||||
cmd := exec.Command(flag.Arg(0), flag.Args()[1:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
err := cmd.Run()
|
||||
check(err)
|
||||
b, err := os.ReadFile(*file)
|
||||
check(err)
|
||||
f, err := os.OpenFile(*file, os.O_TRUNC|os.O_WRONLY, 0644)
|
||||
check(err)
|
||||
_, err = fmt.Fprintf(f, license, *year)
|
||||
check(err)
|
||||
_, err = f.Write(b)
|
||||
check(err)
|
||||
err = f.Close()
|
||||
check(err)
|
||||
}
|
||||
|
||||
func check(err error) {
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
var license = `
|
||||
// Copyright (c) %d 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.
|
||||
|
||||
`[1:]
|
||||
@@ -58,7 +58,12 @@ func loadConfig() config {
|
||||
return config{PrivateKey: mustNewKey()}
|
||||
}
|
||||
if *configPath == "" {
|
||||
log.Fatalf("derper: -c <config path> not specified")
|
||||
if os.Getuid() == 0 {
|
||||
*configPath = "/var/lib/derper/derper.key"
|
||||
} else {
|
||||
log.Fatalf("derper: -c <config path> not specified")
|
||||
}
|
||||
log.Printf("no config path specified; using %s", *configPath)
|
||||
}
|
||||
b, err := ioutil.ReadFile(*configPath)
|
||||
switch {
|
||||
|
||||
@@ -9,7 +9,9 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
@@ -39,6 +41,34 @@ func startMeshWithHost(s *derp.Server, host string) error {
|
||||
return err
|
||||
}
|
||||
c.MeshKey = s.MeshKey()
|
||||
|
||||
// For meshed peers within a region, connect via VPC addresses.
|
||||
c.SetURLDialer(func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var d net.Dialer
|
||||
var r net.Resolver
|
||||
if port == "443" && strings.HasSuffix(host, ".tailscale.com") {
|
||||
base := strings.TrimSuffix(host, ".tailscale.com")
|
||||
subCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
defer cancel()
|
||||
vpcHost := base + "-vpc.tailscale.com"
|
||||
ips, _ := r.LookupIP(subCtx, "ip", vpcHost)
|
||||
if len(ips) > 0 {
|
||||
vpcAddr := net.JoinHostPort(ips[0].String(), port)
|
||||
c, err := d.DialContext(subCtx, network, vpcAddr)
|
||||
if err == nil {
|
||||
log.Printf("connected to %v (%v) instead of %v", vpcHost, ips[0], base)
|
||||
return c, nil
|
||||
}
|
||||
log.Printf("failed to connect to %v (%v): %v; trying non-VPC route", vpcHost, ips[0], err)
|
||||
}
|
||||
}
|
||||
return d.DialContext(ctx, network, addr)
|
||||
})
|
||||
|
||||
add := func(k key.Public) { s.AddPacketForwarder(k, c) }
|
||||
remove := func(k key.Public) { s.RemovePacketForwarder(k, c) }
|
||||
go c.RunWatchConnectionLoop(context.Background(), s.PublicKey(), logf, add, remove)
|
||||
|
||||
354
cmd/derpprobe/derpprobe.go
Normal file
354
cmd/derpprobe/derpprobe.go
Normal file
@@ -0,0 +1,354 @@
|
||||
// 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.
|
||||
|
||||
// The derpprobe binary probes derpers.
|
||||
package main // import "tailscale.com/cmd/derper/derpprobe"
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
var (
|
||||
derpMapURL = flag.String("derp-map", "https://login.tailscale.com/derpmap/default", "URL to DERP map (https:// or file://)")
|
||||
listen = flag.String("listen", ":8030", "HTTP listen address")
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
state = map[nodePair]pairStatus{}
|
||||
lastDERPMap *tailcfg.DERPMap
|
||||
lastDERPMapAt time.Time
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
go probeLoop()
|
||||
log.Fatal(http.ListenAndServe(*listen, http.HandlerFunc(serve)))
|
||||
}
|
||||
|
||||
type overallStatus struct {
|
||||
good, bad []string
|
||||
}
|
||||
|
||||
func (st *overallStatus) addBadf(format string, a ...interface{}) {
|
||||
st.bad = append(st.bad, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
func (st *overallStatus) addGoodf(format string, a ...interface{}) {
|
||||
st.good = append(st.good, fmt.Sprintf(format, a...))
|
||||
}
|
||||
|
||||
func getOverallStatus() (o overallStatus) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if lastDERPMap == nil {
|
||||
o.addBadf("no DERP map")
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
if age := now.Sub(lastDERPMapAt); age > time.Minute {
|
||||
o.addBadf("DERPMap hasn't been successfully refreshed in %v", age.Round(time.Second))
|
||||
}
|
||||
for _, reg := range sortedRegions(lastDERPMap) {
|
||||
for _, from := range reg.Nodes {
|
||||
for _, to := range reg.Nodes {
|
||||
pair := nodePair{from.Name, to.Name}
|
||||
st, ok := state[pair]
|
||||
age := now.Sub(st.at).Round(time.Second)
|
||||
switch {
|
||||
case !ok:
|
||||
o.addBadf("no state for %v", pair)
|
||||
case st.err != nil:
|
||||
o.addBadf("%v: %v", pair, st.err)
|
||||
case age > 90*time.Second:
|
||||
o.addBadf("%v: update is %v old", pair, age)
|
||||
default:
|
||||
o.addGoodf("%v: %v, %v ago", pair, st.latency.Round(time.Millisecond), age)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func serve(w http.ResponseWriter, r *http.Request) {
|
||||
st := getOverallStatus()
|
||||
summary := "All good"
|
||||
if len(st.bad) > 0 {
|
||||
w.WriteHeader(500)
|
||||
summary = fmt.Sprintf("%d problems", len(st.bad))
|
||||
}
|
||||
io.WriteString(w, "<html><head><style>.bad { font-weight: bold; color: #700; }</style></head>\n")
|
||||
fmt.Fprintf(w, "<body><h1>derp probe</h1>\n%s:<ul>", summary)
|
||||
for _, s := range st.bad {
|
||||
fmt.Fprintf(w, "<li class=bad>%s</li>\n", html.EscapeString(s))
|
||||
}
|
||||
for _, s := range st.good {
|
||||
fmt.Fprintf(w, "<li>%s</li>\n", html.EscapeString(s))
|
||||
}
|
||||
io.WriteString(w, "</ul></body></html>\n")
|
||||
}
|
||||
|
||||
func sortedRegions(dm *tailcfg.DERPMap) []*tailcfg.DERPRegion {
|
||||
ret := make([]*tailcfg.DERPRegion, 0, len(dm.Regions))
|
||||
for _, r := range dm.Regions {
|
||||
ret = append(ret, r)
|
||||
}
|
||||
sort.Slice(ret, func(i, j int) bool { return ret[i].RegionID < ret[j].RegionID })
|
||||
return ret
|
||||
}
|
||||
|
||||
type nodePair struct {
|
||||
from, to string // DERPNode.Name
|
||||
}
|
||||
|
||||
func (p nodePair) String() string { return fmt.Sprintf("(%s→%s)", p.from, p.to) }
|
||||
|
||||
type pairStatus struct {
|
||||
err error
|
||||
latency time.Duration
|
||||
at time.Time
|
||||
}
|
||||
|
||||
func setDERPMap(dm *tailcfg.DERPMap) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
lastDERPMap = dm
|
||||
lastDERPMapAt = time.Now()
|
||||
}
|
||||
|
||||
func setState(p nodePair, latency time.Duration, err error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
st := pairStatus{
|
||||
err: err,
|
||||
latency: latency,
|
||||
at: time.Now(),
|
||||
}
|
||||
state[p] = st
|
||||
if err != nil {
|
||||
log.Printf("%+v error: %v", p, err)
|
||||
} else {
|
||||
log.Printf("%+v: %v", p, latency.Round(time.Millisecond))
|
||||
}
|
||||
}
|
||||
|
||||
func probeLoop() {
|
||||
ticker := time.NewTicker(15 * time.Second)
|
||||
for {
|
||||
err := probe()
|
||||
if err != nil {
|
||||
log.Printf("probe: %v", err)
|
||||
}
|
||||
<-ticker.C
|
||||
}
|
||||
}
|
||||
|
||||
func probe() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
dm, err := getDERPMap(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(dm.Regions))
|
||||
for _, reg := range dm.Regions {
|
||||
reg := reg
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for _, from := range reg.Nodes {
|
||||
for _, to := range reg.Nodes {
|
||||
latency, err := probeNodePair(ctx, dm, from, to)
|
||||
setState(nodePair{from.Name, to.Name}, latency, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func probeNodePair(ctx context.Context, dm *tailcfg.DERPMap, from, to *tailcfg.DERPNode) (latency time.Duration, err error) {
|
||||
// The passed in context is a minute for the whole region. The
|
||||
// idea is that each node pair in the region will be done
|
||||
// serially and regularly in the future, reusing connections
|
||||
// (at least in the happy path). For now they don't reuse
|
||||
// connections and probe at most once every 15 seconds. We
|
||||
// bound the duration of a single node pair within a region
|
||||
// so one bad one can't starve others.
|
||||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
fromc, err := newConn(ctx, dm, from)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer fromc.Close()
|
||||
toc, err := newConn(ctx, dm, to)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer toc.Close()
|
||||
|
||||
// Wait a bit for from's node to hear about to existing on the
|
||||
// other node in the region, in the case where the two nodes
|
||||
// are different.
|
||||
if from.Name != to.Name {
|
||||
time.Sleep(100 * time.Millisecond) // pretty arbitrary
|
||||
}
|
||||
|
||||
// Make a random packet
|
||||
pkt := make([]byte, 8)
|
||||
crand.Read(pkt)
|
||||
|
||||
t0 := time.Now()
|
||||
|
||||
// Send the random packet.
|
||||
sendc := make(chan error, 1)
|
||||
go func() {
|
||||
sendc <- fromc.Send(toc.SelfPublicKey(), pkt)
|
||||
}()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return 0, fmt.Errorf("timeout sending via %q: %w", from.Name, ctx.Err())
|
||||
case err := <-sendc:
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("error sending via %q: %w", from.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Receive the random packet.
|
||||
recvc := make(chan interface{}, 1) // either derp.ReceivedPacket or error
|
||||
go func() {
|
||||
for {
|
||||
m, err := toc.Recv()
|
||||
if err != nil {
|
||||
recvc <- err
|
||||
return
|
||||
}
|
||||
switch v := m.(type) {
|
||||
case derp.ReceivedPacket:
|
||||
recvc <- v
|
||||
default:
|
||||
log.Printf("%v: ignoring Recv frame type %T", to.Name, v)
|
||||
// Loop.
|
||||
}
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return 0, fmt.Errorf("timeout receiving from %q: %w", to.Name, ctx.Err())
|
||||
case v := <-recvc:
|
||||
if err, ok := v.(error); ok {
|
||||
return 0, fmt.Errorf("error receiving from %q: %w", to.Name, err)
|
||||
}
|
||||
p := v.(derp.ReceivedPacket)
|
||||
if p.Source != fromc.SelfPublicKey() {
|
||||
return 0, fmt.Errorf("got data packet from unexpected source, %v", p.Source)
|
||||
}
|
||||
if !bytes.Equal(p.Data, pkt) {
|
||||
return 0, fmt.Errorf("unexpected data packet %q", p.Data)
|
||||
}
|
||||
}
|
||||
return time.Since(t0), nil
|
||||
}
|
||||
|
||||
func newConn(ctx context.Context, dm *tailcfg.DERPMap, n *tailcfg.DERPNode) (*derphttp.Client, error) {
|
||||
priv := key.NewPrivate()
|
||||
dc := derphttp.NewRegionClient(priv, log.Printf, func() *tailcfg.DERPRegion {
|
||||
rid := n.RegionID
|
||||
return &tailcfg.DERPRegion{
|
||||
RegionID: rid,
|
||||
RegionCode: fmt.Sprintf("%s-%s", dm.Regions[rid].RegionCode, n.Name),
|
||||
RegionName: dm.Regions[rid].RegionName,
|
||||
Nodes: []*tailcfg.DERPNode{n},
|
||||
}
|
||||
})
|
||||
dc.IsProber = true
|
||||
err := dc.Connect(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
m, err := dc.Recv()
|
||||
if err != nil {
|
||||
errc <- err
|
||||
return
|
||||
}
|
||||
switch m.(type) {
|
||||
case derp.ServerInfoMessage:
|
||||
errc <- nil
|
||||
default:
|
||||
errc <- fmt.Errorf("unexpected first message type %T", errc)
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case err := <-errc:
|
||||
if err != nil {
|
||||
go dc.Close()
|
||||
return nil, err
|
||||
}
|
||||
case <-ctx.Done():
|
||||
go dc.Close()
|
||||
return nil, fmt.Errorf("timeout waiting for ServerInfoMessage: %w", ctx.Err())
|
||||
}
|
||||
return dc, nil
|
||||
}
|
||||
|
||||
var httpOrFileClient = &http.Client{Transport: httpOrFileTransport()}
|
||||
|
||||
func httpOrFileTransport() http.RoundTripper {
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.RegisterProtocol("file", http.NewFileTransport(http.Dir("/")))
|
||||
return tr
|
||||
}
|
||||
|
||||
func getDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", *derpMapURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := httpOrFileClient.Do(req)
|
||||
if err != nil {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if lastDERPMap != nil && time.Since(lastDERPMapAt) < 10*time.Minute {
|
||||
// Assume that control is restarting and use
|
||||
// the same one for a bit.
|
||||
return lastDERPMap, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("fetching %s: %s", *derpMapURL, res.Status)
|
||||
}
|
||||
dm := new(tailcfg.DERPMap)
|
||||
if err := json.NewDecoder(res.Body).Decode(dm); err != nil {
|
||||
return nil, fmt.Errorf("decoding %s JSON: %v", *derpMapURL, err)
|
||||
}
|
||||
setDERPMap(dm)
|
||||
return dm, nil
|
||||
}
|
||||
@@ -1,173 +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.
|
||||
|
||||
// microproxy proxies incoming HTTPS connections to another
|
||||
// destination. Instead of managing its own TLS certificates, it
|
||||
// borrows issued certificates and keys from an autocert directory.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/tsweb"
|
||||
)
|
||||
|
||||
var (
|
||||
addr = flag.String("addr", ":4430", "server address")
|
||||
certdir = flag.String("certdir", "", "directory to borrow LetsEncrypt certificates from")
|
||||
hostname = flag.String("hostname", "", "hostname to serve")
|
||||
logCollection = flag.String("logcollection", "", "If non-empty, logtail collection to log to")
|
||||
nodeExporter = flag.String("node-exporter", "http://localhost:9100", "URL of the local prometheus node exporter")
|
||||
goVarsURL = flag.String("go-vars-url", "http://localhost:8383/debug/vars", "URL of a local Go server's /debug/vars endpoint")
|
||||
insecure = flag.Bool("insecure", false, "serve over http, for development")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
if *logCollection != "" {
|
||||
logpolicy.New(*logCollection)
|
||||
}
|
||||
|
||||
ne, err := url.Parse(*nodeExporter)
|
||||
if err != nil {
|
||||
log.Fatalf("Couldn't parse URL %q: %v", *nodeExporter, err)
|
||||
}
|
||||
proxy := httputil.NewSingleHostReverseProxy(ne)
|
||||
proxy.FlushInterval = time.Second
|
||||
|
||||
if _, err = url.Parse(*goVarsURL); err != nil {
|
||||
log.Fatalf("Couldn't parse URL %q: %v", *goVarsURL, err)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
tsweb.Debugger(mux) // registers /debug/*
|
||||
mux.Handle("/metrics", tsweb.Protected(proxy))
|
||||
mux.Handle("/varz", tsweb.Protected(tsweb.StdHandler(&goVarsHandler{*goVarsURL}, tsweb.HandlerOptions{
|
||||
Quiet200s: true,
|
||||
Logf: log.Printf,
|
||||
})))
|
||||
|
||||
ch := &certHolder{
|
||||
hostname: *hostname,
|
||||
path: filepath.Join(*certdir, *hostname),
|
||||
}
|
||||
|
||||
httpsrv := &http.Server{
|
||||
Addr: *addr,
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
if !*insecure {
|
||||
httpsrv.TLSConfig = &tls.Config{GetCertificate: ch.GetCertificate}
|
||||
err = httpsrv.ListenAndServeTLS("", "")
|
||||
} else {
|
||||
err = httpsrv.ListenAndServe()
|
||||
}
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
type goVarsHandler struct {
|
||||
url string
|
||||
}
|
||||
|
||||
func promPrint(w io.Writer, prefix string, obj map[string]interface{}) {
|
||||
for k, i := range obj {
|
||||
if prefix != "" {
|
||||
k = prefix + "_" + k
|
||||
}
|
||||
switch v := i.(type) {
|
||||
case map[string]interface{}:
|
||||
promPrint(w, k, v)
|
||||
case float64:
|
||||
const saveConfigReject = "control_save_config_rejected_"
|
||||
const saveConfig = "control_save_config_"
|
||||
switch {
|
||||
case strings.HasPrefix(k, saveConfigReject):
|
||||
fmt.Fprintf(w, "control_save_config_rejected{reason=%q} %f\n", k[len(saveConfigReject):], v)
|
||||
case strings.HasPrefix(k, saveConfig):
|
||||
fmt.Fprintf(w, "control_save_config{reason=%q} %f\n", k[len(saveConfig):], v)
|
||||
default:
|
||||
fmt.Fprintf(w, "%s %f\n", k, v)
|
||||
}
|
||||
default:
|
||||
fmt.Fprintf(w, "# Skipping key %q, unhandled type %T\n", k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *goVarsHandler) ServeHTTPReturn(w http.ResponseWriter, r *http.Request) error {
|
||||
resp, err := http.Get(h.url)
|
||||
if err != nil {
|
||||
return tsweb.Error(http.StatusInternalServerError, "fetch failed", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var mon map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&mon); err != nil {
|
||||
return tsweb.Error(http.StatusInternalServerError, "fetch failed", err)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
promPrint(w, "", mon)
|
||||
return nil
|
||||
}
|
||||
|
||||
// certHolder loads and caches a TLS certificate from disk, reloading
|
||||
// it every hour.
|
||||
type certHolder struct {
|
||||
hostname string // only hostname allowed in SNI
|
||||
path string // path of certificate+key combined PEM file
|
||||
|
||||
mu sync.Mutex
|
||||
cert *tls.Certificate // cached parsed cert+key
|
||||
loaded time.Time
|
||||
}
|
||||
|
||||
func (c *certHolder) GetCertificate(ch *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if ch.ServerName != c.hostname {
|
||||
return nil, fmt.Errorf("wrong client SNI %q", ch.ServerName)
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if time.Since(c.loaded) > time.Hour {
|
||||
if err := c.loadLocked(); err != nil {
|
||||
log.Printf("Reloading cert %q: %v", c.path, err)
|
||||
// continue anyway, we might be able to serve off the stale cert.
|
||||
}
|
||||
}
|
||||
return c.cert, nil
|
||||
}
|
||||
|
||||
// load reloads the TLS certificate and key from disk. Caller must
|
||||
// hold mu.
|
||||
func (c *certHolder) loadLocked() error {
|
||||
bs, err := ioutil.ReadFile(c.path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading %q: %v", c.path, err)
|
||||
}
|
||||
cert, err := tls.X509KeyPair(bs, bs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing %q: %v", c.path, err)
|
||||
}
|
||||
|
||||
c.cert = &cert
|
||||
c.loaded = time.Now()
|
||||
return nil
|
||||
}
|
||||
121
cmd/speedtest/speedtest.go
Normal file
121
cmd/speedtest/speedtest.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// 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.
|
||||
|
||||
// Program speedtest provides the speedtest command. The reason to keep it separate from
|
||||
// the normal tailscale cli is because it is not yet ready to go in the tailscale binary.
|
||||
// It will be included in the tailscale cli after it has been added to tailscaled.
|
||||
|
||||
// Example usage for client command: go run cmd/speedtest -host 127.0.0.1:20333 -t 5s
|
||||
// This will connect to the server on 127.0.0.1:20333 and start a 5 second download speedtest.
|
||||
// Example usage for server command: go run cmd/speedtest -s -host :20333
|
||||
// This will start a speedtest server on port 20333.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/net/speedtest"
|
||||
)
|
||||
|
||||
// Runs the speedtest command as a commandline program
|
||||
func main() {
|
||||
args := os.Args[1:]
|
||||
if err := speedtestCmd.Parse(args); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
err := speedtestCmd.Run(context.Background())
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
fmt.Fprintln(os.Stderr, speedtestCmd.ShortUsage)
|
||||
os.Exit(2)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err.Error())
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// speedtestCmd is the root command. It runs either the server or client depending on the
|
||||
// flags passed to it.
|
||||
var speedtestCmd = &ffcli.Command{
|
||||
Name: "speedtest",
|
||||
ShortUsage: "speedtest [-host <host:port>] [-s] [-r] [-t <test duration>]",
|
||||
ShortHelp: "Run a speed test",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("speedtest", flag.ExitOnError)
|
||||
fs.StringVar(&speedtestArgs.host, "host", ":20333", "host:port pair to connect to or listen on")
|
||||
fs.DurationVar(&speedtestArgs.testDuration, "t", speedtest.DefaultDuration, "duration of the speed test")
|
||||
fs.BoolVar(&speedtestArgs.runServer, "s", false, "run a speedtest server")
|
||||
fs.BoolVar(&speedtestArgs.reverse, "r", false, "run in reverse mode (server sends, client receives)")
|
||||
return fs
|
||||
})(),
|
||||
Exec: runSpeedtest,
|
||||
}
|
||||
|
||||
var speedtestArgs struct {
|
||||
host string
|
||||
testDuration time.Duration
|
||||
runServer bool
|
||||
reverse bool
|
||||
}
|
||||
|
||||
func runSpeedtest(ctx context.Context, args []string) error {
|
||||
|
||||
if _, _, err := net.SplitHostPort(speedtestArgs.host); err != nil {
|
||||
var addrErr *net.AddrError
|
||||
if errors.As(err, &addrErr) && addrErr.Err == "missing port in address" {
|
||||
// if no port is provided, append the default port
|
||||
speedtestArgs.host = net.JoinHostPort(speedtestArgs.host, strconv.Itoa(speedtest.DefaultPort))
|
||||
}
|
||||
}
|
||||
|
||||
if speedtestArgs.runServer {
|
||||
listener, err := net.Listen("tcp", speedtestArgs.host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("listening on %v\n", listener.Addr())
|
||||
|
||||
return speedtest.Serve(listener)
|
||||
}
|
||||
|
||||
// Ensure the duration is within the allowed range
|
||||
if speedtestArgs.testDuration < speedtest.MinDuration || speedtestArgs.testDuration > speedtest.MaxDuration {
|
||||
return fmt.Errorf("test duration must be within %v and %v", speedtest.MinDuration, speedtest.MaxDuration)
|
||||
}
|
||||
|
||||
dir := speedtest.Download
|
||||
if speedtestArgs.reverse {
|
||||
dir = speedtest.Upload
|
||||
}
|
||||
|
||||
fmt.Printf("Starting a %s test with %s\n", dir, speedtestArgs.host)
|
||||
results, err := speedtest.RunClient(dir, speedtestArgs.testDuration, speedtestArgs.host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 12, 0, 0, ' ', tabwriter.TabIndent)
|
||||
fmt.Println("Results:")
|
||||
fmt.Fprintln(w, "Interval\t\tTransfer\t\tBandwidth\t\t")
|
||||
for _, r := range results {
|
||||
if r.Total {
|
||||
fmt.Fprintln(w, "-------------------------------------------------------------------------")
|
||||
}
|
||||
fmt.Fprintf(w, "%.2f-%.2f\tsec\t%.4f\tMBits\t%.4f\tMbits/sec\t\n", r.IntervalStart.Seconds(), r.IntervalEnd.Seconds(), r.MegaBits(), r.MBitsPerSecond())
|
||||
}
|
||||
w.Flush()
|
||||
return nil
|
||||
}
|
||||
107
cmd/tailscale/cli/cert.go
Normal file
107
cmd/tailscale/cli/cert.go
Normal file
@@ -0,0 +1,107 @@
|
||||
// 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"
|
||||
"crypto/tls"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
var certCmd = &ffcli.Command{
|
||||
Name: "cert",
|
||||
Exec: runCert,
|
||||
ShortHelp: "get TLS certs",
|
||||
ShortUsage: "cert [flags] <domain>",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("cert", flag.ExitOnError)
|
||||
fs.StringVar(&certArgs.certFile, "cert-file", "", "output cert file; defaults to DOMAIN.crt")
|
||||
fs.StringVar(&certArgs.keyFile, "key-file", "", "output cert file; defaults to DOMAIN.key")
|
||||
fs.BoolVar(&certArgs.serve, "serve-demo", false, "if true, serve on port :443 using the cert as a demo, instead of writing out the files to disk")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var certArgs struct {
|
||||
certFile string
|
||||
keyFile string
|
||||
serve bool
|
||||
}
|
||||
|
||||
func runCert(ctx context.Context, args []string) error {
|
||||
if certArgs.serve {
|
||||
s := &http.Server{
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: tailscale.GetCertificate,
|
||||
},
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.TLS != nil && !strings.Contains(r.Host, ".") && r.Method == "GET" {
|
||||
if v, ok := tailscale.ExpandSNIName(r.Context(), r.Host); ok {
|
||||
http.Redirect(w, r, "https://"+v+r.URL.Path, http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(w, "<h1>Hello from Tailscale</h1>It works.")
|
||||
}),
|
||||
}
|
||||
log.Printf("running TLS server on :443 ...")
|
||||
return s.ListenAndServeTLS("", "")
|
||||
}
|
||||
|
||||
if len(args) != 1 {
|
||||
return fmt.Errorf("Usage: tailscale cert [flags] <domain>")
|
||||
}
|
||||
domain := args[0]
|
||||
|
||||
if certArgs.certFile == "" {
|
||||
certArgs.certFile = domain + ".crt"
|
||||
}
|
||||
if certArgs.keyFile == "" {
|
||||
certArgs.keyFile = domain + ".key"
|
||||
}
|
||||
certPEM, keyPEM, err := tailscale.CertPair(ctx, domain)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
certChanged, err := writeIfChanged(certArgs.certFile, certPEM, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if certChanged {
|
||||
fmt.Printf("Wrote public cert to %v\n", certArgs.certFile)
|
||||
} else {
|
||||
fmt.Printf("Public cert unchanged at %v\n", certArgs.certFile)
|
||||
}
|
||||
keyChanged, err := writeIfChanged(certArgs.keyFile, keyPEM, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if keyChanged {
|
||||
fmt.Printf("Wrote private key to %v\n", certArgs.keyFile)
|
||||
} else {
|
||||
fmt.Printf("Private key unchanged at %v\n", certArgs.keyFile)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeIfChanged(filename string, contents []byte, mode os.FileMode) (changed bool, err error) {
|
||||
if old, err := os.ReadFile(filename); err == nil && bytes.Equal(contents, old) {
|
||||
return false, nil
|
||||
}
|
||||
if err := atomicfile.WriteFile(filename, contents, mode); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
@@ -107,6 +107,7 @@ change in the future.
|
||||
webCmd,
|
||||
fileCmd,
|
||||
bugReportCmd,
|
||||
certCmd,
|
||||
},
|
||||
FlagSet: rootfs,
|
||||
Exec: func(context.Context, []string) error { return flag.ErrHelp },
|
||||
|
||||
@@ -13,9 +13,11 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
)
|
||||
|
||||
@@ -440,8 +442,9 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
}
|
||||
applyImplicitPrefs(newPrefs, tt.curPrefs, tt.curUser)
|
||||
var got string
|
||||
if err := checkForAccidentalSettingReverts(flagSet, tt.curPrefs, newPrefs, upCheckEnv{
|
||||
if err := checkForAccidentalSettingReverts(newPrefs, tt.curPrefs, upCheckEnv{
|
||||
goos: goos,
|
||||
flagSet: flagSet,
|
||||
curExitNodeIP: tt.curExitNodeIP,
|
||||
}); err != nil {
|
||||
got = err.Error()
|
||||
@@ -688,3 +691,108 @@ func TestFlagAppliesToOS(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePrefs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
flags []string // argv to be parsed into env.flagSet and env.upArgs
|
||||
curPrefs *ipn.Prefs
|
||||
env upCheckEnv // empty goos means "linux"
|
||||
|
||||
wantSimpleUp bool
|
||||
wantJustEditMP *ipn.MaskedPrefs
|
||||
wantErrSubtr string
|
||||
}{
|
||||
{
|
||||
name: "bare_up_means_up",
|
||||
flags: []string{},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
Hostname: "foo",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "just_up",
|
||||
flags: []string{},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
},
|
||||
env: upCheckEnv{
|
||||
backendState: "Stopped",
|
||||
},
|
||||
wantSimpleUp: true,
|
||||
},
|
||||
{
|
||||
name: "just_edit",
|
||||
flags: []string{},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
wantSimpleUp: true,
|
||||
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
|
||||
},
|
||||
{
|
||||
name: "control_synonym",
|
||||
flags: []string{},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
wantSimpleUp: true,
|
||||
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
|
||||
},
|
||||
{
|
||||
name: "change_login_server",
|
||||
flags: []string{"--login-server=https://localhost:1000"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
wantSimpleUp: true,
|
||||
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
|
||||
wantErrSubtr: "can't change --login-server without --force-reauth",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.env.goos == "" {
|
||||
tt.env.goos = "linux"
|
||||
}
|
||||
tt.env.flagSet = newUpFlagSet(tt.env.goos, &tt.env.upArgs)
|
||||
tt.env.flagSet.Parse(tt.flags)
|
||||
|
||||
newPrefs, err := prefsFromUpArgs(tt.env.upArgs, t.Logf, new(ipnstate.Status), tt.env.goos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
simpleUp, justEditMP, err := updatePrefs(newPrefs, tt.curPrefs, tt.env)
|
||||
if err != nil {
|
||||
if tt.wantErrSubtr != "" {
|
||||
if !strings.Contains(err.Error(), tt.wantErrSubtr) {
|
||||
t.Fatalf("want error %q, got: %v", tt.wantErrSubtr, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
if simpleUp != tt.wantSimpleUp {
|
||||
t.Fatalf("simpleUp=%v, want %v", simpleUp, tt.wantSimpleUp)
|
||||
}
|
||||
if justEditMP != nil {
|
||||
justEditMP.Prefs = ipn.Prefs{} // uninteresting
|
||||
}
|
||||
if !reflect.DeepEqual(justEditMP, tt.wantJustEditMP) {
|
||||
t.Fatalf("justEditMP: %v", cmp.Diff(justEditMP, tt.wantJustEditMP))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
56
cmd/tailscale/cli/diag.go
Normal file
56
cmd/tailscale/cli/diag.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// 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.
|
||||
|
||||
//go:build linux || windows || darwin
|
||||
// +build linux windows darwin
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
ps "github.com/mitchellh/go-ps"
|
||||
)
|
||||
|
||||
// fixTailscaledConnectError is called when the local tailscaled has
|
||||
// been determined unreachable due to the provided origErr value. It
|
||||
// returns either the same error or a better one to help the user
|
||||
// understand why tailscaled isn't running for their platform.
|
||||
func fixTailscaledConnectError(origErr error) error {
|
||||
procs, err := ps.Processes()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to local Tailscaled process and failed to enumerate processes while looking for it")
|
||||
}
|
||||
found := false
|
||||
for _, proc := range procs {
|
||||
base := filepath.Base(proc.Executable())
|
||||
if base == "tailscaled" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
if runtime.GOOS == "darwin" && base == "IPNExtension" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
if runtime.GOOS == "windows" && strings.EqualFold(base, "tailscaled.exe") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return fmt.Errorf("failed to connect to local tailscaled process; is the Tailscale service running?")
|
||||
case "darwin":
|
||||
return fmt.Errorf("failed to connect to local Tailscale service; is Tailscale running?")
|
||||
case "linux":
|
||||
return fmt.Errorf("failed to connect to local tailscaled; it doesn't appear to be running (sudo systemctl start tailscaled ?)")
|
||||
}
|
||||
return fmt.Errorf("failed to connect to local tailscaled process; it doesn't appear to be running")
|
||||
}
|
||||
return fmt.Errorf("failed to connect to local tailscaled (which appears to be running). Got error: %w", origErr)
|
||||
}
|
||||
17
cmd/tailscale/cli/diag_other.go
Normal file
17
cmd/tailscale/cli/diag_other.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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.
|
||||
|
||||
//go:build !linux && !windows && !darwin
|
||||
// +build !linux,!windows,!darwin
|
||||
|
||||
package cli
|
||||
|
||||
import "fmt"
|
||||
|
||||
// The github.com/mitchellh/go-ps package doesn't work on all platforms,
|
||||
// so just don't diagnose connect failures.
|
||||
|
||||
func fixTailscaledConnectError(origErr error) error {
|
||||
return fmt.Errorf("failed to connect to local tailscaled process (is it running?); got: %w", origErr)
|
||||
}
|
||||
@@ -50,7 +50,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: ")),
|
||||
PortMapper: portmapper.NewClient(logger.WithPrefix(log.Printf, "portmap: "), nil),
|
||||
}
|
||||
if netcheckArgs.verbose {
|
||||
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"github.com/toqueteos/webbrowser"
|
||||
@@ -57,12 +56,12 @@ var statusArgs struct {
|
||||
func runStatus(ctx context.Context, args []string) error {
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
if statusArgs.json {
|
||||
if statusArgs.active {
|
||||
for peer, ps := range st.Peer {
|
||||
if !peerActive(ps) {
|
||||
if !ps.Active {
|
||||
delete(st.Peer, peer)
|
||||
}
|
||||
}
|
||||
@@ -130,7 +129,6 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
var buf bytes.Buffer
|
||||
f := func(format string, a ...interface{}) { fmt.Fprintf(&buf, format, a...) }
|
||||
printPS := func(ps *ipnstate.PeerStatus) {
|
||||
active := peerActive(ps)
|
||||
f("%-15s %-20s %-12s %-7s ",
|
||||
firstIPString(ps.TailscaleIPs),
|
||||
dnsOrQuoteHostname(st, ps),
|
||||
@@ -139,7 +137,7 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
)
|
||||
relay := ps.Relay
|
||||
anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
|
||||
if !active {
|
||||
if !ps.Active {
|
||||
if ps.ExitNode {
|
||||
f("idle; exit node")
|
||||
} else if anyTraffic {
|
||||
@@ -178,8 +176,7 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
}
|
||||
ipnstate.SortPeers(peers)
|
||||
for _, ps := range peers {
|
||||
active := peerActive(ps)
|
||||
if statusArgs.active && !active {
|
||||
if statusArgs.active && !ps.Active {
|
||||
continue
|
||||
}
|
||||
printPS(ps)
|
||||
@@ -189,13 +186,6 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// peerActive reports whether ps has recent activity.
|
||||
//
|
||||
// TODO: have the server report this bool instead.
|
||||
func peerActive(ps *ipnstate.PeerStatus) bool {
|
||||
return !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
|
||||
}
|
||||
|
||||
func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
|
||||
if baseName != "" {
|
||||
|
||||
@@ -51,7 +51,14 @@ flag is also used.
|
||||
Exec: runUp,
|
||||
}
|
||||
|
||||
var upFlagSet = newUpFlagSet(runtime.GOOS, &upArgs)
|
||||
func effectiveGOOS() string {
|
||||
if v := os.Getenv("TS_DEBUG_UP_FLAG_GOOS"); v != "" {
|
||||
return v
|
||||
}
|
||||
return runtime.GOOS
|
||||
}
|
||||
|
||||
var upFlagSet = newUpFlagSet(effectiveGOOS(), &upArgs)
|
||||
|
||||
func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
||||
upf := flag.NewFlagSet("up", flag.ExitOnError)
|
||||
@@ -63,13 +70,13 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
||||
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
|
||||
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "install host routes to other Tailscale nodes")
|
||||
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale IP of the exit node for internet traffic")
|
||||
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale IP of the exit node for internet traffic, or empty string to not use an exit node")
|
||||
upf.BoolVar(&upArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
|
||||
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
||||
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "comma-separated ACL tags to request; each must start with \"tag:\" (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")
|
||||
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.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\") or empty string to not advertise routes")
|
||||
upf.BoolVar(&upArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
|
||||
if safesocket.GOOSUsesPeerCreds(goos) {
|
||||
upf.StringVar(&upArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
|
||||
@@ -240,6 +247,53 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
return prefs, nil
|
||||
}
|
||||
|
||||
// updatePrefs updates prefs based on curPrefs
|
||||
//
|
||||
// It returns a non-nil justEditMP if we're already running and none of
|
||||
// the flags require a restart, so we can just do an EditPrefs call and
|
||||
// change the prefs at runtime (e.g. changing hostname, changing
|
||||
// advertised tags, routes, etc).
|
||||
//
|
||||
// It returns simpleUp if we're running a simple "tailscale up" to
|
||||
// transition to running from a previously-logged-in but down state,
|
||||
// without changing any settings.
|
||||
func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, justEditMP *ipn.MaskedPrefs, err error) {
|
||||
if !env.upArgs.reset {
|
||||
applyImplicitPrefs(prefs, curPrefs, env.user)
|
||||
|
||||
if err := checkForAccidentalSettingReverts(prefs, curPrefs, env); err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
controlURLChanged := curPrefs.ControlURL != prefs.ControlURL &&
|
||||
!(ipn.IsLoginServerSynonym(curPrefs.ControlURL) && ipn.IsLoginServerSynonym(prefs.ControlURL))
|
||||
if controlURLChanged && env.backendState == ipn.Running.String() && !env.upArgs.forceReauth {
|
||||
return false, nil, fmt.Errorf("can't change --login-server without --force-reauth")
|
||||
}
|
||||
|
||||
simpleUp = env.flagSet.NFlag() == 0 &&
|
||||
curPrefs.Persist != nil &&
|
||||
curPrefs.Persist.LoginName != "" &&
|
||||
env.backendState != ipn.NeedsLogin.String()
|
||||
|
||||
justEdit := env.backendState == ipn.Running.String() &&
|
||||
!env.upArgs.forceReauth &&
|
||||
!env.upArgs.reset &&
|
||||
env.upArgs.authKey == "" &&
|
||||
!controlURLChanged
|
||||
if justEdit {
|
||||
justEditMP = new(ipn.MaskedPrefs)
|
||||
justEditMP.WantRunningSet = true
|
||||
justEditMP.Prefs = *prefs
|
||||
env.flagSet.Visit(func(f *flag.Flag) {
|
||||
updateMaskedPrefsFromUpFlag(justEditMP, f.Name)
|
||||
})
|
||||
}
|
||||
|
||||
return simpleUp, justEditMP, nil
|
||||
}
|
||||
|
||||
func runUp(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
fatalf("too many non-flag arguments: %q", args)
|
||||
@@ -247,7 +301,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
fatalf("can't fetch status from tailscaled: %v", err)
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
origAuthURL := st.AuthURL
|
||||
|
||||
@@ -280,7 +334,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
prefs, err := prefsFromUpArgs(upArgs, warnf, st, runtime.GOOS)
|
||||
prefs, err := prefsFromUpArgs(upArgs, warnf, st, effectiveGOOS())
|
||||
if err != nil {
|
||||
fatalf("%s", err)
|
||||
}
|
||||
@@ -296,51 +350,23 @@ func runUp(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if !upArgs.reset {
|
||||
applyImplicitPrefs(prefs, curPrefs, os.Getenv("USER"))
|
||||
|
||||
if err := checkForAccidentalSettingReverts(upFlagSet, curPrefs, prefs, upCheckEnv{
|
||||
goos: runtime.GOOS,
|
||||
curExitNodeIP: exitNodeIP(prefs, st),
|
||||
}); err != nil {
|
||||
fatalf("%s", err)
|
||||
}
|
||||
env := upCheckEnv{
|
||||
goos: effectiveGOOS(),
|
||||
user: os.Getenv("USER"),
|
||||
flagSet: upFlagSet,
|
||||
upArgs: upArgs,
|
||||
backendState: st.BackendState,
|
||||
curExitNodeIP: exitNodeIP(prefs, st),
|
||||
}
|
||||
|
||||
controlURLChanged := curPrefs.ControlURL != prefs.ControlURL
|
||||
if controlURLChanged && st.BackendState == ipn.Running.String() && !upArgs.forceReauth {
|
||||
fatalf("can't change --login-server without --force-reauth")
|
||||
simpleUp, justEditMP, err := updatePrefs(prefs, curPrefs, env)
|
||||
if err != nil {
|
||||
fatalf("%s", err)
|
||||
}
|
||||
|
||||
// If we're already running and none of the flags require a
|
||||
// restart, we can just do an EditPrefs call and change the
|
||||
// prefs at runtime (e.g. changing hostname, changing
|
||||
// advertised tags, routes, etc)
|
||||
justEdit := st.BackendState == ipn.Running.String() &&
|
||||
!upArgs.forceReauth &&
|
||||
!upArgs.reset &&
|
||||
upArgs.authKey == "" &&
|
||||
!controlURLChanged
|
||||
if justEdit {
|
||||
mp := new(ipn.MaskedPrefs)
|
||||
mp.WantRunningSet = true
|
||||
mp.Prefs = *prefs
|
||||
upFlagSet.Visit(func(f *flag.Flag) {
|
||||
updateMaskedPrefsFromUpFlag(mp, f.Name)
|
||||
})
|
||||
|
||||
_, err := tailscale.EditPrefs(ctx, mp)
|
||||
if justEditMP != nil {
|
||||
_, err := tailscale.EditPrefs(ctx, justEditMP)
|
||||
return err
|
||||
}
|
||||
|
||||
// simpleUp is whether we're running a simple "tailscale up"
|
||||
// to transition to running from a previously-logged-in but
|
||||
// down state, without changing any settings.
|
||||
simpleUp := upFlagSet.NFlag() == 0 &&
|
||||
curPrefs.Persist != nil &&
|
||||
curPrefs.Persist.LoginName != "" &&
|
||||
st.BackendState != ipn.NeedsLogin.String()
|
||||
|
||||
// At this point we need to subscribe to the IPN bus to watch
|
||||
// for state transitions and possible need to authenticate.
|
||||
c, bc, pumpCtx, cancel := connect(ctx)
|
||||
@@ -365,7 +391,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
if n.ErrMessage != nil {
|
||||
msg := *n.ErrMessage
|
||||
if msg == ipn.ErrMsgPermissionDenied {
|
||||
switch runtime.GOOS {
|
||||
switch effectiveGOOS() {
|
||||
case "windows":
|
||||
msg += " (Tailscale service in use by other user?)"
|
||||
default:
|
||||
@@ -381,7 +407,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
startLoginInteractive()
|
||||
case ipn.NeedsMachineAuth:
|
||||
printed = true
|
||||
fmt.Fprintf(os.Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s/admin/machines\n\n", upArgs.server)
|
||||
fmt.Fprintf(os.Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
|
||||
case ipn.Starting, ipn.Running:
|
||||
// Done full authentication process
|
||||
if printed {
|
||||
@@ -439,7 +465,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
// Windows service (~tailscaled) is the one that computes the
|
||||
// StateKey based on the connection identity. So for now, just
|
||||
// do as the Windows GUI's always done:
|
||||
if runtime.GOOS == "windows" {
|
||||
if effectiveGOOS() == "windows" {
|
||||
// The Windows service will set this as needed based
|
||||
// on our connection's identity.
|
||||
opts.StateKey = ""
|
||||
@@ -452,6 +478,13 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
// This whole 'up' mechanism is too complicated and results in
|
||||
// hairy stuff like this select. We're ultimately waiting for
|
||||
// 'startingOrRunning' to be done, but even in the case where
|
||||
// it succeeds, other parts may shut down concurrently so we
|
||||
// need to prioritize reads from 'startingOrRunning' if it's
|
||||
// readable; its send does happen before the pump mechanism
|
||||
// shuts down. (Issue 2333)
|
||||
select {
|
||||
case <-startingOrRunning:
|
||||
return nil
|
||||
@@ -463,6 +496,11 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
return pumpCtx.Err()
|
||||
case err := <-pumpErr:
|
||||
select {
|
||||
case <-startingOrRunning:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -538,6 +576,10 @@ const accidentalUpPrefix = "Error: changing settings via 'tailscale up' requires
|
||||
// needed by checkForAccidentalSettingReverts and friends.
|
||||
type upCheckEnv struct {
|
||||
goos string
|
||||
user string
|
||||
flagSet *flag.FlagSet
|
||||
upArgs upArgsT
|
||||
backendState string
|
||||
curExitNodeIP netaddr.IP
|
||||
}
|
||||
|
||||
@@ -555,14 +597,14 @@ type upCheckEnv struct {
|
||||
//
|
||||
// mp is the mask of settings actually set, where mp.Prefs is the new
|
||||
// preferences to set, including any values set from implicit flags.
|
||||
func checkForAccidentalSettingReverts(flagSet *flag.FlagSet, curPrefs, newPrefs *ipn.Prefs, env upCheckEnv) error {
|
||||
func checkForAccidentalSettingReverts(newPrefs, curPrefs *ipn.Prefs, env upCheckEnv) error {
|
||||
if curPrefs.ControlURL == "" {
|
||||
// Don't validate things on initial "up" before a control URL has been set.
|
||||
return nil
|
||||
}
|
||||
|
||||
flagIsSet := map[string]bool{}
|
||||
flagSet.Visit(func(f *flag.Flag) {
|
||||
env.flagSet.Visit(func(f *flag.Flag) {
|
||||
flagIsSet[f.Name] = true
|
||||
})
|
||||
|
||||
@@ -586,7 +628,7 @@ func checkForAccidentalSettingReverts(flagSet *flag.FlagSet, curPrefs, newPrefs
|
||||
if reflect.DeepEqual(valCur, valNew) {
|
||||
continue
|
||||
}
|
||||
if flagName == "login-server" && isLoginServerSynonym(valCur) && isLoginServerSynonym(valNew) {
|
||||
if flagName == "login-server" && ipn.IsLoginServerSynonym(valCur) && ipn.IsLoginServerSynonym(valNew) {
|
||||
continue
|
||||
}
|
||||
missing = append(missing, fmtFlagValueArg(flagName, valCur))
|
||||
@@ -599,7 +641,7 @@ func checkForAccidentalSettingReverts(flagSet *flag.FlagSet, curPrefs, newPrefs
|
||||
// Compute the stringification of the explicitly provided args in flagSet
|
||||
// to prepend to the command to run.
|
||||
var explicit []string
|
||||
flagSet.Visit(func(f *flag.Flag) {
|
||||
env.flagSet.Visit(func(f *flag.Flag) {
|
||||
type isBool interface {
|
||||
IsBoolFlag() bool
|
||||
}
|
||||
@@ -635,10 +677,6 @@ func applyImplicitPrefs(prefs, oldPrefs *ipn.Prefs, curUser string) {
|
||||
}
|
||||
}
|
||||
|
||||
func isLoginServerSynonym(val interface{}) bool {
|
||||
return val == "https://login.tailscale.com" || val == "https://controlplane.tailscale.com"
|
||||
}
|
||||
|
||||
func flagAppliesToOS(flag, goos string) bool {
|
||||
switch flag {
|
||||
case "netfilter-mode", "snat-subnet-routes":
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/cgi"
|
||||
"net/url"
|
||||
@@ -94,9 +95,20 @@ func runWeb(ctx context.Context, args []string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("web server running on: %s", urlOfListenAddr(webArgs.listen))
|
||||
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(webHandler))
|
||||
}
|
||||
|
||||
// urlOfListenAddr parses a given listen address into a formatted URL
|
||||
func urlOfListenAddr(addr string) string {
|
||||
host, port, _ := net.SplitHostPort(addr)
|
||||
if host == "" {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
return fmt.Sprintf("http://%s", net.JoinHostPort(host, port))
|
||||
}
|
||||
|
||||
// authorize returns the name of the user accessing the web UI after verifying
|
||||
// whether the user has access to the web UI. The function will write the
|
||||
// error to the provided http.ResponseWriter.
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<div class="flex items-center justify-end space-x-2 w-2/3">
|
||||
{{ with .Profile.LoginName }}
|
||||
<div class="text-right truncate leading-4">
|
||||
<h4 class="truncate">{{.}}</h4>
|
||||
<h4 class="truncate leading-normal">{{.}}</h4>
|
||||
<a href="#" class="text-xs text-gray-500 hover:text-gray-700 js-loginButton">Switch account</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
44
cmd/tailscale/cli/web_test.go
Normal file
44
cmd/tailscale/cli/web_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// 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 "testing"
|
||||
|
||||
func TestUrlOfListenAddr(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in, want string
|
||||
}{
|
||||
{
|
||||
name: "TestLocalhost",
|
||||
in: "localhost:8088",
|
||||
want: "http://localhost:8088",
|
||||
},
|
||||
{
|
||||
name: "TestNoHost",
|
||||
in: ":8088",
|
||||
want: "http://127.0.0.1:8088",
|
||||
},
|
||||
{
|
||||
name: "TestExplicitHost",
|
||||
in: "127.0.0.2:8088",
|
||||
want: "http://127.0.0.2:8088",
|
||||
},
|
||||
{
|
||||
name: "TestIPv6",
|
||||
in: "[::1]:8088",
|
||||
want: "http://[::1]:8088",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
url := urlOfListenAddr(tt.in)
|
||||
if url != tt.want {
|
||||
t.Errorf("expected url: %q, got: %q", tt.want, url)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,15 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli
|
||||
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/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
|
||||
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
|
||||
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
|
||||
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
|
||||
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
|
||||
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
|
||||
💣 go4.org/intern from inet.af/netaddr
|
||||
@@ -13,11 +20,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
|
||||
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||
tailscale.com/control/controlknobs from tailscale.com/net/portmapper
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
|
||||
tailscale.com/disco from tailscale.com/derp
|
||||
@@ -38,15 +45,19 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
💣 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/syncs from tailscale.com/net/interfaces+
|
||||
💣 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/tstime/mono from tailscale.com/tstime/rate
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
|
||||
tailscale.com/types/dnstype from tailscale.com/tailcfg
|
||||
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
|
||||
tailscale.com/types/opt from tailscale.com/net/netcheck+
|
||||
tailscale.com/types/pad32 from tailscale.com/derp
|
||||
tailscale.com/types/persist from tailscale.com/ipn
|
||||
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/structs from tailscale.com/ipn+
|
||||
@@ -76,7 +87,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
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/sync/errgroup from tailscale.com/derp
|
||||
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+
|
||||
@@ -89,9 +100,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/time/rate from tailscale.com/cmd/tailscale/cli+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
compress/flate from compress/gzip
|
||||
compress/gzip from net/http
|
||||
compress/zlib from debug/elf+
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdsa+
|
||||
@@ -114,10 +124,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
crypto/tls from github.com/tcnksm/go-httpstat+
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509+
|
||||
debug/dwarf from debug/elf+
|
||||
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+
|
||||
@@ -126,13 +132,12 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
encoding/hex from crypto/x509+
|
||||
encoding/json from expvar+
|
||||
encoding/pem from crypto/tls+
|
||||
encoding/xml from tailscale.com/cmd/tailscale/cli
|
||||
encoding/xml from tailscale.com/cmd/tailscale/cli+
|
||||
errors from bufio+
|
||||
expvar from tailscale.com/derp+
|
||||
flag from github.com/peterbourgon/ff/v2+
|
||||
fmt from compress/flate+
|
||||
hash from compress/zlib+
|
||||
hash/adler32 from compress/zlib
|
||||
hash from crypto+
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/maphash from go4.org/mem
|
||||
html from tailscale.com/ipn/ipnstate+
|
||||
@@ -159,10 +164,10 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
os/exec from github.com/toqueteos/webbrowser+
|
||||
os/signal from tailscale.com/cmd/tailscale/cli
|
||||
os/user from tailscale.com/util/groupmember
|
||||
path from debug/dwarf+
|
||||
path from html/template+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from rsc.io/goversion/version+
|
||||
regexp from github.com/tailscale/goupnp/httpu+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from golang.org/x/sync/singleflight
|
||||
sort from compress/flate+
|
||||
|
||||
@@ -14,32 +14,41 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
var debugArgs struct {
|
||||
ifconfig bool // print network state once and exit
|
||||
monitor bool
|
||||
getURL string
|
||||
derpCheck string
|
||||
portmap bool
|
||||
}
|
||||
|
||||
var debugModeFunc = debugMode // so it can be addressable
|
||||
|
||||
func debugMode(args []string) error {
|
||||
fs := flag.NewFlagSet("debug", flag.ExitOnError)
|
||||
fs.BoolVar(&debugArgs.ifconfig, "ifconfig", false, "If true, print network interface state")
|
||||
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
|
||||
fs.BoolVar(&debugArgs.portmap, "portmap", false, "If true, run portmap debugging. Precludes all other options.")
|
||||
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
|
||||
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
@@ -52,8 +61,14 @@ func debugMode(args []string) error {
|
||||
if debugArgs.derpCheck != "" {
|
||||
return checkDerp(ctx, debugArgs.derpCheck)
|
||||
}
|
||||
if debugArgs.ifconfig {
|
||||
return runMonitor(ctx, false)
|
||||
}
|
||||
if debugArgs.monitor {
|
||||
return runMonitor(ctx)
|
||||
return runMonitor(ctx, true)
|
||||
}
|
||||
if debugArgs.portmap {
|
||||
return debugPortmap(ctx)
|
||||
}
|
||||
if debugArgs.getURL != "" {
|
||||
return getURL(ctx, debugArgs.getURL)
|
||||
@@ -61,7 +76,7 @@ func debugMode(args []string) error {
|
||||
return errors.New("only --monitor is available at the moment")
|
||||
}
|
||||
|
||||
func runMonitor(ctx context.Context) error {
|
||||
func runMonitor(ctx context.Context, loop bool) error {
|
||||
dump := func(st *interfaces.State) {
|
||||
j, _ := json.MarshalIndent(st, "", " ")
|
||||
os.Stderr.Write(j)
|
||||
@@ -78,8 +93,13 @@ func runMonitor(ctx context.Context) error {
|
||||
log.Printf("Link monitor fired. New state:")
|
||||
dump(st)
|
||||
})
|
||||
log.Printf("Starting link change monitor; initial state:")
|
||||
if loop {
|
||||
log.Printf("Starting link change monitor; initial state:")
|
||||
}
|
||||
dump(mon.InterfaceState())
|
||||
if !loop {
|
||||
return nil
|
||||
}
|
||||
mon.Start()
|
||||
log.Printf("Started link change monitor; waiting...")
|
||||
select {}
|
||||
@@ -191,3 +211,97 @@ func checkDerp(ctx context.Context, derpRegion string) error {
|
||||
log.Printf("ok")
|
||||
return err
|
||||
}
|
||||
|
||||
func debugPortmap(ctx context.Context) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
portmapper.VerboseLogs = true
|
||||
switch os.Getenv("TS_DEBUG_PORTMAP_TYPE") {
|
||||
case "":
|
||||
case "pmp":
|
||||
portmapper.DisablePCP = true
|
||||
portmapper.DisableUPnP = true
|
||||
case "pcp":
|
||||
portmapper.DisablePMP = true
|
||||
portmapper.DisableUPnP = true
|
||||
case "upnp":
|
||||
portmapper.DisablePCP = true
|
||||
portmapper.DisablePMP = true
|
||||
default:
|
||||
log.Fatalf("TS_DEBUG_PORTMAP_TYPE must be one of pmp,pcp,upnp")
|
||||
}
|
||||
|
||||
done := make(chan bool, 1)
|
||||
|
||||
var c *portmapper.Client
|
||||
logf := log.Printf
|
||||
c = portmapper.NewClient(logger.WithPrefix(logf, "portmapper: "), func() {
|
||||
logf("portmapping changed.")
|
||||
logf("have mapping: %v", c.HaveMapping())
|
||||
|
||||
if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
|
||||
logf("cb: mapping: %v", ext)
|
||||
select {
|
||||
case done <- true:
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
logf("cb: no mapping")
|
||||
})
|
||||
linkMon, err := monitor.New(logger.WithPrefix(logf, "monitor: "))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gatewayAndSelfIP := func() (gw, self netaddr.IP, ok bool) {
|
||||
if v := os.Getenv("TS_DEBUG_GW_SELF"); strings.Contains(v, "/") {
|
||||
i := strings.Index(v, "/")
|
||||
gw = netaddr.MustParseIP(v[:i])
|
||||
self = netaddr.MustParseIP(v[i+1:])
|
||||
return gw, self, true
|
||||
}
|
||||
return linkMon.GatewayAndSelfIP()
|
||||
}
|
||||
|
||||
c.SetGatewayLookupFunc(gatewayAndSelfIP)
|
||||
|
||||
gw, selfIP, ok := gatewayAndSelfIP()
|
||||
if !ok {
|
||||
logf("no gateway or self IP; %v", linkMon.InterfaceState())
|
||||
return nil
|
||||
}
|
||||
logf("gw=%v; self=%v", gw, selfIP)
|
||||
|
||||
uc, err := net.ListenPacket("udp", "0.0.0.0:0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer uc.Close()
|
||||
c.SetLocalPort(uint16(uc.LocalAddr().(*net.UDPAddr).Port))
|
||||
|
||||
res, err := c.Probe(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Probe: %v", err)
|
||||
}
|
||||
logf("Probe: %+v", res)
|
||||
|
||||
if !res.PCP && !res.PMP && !res.UPnP {
|
||||
logf("no portmapping services available")
|
||||
return nil
|
||||
}
|
||||
|
||||
if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
|
||||
logf("mapping: %v", ext)
|
||||
} else {
|
||||
logf("no mapping")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
|
||||
github.com/golang/snappy from github.com/klauspost/compress/zstd
|
||||
github.com/google/btree from inet.af/netstack/tcpip/header+
|
||||
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
|
||||
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/insomniacslk/dhcp/interfaces from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/insomniacslk/dhcp/rfc1035label from github.com/insomniacslk/dhcp/dhcpv4
|
||||
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
|
||||
@@ -23,7 +27,16 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
W github.com/pkg/errors from github.com/tailscale/certstore
|
||||
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
|
||||
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
|
||||
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
|
||||
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
|
||||
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
|
||||
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
|
||||
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
L github.com/u-root/uio/rand from github.com/insomniacslk/dhcp/dhcpv4
|
||||
L github.com/u-root/uio/ubinary from github.com/u-root/uio/uio
|
||||
L github.com/u-root/uio/uio from github.com/insomniacslk/dhcp/dhcpv4+
|
||||
💣 go4.org/intern from inet.af/netaddr
|
||||
💣 go4.org/mem from tailscale.com/derp+
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
@@ -60,7 +73,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
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/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+
|
||||
@@ -74,11 +87,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
inet.af/netstack/waiter from inet.af/netstack/tcpip+
|
||||
inet.af/peercred from tailscale.com/ipn/ipnserver
|
||||
W 💣 inet.af/wf from tailscale.com/wf
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/tailscale from tailscale.com/derp
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/control/controlknobs from tailscale.com/control/controlclient+
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp+
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
|
||||
tailscale.com/disco from tailscale.com/derp+
|
||||
@@ -119,10 +132,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
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/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/control/controlclient+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/wgengine/magicsock
|
||||
💣 tailscale.com/tstime/mono from tailscale.com/net/tstun+
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
|
||||
tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+
|
||||
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+
|
||||
@@ -131,16 +147,17 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
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/net/tstun+
|
||||
tailscale.com/types/persist from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/preftype from tailscale.com/ipn+
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
|
||||
L tailscale.com/util/cmpver from tailscale.com/net/dns
|
||||
tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
|
||||
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/dnsname from tailscale.com/ipn/ipnstate+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
||||
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
@@ -148,7 +165,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock
|
||||
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/version/distro from tailscale.com/cmd/tailscaled+
|
||||
W tailscale.com/wf from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
@@ -160,6 +177,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/wgengine/wglog from tailscale.com/wgengine
|
||||
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
|
||||
golang.org/x/crypto/acme from tailscale.com/ipn/localapi
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from golang.zx2c4.com/wireguard/device+
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
@@ -182,7 +200,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/net/ipv6 from golang.zx2c4.com/wireguard/device+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp
|
||||
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 github.com/mdlayher/netlink+
|
||||
@@ -198,9 +216,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/time/rate from inet.af/netstack/tcpip/stack+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
compress/flate from compress/gzip
|
||||
compress/gzip from internal/profile+
|
||||
compress/zlib from debug/elf+
|
||||
container/heap from inet.af/netstack/tcpip/transport/tcp
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
@@ -224,10 +241,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
crypto/tls from github.com/tcnksm/go-httpstat+
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509+
|
||||
debug/dwarf from debug/elf+
|
||||
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/net/dns+
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
@@ -236,12 +249,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
encoding/hex from crypto/x509+
|
||||
encoding/json from expvar+
|
||||
encoding/pem from crypto/tls+
|
||||
encoding/xml from github.com/tailscale/goupnp+
|
||||
errors from bufio+
|
||||
expvar from tailscale.com/derp+
|
||||
flag from tailscale.com/cmd/tailscaled+
|
||||
fmt from compress/flate+
|
||||
hash from compress/zlib+
|
||||
hash/adler32 from compress/zlib
|
||||
hash from crypto+
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from tailscale.com/wgengine/magicsock+
|
||||
hash/maphash from go4.org/mem
|
||||
@@ -269,7 +282,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
os/exec from github.com/coreos/go-iptables/iptables+
|
||||
os/signal from tailscale.com/cmd/tailscaled+
|
||||
os/user from github.com/godbus/dbus/v5+
|
||||
path from debug/dwarf+
|
||||
path from github.com/godbus/dbus/v5+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from github.com/coreos/go-iptables/iptables+
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
@@ -28,11 +29,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-multierror/multierror"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/socks5/tssocks"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/types/flagtype"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -45,15 +46,6 @@ import (
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
// globalStateKey is the ipn.StateKey that tailscaled loads on
|
||||
// startup.
|
||||
//
|
||||
// We have to support multiple state keys for other OSes (Windows in
|
||||
// particular), but right now Unix daemons run with a single
|
||||
// node-global state. To keep open the option of having per-user state
|
||||
// later, the global state key doesn't look like a username.
|
||||
const globalStateKey = "_daemon"
|
||||
|
||||
// defaultTunName returns the default tun device name for the platform.
|
||||
func defaultTunName() string {
|
||||
switch runtime.GOOS {
|
||||
@@ -76,9 +68,13 @@ func defaultTunName() string {
|
||||
}
|
||||
|
||||
var args struct {
|
||||
// tunname is a /dev/net/tun tunnel name ("tailscale0"), the
|
||||
// string "userspace-networking", "tap:TAPNAME[:BRIDGENAME]"
|
||||
// or comma-separated list thereof.
|
||||
tunname string
|
||||
|
||||
cleanup bool
|
||||
debug string
|
||||
tunname string // tun name, "userspace-networking", or comma-separated list thereof
|
||||
port uint16
|
||||
statepath string
|
||||
socketpath string
|
||||
@@ -146,7 +142,7 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if runtime.GOOS == "darwin" && os.Getuid() != 0 && !strings.Contains(args.tunname, "userspace-networking") {
|
||||
if runtime.GOOS == "darwin" && os.Getuid() != 0 && !strings.Contains(args.tunname, "userspace-networking") && !args.cleanup {
|
||||
log.SetFlags(0)
|
||||
log.Fatalf("tailscaled requires root; use sudo tailscaled (or use --tun=userspace-networking)")
|
||||
}
|
||||
@@ -167,6 +163,56 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func trySynologyMigration(p string) error {
|
||||
if runtime.GOOS != "linux" || distro.Get() != distro.Synology {
|
||||
return nil
|
||||
}
|
||||
|
||||
fi, err := os.Stat(p)
|
||||
if err == nil && fi.Size() > 0 || !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
// File is empty or doesn't exist, try reading from the old path.
|
||||
|
||||
const oldPath = "/var/packages/Tailscale/etc/tailscaled.state"
|
||||
if _, err := os.Stat(oldPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.Chown(oldPath, os.Getuid(), os.Getgid()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(oldPath, p); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ipnServerOpts() (o ipnserver.Options) {
|
||||
// Allow changing the OS-specific IPN behavior for tests
|
||||
// so we can e.g. test Windows-specific behaviors on Linux.
|
||||
goos := os.Getenv("TS_DEBUG_TAILSCALED_IPN_GOOS")
|
||||
if goos == "" {
|
||||
goos = runtime.GOOS
|
||||
}
|
||||
|
||||
o.Port = 41112
|
||||
o.StatePath = args.statepath
|
||||
o.SocketPath = args.socketpath // even for goos=="windows", for tests
|
||||
|
||||
switch goos {
|
||||
default:
|
||||
o.SurviveDisconnects = true
|
||||
o.AutostartStateKey = ipn.GlobalDaemonStateKey
|
||||
case "windows":
|
||||
// Not those.
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func run() error {
|
||||
var err error
|
||||
|
||||
@@ -196,6 +242,9 @@ func run() error {
|
||||
logf = logger.RateLimitedFn(logf, 5*time.Second, 5, 100)
|
||||
|
||||
if args.cleanup {
|
||||
if os.Getenv("TS_PLEASE_PANIC") != "" {
|
||||
panic("TS_PLEASE_PANIC asked us to panic")
|
||||
}
|
||||
dns.Cleanup(logf, args.tunname)
|
||||
router.Cleanup(logf, args.tunname)
|
||||
return nil
|
||||
@@ -204,6 +253,9 @@ func run() error {
|
||||
if args.statepath == "" {
|
||||
log.Fatalf("--state is required")
|
||||
}
|
||||
if err := trySynologyMigration(args.statepath); err != nil {
|
||||
log.Printf("error in synology migration: %v", err)
|
||||
}
|
||||
|
||||
var debugMux *http.ServeMux
|
||||
if args.debug != "" {
|
||||
@@ -271,14 +323,8 @@ func run() error {
|
||||
}
|
||||
}()
|
||||
|
||||
opts := ipnserver.Options{
|
||||
SocketPath: args.socketpath,
|
||||
Port: 41112,
|
||||
StatePath: args.statepath,
|
||||
AutostartStateKey: globalStateKey,
|
||||
SurviveDisconnects: runtime.GOOS != "windows",
|
||||
DebugMux: debugMux,
|
||||
}
|
||||
opts := ipnServerOpts()
|
||||
opts.DebugMux = debugMux
|
||||
err = ipnserver.Run(ctx, logf, pol.PublicID.String(), ipnserver.FixedEngine(e), opts)
|
||||
// Cancelation is not an error: it is the only way to stop ipnserver.
|
||||
if err != nil && err != context.Canceled {
|
||||
@@ -320,9 +366,9 @@ func shouldWrapNetstack() bool {
|
||||
return true
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows", "darwin":
|
||||
case "windows", "darwin", "freebsd":
|
||||
// Enable on Windows and tailscaled-on-macOS (this doesn't
|
||||
// affect the GUI clients).
|
||||
// affect the GUI clients), and on FreeBSD.
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -335,18 +381,33 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
|
||||
}
|
||||
useNetstack = name == "userspace-networking"
|
||||
if !useNetstack {
|
||||
dev, devName, err := tstun.New(logf, name)
|
||||
// dev, devName, err := tstun.New(logf, name)
|
||||
// if err != nil {
|
||||
// tstun.Diagnose(logf, name)
|
||||
// return nil, false, err
|
||||
// }
|
||||
// conf.Tun = dev
|
||||
// if strings.HasPrefix(name, "tap:") {
|
||||
// conf.IsTAP = true
|
||||
// e, err := wgengine.NewUserspaceEngine(logf, conf)
|
||||
// return e, false, err
|
||||
// }
|
||||
|
||||
// HACK
|
||||
exec.Command("ip", "link", "del", "tailscale0").Run()
|
||||
if err := exec.Command("ip", "link", "add", "tailscale0", "type", "wireguard").Run(); err != nil {
|
||||
return nil, false, fmt.Errorf("create device: %v", err)
|
||||
}
|
||||
if err := exec.Command("ip", "link", "set", "tailscale0", "up").Run(); err != nil {
|
||||
return nil, false, fmt.Errorf("create device: %v", err)
|
||||
}
|
||||
|
||||
r, err := router.New(logf, nil, linkMon)
|
||||
if err != nil {
|
||||
tstun.Diagnose(logf, name)
|
||||
//dev.Close()
|
||||
return nil, false, err
|
||||
}
|
||||
conf.Tun = dev
|
||||
r, err := router.New(logf, dev)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
return nil, false, err
|
||||
}
|
||||
d, err := dns.NewOSConfigurator(logf, devName)
|
||||
d, err := dns.NewOSConfigurator(logf, "tailscale0")
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@@ -356,7 +417,7 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
|
||||
conf.Router = netstack.NewSubnetRouterWrapper(conf.Router)
|
||||
}
|
||||
}
|
||||
e, err = wgengine.NewUserspaceEngine(logf, conf)
|
||||
e, err = wgengine.NewKernelEngine(logf, conf)
|
||||
if err != nil {
|
||||
return nil, useNetstack, err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
#!/sbin/openrc-run
|
||||
|
||||
set -a
|
||||
source /etc/default/tailscaled
|
||||
set +a
|
||||
|
||||
command="/usr/sbin/tailscaled"
|
||||
command_args="--state=/var/lib/tailscale/tailscaled.state --port=$PORT --socket=/var/run/tailscale/tailscaled.sock $FLAGS"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package main // import "tailscale.com/cmd/tailscaled"
|
||||
|
||||
@@ -168,7 +168,7 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("TUN: %w", err)
|
||||
}
|
||||
r, err := router.New(logf, dev)
|
||||
r, err := router.New(logf, dev, nil)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
return nil, fmt.Errorf("router: %w", err)
|
||||
@@ -233,12 +233,6 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
}
|
||||
}()
|
||||
|
||||
opts := ipnserver.Options{
|
||||
Port: 41112,
|
||||
SurviveDisconnects: false,
|
||||
StatePath: args.statepath,
|
||||
}
|
||||
|
||||
// getEngine is called by ipnserver to get the engine. It's
|
||||
// not called concurrently and is not called again once it
|
||||
// successfully returns an engine.
|
||||
@@ -263,7 +257,7 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
return nil, fmt.Errorf("%w\n\nlogid: %v", res.Err, logid)
|
||||
}
|
||||
}
|
||||
err := ipnserver.Run(ctx, logf, logid, getEngine, opts)
|
||||
err := ipnserver.Run(ctx, logf, logid, getEngine, ipnServerOpts())
|
||||
if err != nil {
|
||||
logf("ipnserver.Run: %v", err)
|
||||
}
|
||||
|
||||
98
cmd/testcontrol/testcontrol.go
Normal file
98
cmd/testcontrol/testcontrol.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// 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.
|
||||
|
||||
// Program testcontrol runs a simple test control server.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"log"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tstest/integration"
|
||||
"tailscale.com/tstest/integration/testcontrol"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
flagNFake = flag.Int("nfake", 0, "number of fake nodes to add to network")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var t fakeTB
|
||||
derpMap := integration.RunDERPAndSTUN(t, logger.Discard, "127.0.0.1")
|
||||
|
||||
control := &testcontrol.Server{
|
||||
DERPMap: derpMap,
|
||||
ExplicitBaseURL: "http://127.0.0.1:9911",
|
||||
}
|
||||
for i := 0; i < *flagNFake; i++ {
|
||||
control.AddFakeNode()
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", control)
|
||||
addr := "127.0.0.1:9911"
|
||||
log.Printf("listening on %s", addr)
|
||||
err := http.ListenAndServe(addr, mux)
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
type fakeTB struct {
|
||||
*testing.T
|
||||
}
|
||||
|
||||
func (t fakeTB) Cleanup(_ func()) {}
|
||||
func (t fakeTB) Error(args ...interface{}) {
|
||||
t.Fatal(args...)
|
||||
}
|
||||
func (t fakeTB) Errorf(format string, args ...interface{}) {
|
||||
t.Fatalf(format, args...)
|
||||
}
|
||||
func (t fakeTB) Fail() {
|
||||
t.Fatal("failed")
|
||||
}
|
||||
func (t fakeTB) FailNow() {
|
||||
t.Fatal("failed")
|
||||
}
|
||||
func (t fakeTB) Failed() bool {
|
||||
return false
|
||||
}
|
||||
func (t fakeTB) Fatal(args ...interface{}) {
|
||||
log.Fatal(args...)
|
||||
}
|
||||
func (t fakeTB) Fatalf(format string, args ...interface{}) {
|
||||
log.Fatalf(format, args...)
|
||||
}
|
||||
func (t fakeTB) Helper() {}
|
||||
func (t fakeTB) Log(args ...interface{}) {
|
||||
log.Print(args...)
|
||||
}
|
||||
func (t fakeTB) Logf(format string, args ...interface{}) {
|
||||
log.Printf(format, args...)
|
||||
}
|
||||
func (t fakeTB) Name() string {
|
||||
return "faketest"
|
||||
}
|
||||
func (t fakeTB) Setenv(key string, value string) {
|
||||
panic("not implemented")
|
||||
}
|
||||
func (t fakeTB) Skip(args ...interface{}) {
|
||||
t.Fatal("skipped")
|
||||
}
|
||||
func (t fakeTB) SkipNow() {
|
||||
t.Fatal("skipnow")
|
||||
}
|
||||
func (t fakeTB) Skipf(format string, args ...interface{}) {
|
||||
t.Logf(format, args...)
|
||||
t.Fatal("skipped")
|
||||
}
|
||||
func (t fakeTB) Skipped() bool {
|
||||
return false
|
||||
}
|
||||
func (t fakeTB) TempDir() string {
|
||||
panic("not implemented")
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
// The tsshd binary is an SSH server that accepts connections
|
||||
@@ -29,8 +30,8 @@ import (
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/kr/pty"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
@@ -75,10 +75,3 @@ func TestStatusEqual(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestOSVersion(t *testing.T) {
|
||||
if osVersion == nil {
|
||||
t.Skip("not available for OS")
|
||||
}
|
||||
t.Logf("Got: %#q", osVersion())
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -42,28 +41,82 @@ func dumpGoroutinesToURL(c *http.Client, targetURL string) {
|
||||
}
|
||||
}
|
||||
|
||||
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)]
|
||||
var buf []byte
|
||||
// Grab stacks multiple times into increasingly larger buffer sizes
|
||||
// to minimize the risk that we blow past our iOS memory limit.
|
||||
for size := 1 << 10; size <= 1<<20; size += 1 << 10 {
|
||||
buf = make([]byte, size)
|
||||
buf = buf[:runtime.Stack(buf, true)]
|
||||
if len(buf) < size {
|
||||
// It fit.
|
||||
break
|
||||
}
|
||||
}
|
||||
return scrubHex(buf)
|
||||
}
|
||||
|
||||
func scrubHex(buf []byte) []byte {
|
||||
saw := map[string][]byte{} // "0x123" => "v1%3" (unique value 1 and its value mod 8)
|
||||
return reHexArgs.ReplaceAllFunc(buf, func(in []byte) []byte {
|
||||
|
||||
foreachHexAddress(buf, func(in []byte) {
|
||||
if string(in) == "0x0" {
|
||||
return in
|
||||
return
|
||||
}
|
||||
if v, ok := saw[string(in)]; ok {
|
||||
return v
|
||||
for i := range in {
|
||||
in[i] = '_'
|
||||
}
|
||||
copy(in, v)
|
||||
return
|
||||
}
|
||||
inStr := string(in)
|
||||
u64, err := strconv.ParseUint(string(in[2:]), 16, 64)
|
||||
for i := range in {
|
||||
in[i] = '_'
|
||||
}
|
||||
if err != nil {
|
||||
return []byte("??")
|
||||
in[0] = '?'
|
||||
return
|
||||
}
|
||||
v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8))
|
||||
saw[string(in)] = v
|
||||
return v
|
||||
saw[inStr] = v
|
||||
copy(in, v)
|
||||
})
|
||||
return buf
|
||||
}
|
||||
|
||||
var ohx = []byte("0x")
|
||||
|
||||
// foreachHexAddress calls f with each subslice of b that matches
|
||||
// regexp `0x[0-9a-f]*`.
|
||||
func foreachHexAddress(b []byte, f func([]byte)) {
|
||||
for len(b) > 0 {
|
||||
i := bytes.Index(b, ohx)
|
||||
if i == -1 {
|
||||
return
|
||||
}
|
||||
b = b[i:]
|
||||
hx := hexPrefix(b)
|
||||
f(hx)
|
||||
b = b[len(hx):]
|
||||
}
|
||||
}
|
||||
|
||||
func hexPrefix(b []byte) []byte {
|
||||
for i, c := range b {
|
||||
if i < 2 {
|
||||
continue
|
||||
}
|
||||
if !isHexByte(c) {
|
||||
return b[:i]
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func isHexByte(b byte) bool {
|
||||
return '0' <= b && b <= '9' || 'a' <= b && b <= 'f' || 'A' <= b && b <= 'F'
|
||||
}
|
||||
|
||||
@@ -9,3 +9,22 @@ import "testing"
|
||||
func TestScrubbedGoroutineDump(t *testing.T) {
|
||||
t.Logf("Got:\n%s\n", scrubbedGoroutineDump())
|
||||
}
|
||||
|
||||
func TestScrubHex(t *testing.T) {
|
||||
tests := []struct {
|
||||
in, want string
|
||||
}{
|
||||
{"foo", "foo"},
|
||||
{"", ""},
|
||||
{"0x", "?_"},
|
||||
{"0x001 and same 0x001", "v1%1_ and same v1%1_"},
|
||||
{"0x008 and same 0x008", "v1%0_ and same v1%0_"},
|
||||
{"0x001 and diff 0x002", "v1%1_ and diff v2%2_"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := scrubHex([]byte(tt.in))
|
||||
if string(got) != tt.want {
|
||||
t.Errorf("for input:\n%s\n\ngot:\n%s\n\nwant:\n%s\n", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
@@ -31,7 +30,9 @@ import (
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlknobs"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/log/logheap"
|
||||
"tailscale.com/net/dnscache"
|
||||
@@ -46,9 +47,7 @@ import (
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
@@ -183,46 +182,13 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
pinger: opts.Pinger,
|
||||
}
|
||||
if opts.Hostinfo == nil {
|
||||
c.SetHostinfo(NewHostinfo())
|
||||
c.SetHostinfo(hostinfo.New())
|
||||
} else {
|
||||
c.SetHostinfo(opts.Hostinfo)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
var osVersion func() string // non-nil on some platforms
|
||||
|
||||
func NewHostinfo() *tailcfg.Hostinfo {
|
||||
hostname, _ := os.Hostname()
|
||||
hostname = dnsname.FirstLabel(hostname)
|
||||
var osv string
|
||||
if osVersion != nil {
|
||||
osv = osVersion()
|
||||
}
|
||||
return &tailcfg.Hostinfo{
|
||||
IPNVersion: version.Long,
|
||||
Hostname: hostname,
|
||||
OS: version.OS(),
|
||||
OSVersion: osv,
|
||||
Package: packageType(),
|
||||
GoArch: runtime.GOARCH,
|
||||
}
|
||||
}
|
||||
|
||||
func packageType() string {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if _, err := os.Stat(`C:\ProgramData\chocolatey\lib\tailscale`); err == nil {
|
||||
return "choco"
|
||||
}
|
||||
case "darwin":
|
||||
// Using tailscaled or IPNExtension?
|
||||
exe, _ := os.Executable()
|
||||
return filepath.Base(exe)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetHostinfo clones the provided Hostinfo and remembers it for the
|
||||
// next update. It reports whether the Hostinfo has changed.
|
||||
func (c *Direct) SetHostinfo(hi *tailcfg.Hostinfo) bool {
|
||||
@@ -797,7 +763,10 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
continue
|
||||
}
|
||||
|
||||
if resp.Debug != nil {
|
||||
hasDebug := resp.Debug != nil
|
||||
// being conservative here, if Debug not present set to False
|
||||
controlknobs.SetDisableUPnP(hasDebug && resp.Debug.DisableUPnP.EqualBool(true))
|
||||
if hasDebug {
|
||||
if resp.Debug.LogHeapPprof {
|
||||
go logheap.LogHeap(resp.Debug.LogHeapURL)
|
||||
}
|
||||
@@ -854,22 +823,21 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
}
|
||||
|
||||
// Get latest localPort. This might've changed if
|
||||
// a lite map update occured meanwhile. This only affects
|
||||
// a lite map update occurred meanwhile. This only affects
|
||||
// the end-to-end test.
|
||||
// TODO(bradfitz): remove the NetworkMap.LocalPort field entirely.
|
||||
c.mu.Lock()
|
||||
nm.LocalPort = c.localPort
|
||||
c.mu.Unlock()
|
||||
|
||||
// Printing the netmap can be extremely verbose, but is very
|
||||
// handy for debugging. Let's limit how often we do it.
|
||||
// Code elsewhere prints netmap diffs every time, so this
|
||||
// occasional full dump, plus incremental diffs, should do
|
||||
// the job.
|
||||
// Occasionally print the netmap header.
|
||||
// This is handy for debugging, and our logs processing
|
||||
// pipeline depends on it. (TODO: Remove this dependency.)
|
||||
// Code elsewhere prints netmap diffs every time they are received.
|
||||
now := c.timeNow()
|
||||
if now.Sub(c.lastPrintMap) >= 5*time.Minute {
|
||||
c.lastPrintMap = now
|
||||
c.logf("[v1] new network map[%d]:\n%s", i, nm.Concise())
|
||||
c.logf("[v1] new network map[%d]:\n%s", i, nm.VeryConcise())
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
@@ -1292,3 +1260,59 @@ func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// tsmpPing sends a Ping to pr.IP, and sends an http request back to pr.URL
|
||||
// with ping response data.
|
||||
func tsmpPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pinger Pinger) error {
|
||||
var err error
|
||||
if pr.URL == "" {
|
||||
return errors.New("invalid PingRequest with no URL")
|
||||
}
|
||||
if pr.IP.IsZero() {
|
||||
return errors.New("PingRequest without IP")
|
||||
}
|
||||
if !strings.Contains(pr.Types, "TSMP") {
|
||||
return fmt.Errorf("PingRequest with no TSMP in Types, got %q", pr.Types)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
pinger.Ping(pr.IP, true, func(res *ipnstate.PingResult) {
|
||||
// Currently does not check for error since we just return if it fails.
|
||||
err = postPingResult(now, logf, c, pr, res)
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
func postPingResult(now time.Time, logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, res *ipnstate.PingResult) error {
|
||||
if res.Err != "" {
|
||||
return errors.New(res.Err)
|
||||
}
|
||||
duration := time.Since(now)
|
||||
if pr.Log {
|
||||
logf("TSMP ping to %v completed in %v seconds. pinger.Ping took %v seconds", pr.IP, res.LatencySeconds, duration.Seconds())
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
jsonPingRes, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Send the results of the Ping, back to control URL.
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", pr.URL, bytes.NewBuffer(jsonPingRes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("http.NewRequestWithContext(%q): %w", pr.URL, err)
|
||||
}
|
||||
if pr.Log {
|
||||
logf("tsmpPing: sending ping results to %v ...", pr.URL)
|
||||
}
|
||||
t0 := time.Now()
|
||||
_, err = c.Do(req)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tsmpPing error: %w to %v (after %v)", err, pr.URL, d)
|
||||
} else if pr.Log {
|
||||
logf("tsmpPing complete to %v (after %v)", pr.URL, d)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,15 +6,20 @@ package controlclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func TestNewDirect(t *testing.T) {
|
||||
hi := NewHostinfo()
|
||||
hi := hostinfo.New()
|
||||
ni := tailcfg.NetInfo{LinkType: "wired"}
|
||||
hi.NetInfo = &ni
|
||||
|
||||
@@ -56,7 +61,7 @@ func TestNewDirect(t *testing.T) {
|
||||
if changed {
|
||||
t.Errorf("c.SetHostinfo(hi) want false got %v", changed)
|
||||
}
|
||||
hi = NewHostinfo()
|
||||
hi = hostinfo.New()
|
||||
hi.Hostname = "different host name"
|
||||
changed = c.SetHostinfo(hi)
|
||||
if !changed {
|
||||
@@ -92,14 +97,55 @@ func fakeEndpoints(ports ...uint16) (ret []tailcfg.Endpoint) {
|
||||
return
|
||||
}
|
||||
|
||||
func TestNewHostinfo(t *testing.T) {
|
||||
hi := NewHostinfo()
|
||||
if hi == nil {
|
||||
t.Fatal("no Hostinfo")
|
||||
func TestTsmpPing(t *testing.T) {
|
||||
hi := hostinfo.New()
|
||||
ni := tailcfg.NetInfo{LinkType: "wired"}
|
||||
hi.NetInfo = &ni
|
||||
|
||||
key, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
j, err := json.MarshalIndent(hi, " ", "")
|
||||
opts := Options{
|
||||
ServerURL: "https://example.com",
|
||||
Hostinfo: hi,
|
||||
GetMachinePrivateKey: func() (wgkey.Private, error) {
|
||||
return key, nil
|
||||
},
|
||||
}
|
||||
|
||||
c, err := NewDirect(opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pingRes := &ipnstate.PingResult{
|
||||
IP: "123.456.7890",
|
||||
Err: "",
|
||||
NodeName: "testnode",
|
||||
}
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
body := new(ipnstate.PingResult)
|
||||
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pingRes.IP != body.IP {
|
||||
t.Fatalf("PingResult did not have the correct IP : got %v, expected : %v", body.IP, pingRes.IP)
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
pr := &tailcfg.PingRequest{
|
||||
URL: ts.URL,
|
||||
}
|
||||
|
||||
err = postPingResult(now, t.Logf, c.httpc, pr, pingRes)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Got: %s", j)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build windows && cgo
|
||||
// +build windows,cgo
|
||||
|
||||
// darwin,cgo is also supported by certstore but machineCertificateSubject will
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows || !cgo
|
||||
// +build !windows !cgo
|
||||
|
||||
package controlclient
|
||||
|
||||
34
control/controlknobs/controlknobs.go
Normal file
34
control/controlknobs/controlknobs.go
Normal file
@@ -0,0 +1,34 @@
|
||||
// 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 controlknobs contains client options configurable from control which can be turned on
|
||||
// or off. The ability to turn options on and off is for incrementally adding features in.
|
||||
package controlknobs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
)
|
||||
|
||||
// disableUPnP indicates whether to attempt UPnP mapping.
|
||||
var disableUPnP syncs.AtomicBool
|
||||
|
||||
func init() {
|
||||
v, _ := strconv.ParseBool(os.Getenv("TS_DISABLE_UPNP"))
|
||||
SetDisableUPnP(v)
|
||||
}
|
||||
|
||||
// DisableUPnP reports the last reported value from control
|
||||
// whether UPnP portmapping should be disabled.
|
||||
func DisableUPnP() bool {
|
||||
return disableUPnP.Get()
|
||||
}
|
||||
|
||||
// SetDisableUPnP sets whether control says that UPnP should be
|
||||
// disabled.
|
||||
func SetDisableUPnP(v bool) {
|
||||
disableUPnP.Set(v)
|
||||
}
|
||||
@@ -29,6 +29,7 @@ type Client struct {
|
||||
br *bufio.Reader
|
||||
meshKey string
|
||||
canAckPings bool
|
||||
isProber bool
|
||||
|
||||
wmu sync.Mutex // hold while writing to bw
|
||||
bw *bufio.Writer
|
||||
@@ -52,6 +53,7 @@ type clientOpt struct {
|
||||
MeshKey string
|
||||
ServerPub key.Public
|
||||
CanAckPings bool
|
||||
IsProber bool
|
||||
}
|
||||
|
||||
// MeshKey returns a ClientOpt to pass to the DERP server during connect to get
|
||||
@@ -60,6 +62,10 @@ type clientOpt struct {
|
||||
// An empty key means to not use a mesh key.
|
||||
func MeshKey(key string) ClientOpt { return clientOptFunc(func(o *clientOpt) { o.MeshKey = key }) }
|
||||
|
||||
// IsProber returns a ClientOpt to pass to the DERP server during connect to
|
||||
// declare that this client is a a prober.
|
||||
func IsProber(v bool) ClientOpt { return clientOptFunc(func(o *clientOpt) { o.IsProber = v }) }
|
||||
|
||||
// ServerPublicKey returns a ClientOpt to declare that the server's DERP public key is known.
|
||||
// If key is the zero value, the returned ClientOpt is a no-op.
|
||||
func ServerPublicKey(key key.Public) ClientOpt {
|
||||
@@ -93,6 +99,7 @@ func newClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logg
|
||||
bw: brw.Writer,
|
||||
meshKey: opt.MeshKey,
|
||||
canAckPings: opt.CanAckPings,
|
||||
isProber: opt.IsProber,
|
||||
}
|
||||
if opt.ServerPub.IsZero() {
|
||||
if err := c.recvServerKey(); err != nil {
|
||||
@@ -160,6 +167,9 @@ type clientInfo struct {
|
||||
// CanAckPings is whether the client declares it's able to ack
|
||||
// pings.
|
||||
CanAckPings bool
|
||||
|
||||
// IsProber is whether this client is a prober.
|
||||
IsProber bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) sendClientKey() error {
|
||||
@@ -171,6 +181,7 @@ func (c *Client) sendClientKey() error {
|
||||
Version: ProtocolVersion,
|
||||
MeshKey: c.meshKey,
|
||||
CanAckPings: c.canAckPings,
|
||||
IsProber: c.isProber,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -36,12 +36,14 @@ import (
|
||||
"go4.org/mem"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/sync/errgroup"
|
||||
"golang.org/x/time/rate"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/disco"
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/pad32"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -75,13 +77,6 @@ const (
|
||||
writeTimeout = 2 * time.Second
|
||||
)
|
||||
|
||||
const host64bit = (^uint(0) >> 32) & 1 // 1 on 64-bit, 0 on 32-bit
|
||||
|
||||
// pad32bit is 4 on 32-bit machines and 0 on 64-bit.
|
||||
// It exists so the Server struct's atomic fields can be aligned to 8
|
||||
// byte boundaries. (As tested by GOARCH=386 go test, etc)
|
||||
const pad32bit = 4 - host64bit*4 // 0 on 64-bit, 4 on 32-bit
|
||||
|
||||
// Server is a DERP server.
|
||||
type Server struct {
|
||||
// WriteTimeout, if non-zero, specifies how long to wait
|
||||
@@ -97,47 +92,46 @@ type Server struct {
|
||||
metaCert []byte // the encoded x509 cert to send after LetsEncrypt cert+intermediate
|
||||
|
||||
// Counters:
|
||||
_ [pad32bit]byte
|
||||
packetsSent, bytesSent expvar.Int
|
||||
packetsRecv, bytesRecv expvar.Int
|
||||
packetsRecvByKind metrics.LabelMap
|
||||
packetsRecvDisco *expvar.Int
|
||||
packetsRecvOther *expvar.Int
|
||||
_ [pad32bit]byte
|
||||
packetsDropped expvar.Int
|
||||
packetsDroppedReason metrics.LabelMap
|
||||
packetsDroppedUnknown *expvar.Int // unknown dst pubkey
|
||||
packetsDroppedFwdUnknown *expvar.Int // unknown dst pubkey on forward
|
||||
packetsDroppedGone *expvar.Int // dst conn shutting down
|
||||
packetsDroppedQueueHead *expvar.Int // queue full, drop head packet
|
||||
packetsDroppedQueueTail *expvar.Int // queue full, drop tail packet
|
||||
packetsDroppedWrite *expvar.Int // error writing to dst conn
|
||||
_ [pad32bit]byte
|
||||
packetsForwardedOut expvar.Int
|
||||
packetsForwardedIn expvar.Int
|
||||
peerGoneFrames expvar.Int // number of peer gone frames sent
|
||||
accepts expvar.Int
|
||||
curClients expvar.Int
|
||||
curHomeClients expvar.Int // ones with preferred
|
||||
clientsReplaced expvar.Int
|
||||
unknownFrames expvar.Int
|
||||
homeMovesIn expvar.Int // established clients announce home server moves in
|
||||
homeMovesOut expvar.Int // established clients announce home server moves out
|
||||
multiForwarderCreated expvar.Int
|
||||
multiForwarderDeleted expvar.Int
|
||||
removePktForwardOther expvar.Int
|
||||
avgQueueDuration *uint64 // In milliseconds; accessed atomically
|
||||
_ pad32.Four
|
||||
packetsSent, bytesSent expvar.Int
|
||||
packetsRecv, bytesRecv expvar.Int
|
||||
packetsRecvByKind metrics.LabelMap
|
||||
packetsRecvDisco *expvar.Int
|
||||
packetsRecvOther *expvar.Int
|
||||
_ pad32.Four
|
||||
packetsDropped expvar.Int
|
||||
packetsDroppedReason metrics.LabelMap
|
||||
packetsDroppedReasonCounters []*expvar.Int // indexed by dropReason
|
||||
packetsDroppedType metrics.LabelMap
|
||||
packetsDroppedTypeDisco *expvar.Int
|
||||
packetsDroppedTypeOther *expvar.Int
|
||||
_ pad32.Four
|
||||
packetsForwardedOut expvar.Int
|
||||
packetsForwardedIn expvar.Int
|
||||
peerGoneFrames expvar.Int // number of peer gone frames sent
|
||||
accepts expvar.Int
|
||||
curClients expvar.Int
|
||||
curHomeClients expvar.Int // ones with preferred
|
||||
clientsReplaced expvar.Int
|
||||
clientsReplaceLimited expvar.Int
|
||||
clientsReplaceSleeping expvar.Int
|
||||
unknownFrames expvar.Int
|
||||
homeMovesIn expvar.Int // established clients announce home server moves in
|
||||
homeMovesOut expvar.Int // established clients announce home server moves out
|
||||
multiForwarderCreated expvar.Int
|
||||
multiForwarderDeleted expvar.Int
|
||||
removePktForwardOther expvar.Int
|
||||
avgQueueDuration *uint64 // In milliseconds; accessed atomically
|
||||
|
||||
// verifyClients only accepts client connections to the DERP server if the clientKey is a
|
||||
// known peer in the network, as specified by a running tailscaled's client's local api.
|
||||
verifyClients bool
|
||||
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
netConns map[Conn]chan struct{} // chan is closed when conn closes
|
||||
clients map[key.Public]*sclient
|
||||
clientsEver map[key.Public]bool // never deleted from, for stats; fine for now
|
||||
watchers map[*sclient]bool // mesh peer -> true
|
||||
mu sync.Mutex
|
||||
closed bool
|
||||
netConns map[Conn]chan struct{} // chan is closed when conn closes
|
||||
clients map[key.Public]*sclient
|
||||
watchers map[*sclient]bool // mesh peer -> true
|
||||
// clientsMesh tracks all clients in the cluster, both locally
|
||||
// and to mesh peers. If the value is nil, that means the
|
||||
// peer is only local (and thus in the clients Map, but not
|
||||
@@ -156,7 +150,7 @@ type Server struct {
|
||||
|
||||
// PacketForwarder is something that can forward packets.
|
||||
//
|
||||
// It's mostly an inteface for circular dependency reasons; the
|
||||
// It's mostly an interface for circular dependency reasons; the
|
||||
// typical implementation is derphttp.Client. The other implementation
|
||||
// is a multiForwarder, which this package creates as needed if a
|
||||
// public key gets more than one PacketForwarder registered for it.
|
||||
@@ -167,7 +161,7 @@ type PacketForwarder interface {
|
||||
// Conn is the subset of the underlying net.Conn the DERP Server needs.
|
||||
// It is a defined type so that non-net connections can be used.
|
||||
type Conn interface {
|
||||
io.Closer
|
||||
io.WriteCloser
|
||||
|
||||
// The *Deadline methods follow the semantics of net.Conn.
|
||||
|
||||
@@ -189,8 +183,8 @@ func NewServer(privateKey key.Private, logf logger.Logf) *Server {
|
||||
limitedLogf: logger.RateLimitedFn(logf, 30*time.Second, 5, 100),
|
||||
packetsRecvByKind: metrics.LabelMap{Label: "kind"},
|
||||
packetsDroppedReason: metrics.LabelMap{Label: "reason"},
|
||||
packetsDroppedType: metrics.LabelMap{Label: "type"},
|
||||
clients: map[key.Public]*sclient{},
|
||||
clientsEver: map[key.Public]bool{},
|
||||
clientsMesh: map[key.Public]PacketForwarder{},
|
||||
netConns: map[Conn]chan struct{}{},
|
||||
memSys0: ms.Sys,
|
||||
@@ -202,12 +196,16 @@ func NewServer(privateKey key.Private, logf logger.Logf) *Server {
|
||||
s.initMetacert()
|
||||
s.packetsRecvDisco = s.packetsRecvByKind.Get("disco")
|
||||
s.packetsRecvOther = s.packetsRecvByKind.Get("other")
|
||||
s.packetsDroppedUnknown = s.packetsDroppedReason.Get("unknown_dest")
|
||||
s.packetsDroppedFwdUnknown = s.packetsDroppedReason.Get("unknown_dest_on_fwd")
|
||||
s.packetsDroppedGone = s.packetsDroppedReason.Get("gone")
|
||||
s.packetsDroppedQueueHead = s.packetsDroppedReason.Get("queue_head")
|
||||
s.packetsDroppedQueueTail = s.packetsDroppedReason.Get("queue_tail")
|
||||
s.packetsDroppedWrite = s.packetsDroppedReason.Get("write_error")
|
||||
s.packetsDroppedReasonCounters = []*expvar.Int{
|
||||
s.packetsDroppedReason.Get("unknown_dest"),
|
||||
s.packetsDroppedReason.Get("unknown_dest_on_fwd"),
|
||||
s.packetsDroppedReason.Get("gone"),
|
||||
s.packetsDroppedReason.Get("queue_head"),
|
||||
s.packetsDroppedReason.Get("queue_tail"),
|
||||
s.packetsDroppedReason.Get("write_error"),
|
||||
}
|
||||
s.packetsDroppedTypeDisco = s.packetsDroppedType.Get("disco")
|
||||
s.packetsDroppedTypeOther = s.packetsDroppedType.Get("other")
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -345,26 +343,40 @@ func (s *Server) initMetacert() {
|
||||
func (s *Server) MetaCert() []byte { return s.metaCert }
|
||||
|
||||
// registerClient notes that client c is now authenticated and ready for packets.
|
||||
// If c's public key was already connected with a different connection, the prior one is closed.
|
||||
func (s *Server) registerClient(c *sclient) {
|
||||
//
|
||||
// If c's public key was already connected with a different
|
||||
// connection, the prior one is closed, unless it's fighting rapidly
|
||||
// with another client with the same key, in which case the returned
|
||||
// ok is false, and the caller should wait the provided duration
|
||||
// before trying again.
|
||||
func (s *Server) registerClient(c *sclient) (ok bool, d time.Duration) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
old := s.clients[c.key]
|
||||
if old == nil {
|
||||
c.logf("adding connection")
|
||||
} else {
|
||||
// Take over the old rate limiter, discarding the one
|
||||
// our caller just made.
|
||||
c.replaceLimiter = old.replaceLimiter
|
||||
if rr := c.replaceLimiter.ReserveN(timeNow(), 1); rr.OK() {
|
||||
if d := rr.DelayFrom(timeNow()); d > 0 {
|
||||
s.clientsReplaceLimited.Add(1)
|
||||
return false, d
|
||||
}
|
||||
}
|
||||
s.clientsReplaced.Add(1)
|
||||
c.logf("adding connection, replacing %s", old.remoteAddr)
|
||||
go old.nc.Close()
|
||||
}
|
||||
s.clients[c.key] = c
|
||||
s.clientsEver[c.key] = true
|
||||
if _, ok := s.clientsMesh[c.key]; !ok {
|
||||
s.clientsMesh[c.key] = nil // just for varz of total users in cluster
|
||||
}
|
||||
s.keyOfAddr[c.remoteIPPort] = c.key
|
||||
s.curClients.Add(1)
|
||||
s.broadcastPeerStateChangeLocked(c.key, true)
|
||||
return true, 0
|
||||
}
|
||||
|
||||
// broadcastPeerStateChangeLocked enqueues a message to all watchers
|
||||
@@ -452,8 +464,9 @@ func (s *Server) addWatcher(c *sclient) {
|
||||
}
|
||||
|
||||
func (s *Server) accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string, connNum int64) error {
|
||||
br, bw := brw.Reader, brw.Writer
|
||||
br := brw.Reader
|
||||
nc.SetDeadline(time.Now().Add(10 * time.Second))
|
||||
bw := &lazyBufioWriter{w: nc, lbw: brw.Writer}
|
||||
if err := s.sendServerKey(bw); err != nil {
|
||||
return fmt.Errorf("send server key: %v", err)
|
||||
}
|
||||
@@ -475,21 +488,29 @@ func (s *Server) accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string, connN
|
||||
remoteIPPort, _ := netaddr.ParseIPPort(remoteAddr)
|
||||
|
||||
c := &sclient{
|
||||
connNum: connNum,
|
||||
s: s,
|
||||
key: clientKey,
|
||||
nc: nc,
|
||||
br: br,
|
||||
bw: bw,
|
||||
logf: logger.WithPrefix(s.logf, fmt.Sprintf("derp client %v/%x: ", remoteAddr, clientKey)),
|
||||
done: ctx.Done(),
|
||||
remoteAddr: remoteAddr,
|
||||
remoteIPPort: remoteIPPort,
|
||||
connectedAt: time.Now(),
|
||||
sendQueue: make(chan pkt, perClientSendQueueDepth),
|
||||
peerGone: make(chan key.Public),
|
||||
canMesh: clientInfo.MeshKey != "" && clientInfo.MeshKey == s.meshKey,
|
||||
connNum: connNum,
|
||||
s: s,
|
||||
key: clientKey,
|
||||
nc: nc,
|
||||
br: br,
|
||||
bw: bw,
|
||||
logf: logger.WithPrefix(s.logf, fmt.Sprintf("derp client %v/%x: ", remoteAddr, clientKey)),
|
||||
done: ctx.Done(),
|
||||
remoteAddr: remoteAddr,
|
||||
remoteIPPort: remoteIPPort,
|
||||
connectedAt: time.Now(),
|
||||
sendQueue: make(chan pkt, perClientSendQueueDepth),
|
||||
discoSendQueue: make(chan pkt, perClientSendQueueDepth),
|
||||
peerGone: make(chan key.Public),
|
||||
canMesh: clientInfo.MeshKey != "" && clientInfo.MeshKey == s.meshKey,
|
||||
|
||||
// Allow kicking out previous connections once a
|
||||
// minute, with a very high burst of 100. Once a
|
||||
// minute is less than the client's 2 minute
|
||||
// inactivity timeout.
|
||||
replaceLimiter: rate.NewLimiter(rate.Every(time.Minute), 100),
|
||||
}
|
||||
|
||||
if c.canMesh {
|
||||
c.meshUpdate = make(chan struct{})
|
||||
}
|
||||
@@ -497,10 +518,18 @@ func (s *Server) accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string, connN
|
||||
c.info = *clientInfo
|
||||
}
|
||||
|
||||
s.registerClient(c)
|
||||
for {
|
||||
ok, d := s.registerClient(c)
|
||||
if ok {
|
||||
break
|
||||
}
|
||||
s.clientsReplaceSleeping.Add(1)
|
||||
timeSleep(d)
|
||||
s.clientsReplaceSleeping.Add(-1)
|
||||
}
|
||||
defer s.unregisterClient(c)
|
||||
|
||||
err = s.sendServerInfo(bw, clientKey)
|
||||
err = s.sendServerInfo(c.bw, clientKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("send server info: %v", err)
|
||||
}
|
||||
@@ -508,6 +537,12 @@ func (s *Server) accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string, connN
|
||||
return c.run(ctx)
|
||||
}
|
||||
|
||||
// for testing
|
||||
var (
|
||||
timeSleep = time.Sleep
|
||||
timeNow = time.Now
|
||||
)
|
||||
|
||||
// run serves the client until there's an error.
|
||||
// If the client hangs up or the server is closed, run returns nil, otherwise run returns an error.
|
||||
func (c *sclient) run(ctx context.Context) error {
|
||||
@@ -631,11 +666,7 @@ func (c *sclient) handleFrameForwardPacket(ft frameType, fl uint32) error {
|
||||
s.mu.Unlock()
|
||||
|
||||
if dst == nil {
|
||||
s.packetsDropped.Add(1)
|
||||
s.packetsDroppedFwdUnknown.Add(1)
|
||||
if debug {
|
||||
c.logf("dropping forwarded packet for unknown %x", dstKey)
|
||||
}
|
||||
s.recordDrop(contents, srcKey, dstKey, dropReasonUnknownDestOnFwd)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -686,11 +717,7 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
s.packetsDropped.Add(1)
|
||||
s.packetsDroppedUnknown.Add(1)
|
||||
if debug {
|
||||
c.logf("dropping packet for unknown %x", dstKey)
|
||||
}
|
||||
s.recordDrop(contents, c.key, dstKey, dropReasonUnknownDest)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -702,6 +729,41 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
|
||||
return c.sendPkt(dst, p)
|
||||
}
|
||||
|
||||
// dropReason is why we dropped a DERP frame.
|
||||
type dropReason int
|
||||
|
||||
//go:generate go run tailscale.com/cmd/addlicense -year 2021 -file dropreason_string.go go run golang.org/x/tools/cmd/stringer -type=dropReason -trimprefix=dropReason
|
||||
|
||||
const (
|
||||
dropReasonUnknownDest dropReason = iota // unknown destination pubkey
|
||||
dropReasonUnknownDestOnFwd // unknown destination pubkey on a derp-forwarded packet
|
||||
dropReasonGone // destination tailscaled disconnected before we could send
|
||||
dropReasonQueueHead // destination queue is full, dropped packet at queue head
|
||||
dropReasonQueueTail // destination queue is full, dropped packet at queue tail
|
||||
dropReasonWriteError // OS write() failed
|
||||
)
|
||||
|
||||
func (s *Server) recordDrop(packetBytes []byte, srcKey, dstKey key.Public, reason dropReason) {
|
||||
s.packetsDropped.Add(1)
|
||||
s.packetsDroppedReasonCounters[reason].Add(1)
|
||||
if disco.LooksLikeDiscoWrapper(packetBytes) {
|
||||
s.packetsDroppedTypeDisco.Add(1)
|
||||
} else {
|
||||
s.packetsDroppedTypeOther.Add(1)
|
||||
}
|
||||
if verboseDropKeys[dstKey] {
|
||||
// Preformat the log string prior to calling limitedLogf. The
|
||||
// limiter acts based on the format string, and we want to
|
||||
// rate-limit per src/dst keys, not on the generic "dropped
|
||||
// stuff" message.
|
||||
msg := fmt.Sprintf("drop (%s) %s -> %s", srcKey.ShortString(), reason, dstKey.ShortString())
|
||||
s.limitedLogf(msg)
|
||||
}
|
||||
if debug {
|
||||
s.logf("dropping packet reason=%s dst=%s disco=%v", reason, dstKey, disco.LooksLikeDiscoWrapper(packetBytes))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *sclient) sendPkt(dst *sclient, p pkt) error {
|
||||
s := c.s
|
||||
dstKey := dst.key
|
||||
@@ -709,54 +771,34 @@ func (c *sclient) sendPkt(dst *sclient, p pkt) error {
|
||||
// Attempt to queue for sending up to 3 times. On each attempt, if
|
||||
// the queue is full, try to drop from queue head to prioritize
|
||||
// fresher packets.
|
||||
sendQueue := dst.sendQueue
|
||||
if disco.LooksLikeDiscoWrapper(p.bs) {
|
||||
sendQueue = dst.discoSendQueue
|
||||
}
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
select {
|
||||
case <-dst.done:
|
||||
s.packetsDropped.Add(1)
|
||||
s.packetsDroppedGone.Add(1)
|
||||
if debug {
|
||||
c.logf("dropping packet for shutdown client %x", dstKey)
|
||||
}
|
||||
s.recordDrop(p.bs, c.key, dstKey, dropReasonGone)
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case dst.sendQueue <- p:
|
||||
case sendQueue <- p:
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
|
||||
select {
|
||||
case pkt := <-dst.sendQueue:
|
||||
s.packetsDropped.Add(1)
|
||||
s.packetsDroppedQueueHead.Add(1)
|
||||
if verboseDropKeys[dstKey] {
|
||||
// Generate a full string including src and dst, so
|
||||
// the limiter kicks in once per src.
|
||||
msg := fmt.Sprintf("tail drop %s -> %s", p.src.ShortString(), dstKey.ShortString())
|
||||
c.s.limitedLogf(msg)
|
||||
}
|
||||
case pkt := <-sendQueue:
|
||||
s.recordDrop(pkt.bs, c.key, dstKey, dropReasonQueueHead)
|
||||
c.recordQueueTime(pkt.enqueuedAt)
|
||||
if debug {
|
||||
c.logf("dropping packet from client %x queue head", dstKey)
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
// Failed to make room for packet. This can happen in a heavily
|
||||
// contended queue with racing writers. Give up and tail-drop in
|
||||
// this case to keep reader unblocked.
|
||||
s.packetsDropped.Add(1)
|
||||
s.packetsDroppedQueueTail.Add(1)
|
||||
if verboseDropKeys[dstKey] {
|
||||
// Generate a full string including src and dst, so
|
||||
// the limiter kicks in once per src.
|
||||
msg := fmt.Sprintf("head drop %s -> %s", p.src.ShortString(), dstKey.ShortString())
|
||||
c.s.limitedLogf(msg)
|
||||
}
|
||||
if debug {
|
||||
c.logf("dropping packet from client %x queue tail", dstKey)
|
||||
}
|
||||
s.recordDrop(p.bs, c.key, dstKey, dropReasonQueueTail)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -789,6 +831,9 @@ func (s *Server) verifyClient(clientKey key.Public, info *clientInfo) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to query local tailscaled status: %w", err)
|
||||
}
|
||||
if clientKey == status.Self.PublicKey {
|
||||
return nil
|
||||
}
|
||||
if _, exists := status.Peer[clientKey]; !exists {
|
||||
return fmt.Errorf("client %v not in set of peers", clientKey)
|
||||
}
|
||||
@@ -796,18 +841,20 @@ func (s *Server) verifyClient(clientKey key.Public, info *clientInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) sendServerKey(bw *bufio.Writer) error {
|
||||
func (s *Server) sendServerKey(lw *lazyBufioWriter) error {
|
||||
buf := make([]byte, 0, len(magic)+len(s.publicKey))
|
||||
buf = append(buf, magic...)
|
||||
buf = append(buf, s.publicKey[:]...)
|
||||
return writeFrame(bw, frameServerKey, buf)
|
||||
err := writeFrame(lw.bw(), frameServerKey, buf)
|
||||
lw.Flush() // redundant (no-op) flush to release bufio.Writer
|
||||
return err
|
||||
}
|
||||
|
||||
type serverInfo struct {
|
||||
Version int `json:"version,omitempty"`
|
||||
}
|
||||
|
||||
func (s *Server) sendServerInfo(bw *bufio.Writer, clientKey key.Public) error {
|
||||
func (s *Server) sendServerInfo(bw *lazyBufioWriter, clientKey key.Public) error {
|
||||
var nonce [24]byte
|
||||
if _, err := crand.Read(nonce[:]); err != nil {
|
||||
return err
|
||||
@@ -818,7 +865,7 @@ func (s *Server) sendServerInfo(bw *bufio.Writer, clientKey key.Public) error {
|
||||
}
|
||||
|
||||
msgbox := box.Seal(nil, msg, &nonce, clientKey.B32(), s.privateKey.B32())
|
||||
if err := writeFrameHeader(bw, frameServerInfo, nonceLen+uint32(len(msgbox))); err != nil {
|
||||
if err := writeFrameHeader(bw.bw(), frameServerInfo, nonceLen+uint32(len(msgbox))); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := bw.Write(nonce[:]); err != nil {
|
||||
@@ -926,19 +973,25 @@ func (s *Server) recvForwardPacket(br *bufio.Reader, frameLen uint32) (srcKey, d
|
||||
// (The "s" prefix is to more explicitly distinguish it from Client in derp_client.go)
|
||||
type sclient struct {
|
||||
// Static after construction.
|
||||
connNum int64 // process-wide unique counter, incremented each Accept
|
||||
s *Server
|
||||
nc Conn
|
||||
key key.Public
|
||||
info clientInfo
|
||||
logf logger.Logf
|
||||
done <-chan struct{} // closed when connection closes
|
||||
remoteAddr string // usually ip:port from net.Conn.RemoteAddr().String()
|
||||
remoteIPPort netaddr.IPPort // zero if remoteAddr is not ip:port.
|
||||
sendQueue chan pkt // packets queued to this client; never closed
|
||||
peerGone chan key.Public // write request that a previous sender has disconnected (not used by mesh peers)
|
||||
meshUpdate chan struct{} // write request to write peerStateChange
|
||||
canMesh bool // clientInfo had correct mesh token for inter-region routing
|
||||
connNum int64 // process-wide unique counter, incremented each Accept
|
||||
s *Server
|
||||
nc Conn
|
||||
key key.Public
|
||||
info clientInfo
|
||||
logf logger.Logf
|
||||
done <-chan struct{} // closed when connection closes
|
||||
remoteAddr string // usually ip:port from net.Conn.RemoteAddr().String()
|
||||
remoteIPPort netaddr.IPPort // zero if remoteAddr is not ip:port.
|
||||
sendQueue chan pkt // packets queued to this client; never closed
|
||||
discoSendQueue chan pkt // important packets queued to this client; never closed
|
||||
peerGone chan key.Public // write request that a previous sender has disconnected (not used by mesh peers)
|
||||
meshUpdate chan struct{} // write request to write peerStateChange
|
||||
canMesh bool // clientInfo had correct mesh token for inter-region routing
|
||||
|
||||
// replaceLimiter controls how quickly two connections with
|
||||
// the same client key can kick each other off the server by
|
||||
// taking over ownership of a key.
|
||||
replaceLimiter *rate.Limiter
|
||||
|
||||
// Owned by run, not thread-safe.
|
||||
br *bufio.Reader
|
||||
@@ -946,7 +999,7 @@ type sclient struct {
|
||||
preferred bool
|
||||
|
||||
// Owned by sender, not thread-safe.
|
||||
bw *bufio.Writer
|
||||
bw *lazyBufioWriter
|
||||
|
||||
// Guarded by s.mu
|
||||
//
|
||||
@@ -1031,12 +1084,10 @@ func (c *sclient) sendLoop(ctx context.Context) error {
|
||||
// Drain the send queue to count dropped packets
|
||||
for {
|
||||
select {
|
||||
case <-c.sendQueue:
|
||||
c.s.packetsDropped.Add(1)
|
||||
c.s.packetsDroppedGone.Add(1)
|
||||
if debug {
|
||||
c.logf("dropping packet for shutdown %x", c.key)
|
||||
}
|
||||
case pkt := <-c.sendQueue:
|
||||
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGone)
|
||||
case pkt := <-c.discoSendQueue:
|
||||
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGone)
|
||||
default:
|
||||
return
|
||||
}
|
||||
@@ -1067,6 +1118,10 @@ func (c *sclient) sendLoop(ctx context.Context) error {
|
||||
werr = c.sendPacket(msg.src, msg.bs)
|
||||
c.recordQueueTime(msg.enqueuedAt)
|
||||
continue
|
||||
case msg := <-c.discoSendQueue:
|
||||
werr = c.sendPacket(msg.src, msg.bs)
|
||||
c.recordQueueTime(msg.enqueuedAt)
|
||||
continue
|
||||
case <-keepAliveTick.C:
|
||||
werr = c.sendKeepAlive()
|
||||
continue
|
||||
@@ -1090,6 +1145,9 @@ func (c *sclient) sendLoop(ctx context.Context) error {
|
||||
case msg := <-c.sendQueue:
|
||||
werr = c.sendPacket(msg.src, msg.bs)
|
||||
c.recordQueueTime(msg.enqueuedAt)
|
||||
case msg := <-c.discoSendQueue:
|
||||
werr = c.sendPacket(msg.src, msg.bs)
|
||||
c.recordQueueTime(msg.enqueuedAt)
|
||||
case <-keepAliveTick.C:
|
||||
werr = c.sendKeepAlive()
|
||||
}
|
||||
@@ -1103,14 +1161,14 @@ func (c *sclient) setWriteDeadline() {
|
||||
// sendKeepAlive sends a keep-alive frame, without flushing.
|
||||
func (c *sclient) sendKeepAlive() error {
|
||||
c.setWriteDeadline()
|
||||
return writeFrameHeader(c.bw, frameKeepAlive, 0)
|
||||
return writeFrameHeader(c.bw.bw(), frameKeepAlive, 0)
|
||||
}
|
||||
|
||||
// sendPeerGone sends a peerGone frame, without flushing.
|
||||
func (c *sclient) sendPeerGone(peer key.Public) error {
|
||||
c.s.peerGoneFrames.Add(1)
|
||||
c.setWriteDeadline()
|
||||
if err := writeFrameHeader(c.bw, framePeerGone, keyLen); err != nil {
|
||||
if err := writeFrameHeader(c.bw.bw(), framePeerGone, keyLen); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.bw.Write(peer[:])
|
||||
@@ -1120,7 +1178,7 @@ func (c *sclient) sendPeerGone(peer key.Public) error {
|
||||
// sendPeerPresent sends a peerPresent frame, without flushing.
|
||||
func (c *sclient) sendPeerPresent(peer key.Public) error {
|
||||
c.setWriteDeadline()
|
||||
if err := writeFrameHeader(c.bw, framePeerPresent, keyLen); err != nil {
|
||||
if err := writeFrameHeader(c.bw.bw(), framePeerPresent, keyLen); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.bw.Write(peer[:])
|
||||
@@ -1179,11 +1237,7 @@ func (c *sclient) sendPacket(srcKey key.Public, contents []byte) (err error) {
|
||||
defer func() {
|
||||
// Stats update.
|
||||
if err != nil {
|
||||
c.s.packetsDropped.Add(1)
|
||||
c.s.packetsDroppedWrite.Add(1)
|
||||
if debug {
|
||||
c.logf("dropping packet to %x: %v", c.key, err)
|
||||
}
|
||||
c.s.recordDrop(contents, srcKey, c.key, dropReasonWriteError)
|
||||
} else {
|
||||
c.s.packetsSent.Add(1)
|
||||
c.s.bytesSent.Add(int64(len(contents)))
|
||||
@@ -1197,11 +1251,11 @@ func (c *sclient) sendPacket(srcKey key.Public, contents []byte) (err error) {
|
||||
if withKey {
|
||||
pktLen += len(srcKey)
|
||||
}
|
||||
if err = writeFrameHeader(c.bw, frameRecvPacket, uint32(pktLen)); err != nil {
|
||||
if err = writeFrameHeader(c.bw.bw(), frameRecvPacket, uint32(pktLen)); err != nil {
|
||||
return err
|
||||
}
|
||||
if withKey {
|
||||
err := writePublicKey(c.bw, &srcKey)
|
||||
err := writePublicKey(c.bw.bw(), &srcKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1221,7 +1275,7 @@ func (s *Server) AddPacketForwarder(dst key.Public, fwd PacketForwarder) {
|
||||
return
|
||||
}
|
||||
if m, ok := prev.(multiForwarder); ok {
|
||||
if _, ok := m[fwd]; !ok {
|
||||
if _, ok := m[fwd]; ok {
|
||||
// Duplicate registration of same forwarder in set; ignore.
|
||||
return
|
||||
}
|
||||
@@ -1329,7 +1383,6 @@ func (s *Server) expVarFunc(f func() interface{}) expvar.Func {
|
||||
// ExpVar returns an expvar variable suitable for registering with expvar.Publish.
|
||||
func (s *Server) ExpVar() expvar.Var {
|
||||
m := new(metrics.Set)
|
||||
m.Set("counter_unique_clients_ever", s.expVarFunc(func() interface{} { return len(s.clientsEver) }))
|
||||
m.Set("gauge_memstats_sys0", expvar.Func(func() interface{} { return int64(s.memSys0) }))
|
||||
m.Set("gauge_watchers", s.expVarFunc(func() interface{} { return len(s.watchers) }))
|
||||
m.Set("gauge_current_connections", &s.curClients)
|
||||
@@ -1339,10 +1392,13 @@ func (s *Server) ExpVar() expvar.Var {
|
||||
m.Set("gauge_clients_remote", expvar.Func(func() interface{} { return len(s.clientsMesh) - len(s.clients) }))
|
||||
m.Set("accepts", &s.accepts)
|
||||
m.Set("clients_replaced", &s.clientsReplaced)
|
||||
m.Set("clients_replace_limited", &s.clientsReplaceLimited)
|
||||
m.Set("gauge_clients_replace_sleeping", &s.clientsReplaceSleeping)
|
||||
m.Set("bytes_received", &s.bytesRecv)
|
||||
m.Set("bytes_sent", &s.bytesSent)
|
||||
m.Set("packets_dropped", &s.packetsDropped)
|
||||
m.Set("counter_packets_dropped_reason", &s.packetsDroppedReason)
|
||||
m.Set("counter_packets_dropped_type", &s.packetsDroppedType)
|
||||
m.Set("counter_packets_received_kind", &s.packetsRecvByKind)
|
||||
m.Set("packets_sent", &s.packetsSent)
|
||||
m.Set("packets_received", &s.packetsRecv)
|
||||
@@ -1514,3 +1570,45 @@ func (s *Server) ServeDebugTraffic(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(minTimeBetweenLogs)
|
||||
}
|
||||
}
|
||||
|
||||
var bufioWriterPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
return bufio.NewWriterSize(ioutil.Discard, 2<<10)
|
||||
},
|
||||
}
|
||||
|
||||
// lazyBufioWriter is a bufio.Writer-like wrapping writer that lazily
|
||||
// allocates its actual bufio.Writer from a sync.Pool, releasing it to
|
||||
// the pool upon flush.
|
||||
//
|
||||
// We do this to reduce memory overhead; most DERP connections are
|
||||
// idle and the idle bufio.Writers were 30% of overall memory usage.
|
||||
type lazyBufioWriter struct {
|
||||
w io.Writer // underlying
|
||||
lbw *bufio.Writer // lazy; nil means it needs an associated buffer
|
||||
}
|
||||
|
||||
func (w *lazyBufioWriter) bw() *bufio.Writer {
|
||||
if w.lbw == nil {
|
||||
w.lbw = bufioWriterPool.Get().(*bufio.Writer)
|
||||
w.lbw.Reset(w.w)
|
||||
}
|
||||
return w.lbw
|
||||
}
|
||||
|
||||
func (w *lazyBufioWriter) Available() int { return w.bw().Available() }
|
||||
|
||||
func (w *lazyBufioWriter) Write(p []byte) (int, error) { return w.bw().Write(p) }
|
||||
|
||||
func (w *lazyBufioWriter) Flush() error {
|
||||
if w.lbw == nil {
|
||||
return nil
|
||||
}
|
||||
err := w.lbw.Flush()
|
||||
|
||||
w.lbw.Reset(ioutil.Discard)
|
||||
bufioWriterPool.Put(w.lbw)
|
||||
w.lbw = nil
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
"tailscale.com/net/nettest"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -711,6 +712,7 @@ func TestForwarderRegistration(t *testing.T) {
|
||||
// Adding a dup for a user.
|
||||
wantCounter(&s.multiForwarderCreated, 0)
|
||||
s.AddPacketForwarder(u1, testFwd(100))
|
||||
s.AddPacketForwarder(u1, testFwd(100)) // dup to trigger dup path
|
||||
want(map[key.Public]PacketForwarder{
|
||||
u1: multiForwarder{
|
||||
testFwd(1): 1,
|
||||
@@ -849,6 +851,136 @@ func TestClientSendPong(t *testing.T) {
|
||||
|
||||
}
|
||||
|
||||
func TestServerReplaceClients(t *testing.T) {
|
||||
defer func() {
|
||||
timeSleep = time.Sleep
|
||||
timeNow = time.Now
|
||||
}()
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
now = time.Unix(123, 0)
|
||||
sleeps int
|
||||
slept time.Duration
|
||||
)
|
||||
timeSleep = func(d time.Duration) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
sleeps++
|
||||
slept += d
|
||||
now = now.Add(d)
|
||||
}
|
||||
timeNow = func() time.Time {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return now
|
||||
}
|
||||
|
||||
serverPrivateKey := newPrivateKey(t)
|
||||
var logger logger.Logf = logger.Discard
|
||||
const debug = false
|
||||
if debug {
|
||||
logger = t.Logf
|
||||
}
|
||||
|
||||
s := NewServer(serverPrivateKey, logger)
|
||||
defer s.Close()
|
||||
|
||||
priv := newPrivateKey(t)
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
connNum := 0
|
||||
connect := func() *Client {
|
||||
connNum++
|
||||
cout, err := net.Dial("tcp", ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cin, err := ln.Accept()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
brwServer := bufio.NewReadWriter(bufio.NewReader(cin), bufio.NewWriter(cin))
|
||||
go s.Accept(cin, brwServer, fmt.Sprintf("test-client-%d", connNum))
|
||||
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(cout), bufio.NewWriter(cout))
|
||||
c, err := NewClient(priv, cout, brw, logger)
|
||||
if err != nil {
|
||||
t.Fatalf("client %d: %v", connNum, err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
wantVar := func(v *expvar.Int, want int64) {
|
||||
t.Helper()
|
||||
if got := v.Value(); got != want {
|
||||
t.Errorf("got %d; want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
wantClosed := func(c *Client) {
|
||||
t.Helper()
|
||||
for {
|
||||
m, err := c.Recv()
|
||||
if err != nil {
|
||||
t.Logf("got expected error: %v", err)
|
||||
return
|
||||
}
|
||||
switch m.(type) {
|
||||
case ServerInfoMessage:
|
||||
continue
|
||||
default:
|
||||
t.Fatalf("client got %T; wanted an error", m)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c1 := connect()
|
||||
waitConnect(t, c1)
|
||||
c2 := connect()
|
||||
waitConnect(t, c2)
|
||||
wantVar(&s.clientsReplaced, 1)
|
||||
wantClosed(c1)
|
||||
|
||||
for i := 0; i < 100+5; i++ {
|
||||
c := connect()
|
||||
defer c.nc.Close()
|
||||
if s.clientsReplaceLimited.Value() == 0 && i < 90 {
|
||||
continue
|
||||
}
|
||||
t.Logf("for %d: replaced=%d, limited=%d, sleeping=%d", i,
|
||||
s.clientsReplaced.Value(),
|
||||
s.clientsReplaceLimited.Value(),
|
||||
s.clientsReplaceSleeping.Value(),
|
||||
)
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if sleeps == 0 {
|
||||
t.Errorf("no sleeps")
|
||||
}
|
||||
if slept == 0 {
|
||||
t.Errorf("total sleep duration was 0")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimiter(t *testing.T) {
|
||||
rl := rate.NewLimiter(rate.Every(time.Minute), 100)
|
||||
for i := 0; i < 200; i++ {
|
||||
r := rl.Reserve()
|
||||
d := r.Delay()
|
||||
t.Logf("i=%d, allow=%v, d=%v", i, r.OK(), d)
|
||||
}
|
||||
}
|
||||
|
||||
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) })
|
||||
|
||||
@@ -50,9 +50,11 @@ type Client struct {
|
||||
TLSConfig *tls.Config // optional; nil means default
|
||||
DNSCache *dnscache.Resolver // optional; nil means no caching
|
||||
MeshKey string // optional; for trusted clients
|
||||
IsProber bool // optional; for probers to optional declare themselves as such
|
||||
|
||||
privateKey key.Private
|
||||
logf logger.Logf
|
||||
dialer func(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
|
||||
// Either url or getRegion is non-nil:
|
||||
url *url.URL
|
||||
@@ -130,6 +132,11 @@ func (c *Client) ServerPublicKey() key.Public {
|
||||
return c.serverPubKey
|
||||
}
|
||||
|
||||
// SelfPublicKey returns our own public key.
|
||||
func (c *Client) SelfPublicKey() key.Public {
|
||||
return c.privateKey.Public()
|
||||
}
|
||||
|
||||
func urlPort(u *url.URL) string {
|
||||
if p := u.Port(); p != "" {
|
||||
return p
|
||||
@@ -338,6 +345,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
derp.MeshKey(c.MeshKey),
|
||||
derp.ServerPublicKey(serverPub),
|
||||
derp.CanAckPings(c.canAckPings),
|
||||
derp.IsProber(c.IsProber),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
@@ -356,14 +364,26 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
return c.client, c.connGen, nil
|
||||
}
|
||||
|
||||
// SetURLDialer sets the dialer to use for dialing URLs.
|
||||
// This dialer is only use for clients created with NewClient, not NewRegionClient.
|
||||
// If unset or nil, the default dialer is used.
|
||||
//
|
||||
// The primary use for this is the derper mesh mode to connect to each
|
||||
// other over a VPC network.
|
||||
func (c *Client) SetURLDialer(dialer func(ctx context.Context, network, addr string) (net.Conn, error)) {
|
||||
c.dialer = dialer
|
||||
}
|
||||
|
||||
func (c *Client) dialURL(ctx context.Context) (net.Conn, error) {
|
||||
host := c.url.Hostname()
|
||||
if c.dialer != nil {
|
||||
return c.dialer(ctx, "tcp", net.JoinHostPort(host, urlPort(c.url)))
|
||||
}
|
||||
hostOrIP := host
|
||||
|
||||
dialer := netns.NewDialer()
|
||||
|
||||
if c.DNSCache != nil {
|
||||
ip, _, err := c.DNSCache.LookupIP(ctx, host)
|
||||
ip, _, _, err := c.DNSCache.LookupIP(ctx, host)
|
||||
if err == nil {
|
||||
hostOrIP = ip.String()
|
||||
}
|
||||
@@ -410,9 +430,7 @@ func (c *Client) dialRegion(ctx context.Context, reg *tailcfg.DERPRegion) (net.C
|
||||
func (c *Client) tlsClient(nc net.Conn, node *tailcfg.DERPNode) *tls.Conn {
|
||||
tlsConf := tlsdial.Config(c.tlsServerName(node), c.TLSConfig)
|
||||
if node != nil {
|
||||
if node.DERPTestPort != 0 {
|
||||
tlsConf.InsecureSkipVerify = true
|
||||
}
|
||||
tlsConf.InsecureSkipVerify = node.InsecureForTests
|
||||
if node.CertName != "" {
|
||||
tlsdial.SetConfigExpectedCert(tlsConf, node.CertName)
|
||||
}
|
||||
@@ -511,8 +529,8 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
|
||||
dst = n.HostName
|
||||
}
|
||||
port := "443"
|
||||
if n.DERPTestPort != 0 {
|
||||
port = fmt.Sprint(n.DERPTestPort)
|
||||
if n.DERPPort != 0 {
|
||||
port = fmt.Sprint(n.DERPPort)
|
||||
}
|
||||
c, err := c.dialContext(ctx, proto, net.JoinHostPort(dst, port))
|
||||
select {
|
||||
|
||||
32
derp/dropreason_string.go
Normal file
32
derp/dropreason_string.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// 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.
|
||||
|
||||
// Code generated by "stringer -type=dropReason -trimprefix=dropReason"; DO NOT EDIT.
|
||||
|
||||
package derp
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[dropReasonUnknownDest-0]
|
||||
_ = x[dropReasonUnknownDestOnFwd-1]
|
||||
_ = x[dropReasonGone-2]
|
||||
_ = x[dropReasonQueueHead-3]
|
||||
_ = x[dropReasonQueueTail-4]
|
||||
_ = x[dropReasonWriteError-5]
|
||||
}
|
||||
|
||||
const _dropReason_name = "UnknownDestUnknownDestOnFwdGoneQueueHeadQueueTailWriteError"
|
||||
|
||||
var _dropReason_index = [...]uint8{0, 11, 27, 31, 40, 49, 59}
|
||||
|
||||
func (i dropReason) String() string {
|
||||
if i < 0 || i >= dropReason(len(_dropReason_index)-1) {
|
||||
return "dropReason(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _dropReason_name[_dropReason_index[i]:_dropReason_index[i+1]]
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
// 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.
|
||||
//go:build gofuzz
|
||||
// +build gofuzz
|
||||
|
||||
package disco
|
||||
|
||||
22
go.mod
22
go.mod
@@ -7,6 +7,8 @@ require (
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||
github.com/aws/aws-sdk-go v1.38.52
|
||||
github.com/coreos/go-iptables v0.6.0
|
||||
github.com/creack/pty v1.1.9
|
||||
github.com/dave/jennifer v1.4.1
|
||||
github.com/frankban/quicktest v1.13.0
|
||||
github.com/gliderlabs/ssh v0.3.2
|
||||
github.com/go-multierror/multierror v1.0.2
|
||||
@@ -16,34 +18,38 @@ require (
|
||||
github.com/google/goexpect v0.0.0-20210430020637-ab937bf7fd6f
|
||||
github.com/google/uuid v1.1.2
|
||||
github.com/goreleaser/nfpm v1.10.3
|
||||
github.com/iancoleman/strcase v0.2.0
|
||||
github.com/insomniacslk/dhcp v0.0.0-20210621130208-1cac67f12b1e
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/klauspost/compress v1.12.2
|
||||
github.com/kr/pty v1.1.8
|
||||
github.com/mdlayher/netlink v1.4.1
|
||||
github.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697
|
||||
github.com/miekg/dns v1.1.42
|
||||
github.com/mitchellh/go-ps v1.0.0
|
||||
github.com/pborman/getopt v1.1.0
|
||||
github.com/peterbourgon/ff/v2 v2.0.0
|
||||
github.com/pkg/sftp v1.13.0
|
||||
github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
|
||||
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2
|
||||
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-20210513164829-c07d793c2f9a
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22
|
||||
golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
|
||||
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6
|
||||
golang.org/x/tools v0.1.2
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210525143454-64cb82f2b3f5
|
||||
golang.zx2c4.com/wireguard/windows v0.3.15-0.20210525143335-94c0476d63e3
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210624150102-15b24b6179e0
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c // indirect
|
||||
golang.zx2c4.com/wireguard/windows v0.3.16
|
||||
honnef.co/go/tools v0.1.4
|
||||
inet.af/netaddr v0.0.0-20210602152128-50f8686885e3
|
||||
inet.af/netaddr v0.0.0-20210721214506-ce7a8ad02cc1
|
||||
inet.af/netstack v0.0.0-20210622165351-29b14ebc044e
|
||||
inet.af/peercred v0.0.0-20210318190834-4259e17bb763
|
||||
inet.af/wf v0.0.0-20210516214145-a5343001b756
|
||||
rsc.io/goversion v1.2.0
|
||||
)
|
||||
|
||||
59
go.sum
59
go.sum
@@ -82,12 +82,13 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/daixiang0/gci v0.2.4/go.mod h1:+AV8KmHTGxxwp/pY84TLQfFKp2vuKXXJVzF3kD/hfR4=
|
||||
github.com/daixiang0/gci v0.2.7 h1:bosLNficubzJZICsVzxuyNc6oAbdz0zcqLG2G/RxtY4=
|
||||
github.com/daixiang0/gci v0.2.7/go.mod h1:+4dZ7TISfSmqfAGv59ePaHfNzgGtIkHAhhdKggP1JAc=
|
||||
github.com/dave/jennifer v1.4.1 h1:XyqG6cn5RQsTj3qlWQTKlRGAyrTcsk1kUmWdZBzRjDw=
|
||||
github.com/dave/jennifer v1.4.1/go.mod h1:7jEdnm+qBcxl8PC0zyp7vxcpSRnzXSt9r39tpTVGlwA=
|
||||
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=
|
||||
@@ -101,6 +102,7 @@ github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/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/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
github.com/fatih/color v1.10.0 h1:s36xzo75JdqLaaWoiEHk767eHiwo0598uUxyfiPkDsg=
|
||||
@@ -296,11 +298,16 @@ github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/J
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw=
|
||||
github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
|
||||
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
|
||||
github.com/iancoleman/strcase v0.2.0 h1:05I4QRnGpI0m37iZQRuskXh+w77mr6Z41lwQzuHLwW0=
|
||||
github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.11 h1:3tnifQM4i+fbajXKBHXWEH+KvNHqojZ778UH75j3bGA=
|
||||
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20210621130208-1cac67f12b1e h1:sgh63o+pm5kcdrgyYaCIoeD7mccyL6MscVmy+DvY6C4=
|
||||
github.com/insomniacslk/dhcp v0.0.0-20210621130208-1cac67f12b1e/go.mod h1:h+MxyHxRg9NH3terB1nfRIUaQEcI0XOVkdR9LNBlp8E=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
@@ -323,6 +330,7 @@ github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/rasw
|
||||
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-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
|
||||
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=
|
||||
@@ -355,8 +363,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/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/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
@@ -390,6 +396,7 @@ github.com/mattn/goveralls v0.0.2/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpe
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mbilski/exhaustivestruct v1.1.0 h1:4ykwscnAFeHJruT+EY3M3vdeP8uXMh0VV2E61iR7XD8=
|
||||
github.com/mbilski/exhaustivestruct v1.1.0/go.mod h1:OeTBVxQWoEmB2J2JCHmXWPJ0aksxSUOUy+nvtVEfzXc=
|
||||
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
|
||||
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=
|
||||
@@ -405,6 +412,8 @@ github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuri
|
||||
github.com/mdlayher/netlink v1.4.0/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
|
||||
github.com/mdlayher/netlink v1.4.1 h1:I154BCU+mKlIf7BgcAJB2r7QjveNPty6uNY1g9ChVfI=
|
||||
github.com/mdlayher/netlink v1.4.1/go.mod h1:e4/KuJ+s8UhfUpO9z00/fDZZmhSrs+oxyqAS9cNgn6Q=
|
||||
github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
|
||||
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
|
||||
github.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697 h1:PBb7ld5cQGfxHF2pKvb/ydtuPwdRaltGI4e0QSCuiNI=
|
||||
github.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697/go.mod h1:HtjVsQfsrBm1GDcDTUFn4ZXhftxTwO/hxrvEiRc61U4=
|
||||
github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00 h1:qEtkL8n1DAHpi5/AOgAckwGQUlMe4+jhL/GMt+GKIks=
|
||||
@@ -412,12 +421,14 @@ github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00/go.mod h1:GAFlyu4/
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.42 h1:gWGe42RGaIqXQZ+r3WUGEKBEtvPHY2SXo4dqixDNxuY=
|
||||
github.com/miekg/dns v1.1.42/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4=
|
||||
github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
|
||||
github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
|
||||
@@ -579,6 +590,10 @@ github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3 h1:fEubocuQkrl
|
||||
github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3/go.mod h1:2P+hpOwd53e7JMX/L4f3VXkv1G+33ES6IWZSrkIeWNs=
|
||||
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/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
|
||||
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2 h1:reREUgl2FG+o7YCsrZB8XLjnuKv5hEIWtnOdAbRAXZI=
|
||||
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2/go.mod h1:STqf+YV0ADdzk4ejtXFsGqDpATP9JoL0OB+hiFQbkdE=
|
||||
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/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
|
||||
@@ -598,6 +613,8 @@ github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa h1:RC4maTWLK
|
||||
github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa/go.mod h1:dSUh0FtTP8VhvkL1S+gUR1OKd9ZnSaozuI6r3m6wOig=
|
||||
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
|
||||
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
|
||||
github.com/u-root/uio v0.0.0-20210528114334-82958018845c h1:BFvcl34IGnw8yvJi8hlqLFo9EshRInwWBs2M5fGWzQA=
|
||||
github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
|
||||
github.com/ulikunitz/xz v0.5.7 h1:YvTNdFzX6+W5m9msiYg/zpkSURPPtOlzbqYjrFn7Yt4=
|
||||
github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/ultraware/funlen v0.0.3 h1:5ylVWm8wsNwH5aWo9438pwvsK0QiqVuUrt9bn7S/iLA=
|
||||
@@ -650,8 +667,9 @@ golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
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=
|
||||
@@ -693,6 +711,7 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
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-20190419010253-1f3472d942ba/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=
|
||||
@@ -716,8 +735,10 @@ golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5 h1:wjuX4b5yYQnEQHzd+CBcrcC6OVR2J1CN6mUy0oSxIPo=
|
||||
golang.org/x/net v0.0.0-20210504132125-bbd867fde50d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
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=
|
||||
@@ -748,9 +769,11 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/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-20190418153312-f0ce4c0180be/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-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -770,6 +793,7 @@ golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
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-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201109165425-215b40eba54c/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=
|
||||
@@ -787,10 +811,13 @@ golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210525143221-35b2ab0089ea/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2 h1:c8PlLMqBbOHoqtjteWm5/kbe6rNY2pbRfbIMVnepueo=
|
||||
golang.org/x/sys v0.0.0-20210817190340-bfb29a6856f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
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-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w=
|
||||
@@ -869,11 +896,13 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210521230051-c27ff9b9f6f7/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210525143454-64cb82f2b3f5 h1:5D3v3AKu7ktIhDlqZhZ4+YeNKsW+dnc2+zfFAdhwa8M=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210525143454-64cb82f2b3f5/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
|
||||
golang.zx2c4.com/wireguard/windows v0.3.15-0.20210525143335-94c0476d63e3 h1:Xw0ZuZcvq981iPGZoLrUXhrK2jOJAw/B6gZxc6g8FsU=
|
||||
golang.zx2c4.com/wireguard/windows v0.3.15-0.20210525143335-94c0476d63e3/go.mod h1:f/UVhQ6vXZKDodGB3Glgwu9B3djRxR14jIbcuxD8NBw=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210427022245-097af6e1351b/go.mod h1:a057zjmoc00UN7gVkaJt2sXVK523kMJcogDTEvPIasg=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210624150102-15b24b6179e0 h1:qINUmOnDCCF7i14oomDDkGmlda7BSDTGfge77/aqdfk=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210624150102-15b24b6179e0/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c h1:ADNrRDI5NR23/TUCnEmlLZLt4u9DnZ2nwRkPrAcFvto=
|
||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210803171230-4253848d036c/go.mod h1:+1XihzyZUBJcSc5WO9SwNA7v26puQwOEDwanaxfNXPQ=
|
||||
golang.zx2c4.com/wireguard/windows v0.3.16 h1:S42i0kp3SFHZm1mMFTtiU3OnEQJ0GRVOVlMkBhSDTZI=
|
||||
golang.zx2c4.com/wireguard/windows v0.3.16/go.mod h1:f80rkFY2CKQklps1GHE15k/M4Tq78aofbr1iQM5MTVY=
|
||||
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=
|
||||
@@ -949,8 +978,8 @@ honnef.co/go/tools v0.0.1-2020.1.6/go.mod h1:pyyisuGw24ruLjrr1ddx39WE0y9OooInRzE
|
||||
honnef.co/go/tools v0.1.4 h1:SadWOkti5uVN1FAMgxn165+Mw00fuQKyk4Gyn/inxNQ=
|
||||
honnef.co/go/tools v0.1.4/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
|
||||
inet.af/netaddr v0.0.0-20210515010201-ad03edc7c841/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20210602152128-50f8686885e3 h1:RlarOdsmOUCCvy7Xm1JchJIGuQsuKwD/Lo1bjYmfuQI=
|
||||
inet.af/netaddr v0.0.0-20210602152128-50f8686885e3/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20210721214506-ce7a8ad02cc1 h1:mxmfTV6kjXTlFqqFETnG9FQZzNFc6AKunZVAgQ3b7WA=
|
||||
inet.af/netaddr v0.0.0-20210721214506-ce7a8ad02cc1/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netstack v0.0.0-20210622165351-29b14ebc044e h1:z11NK94NQcI3DA+a3pUC/2dRYTph1kPX6B0FnCaMDzk=
|
||||
inet.af/netstack v0.0.0-20210622165351-29b14ebc044e/go.mod h1:fG3G1dekmK8oDX3iVzt8c0zICLMLSN8SjdxbXVt0WjU=
|
||||
inet.af/peercred v0.0.0-20210318190834-4259e17bb763 h1:gPSJmmVzmdy4kHhlCMx912GdiUz3k/RzJGg0ADqy1dg=
|
||||
@@ -967,5 +996,3 @@ mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jC
|
||||
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7 h1:kAREL6MPwpsk1/PQPFD3Eg7WAQR5mPTWZJaBiG5LDbY=
|
||||
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7/go.mod h1:HGC5lll35J70Y5v7vCGb9oLhHoScFwkHDJm/05RdSTc=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w=
|
||||
rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=
|
||||
|
||||
@@ -4,20 +4,64 @@
|
||||
|
||||
// Package hostinfo answers questions about the host environment that Tailscale is
|
||||
// running on.
|
||||
//
|
||||
// TODO(bradfitz): move more of control/controlclient/hostinfo_* into this package.
|
||||
package hostinfo
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/lineread"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var osVersion func() string // non-nil on some platforms
|
||||
|
||||
// New returns a partially populated Hostinfo for the current host.
|
||||
func New() *tailcfg.Hostinfo {
|
||||
hostname, _ := os.Hostname()
|
||||
hostname = dnsname.FirstLabel(hostname)
|
||||
var osv string
|
||||
if osVersion != nil {
|
||||
osv = osVersion()
|
||||
}
|
||||
return &tailcfg.Hostinfo{
|
||||
IPNVersion: version.Long,
|
||||
Hostname: hostname,
|
||||
OS: version.OS(),
|
||||
OSVersion: osv,
|
||||
Package: packageType(),
|
||||
GoArch: runtime.GOARCH,
|
||||
DeviceModel: deviceModel(),
|
||||
}
|
||||
}
|
||||
|
||||
func packageType() string {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
if _, err := os.Stat(`C:\ProgramData\chocolatey\lib\tailscale`); err == nil {
|
||||
return "choco"
|
||||
}
|
||||
case "darwin":
|
||||
// Using tailscaled or IPNExtension?
|
||||
exe, _ := os.Executable()
|
||||
return filepath.Base(exe)
|
||||
case "linux":
|
||||
// Report whether this is in a snap.
|
||||
// See https://snapcraft.io/docs/environment-variables
|
||||
// We just look at two somewhat arbitrarily.
|
||||
if os.Getenv("SNAP_NAME") != "" && os.Getenv("SNAP") != "" {
|
||||
return "snap"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// EnvType represents a known environment type.
|
||||
// The empty string, the default, means unknown.
|
||||
type EnvType string
|
||||
@@ -27,6 +71,8 @@ const (
|
||||
AWSLambda = EnvType("lm")
|
||||
Heroku = EnvType("hr")
|
||||
AzureAppService = EnvType("az")
|
||||
AWSFargate = EnvType("fg")
|
||||
FlyDotIo = EnvType("fly")
|
||||
)
|
||||
|
||||
var envType atomic.Value // of EnvType
|
||||
@@ -40,6 +86,16 @@ func GetEnvType() EnvType {
|
||||
return e
|
||||
}
|
||||
|
||||
var deviceModelAtomic atomic.Value // of string
|
||||
|
||||
// SetDeviceModel sets the device model for use in Hostinfo updates.
|
||||
func SetDeviceModel(model string) { deviceModelAtomic.Store(model) }
|
||||
|
||||
func deviceModel() string {
|
||||
s, _ := deviceModelAtomic.Load().(string)
|
||||
return s
|
||||
}
|
||||
|
||||
func getEnvType() EnvType {
|
||||
if inKnative() {
|
||||
return KNative
|
||||
@@ -53,11 +109,17 @@ func getEnvType() EnvType {
|
||||
if inAzureAppService() {
|
||||
return AzureAppService
|
||||
}
|
||||
if inAWSFargate() {
|
||||
return AWSFargate
|
||||
}
|
||||
if inFlyDotIo() {
|
||||
return FlyDotIo
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// InContainer reports whether we're running in a container.
|
||||
func InContainer() bool {
|
||||
// inContainer reports whether we're running in a container.
|
||||
func inContainer() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
return false
|
||||
}
|
||||
@@ -115,3 +177,17 @@ func inAzureAppService() bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func inAWSFargate() bool {
|
||||
if os.Getenv("AWS_EXECUTION_ENV") == "AWS_ECS_FARGATE" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func inFlyDotIo() bool {
|
||||
if os.Getenv("FLY_APP_NAME") != "" && os.Getenv("FLY_REGION") != "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -2,24 +2,31 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux && !android
|
||||
// +build linux,!android
|
||||
|
||||
package controlclient
|
||||
package hostinfo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/util/lineread"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
func init() {
|
||||
osVersion = osVersionLinux
|
||||
|
||||
if v, _ := os.ReadFile("/sys/firmware/devicetree/base/model"); len(v) > 0 {
|
||||
// Look up "Raspberry Pi 4 Model B Rev 1.2",
|
||||
// etc. Usually set on ARM SBCs.
|
||||
SetDeviceModel(strings.Trim(string(v), "\x00\r\n\t "))
|
||||
}
|
||||
}
|
||||
|
||||
func osVersionLinux() string {
|
||||
@@ -54,10 +61,10 @@ func osVersionLinux() string {
|
||||
}
|
||||
attrBuf.WriteByte(byte(b))
|
||||
}
|
||||
if hostinfo.InContainer() {
|
||||
if inContainer() {
|
||||
attrBuf.WriteString("; container")
|
||||
}
|
||||
if env := hostinfo.GetEnvType(); env != "" {
|
||||
if env := GetEnvType(); env != "" {
|
||||
fmt.Fprintf(&attrBuf, "; env=%s", env)
|
||||
}
|
||||
attr := attrBuf.String()
|
||||
29
hostinfo/hostinfo_test.go
Normal file
29
hostinfo/hostinfo_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package hostinfo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
hi := New()
|
||||
if hi == nil {
|
||||
t.Fatal("no Hostinfo")
|
||||
}
|
||||
j, err := json.MarshalIndent(hi, " ", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Got: %s", j)
|
||||
}
|
||||
|
||||
func TestOSVersion(t *testing.T) {
|
||||
if osVersion == nil {
|
||||
t.Skip("not available for OS")
|
||||
}
|
||||
t.Logf("Got: %#q", osVersion())
|
||||
}
|
||||
@@ -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 controlclient
|
||||
package hostinfo
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
@@ -156,7 +156,7 @@ func (h *Handle) Expiry() time.Time {
|
||||
}
|
||||
|
||||
func (h *Handle) AdminPageURL() string {
|
||||
return h.prefsCache.ControlURLOrDefault() + "/admin/machines"
|
||||
return h.prefsCache.AdminPageURL()
|
||||
}
|
||||
|
||||
func (h *Handle) StartLoginInteractive() {
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn/policy"
|
||||
@@ -38,6 +39,7 @@ import (
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/portlist"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -95,7 +97,7 @@ type LocalBackend struct {
|
||||
serverURL string // tailcontrol URL
|
||||
newDecompressor func() (controlclient.Decompressor, error)
|
||||
|
||||
filterHash string
|
||||
filterHash deephash.Sum
|
||||
|
||||
// The mutex protects the following elements.
|
||||
mu sync.Mutex
|
||||
@@ -179,6 +181,7 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge
|
||||
gotPortPollRes: make(chan struct{}),
|
||||
}
|
||||
b.statusChanged = sync.NewCond(&b.statusLock)
|
||||
b.e.SetStatusCallback(b.setWgengineStatus)
|
||||
|
||||
linkMon := e.GetLinkMonitor()
|
||||
b.prevIfState = linkMon.InterfaceState()
|
||||
@@ -214,6 +217,15 @@ func (b *LocalBackend) SetDirectFileRoot(dir string) {
|
||||
b.directFileRoot = dir
|
||||
}
|
||||
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) maybePauseControlClientLocked() {
|
||||
if b.cc == nil {
|
||||
return
|
||||
}
|
||||
networkUp := b.prevIfState.AnyInterfaceUp()
|
||||
b.cc.SetPaused((b.state == ipn.Stopped && b.netMap != nil) || !networkUp)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -222,11 +234,7 @@ func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
|
||||
|
||||
hadPAC := b.prevIfState.HasPAC()
|
||||
b.prevIfState = ifst
|
||||
|
||||
networkUp := ifst.AnyInterfaceUp()
|
||||
if b.cc != nil {
|
||||
go b.cc.SetPaused((b.state == ipn.Stopped && b.netMap != nil) || !networkUp)
|
||||
}
|
||||
b.maybePauseControlClientLocked()
|
||||
|
||||
// If the PAC-ness of the network changed, reconfig wireguard+route to
|
||||
// add/remove subnets.
|
||||
@@ -605,8 +613,8 @@ func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) {
|
||||
|
||||
if cc != nil {
|
||||
cc.UpdateEndpoints(0, s.LocalAddrs)
|
||||
b.stateMachine()
|
||||
}
|
||||
b.stateMachine()
|
||||
|
||||
b.statusLock.Lock()
|
||||
b.statusChanged.Broadcast()
|
||||
@@ -723,7 +731,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
hostinfo := controlclient.NewHostinfo()
|
||||
hostinfo := hostinfo.New()
|
||||
hostinfo.BackendLogID = b.backendLogID
|
||||
hostinfo.FrontendLogID = opts.FrontendLogID
|
||||
|
||||
@@ -757,6 +765,12 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
newPrefs := opts.UpdatePrefs
|
||||
newPrefs.Persist = b.prefs.Persist
|
||||
b.prefs = newPrefs
|
||||
|
||||
if opts.StateKey != "" {
|
||||
if err := b.store.WriteState(opts.StateKey, b.prefs.ToBytes()); err != nil {
|
||||
b.logf("failed to save UpdatePrefs state: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wantRunning := b.prefs.WantRunning
|
||||
@@ -770,8 +784,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
|
||||
b.inServerMode = b.prefs.ForceDaemon
|
||||
b.serverURL = b.prefs.ControlURLOrDefault()
|
||||
hostinfo.RoutableIPs = append(hostinfo.RoutableIPs, b.prefs.AdvertiseRoutes...)
|
||||
hostinfo.RequestTags = append(hostinfo.RequestTags, b.prefs.AdvertiseTags...)
|
||||
if b.inServerMode || runtime.GOOS == "windows" {
|
||||
b.logf("Start: serverMode=%v", b.inServerMode)
|
||||
}
|
||||
@@ -838,6 +850,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
DiscoPublicKey: discoPublic,
|
||||
DebugFlags: debugFlags,
|
||||
LinkMonitor: b.e.GetLinkMonitor(),
|
||||
Pinger: b.e,
|
||||
|
||||
// Don't warn about broken Linux IP forwading when
|
||||
// netstack is being used.
|
||||
@@ -857,7 +870,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
}
|
||||
|
||||
cc.SetStatusFunc(b.setClientStatus)
|
||||
b.e.SetStatusCallback(b.setWgengineStatus)
|
||||
b.e.SetNetInfoCallback(b.setNetInfo)
|
||||
|
||||
b.mu.Lock()
|
||||
@@ -933,7 +945,7 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
|
||||
localNets, _ := localNetsB.IPSet()
|
||||
logNets, _ := logNetsB.IPSet()
|
||||
|
||||
changed := deephash.UpdateHash(&b.filterHash, haveNetmap, addrs, packetFilter, localNets.Ranges(), logNets.Ranges(), shieldsUp)
|
||||
changed := deephash.Update(&b.filterHash, haveNetmap, addrs, packetFilter, localNets.Ranges(), logNets.Ranges(), shieldsUp)
|
||||
if !changed {
|
||||
return
|
||||
}
|
||||
@@ -949,7 +961,7 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
|
||||
b.logf("netmap packet filter: (shields up)")
|
||||
b.e.SetFilter(filter.NewShieldsUpFilter(localNets, logNets, oldFilter, b.logf))
|
||||
} else {
|
||||
b.logf("netmap packet filter: %v", packetFilter)
|
||||
b.logf("netmap packet filter: %v filters", len(packetFilter))
|
||||
b.e.SetFilter(filter.New(packetFilter, localNets, logNets, oldFilter, b.logf))
|
||||
}
|
||||
}
|
||||
@@ -973,6 +985,44 @@ var removeFromDefaultRoute = []netaddr.IPPrefix{
|
||||
tsaddr.TailscaleULARange(),
|
||||
}
|
||||
|
||||
// internalAndExternalInterfaces splits interface routes into "internal"
|
||||
// and "external" sets. Internal routes are those of virtual ethernet
|
||||
// network interfaces used by guest VMs and containers, such as WSL and
|
||||
// Docker.
|
||||
//
|
||||
// Given that "internal" routes don't leave the device, we choose to
|
||||
// trust them more, allowing access to them when an Exit Node is enabled.
|
||||
func internalAndExternalInterfaces() (internal, external []netaddr.IPPrefix, err error) {
|
||||
if err := interfaces.ForeachInterfaceAddress(func(iface interfaces.Interface, pfx netaddr.IPPrefix) {
|
||||
if tsaddr.IsTailscaleIP(pfx.IP()) {
|
||||
return
|
||||
}
|
||||
if pfx.IsSingleIP() {
|
||||
return
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows Hyper-V prefixes all MAC addresses with 00:15:5d.
|
||||
// https://docs.microsoft.com/en-us/troubleshoot/windows-server/virtualization/default-limit-256-dynamic-mac-addresses
|
||||
//
|
||||
// This includes WSL2 vEthernet.
|
||||
// Importantly: by default WSL2 /etc/resolv.conf points to
|
||||
// a stub resolver running on the host vEthernet IP.
|
||||
// So enabling exit nodes with the default tailnet
|
||||
// configuration breaks WSL2 DNS without this.
|
||||
mac := iface.Interface.HardwareAddr
|
||||
if len(mac) == 6 && mac[0] == 0x00 && mac[1] == 0x15 && mac[2] == 0x5d {
|
||||
internal = append(internal, pfx)
|
||||
return
|
||||
}
|
||||
}
|
||||
external = append(external, pfx)
|
||||
}); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return internal, external, nil
|
||||
}
|
||||
|
||||
func interfaceRoutes() (ips *netaddr.IPSet, hostIPs []netaddr.IP, err error) {
|
||||
var b netaddr.IPSetBuilder
|
||||
if err := interfaces.ForeachInterfaceAddress(func(_ interfaces.Interface, pfx netaddr.IPPrefix) {
|
||||
@@ -1529,7 +1579,6 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
||||
|
||||
oldHi := b.hostinfo
|
||||
newHi := oldHi.Clone()
|
||||
newHi.RoutableIPs = append([]netaddr.IPPrefix(nil), b.prefs.AdvertiseRoutes...)
|
||||
applyPrefsToHostinfo(newHi, newp)
|
||||
b.hostinfo = newHi
|
||||
hostInfoChanged := !oldHi.Equal(newHi)
|
||||
@@ -1771,7 +1820,7 @@ func (b *LocalBackend) authReconfig() {
|
||||
}
|
||||
|
||||
if uc.CorpDNS {
|
||||
addDefault := func(resolvers []tailcfg.DNSResolver) {
|
||||
addDefault := func(resolvers []dnstype.Resolver) {
|
||||
for _, resolver := range resolvers {
|
||||
res, err := parseResolver(resolver)
|
||||
if err != nil {
|
||||
@@ -1788,6 +1837,17 @@ func (b *LocalBackend) authReconfig() {
|
||||
if err != nil {
|
||||
b.logf("[unexpected] non-FQDN route suffix %q", suffix)
|
||||
}
|
||||
|
||||
// Create map entry even if len(resolvers) == 0; Issue 2706.
|
||||
// This lets the control plane send ExtraRecords for which we
|
||||
// can authoritatively answer "name not exists" for when the
|
||||
// control plane also sends this explicit but empty route
|
||||
// making it as something we handle.
|
||||
//
|
||||
// While we're already populating it, might as well size the
|
||||
// slice appropriately.
|
||||
dcfg.Routes[fqdn] = make([]netaddr.IPPort, 0, len(resolvers))
|
||||
|
||||
for _, resolver := range resolvers {
|
||||
res, err := parseResolver(resolver)
|
||||
if err != nil {
|
||||
@@ -1847,7 +1907,7 @@ func (b *LocalBackend) authReconfig() {
|
||||
b.initPeerAPIListener()
|
||||
}
|
||||
|
||||
func parseResolver(cfg tailcfg.DNSResolver) (netaddr.IPPort, error) {
|
||||
func parseResolver(cfg dnstype.Resolver) (netaddr.IPPort, error) {
|
||||
ip, err := netaddr.ParseIP(cfg.Addr)
|
||||
if err != nil {
|
||||
return netaddr.IPPort{}, fmt.Errorf("[unexpected] non-IP resolver %q", cfg.Addr)
|
||||
@@ -2108,18 +2168,21 @@ func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router
|
||||
if !default6 {
|
||||
rs.Routes = append(rs.Routes, ipv6Default)
|
||||
}
|
||||
internalIPs, externalIPs, err := internalAndExternalInterfaces()
|
||||
if err != nil {
|
||||
b.logf("failed to discover interface ips: %v", err)
|
||||
}
|
||||
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" || runtime.GOOS == "windows" {
|
||||
// Only allow local lan access on linux machines for now.
|
||||
ips, _, err := interfaceRoutes()
|
||||
if err != nil {
|
||||
b.logf("failed to discover interface ips: %v", err)
|
||||
}
|
||||
rs.LocalRoutes = internalIPs // unconditionally allow access to guest VM networks
|
||||
if prefs.ExitNodeAllowLANAccess {
|
||||
rs.LocalRoutes = ips.Prefixes()
|
||||
rs.LocalRoutes = append(rs.LocalRoutes, externalIPs...)
|
||||
if len(externalIPs) != 0 {
|
||||
b.logf("allowing exit node access to internal IPs: %v", internalIPs)
|
||||
}
|
||||
} else {
|
||||
// Explicitly add routes to the local network so that we do not
|
||||
// leak any traffic.
|
||||
rs.Routes = append(rs.Routes, ips.Prefixes()...)
|
||||
rs.Routes = append(rs.Routes, externalIPs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2148,10 +2211,24 @@ func applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *ipn.Prefs) {
|
||||
}
|
||||
if v := prefs.OSVersion; v != "" {
|
||||
hi.OSVersion = v
|
||||
|
||||
// The Android app annotates when Google Play Services
|
||||
// aren't available by tacking on a string to the
|
||||
// OSVersion. Promote that to the Hostinfo.Package
|
||||
// field instead, rather than adding a new pref, as
|
||||
// this applyPrefsToHostinfo mechanism is mostly
|
||||
// abused currently. TODO(bradfitz): instead let
|
||||
// frontends update Hostinfo, without using Prefs.
|
||||
if runtime.GOOS == "android" && strings.HasSuffix(v, " [nogoogle]") {
|
||||
hi.Package = "nogoogle"
|
||||
hi.OSVersion = strings.TrimSuffix(v, " [nogoogle]")
|
||||
}
|
||||
}
|
||||
if m := prefs.DeviceModel; m != "" {
|
||||
hi.DeviceModel = m
|
||||
}
|
||||
hi.RoutableIPs = append(prefs.AdvertiseRoutes[:0:0], prefs.AdvertiseRoutes...)
|
||||
hi.RequestTags = append(prefs.AdvertiseTags[:0:0], prefs.AdvertiseTags...)
|
||||
hi.ShieldsUp = prefs.ShieldsUp
|
||||
}
|
||||
|
||||
@@ -2167,9 +2244,7 @@ func (b *LocalBackend) enterState(newState ipn.State) {
|
||||
oldState := b.state
|
||||
b.state = newState
|
||||
prefs := b.prefs
|
||||
cc := b.cc
|
||||
netMap := b.netMap
|
||||
networkUp := b.prevIfState.AnyInterfaceUp()
|
||||
activeLogin := b.activeLogin
|
||||
authURL := b.authURL
|
||||
if newState == ipn.Running {
|
||||
@@ -2179,6 +2254,7 @@ func (b *LocalBackend) enterState(newState ipn.State) {
|
||||
// Transitioning away from running.
|
||||
b.closePeerAPIListenersLocked()
|
||||
}
|
||||
b.maybePauseControlClientLocked()
|
||||
b.mu.Unlock()
|
||||
|
||||
if oldState == newState {
|
||||
@@ -2189,10 +2265,6 @@ func (b *LocalBackend) enterState(newState ipn.State) {
|
||||
health.SetIPNState(newState.String(), prefs.WantRunning)
|
||||
b.send(ipn.Notify{State: &newState})
|
||||
|
||||
if cc != nil {
|
||||
cc.SetPaused((newState == ipn.Stopped && netMap != nil) || !networkUp)
|
||||
}
|
||||
|
||||
switch newState {
|
||||
case ipn.NeedsLogin:
|
||||
systemd.Status("Needs login: %s", authURL)
|
||||
@@ -2453,6 +2525,7 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
b.logf("active login: %v", login)
|
||||
b.activeLogin = login
|
||||
}
|
||||
b.maybePauseControlClientLocked()
|
||||
|
||||
// Determine if file sharing is enabled
|
||||
fs := hasCapability(nm, tailcfg.CapabilityFileSharing)
|
||||
@@ -2724,17 +2797,18 @@ func (b *LocalBackend) CheckIPForwarding() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const suffix = "\nSubnet routes won't work without IP forwarding.\nSee https://tailscale.com/kb/1104/enable-ip-forwarding/"
|
||||
for _, key := range keys {
|
||||
bs, err := exec.Command("sysctl", "-n", key).Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't check %s (%v).\nSubnet routes won't work without IP forwarding.", key, err)
|
||||
return fmt.Errorf("couldn't check %s (%v)%s", key, err, suffix)
|
||||
}
|
||||
on, err := strconv.ParseBool(string(bytes.TrimSpace(bs)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't parse %s (%v).\nSubnet routes won't work without IP forwarding.", key, err)
|
||||
return fmt.Errorf("couldn't parse %s (%v)%s.", key, err, suffix)
|
||||
}
|
||||
if !on {
|
||||
return fmt.Errorf("%s is disabled. Subnet routes won't work.", key)
|
||||
return fmt.Errorf("%s is disabled.%s", key, suffix)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin,ts_macext ios,ts_macext
|
||||
//go:build ts_macext && (darwin || ios)
|
||||
// +build ts_macext
|
||||
// +build darwin ios
|
||||
|
||||
package ipnlocal
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -277,7 +278,7 @@ func TestStateMachine(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
logf := t.Logf
|
||||
store := new(ipn.MemoryStore)
|
||||
store := new(testStateStorage)
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logf, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFakeUserspaceEngine: %v", err)
|
||||
@@ -353,7 +354,7 @@ func TestStateMachine(t *testing.T) {
|
||||
c.Assert(b.Start(ipn.Options{StateKey: ipn.GlobalDaemonStateKey}), qt.IsNil)
|
||||
{
|
||||
// BUG: strictly, it should pause, not unpause, here, since !WantRunning.
|
||||
c.Assert([]string{"Shutdown", "New", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert([]string{"Shutdown", "unpause", "New", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
|
||||
nn := notifies.drain(2)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
@@ -388,7 +389,7 @@ func TestStateMachine(t *testing.T) {
|
||||
url1 := "http://localhost:1/1"
|
||||
cc.send(nil, url1, false, nil)
|
||||
{
|
||||
c.Assert(cc.getCalls(), qt.DeepEquals, []string{})
|
||||
c.Assert(cc.getCalls(), qt.DeepEquals, []string{"unpause"})
|
||||
|
||||
// ...but backend eats that notification, because the user
|
||||
// didn't explicitly request interactive login yet, and
|
||||
@@ -413,7 +414,7 @@ func TestStateMachine(t *testing.T) {
|
||||
// We're still not logged in so there's nothing we can do
|
||||
// with it. (And empirically, it's providing an empty list
|
||||
// of endpoints.)
|
||||
c.Assert([]string{"UpdateEndpoints"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert([]string{"UpdateEndpoints", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].BrowseToURL, qt.Not(qt.IsNil))
|
||||
c.Assert(url1, qt.Equals, *nn[0].BrowseToURL)
|
||||
}
|
||||
@@ -439,7 +440,7 @@ func TestStateMachine(t *testing.T) {
|
||||
cc.send(nil, url2, false, nil)
|
||||
{
|
||||
// BUG: UpdateEndpoints again, this is getting silly.
|
||||
c.Assert([]string{"UpdateEndpoints"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert([]string{"UpdateEndpoints", "unpause", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
|
||||
// This time, backend should emit it to the UI right away,
|
||||
// because the UI is anxiously awaiting a new URL to visit.
|
||||
@@ -469,7 +470,7 @@ func TestStateMachine(t *testing.T) {
|
||||
// wait until it gets into Starting.
|
||||
// TODO: (Currently this test doesn't detect that bug, but
|
||||
// it's visible in the logs)
|
||||
c.Assert([]string{"unpause", "UpdateEndpoints"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert([]string{"unpause", "unpause", "UpdateEndpoints", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].LoginFinished, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[2].State, qt.Not(qt.IsNil))
|
||||
@@ -482,7 +483,7 @@ func TestStateMachine(t *testing.T) {
|
||||
notifies.expect(1)
|
||||
// BUG: the real controlclient sends LoginFinished with every
|
||||
// notification while it's in StateAuthenticated, but not StateSynced.
|
||||
// We should send it exactly once, or every time we're authenticated,
|
||||
// It should send it exactly once, or every time we're authenticated,
|
||||
// but the current code is brittle.
|
||||
// (ie. I suspect it would be better to change false->true in send()
|
||||
// below, and do the same in the real controlclient.)
|
||||
@@ -491,7 +492,7 @@ func TestStateMachine(t *testing.T) {
|
||||
})
|
||||
{
|
||||
nn := notifies.drain(1)
|
||||
c.Assert([]string{"unpause", "UpdateEndpoints"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert([]string{"unpause", "unpause", "UpdateEndpoints", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].State, qt.Not(qt.IsNil))
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[0].State)
|
||||
}
|
||||
@@ -523,6 +524,7 @@ func TestStateMachine(t *testing.T) {
|
||||
|
||||
// The user changes their preference to WantRunning after all.
|
||||
t.Logf("\n\nWantRunning -> true")
|
||||
store.awaitWrite()
|
||||
notifies.expect(2)
|
||||
b.EditPrefs(&ipn.MaskedPrefs{
|
||||
WantRunningSet: true,
|
||||
@@ -532,15 +534,17 @@ func TestStateMachine(t *testing.T) {
|
||||
nn := notifies.drain(2)
|
||||
// BUG: UpdateEndpoints isn't needed here.
|
||||
// BUG: Login isn't needed here. We never logged out.
|
||||
c.Assert([]string{"Login", "unpause", "UpdateEndpoints"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert([]string{"Login", "unpause", "UpdateEndpoints", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
// BUG: I would expect Prefs to change first, and state after.
|
||||
c.Assert(nn[0].State, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[0].State)
|
||||
c.Assert(store.sawWrite(), qt.IsTrue)
|
||||
}
|
||||
|
||||
// Test the fast-path frontend reconnection.
|
||||
// This one is very finicky, so we have to force State==Running.
|
||||
// This one is very finicky, so we have to force State==Running
|
||||
// or it won't use the fast path.
|
||||
// TODO: actually get to State==Running, rather than cheating.
|
||||
// That'll require spinning up a fake DERP server and putting it in
|
||||
// the netmap.
|
||||
@@ -561,18 +565,20 @@ func TestStateMachine(t *testing.T) {
|
||||
b.state = ipn.Starting
|
||||
|
||||
// User wants to logout.
|
||||
store.awaitWrite()
|
||||
t.Logf("\n\nLogout (async)")
|
||||
notifies.expect(2)
|
||||
b.Logout()
|
||||
{
|
||||
nn := notifies.drain(2)
|
||||
c.Assert([]string{"pause", "StartLogout"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert([]string{"pause", "StartLogout", "pause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].State, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(ipn.Stopped, qt.Equals, *nn[0].State)
|
||||
c.Assert(nn[1].Prefs.LoggedOut, qt.IsTrue)
|
||||
c.Assert(nn[1].Prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(ipn.Stopped, qt.Equals, b.State())
|
||||
c.Assert(store.sawWrite(), qt.IsTrue)
|
||||
}
|
||||
|
||||
// Let's make the logout succeed.
|
||||
@@ -582,7 +588,7 @@ func TestStateMachine(t *testing.T) {
|
||||
cc.send(nil, "", false, nil)
|
||||
{
|
||||
nn := notifies.drain(1)
|
||||
c.Assert([]string{"unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert([]string{"unpause", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].State, qt.Not(qt.IsNil))
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[0].State)
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
@@ -598,7 +604,7 @@ func TestStateMachine(t *testing.T) {
|
||||
notifies.drain(0)
|
||||
// BUG: the backend has already called StartLogout, and we're
|
||||
// still logged out. So it shouldn't call it again.
|
||||
c.Assert([]string{"StartLogout"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert([]string{"StartLogout", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
@@ -612,8 +618,7 @@ func TestStateMachine(t *testing.T) {
|
||||
cc.send(nil, "", false, nil)
|
||||
{
|
||||
notifies.drain(0)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(cc.getCalls(), qt.DeepEquals, []string{"unpause", "unpause"})
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
@@ -627,7 +632,7 @@ func TestStateMachine(t *testing.T) {
|
||||
// I guess, since that's supposed to be synchronous.
|
||||
{
|
||||
notifies.drain(0)
|
||||
c.Assert([]string{"Logout"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert([]string{"Logout", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
@@ -641,8 +646,7 @@ func TestStateMachine(t *testing.T) {
|
||||
cc.send(nil, "", false, nil)
|
||||
{
|
||||
notifies.drain(0)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(cc.getCalls(), qt.DeepEquals, []string{"unpause", "unpause"})
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
@@ -673,7 +677,7 @@ func TestStateMachine(t *testing.T) {
|
||||
// BUG: We already called Shutdown(), no need to do it again.
|
||||
// BUG: Way too soon for UpdateEndpoints.
|
||||
// BUG: don't unpause because we're not logged in.
|
||||
c.Assert([]string{"Shutdown", "New", "UpdateEndpoints", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert([]string{"Shutdown", "unpause", "New", "UpdateEndpoints", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
|
||||
nn := notifies.drain(2)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
@@ -698,7 +702,7 @@ func TestStateMachine(t *testing.T) {
|
||||
})
|
||||
{
|
||||
nn := notifies.drain(3)
|
||||
c.Assert([]string{"unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert([]string{"unpause", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].LoginFinished, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[2].State, qt.Not(qt.IsNil))
|
||||
@@ -738,9 +742,8 @@ func TestStateMachine(t *testing.T) {
|
||||
// on startup, otherwise UIs can't show the node list, login
|
||||
// name, etc when in state ipn.Stopped.
|
||||
// Arguably they shouldn't try. But they currently do.
|
||||
c.Assert([]string{"Shutdown", "New", "UpdateEndpoints", "Login", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
|
||||
nn := notifies.drain(2)
|
||||
c.Assert([]string{"Shutdown", "unpause", "New", "UpdateEndpoints", "Login", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].State, qt.Not(qt.IsNil))
|
||||
@@ -749,6 +752,25 @@ func TestStateMachine(t *testing.T) {
|
||||
c.Assert(ipn.Stopped, qt.Equals, *nn[1].State)
|
||||
}
|
||||
|
||||
// When logged in but !WantRunning, ipn leaves us unpaused to retrieve
|
||||
// the first netmap. Simulate that netmap being received, after which
|
||||
// it should pause us, to avoid wasting CPU retrieving unnecessarily
|
||||
// additional netmap updates.
|
||||
//
|
||||
// TODO: really the various GUIs and prefs should be refactored to
|
||||
// not require the netmap structure at all when starting while
|
||||
// !WantRunning. That would remove the need for this (or contacting
|
||||
// the control server at all when stopped).
|
||||
t.Logf("\n\nStart4 -> netmap")
|
||||
notifies.expect(0)
|
||||
cc.send(nil, "", true, &netmap.NetworkMap{
|
||||
MachineStatus: tailcfg.MachineAuthorized,
|
||||
})
|
||||
{
|
||||
notifies.drain(0)
|
||||
c.Assert([]string{"pause", "pause"}, qt.DeepEquals, cc.getCalls())
|
||||
}
|
||||
|
||||
// Request connection.
|
||||
// The state machine didn't call Login() earlier, so now it needs to.
|
||||
t.Logf("\n\nWantRunning4 -> true")
|
||||
@@ -775,7 +797,7 @@ func TestStateMachine(t *testing.T) {
|
||||
})
|
||||
{
|
||||
nn := notifies.drain(2)
|
||||
c.Assert([]string{"unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert([]string{"pause"}, qt.DeepEquals, cc.getCalls())
|
||||
// BUG: I would expect Prefs to change first, and state after.
|
||||
c.Assert(nn[0].State, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
|
||||
@@ -785,25 +807,27 @@ func TestStateMachine(t *testing.T) {
|
||||
// We want to try logging in as a different user, while Stopped.
|
||||
// First, start the login process (without logging out first).
|
||||
t.Logf("\n\nLoginDifferent")
|
||||
notifies.expect(2)
|
||||
notifies.expect(1)
|
||||
b.StartLoginInteractive()
|
||||
url3 := "http://localhost:1/3"
|
||||
cc.send(nil, url3, false, nil)
|
||||
{
|
||||
nn := notifies.drain(2)
|
||||
nn := notifies.drain(1)
|
||||
// It might seem like WantRunning should switch to true here,
|
||||
// but that would be risky since we already have a valid
|
||||
// user account. It might try to reconnect to the old account
|
||||
// before the new one is ready. So no change yet.
|
||||
c.Assert([]string{"Login", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
//
|
||||
// Because the login hasn't yet completed, the old login
|
||||
// is still valid, so it's correct that we stay paused.
|
||||
c.Assert([]string{"Login", "pause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].BrowseToURL, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].State, qt.Not(qt.IsNil))
|
||||
c.Assert(*nn[0].BrowseToURL, qt.Equals, url3)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[1].State)
|
||||
}
|
||||
|
||||
// Now, let's say the interactive login completed, using a different
|
||||
// user account than before.
|
||||
// Now, let's complete the interactive login, using a different
|
||||
// user account than before. WantRunning changes to true after an
|
||||
// interactive login, so we end up unpaused.
|
||||
t.Logf("\n\nLoginDifferent URL visited")
|
||||
notifies.expect(3)
|
||||
cc.persist.LoginName = "user3"
|
||||
@@ -812,7 +836,13 @@ func TestStateMachine(t *testing.T) {
|
||||
})
|
||||
{
|
||||
nn := notifies.drain(3)
|
||||
c.Assert([]string{"unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
// BUG: pause() being called here is a bad sign.
|
||||
// It means that either the state machine ran at least once
|
||||
// with the old netmap, or it ran with the new login+netmap
|
||||
// and !WantRunning. But since it's a fresh and successful
|
||||
// new login, WantRunning is true, so there was never a
|
||||
// reason to pause().
|
||||
c.Assert([]string{"pause", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].LoginFinished, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[2].State, qt.Not(qt.IsNil))
|
||||
@@ -831,7 +861,7 @@ func TestStateMachine(t *testing.T) {
|
||||
{
|
||||
// NOTE: cc.Shutdown() is correct here, since we didn't call
|
||||
// b.Shutdown() ourselves.
|
||||
c.Assert([]string{"Shutdown", "New", "UpdateEndpoints", "Login"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert([]string{"Shutdown", "unpause", "New", "UpdateEndpoints", "Login", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
|
||||
nn := notifies.drain(1)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
@@ -850,7 +880,7 @@ func TestStateMachine(t *testing.T) {
|
||||
})
|
||||
{
|
||||
nn := notifies.drain(1)
|
||||
c.Assert([]string{"unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert([]string{"unpause", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
// NOTE: No LoginFinished message since no interactive
|
||||
// login was needed.
|
||||
c.Assert(nn[0].State, qt.Not(qt.IsNil))
|
||||
@@ -861,3 +891,29 @@ func TestStateMachine(t *testing.T) {
|
||||
c.Assert(ipn.Starting, qt.Equals, b.State())
|
||||
}
|
||||
}
|
||||
|
||||
type testStateStorage struct {
|
||||
mem ipn.MemoryStore
|
||||
written syncs.AtomicBool
|
||||
}
|
||||
|
||||
func (s *testStateStorage) ReadState(id ipn.StateKey) ([]byte, error) {
|
||||
return s.mem.ReadState(id)
|
||||
}
|
||||
|
||||
func (s *testStateStorage) WriteState(id ipn.StateKey, bs []byte) error {
|
||||
s.written.Set(true)
|
||||
return s.mem.WriteState(id, bs)
|
||||
}
|
||||
|
||||
// awaitWrite clears the "I've seen writes" bit, in prep for a future
|
||||
// call to sawWrite to see if a write arrived.
|
||||
func (s *testStateStorage) awaitWrite() { s.written.Set(false) }
|
||||
|
||||
// sawWrite reports whether there's been a WriteState call since the most
|
||||
// recent awaitWrite call.
|
||||
func (s *testStateStorage) sawWrite() bool {
|
||||
v := s.written.Get()
|
||||
s.awaitWrite()
|
||||
return v
|
||||
}
|
||||
|
||||
@@ -96,6 +96,13 @@ type PeerStatus struct {
|
||||
KeepAlive bool
|
||||
ExitNode bool // true if this is the currently selected exit node.
|
||||
|
||||
// Active is whether the node was recently active. The
|
||||
// definition is somewhat undefined but has historically and
|
||||
// currently means that there was some packet sent to this
|
||||
// peer in the past two minutes. That definition is subject to
|
||||
// change.
|
||||
Active bool
|
||||
|
||||
PeerAPIURL []string
|
||||
Capabilities []string `json:",omitempty"`
|
||||
|
||||
@@ -277,6 +284,9 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
|
||||
if st.ShareeNode {
|
||||
e.ShareeNode = true
|
||||
}
|
||||
if st.Active {
|
||||
e.Active = true
|
||||
}
|
||||
}
|
||||
|
||||
type StatusUpdater interface {
|
||||
@@ -377,9 +387,7 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
|
||||
)
|
||||
f("<td>")
|
||||
|
||||
// TODO: let server report this active bool instead
|
||||
active := !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
|
||||
if active {
|
||||
if ps.Active {
|
||||
if ps.Relay != "" && ps.CurAddr == "" {
|
||||
f("relay <b>%s</b>", html.EscapeString(ps.Relay))
|
||||
} else if ps.CurAddr != "" {
|
||||
|
||||
450
ipn/localapi/cert.go
Normal file
450
ipn/localapi/cert.go
Normal file
@@ -0,0 +1,450 @@
|
||||
// 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.
|
||||
|
||||
//go:build !ios && !android
|
||||
// +build !ios,!android
|
||||
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/acme"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// Process-wide cache. (A new *Handler is created per connection,
|
||||
// effectively per request)
|
||||
var (
|
||||
// acmeMu guards all ACME operations, so concurrent requests
|
||||
// for certs don't slam ACME. The first will go through and
|
||||
// populate the on-disk cache and the rest should use that.
|
||||
acmeMu sync.Mutex
|
||||
|
||||
renewMu sync.Mutex // lock order: don't hold acmeMu and renewMu at the same time
|
||||
lastRenewCheck = map[string]time.Time{}
|
||||
)
|
||||
|
||||
func (h *Handler) certDir() (string, error) {
|
||||
base := paths.DefaultTailscaledStateFile()
|
||||
if base == "" {
|
||||
return "", errors.New("no default DefaultTailscaledStateFile")
|
||||
}
|
||||
full := filepath.Join(filepath.Dir(base), "certs")
|
||||
if err := os.MkdirAll(full, 0700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return full, nil
|
||||
}
|
||||
|
||||
var acmeDebug, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_ACME"))
|
||||
|
||||
func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "cert access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
dir, err := h.certDir()
|
||||
if err != nil {
|
||||
h.logf("certDir: %v", err)
|
||||
http.Error(w, "failed to get cert dir", 500)
|
||||
return
|
||||
}
|
||||
|
||||
domain := strings.TrimPrefix(r.URL.Path, "/localapi/v0/cert/")
|
||||
if domain == r.URL.Path {
|
||||
http.Error(w, "internal handler config wired wrong", 500)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
logf := logger.WithPrefix(h.logf, fmt.Sprintf("cert(%q): ", domain))
|
||||
traceACME := func(v interface{}) {
|
||||
if !acmeDebug {
|
||||
return
|
||||
}
|
||||
j, _ := json.MarshalIndent(v, "", "\t")
|
||||
log.Printf("acme %T: %s", v, j)
|
||||
}
|
||||
|
||||
if pair, ok := h.getCertPEMCached(dir, domain, now); ok {
|
||||
future := now.AddDate(0, 0, 14)
|
||||
if h.shouldStartDomainRenewal(dir, domain, future) {
|
||||
logf("starting async renewal")
|
||||
// Start renewal in the background.
|
||||
go h.getCertPEM(context.Background(), logf, traceACME, dir, domain, future)
|
||||
}
|
||||
serveKeyPair(w, r, pair)
|
||||
return
|
||||
}
|
||||
|
||||
pair, err := h.getCertPEM(r.Context(), logf, traceACME, dir, domain, now)
|
||||
if err != nil {
|
||||
logf("getCertPEM: %v", err)
|
||||
http.Error(w, fmt.Sprint(err), 500)
|
||||
return
|
||||
}
|
||||
serveKeyPair(w, r, pair)
|
||||
}
|
||||
|
||||
func (h *Handler) shouldStartDomainRenewal(dir, domain string, future time.Time) bool {
|
||||
renewMu.Lock()
|
||||
defer renewMu.Unlock()
|
||||
now := time.Now()
|
||||
if last, ok := lastRenewCheck[domain]; ok && now.Sub(last) < time.Minute {
|
||||
// We checked very recently. Don't bother reparsing &
|
||||
// validating the x509 cert.
|
||||
return false
|
||||
}
|
||||
lastRenewCheck[domain] = now
|
||||
_, ok := h.getCertPEMCached(dir, domain, future)
|
||||
return !ok
|
||||
}
|
||||
|
||||
func serveKeyPair(w http.ResponseWriter, r *http.Request, p *keyPair) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
switch r.URL.Query().Get("type") {
|
||||
case "", "crt", "cert":
|
||||
w.Write(p.certPEM)
|
||||
case "key":
|
||||
w.Write(p.keyPEM)
|
||||
case "pair":
|
||||
w.Write(p.keyPEM)
|
||||
w.Write(p.certPEM)
|
||||
default:
|
||||
http.Error(w, `invalid type; want "cert" (default), "key", or "pair"`, 400)
|
||||
}
|
||||
}
|
||||
|
||||
type keyPair struct {
|
||||
certPEM []byte
|
||||
keyPEM []byte
|
||||
cached bool
|
||||
}
|
||||
|
||||
func keyFile(dir, domain string) string { return filepath.Join(dir, domain+".key") }
|
||||
func certFile(dir, domain string) string { return filepath.Join(dir, domain+".crt") }
|
||||
|
||||
// getCertPEMCached returns a non-nil keyPair and true if a cached
|
||||
// keypair for domain exists on disk in dir that is valid at the
|
||||
// provided now time.
|
||||
func (h *Handler) getCertPEMCached(dir, domain string, now time.Time) (p *keyPair, ok bool) {
|
||||
if keyPEM, err := os.ReadFile(keyFile(dir, domain)); err == nil {
|
||||
certPEM, _ := os.ReadFile(certFile(dir, domain))
|
||||
if validCertPEM(domain, keyPEM, certPEM, now) {
|
||||
return &keyPair{certPEM: certPEM, keyPEM: keyPEM, cached: true}, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (h *Handler) getCertPEM(ctx context.Context, logf logger.Logf, traceACME func(interface{}), dir, domain string, now time.Time) (*keyPair, error) {
|
||||
acmeMu.Lock()
|
||||
defer acmeMu.Unlock()
|
||||
|
||||
if p, ok := h.getCertPEMCached(dir, domain, now); ok {
|
||||
return p, nil
|
||||
}
|
||||
|
||||
key, err := acmeKey(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acmeKey: %w", err)
|
||||
}
|
||||
ac := &acme.Client{Key: key}
|
||||
|
||||
a, err := ac.GetReg(ctx, "" /* pre-RFC param */)
|
||||
switch {
|
||||
case err == nil:
|
||||
// Great, already registered.
|
||||
logf("already had ACME account.")
|
||||
case err == acme.ErrNoAccount:
|
||||
a, err = ac.Register(ctx, new(acme.Account), acme.AcceptTOS)
|
||||
if err == acme.ErrAccountAlreadyExists {
|
||||
// Potential race. Double check.
|
||||
a, err = ac.GetReg(ctx, "" /* pre-RFC param */)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acme.Register: %w", err)
|
||||
}
|
||||
logf("registered ACME account.")
|
||||
traceACME(a)
|
||||
default:
|
||||
return nil, fmt.Errorf("acme.GetReg: %w", err)
|
||||
|
||||
}
|
||||
if a.Status != acme.StatusValid {
|
||||
return nil, fmt.Errorf("unexpected ACME account status %q", a.Status)
|
||||
}
|
||||
|
||||
// Before hitting LetsEncrypt, see if this is a domain that Tailscale will do DNS challenges for.
|
||||
st := h.b.StatusWithoutPeers()
|
||||
if err := checkCertDomain(st, domain); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
order, err := ac.AuthorizeOrder(ctx, []acme.AuthzID{{Type: "dns", Value: domain}})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
traceACME(order)
|
||||
|
||||
for _, aurl := range order.AuthzURLs {
|
||||
az, err := ac.GetAuthorization(ctx, aurl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
traceACME(az)
|
||||
for _, ch := range az.Challenges {
|
||||
if ch.Type == "dns-01" {
|
||||
rec, err := ac.DNS01ChallengeRecord(ch.Token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := "_acme-challenge." + domain
|
||||
|
||||
var resolver net.Resolver
|
||||
var ok bool
|
||||
txts, _ := resolver.LookupTXT(ctx, key)
|
||||
for _, txt := range txts {
|
||||
if txt == rec {
|
||||
ok = true
|
||||
logf("TXT record already existed")
|
||||
break
|
||||
}
|
||||
}
|
||||
if !ok {
|
||||
err = h.b.SetDNS(ctx, key, rec)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SetDNS %q => %q: %w", key, rec, err)
|
||||
}
|
||||
logf("did SetDNS")
|
||||
}
|
||||
|
||||
chal, err := ac.Accept(ctx, ch)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Accept: %v", err)
|
||||
}
|
||||
traceACME(chal)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wait0 := time.Now()
|
||||
orderURI := order.URI
|
||||
for {
|
||||
order, err = ac.WaitOrder(ctx, orderURI)
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
if oe, ok := err.(*acme.OrderError); ok && oe.Status == acme.StatusInvalid {
|
||||
if time.Since(wait0) > 2*time.Minute {
|
||||
return nil, errors.New("timeout waiting for order to not be invalid")
|
||||
}
|
||||
log.Printf("order invalid; waiting...")
|
||||
select {
|
||||
case <-time.After(5 * time.Second):
|
||||
continue
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("WaitOrder: %v", err)
|
||||
}
|
||||
traceACME(order)
|
||||
|
||||
certPrivKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var privPEM bytes.Buffer
|
||||
if err := encodeECDSAKey(&privPEM, certPrivKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ioutil.WriteFile(keyFile(dir, domain), privPEM.Bytes(), 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
csr, err := certRequest(certPrivKey, domain, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
der, _, err := ac.CreateOrderCert(ctx, order.FinalizeURL, csr, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CreateOrder: %v", err)
|
||||
}
|
||||
|
||||
var certPEM bytes.Buffer
|
||||
for _, b := range der {
|
||||
pb := &pem.Block{Type: "CERTIFICATE", Bytes: b}
|
||||
if err := pem.Encode(&certPEM, pb); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := ioutil.WriteFile(certFile(dir, domain), certPEM.Bytes(), 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &keyPair{certPEM: certPEM.Bytes(), keyPEM: privPEM.Bytes()}, nil
|
||||
}
|
||||
|
||||
// certRequest generates a CSR for the given common name cn and optional SANs.
|
||||
func certRequest(key crypto.Signer, cn string, ext []pkix.Extension, san ...string) ([]byte, error) {
|
||||
req := &x509.CertificateRequest{
|
||||
Subject: pkix.Name{CommonName: cn},
|
||||
DNSNames: san,
|
||||
ExtraExtensions: ext,
|
||||
}
|
||||
return x509.CreateCertificateRequest(rand.Reader, req, key)
|
||||
}
|
||||
|
||||
func encodeECDSAKey(w io.Writer, key *ecdsa.PrivateKey) error {
|
||||
b, err := x509.MarshalECPrivateKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pb := &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}
|
||||
return pem.Encode(w, pb)
|
||||
}
|
||||
|
||||
// parsePrivateKey is a copy of x/crypto/acme's parsePrivateKey.
|
||||
//
|
||||
// Attempt to parse the given private key DER block. OpenSSL 0.9.8 generates
|
||||
// PKCS#1 private keys by default, while OpenSSL 1.0.0 generates PKCS#8 keys.
|
||||
// OpenSSL ecparam generates SEC1 EC private keys for ECDSA. We try all three.
|
||||
//
|
||||
// Inspired by parsePrivateKey in crypto/tls/tls.go.
|
||||
func parsePrivateKey(der []byte) (crypto.Signer, error) {
|
||||
if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
|
||||
return key, nil
|
||||
}
|
||||
if key, err := x509.ParsePKCS8PrivateKey(der); err == nil {
|
||||
switch key := key.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return key, nil
|
||||
case *ecdsa.PrivateKey:
|
||||
return key, nil
|
||||
default:
|
||||
return nil, errors.New("acme/autocert: unknown private key type in PKCS#8 wrapping")
|
||||
}
|
||||
}
|
||||
if key, err := x509.ParseECPrivateKey(der); err == nil {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("acme/autocert: failed to parse private key")
|
||||
}
|
||||
|
||||
func acmeKey(dir string) (crypto.Signer, error) {
|
||||
pemName := filepath.Join(dir, "acme-account.key.pem")
|
||||
if v, err := ioutil.ReadFile(pemName); err == nil {
|
||||
priv, _ := pem.Decode(v)
|
||||
if priv == nil || !strings.Contains(priv.Type, "PRIVATE") {
|
||||
return nil, errors.New("acme/autocert: invalid account key found in cache")
|
||||
}
|
||||
return parsePrivateKey(priv.Bytes)
|
||||
}
|
||||
|
||||
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pemBuf bytes.Buffer
|
||||
if err := encodeECDSAKey(&pemBuf, privKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ioutil.WriteFile(pemName, pemBuf.Bytes(), 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return privKey, nil
|
||||
}
|
||||
|
||||
func validCertPEM(domain string, keyPEM, certPEM []byte, now time.Time) bool {
|
||||
if len(keyPEM) == 0 || len(certPEM) == 0 {
|
||||
return false
|
||||
}
|
||||
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var leaf *x509.Certificate
|
||||
intermediates := x509.NewCertPool()
|
||||
for i, certDER := range tlsCert.Certificate {
|
||||
cert, err := x509.ParseCertificate(certDER)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if i == 0 {
|
||||
leaf = cert
|
||||
} else {
|
||||
intermediates.AddCert(cert)
|
||||
}
|
||||
}
|
||||
if leaf == nil {
|
||||
return false
|
||||
}
|
||||
_, err = leaf.Verify(x509.VerifyOptions{
|
||||
DNSName: domain,
|
||||
CurrentTime: now,
|
||||
Intermediates: intermediates,
|
||||
})
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func checkCertDomain(st *ipnstate.Status, domain string) error {
|
||||
if domain == "" {
|
||||
return errors.New("missing domain name")
|
||||
}
|
||||
for _, d := range st.CertDomains {
|
||||
if d == domain {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
// Transitional way while server doesn't yet populate CertDomains: also permit the client
|
||||
// attempting Self.DNSName.
|
||||
okay := st.CertDomains[:len(st.CertDomains):len(st.CertDomains)]
|
||||
if st.Self != nil {
|
||||
if v := strings.Trim(st.Self.DNSName, "."); v != "" {
|
||||
if v == domain {
|
||||
return nil
|
||||
}
|
||||
okay = append(okay, v)
|
||||
}
|
||||
}
|
||||
switch len(okay) {
|
||||
case 0:
|
||||
return errors.New("your Tailscale account does not support getting TLS certs")
|
||||
case 1:
|
||||
return fmt.Errorf("invalid domain %q; only %q is permitted", domain, okay[0])
|
||||
default:
|
||||
return fmt.Errorf("invalid domain %q; must be one of %q", domain, okay)
|
||||
}
|
||||
}
|
||||
17
ipn/localapi/disabled_stubs.go
Normal file
17
ipn/localapi/disabled_stubs.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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.
|
||||
|
||||
//go:build ios || android
|
||||
// +build ios android
|
||||
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "disabled on "+runtime.GOOS, http.StatusNotFound)
|
||||
}
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
func randHex(n int) string {
|
||||
@@ -64,6 +65,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "server has no local backend", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Tailscale-Version", version.Long)
|
||||
if h.RequiredPassword != "" {
|
||||
_, pass, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
@@ -83,6 +85,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.serveFilePut(w, r)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/localapi/v0/cert/") {
|
||||
h.serveCert(w, r)
|
||||
return
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/localapi/v0/whois":
|
||||
h.serveWhoIs(w, r)
|
||||
@@ -266,7 +272,7 @@ func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "file access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
suffix := strings.TrimPrefix(r.URL.Path, "/localapi/v0/files/")
|
||||
suffix := strings.TrimPrefix(r.URL.EscapedPath(), "/localapi/v0/files/")
|
||||
if suffix == "" {
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "want GET to list files", 400)
|
||||
|
||||
18
ipn/prefs.go
18
ipn/prefs.go
@@ -25,11 +25,17 @@ import (
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=Prefs -output=prefs_clone.go
|
||||
|
||||
// DefaultControlURL returns the URL base of the control plane
|
||||
// DefaultControlURL is the URL base of the control plane
|
||||
// ("coordination server") for use when no explicit one is configured.
|
||||
// The default control plane is the hosted version run by Tailscale.com.
|
||||
const DefaultControlURL = "https://controlplane.tailscale.com"
|
||||
|
||||
// IsLoginServerSynonym reports whether a URL is a drop-in replacement
|
||||
// for the primary Tailscale login server.
|
||||
func IsLoginServerSynonym(val interface{}) bool {
|
||||
return val == "https://login.tailscale.com" || val == "https://controlplane.tailscale.com"
|
||||
}
|
||||
|
||||
// Prefs are the user modifiable settings of the Tailscale node agent.
|
||||
type Prefs struct {
|
||||
// ControlURL is the URL of the control server to use.
|
||||
@@ -405,6 +411,16 @@ func (p *Prefs) ControlURLOrDefault() string {
|
||||
return DefaultControlURL
|
||||
}
|
||||
|
||||
// AdminPageURL returns the admin web site URL for the current ControlURL.
|
||||
func (p *Prefs) AdminPageURL() string {
|
||||
url := p.ControlURLOrDefault()
|
||||
if IsLoginServerSynonym(url) {
|
||||
// TODO(crawshaw): In future release, make this https://console.tailscale.com
|
||||
url = "https://login.tailscale.com"
|
||||
}
|
||||
return url + "/admin/machines"
|
||||
}
|
||||
|
||||
// PrefsFromBytes deserializes Prefs from a JSON blob. If
|
||||
// enforceDefaults is true, Prefs.RouteAll and Prefs.AllowSingleHosts
|
||||
// are forced on.
|
||||
|
||||
@@ -67,9 +67,6 @@ func (s *MemoryStore) String() string { return "MemoryStore" }
|
||||
func (s *MemoryStore) ReadState(id StateKey) ([]byte, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.cache == nil {
|
||||
s.cache = map[StateKey][]byte{}
|
||||
}
|
||||
bs, ok := s.cache[id]
|
||||
if !ok {
|
||||
return nil, ErrStateNotExist
|
||||
|
||||
@@ -128,6 +128,13 @@ func (l logWriter) Write(buf []byte) (int, error) {
|
||||
// logsDir returns the directory to use for log configuration and
|
||||
// buffer storage.
|
||||
func logsDir(logf logger.Logf) string {
|
||||
if d := os.Getenv("TS_LOGS_DIR"); d != "" {
|
||||
fi, err := os.Stat(d)
|
||||
if err == nil && fi.IsDir() {
|
||||
return d
|
||||
}
|
||||
}
|
||||
|
||||
// STATE_DIRECTORY is set by systemd 240+ but we support older
|
||||
// systems-d. For example, Ubuntu 18.04 (Bionic Beaver) is 237.
|
||||
systemdStateDir := os.Getenv("STATE_DIRECTORY")
|
||||
@@ -180,6 +187,10 @@ func runningUnderSystemd() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func redirectStderrToLogPanics() bool {
|
||||
return runningUnderSystemd() || os.Getenv("TS_PLEASE_PANIC") != ""
|
||||
}
|
||||
|
||||
// tryFixLogStateLocation is a temporary fixup for
|
||||
// https://github.com/tailscale/tailscale/issues/247 . We accidentally
|
||||
// wrote logging state files to /, and then later to $CACHE_DIRECTORY
|
||||
@@ -428,9 +439,14 @@ func New(collection string) *Policy {
|
||||
c.HTTPC = &http.Client{Transport: newLogtailTransport(u.Host)}
|
||||
}
|
||||
|
||||
filchBuf, filchErr := filch.New(filepath.Join(dir, cmdName), filch.Options{})
|
||||
filchBuf, filchErr := filch.New(filepath.Join(dir, cmdName), filch.Options{
|
||||
ReplaceStderr: redirectStderrToLogPanics(),
|
||||
})
|
||||
if filchBuf != nil {
|
||||
c.Buffer = filchBuf
|
||||
if filchBuf.OrigStderr != nil {
|
||||
c.Stderr = filchBuf.OrigStderr
|
||||
}
|
||||
}
|
||||
lw := logtail.NewLogger(c, log.Printf)
|
||||
log.SetFlags(0) // other logflags are set on console, not here
|
||||
|
||||
@@ -30,6 +30,15 @@ type Filch struct {
|
||||
alt *os.File
|
||||
altscan *bufio.Scanner
|
||||
recovered int64
|
||||
// buf is an initial buffer for altscan.
|
||||
// As of August 2021, 99.96% of all log lines
|
||||
// are below 4096 bytes in length.
|
||||
// Since this cutoff is arbitrary, instead of using 4096,
|
||||
// we subtract off the size of the rest of the struct
|
||||
// so that the whole struct takes 4096 bytes
|
||||
// (less on 32 bit platforms).
|
||||
// This reduces allocation waste.
|
||||
buf [4096 - 48]byte
|
||||
}
|
||||
|
||||
// TryReadline implements the logtail.Buffer interface.
|
||||
@@ -53,6 +62,7 @@ func (f *Filch) TryReadLine() ([]byte, error) {
|
||||
return nil, err
|
||||
}
|
||||
f.altscan = bufio.NewScanner(f.alt)
|
||||
f.altscan.Buffer(f.buf[:], bufio.MaxScanTokenSize)
|
||||
f.altscan.Split(splitLines)
|
||||
return f.scan()
|
||||
}
|
||||
@@ -188,6 +198,7 @@ func New(filePrefix string, opts Options) (f *Filch, err error) {
|
||||
}
|
||||
if f.recovered > 0 {
|
||||
f.altscan = bufio.NewScanner(f.alt)
|
||||
f.altscan.Buffer(f.buf[:], bufio.MaxScanTokenSize)
|
||||
f.altscan.Split(splitLines)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type filchTest struct {
|
||||
@@ -169,3 +170,10 @@ func TestFilchStderr(t *testing.T) {
|
||||
t.Errorf("unexpected write to fake stderr: %s", b)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSizeOf(t *testing.T) {
|
||||
s := unsafe.Sizeof(Filch{})
|
||||
if s > 4096 {
|
||||
t.Fatalf("Filch{} has size %d on %v, decrease size of buf field", s, runtime.GOARCH)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//+build !windows
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package filch
|
||||
|
||||
|
||||
@@ -118,9 +118,10 @@ type Logger struct {
|
||||
bo *backoff.Backoff
|
||||
zstdEncoder Encoder
|
||||
uploadCancel func()
|
||||
explainedRaw bool
|
||||
|
||||
shutdownStart chan struct{} // closed when shutdown begins
|
||||
shutdownDone chan struct{} // closd when shutdown complete
|
||||
shutdownDone chan struct{} // closed when shutdown complete
|
||||
}
|
||||
|
||||
// SetVerbosityLevel controls the verbosity level that should be
|
||||
@@ -199,9 +200,11 @@ func (l *Logger) drainBlock() (shuttingDown bool) {
|
||||
}
|
||||
|
||||
// drainPending drains and encodes a batch of logs from the buffer for upload.
|
||||
// It uses scratch as its initial buffer.
|
||||
// If no logs are available, drainPending blocks until logs are available.
|
||||
func (l *Logger) drainPending() (res []byte) {
|
||||
buf := new(bytes.Buffer)
|
||||
func (l *Logger) drainPending(scratch []byte) (res []byte) {
|
||||
buf := bytes.NewBuffer(scratch[:0])
|
||||
buf.WriteByte('[')
|
||||
entries := 0
|
||||
|
||||
var batchDone bool
|
||||
@@ -230,31 +233,26 @@ func (l *Logger) drainPending() (res []byte) {
|
||||
// outside of the logtail logger. Encode it.
|
||||
// Do not add a client time, as it could have been
|
||||
// been written a long time ago.
|
||||
if !l.explainedRaw {
|
||||
fmt.Fprintf(l.stderr, "RAW-STDERR: ***\n")
|
||||
fmt.Fprintf(l.stderr, "RAW-STDERR: *** Lines prefixed with RAW-STDERR below bypassed logtail and probably come from a previous run of the program\n")
|
||||
fmt.Fprintf(l.stderr, "RAW-STDERR: ***\n")
|
||||
fmt.Fprintf(l.stderr, "RAW-STDERR:\n")
|
||||
l.explainedRaw = true
|
||||
}
|
||||
fmt.Fprintf(l.stderr, "RAW-STDERR: %s", b)
|
||||
b = l.encodeText(b, true)
|
||||
}
|
||||
|
||||
switch {
|
||||
case entries == 0:
|
||||
buf.Write(b)
|
||||
case entries == 1:
|
||||
buf2 := new(bytes.Buffer)
|
||||
buf2.WriteByte('[')
|
||||
buf2.Write(buf.Bytes())
|
||||
buf2.WriteByte(',')
|
||||
buf2.Write(b)
|
||||
buf.Reset()
|
||||
buf.Write(buf2.Bytes())
|
||||
default:
|
||||
if entries > 0 {
|
||||
buf.WriteByte(',')
|
||||
buf.Write(b)
|
||||
}
|
||||
buf.Write(b)
|
||||
entries++
|
||||
}
|
||||
|
||||
if entries > 1 {
|
||||
buf.WriteByte(']')
|
||||
}
|
||||
if buf.Len() == 0 {
|
||||
buf.WriteByte(']')
|
||||
if buf.Len() <= len("[]") {
|
||||
return nil
|
||||
}
|
||||
return buf.Bytes()
|
||||
@@ -264,8 +262,9 @@ func (l *Logger) drainPending() (res []byte) {
|
||||
func (l *Logger) uploading(ctx context.Context) {
|
||||
defer close(l.shutdownDone)
|
||||
|
||||
scratch := make([]byte, 4096) // reusable buffer to write into
|
||||
for {
|
||||
body := l.drainPending()
|
||||
body := l.drainPending(scratch)
|
||||
origlen := -1 // sentinel value: uncompressed
|
||||
// Don't attempt to compress tiny bodies; not worth the CPU cycles.
|
||||
if l.zstdEncoder != nil && len(body) > 256 {
|
||||
|
||||
@@ -117,12 +117,7 @@ func TestEncodeAndUploadMessages(t *testing.T) {
|
||||
io.WriteString(l, tt.log)
|
||||
body := <-ts.uploaded
|
||||
|
||||
data := make(map[string]interface{})
|
||||
err := json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
data := unmarshalOne(t, body)
|
||||
got := data["text"]
|
||||
if got != tt.want {
|
||||
t.Errorf("%s: got %q; want %q", tt.name, got.(string), tt.want)
|
||||
@@ -154,11 +149,7 @@ func TestEncodeSpecialCases(t *testing.T) {
|
||||
// JSON log message already contains a logtail field.
|
||||
io.WriteString(l, `{"logtail": "LOGTAIL", "text": "text"}`)
|
||||
body := <-ts.uploaded
|
||||
data := make(map[string]interface{})
|
||||
err := json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
data := unmarshalOne(t, body)
|
||||
errorHasLogtail, ok := data["error_has_logtail"]
|
||||
if ok {
|
||||
if errorHasLogtail != "LOGTAIL" {
|
||||
@@ -186,11 +177,7 @@ func TestEncodeSpecialCases(t *testing.T) {
|
||||
l.skipClientTime = true
|
||||
io.WriteString(l, "text")
|
||||
body = <-ts.uploaded
|
||||
data = make(map[string]interface{})
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
data = unmarshalOne(t, body)
|
||||
_, ok = data["logtail"]
|
||||
if ok {
|
||||
t.Errorf("skipClientTime: unexpected logtail map present: %v", data)
|
||||
@@ -204,11 +191,7 @@ func TestEncodeSpecialCases(t *testing.T) {
|
||||
longStr := strings.Repeat("0", 512)
|
||||
io.WriteString(l, longStr)
|
||||
body = <-ts.uploaded
|
||||
data = make(map[string]interface{})
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
data = unmarshalOne(t, body)
|
||||
text, ok := data["text"]
|
||||
if !ok {
|
||||
t.Errorf("lowMem: no text %v", data)
|
||||
@@ -219,7 +202,7 @@ func TestEncodeSpecialCases(t *testing.T) {
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
err = l.Shutdown(context.Background())
|
||||
err := l.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -326,3 +309,16 @@ func TestPublicIDUnmarshalText(t *testing.T) {
|
||||
t.Errorf("allocs = %v; want 0", n)
|
||||
}
|
||||
}
|
||||
|
||||
func unmarshalOne(t *testing.T, body []byte) map[string]interface{} {
|
||||
t.Helper()
|
||||
var entries []map[string]interface{}
|
||||
err := json.Unmarshal(body, &entries)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(entries) != 1 {
|
||||
t.Fatalf("expected one entry, got %d", len(entries))
|
||||
}
|
||||
return entries[0]
|
||||
}
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"sort"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/dns/resolver"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
@@ -38,6 +41,20 @@ type Config struct {
|
||||
Hosts map[dnsname.FQDN][]netaddr.IP
|
||||
}
|
||||
|
||||
// WriteToBufioWriter write a debug version of c for logs to w, omitting
|
||||
// spammy stuff like *.arpa entries and replacing it with a total count.
|
||||
func (c *Config) WriteToBufioWriter(w *bufio.Writer) {
|
||||
w.WriteString("{DefaultResolvers:")
|
||||
resolver.WriteIPPorts(w, c.DefaultResolvers)
|
||||
|
||||
w.WriteString(" Routes:")
|
||||
resolver.WriteRoutes(w, c.Routes)
|
||||
|
||||
fmt.Fprintf(w, " SearchDomains:%v", c.SearchDomains)
|
||||
fmt.Fprintf(w, " Hosts:%v", len(c.Hosts))
|
||||
w.WriteString("}")
|
||||
}
|
||||
|
||||
// needsAnyResolvers reports whether c requires a resolver to be set
|
||||
// at the OS level.
|
||||
func (c Config) needsOSResolver() bool {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux || freebsd || openbsd
|
||||
// +build linux freebsd openbsd
|
||||
|
||||
package dns
|
||||
|
||||
@@ -216,7 +216,8 @@ func (m directManager) restoreBackup() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := m.fs.Stat(resolvConf); err != nil && !os.IsNotExist(err) {
|
||||
_, err = m.fs.Stat(resolvConf)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
resolvConfExists := !os.IsNotExist(err)
|
||||
@@ -259,7 +260,7 @@ func (m directManager) SetDNS(config OSConfig) error {
|
||||
// try to manage DNS through resolved when it's around, but as a
|
||||
// best-effort fallback if we messed up the detection, try to
|
||||
// restart resolved to make the system configuration consistent.
|
||||
if isResolvedRunning() {
|
||||
if isResolvedRunning() && !runningAsGUIDesktopUser() {
|
||||
exec.Command("systemctl", "restart", "systemd-resolved.service").Run()
|
||||
}
|
||||
|
||||
@@ -319,7 +320,7 @@ func (m directManager) Close() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if isResolvedRunning() {
|
||||
if isResolvedRunning() && !runningAsGUIDesktopUser() {
|
||||
exec.Command("systemctl", "restart", "systemd-resolved.service").Run() // Best-effort.
|
||||
}
|
||||
|
||||
@@ -385,3 +386,12 @@ func (fs directFS) ReadFile(name string) ([]byte, error) {
|
||||
func (fs directFS) WriteFile(name string, contents []byte, perm os.FileMode) error {
|
||||
return ioutil.WriteFile(fs.path(name), contents, perm)
|
||||
}
|
||||
|
||||
// runningAsGUIDesktopUser reports whether it seems that this code is
|
||||
// being run as a regular user on a Linux desktop. This is a quick
|
||||
// hack to fix Issue 2672 where PolicyKit pops up a GUI dialog asking
|
||||
// to proceed we do a best effort attempt to restart
|
||||
// systemd-resolved.service. There's surely a better way.
|
||||
func runningAsGUIDesktopUser() bool {
|
||||
return os.Getuid() != 0 && os.Getenv("DISPLAY") != ""
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build windows
|
||||
// +build windows
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
@@ -50,14 +51,18 @@ func NewManager(logf logger.Logf, oscfg OSConfigurator, linkMon *monitor.Mon, li
|
||||
}
|
||||
|
||||
func (m *Manager) Set(cfg Config) error {
|
||||
m.logf("Set: %+v", cfg)
|
||||
m.logf("Set: %v", logger.ArgWriter(func(w *bufio.Writer) {
|
||||
cfg.WriteToBufioWriter(w)
|
||||
}))
|
||||
|
||||
rcfg, ocfg, err := m.compileConfig(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.logf("Resolvercfg: %+v", rcfg)
|
||||
m.logf("Resolvercfg: %v", logger.ArgWriter(func(w *bufio.Writer) {
|
||||
rcfg.WriteToBufioWriter(w)
|
||||
}))
|
||||
m.logf("OScfg: %+v", ocfg)
|
||||
|
||||
if err := m.resolver.SetConfig(rcfg); err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !linux && !freebsd && !openbsd && !windows
|
||||
// +build !linux,!freebsd,!openbsd,!windows
|
||||
|
||||
package dns
|
||||
|
||||
@@ -293,7 +293,7 @@ func (m windowsManager) SetDNS(cfg OSConfig) error {
|
||||
}
|
||||
|
||||
t0 = time.Now()
|
||||
m.logf("running ipconfig /registerdns ...")
|
||||
m.logf("running ipconfig /flushdns ...")
|
||||
cmd = exec.Command("ipconfig", "/flushdns")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
err = cmd.Run()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package dns
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux || freebsd || openbsd
|
||||
// +build linux freebsd openbsd
|
||||
|
||||
package dns
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux || freebsd || openbsd
|
||||
// +build linux freebsd openbsd
|
||||
|
||||
package dns
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux
|
||||
// +build linux
|
||||
|
||||
package dns
|
||||
|
||||
98
net/dns/resolver/doh_test.go
Normal file
98
net/dns/resolver/doh_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// 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 resolver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
)
|
||||
|
||||
var testDoH = flag.Bool("test-doh", false, "do real DoH tests against the network")
|
||||
|
||||
const someDNSID = 123 // something non-zero as a test; in violation of spec's SHOULD of 0
|
||||
|
||||
func someDNSQuestion(t testing.TB) []byte {
|
||||
b := dnsmessage.NewBuilder(nil, dnsmessage.Header{
|
||||
OpCode: 0, // query
|
||||
RecursionDesired: true,
|
||||
ID: someDNSID,
|
||||
})
|
||||
b.StartQuestions() // err
|
||||
b.Question(dnsmessage.Question{
|
||||
Name: dnsmessage.MustNewName("tailscale.com."),
|
||||
Type: dnsmessage.TypeA,
|
||||
Class: dnsmessage.ClassINET,
|
||||
})
|
||||
msg, err := b.Finish()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func TestDoH(t *testing.T) {
|
||||
if !*testDoH {
|
||||
t.Skip("skipping manual test without --test-doh flag")
|
||||
}
|
||||
if len(knownDoH) == 0 {
|
||||
t.Fatal("no known DoH")
|
||||
}
|
||||
|
||||
f := &forwarder{
|
||||
dohSem: make(chan struct{}, 10),
|
||||
}
|
||||
|
||||
for ip := range knownDoH {
|
||||
t.Run(ip.String(), func(t *testing.T) {
|
||||
urlBase, c, ok := f.getDoHClient(ip)
|
||||
if !ok {
|
||||
t.Fatal("expected DoH")
|
||||
}
|
||||
res, err := f.sendDoH(context.Background(), urlBase, c, someDNSQuestion(t))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.Transport.(*http.Transport).CloseIdleConnections()
|
||||
|
||||
var p dnsmessage.Parser
|
||||
h, err := p.Start(res)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h.ID != someDNSID {
|
||||
t.Errorf("response DNS ID = %v; want %v", h.ID, someDNSID)
|
||||
}
|
||||
|
||||
p.SkipAllQuestions()
|
||||
aa, err := p.AllAnswers()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(aa) == 0 {
|
||||
t.Fatal("no answers")
|
||||
}
|
||||
for _, r := range aa {
|
||||
t.Logf("got: %v", r.GoString())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoHV6Fallback(t *testing.T) {
|
||||
for ip, base := range knownDoH {
|
||||
if ip.Is4() {
|
||||
ip6, ok := dohV6(base)
|
||||
if !ok {
|
||||
t.Errorf("no v6 DoH known for %v", ip)
|
||||
} else if !ip6.Is6() {
|
||||
t.Errorf("dohV6(%q) returned non-v6 address %v", base, ip6)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,15 +9,21 @@ import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"hash/crc32"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
dns "golang.org/x/net/dns/dnsmessage"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
@@ -29,6 +35,16 @@ const headerBytes = 12
|
||||
const (
|
||||
// responseTimeout is the maximal amount of time to wait for a DNS response.
|
||||
responseTimeout = 5 * time.Second
|
||||
|
||||
// dohTransportTimeout is how long to keep idle HTTP
|
||||
// connections open to DNS-over-HTTPs servers. This is pretty
|
||||
// arbitrary.
|
||||
dohTransportTimeout = 30 * time.Second
|
||||
|
||||
// wellKnownHostBackupDelay is how long to artificially delay upstream
|
||||
// DNS queries to the "fallback" DNS server IP for a known provider
|
||||
// (e.g. how long to wait to query Google's 8.8.4.4 after 8.8.8.8).
|
||||
wellKnownHostBackupDelay = 200 * time.Millisecond
|
||||
)
|
||||
|
||||
var errNoUpstreams = errors.New("upstream nameservers not set")
|
||||
@@ -48,44 +64,13 @@ func getTxID(packet []byte) txid {
|
||||
}
|
||||
|
||||
dnsid := binary.BigEndian.Uint16(packet[0:2])
|
||||
qcount := binary.BigEndian.Uint16(packet[4:6])
|
||||
if qcount == 0 {
|
||||
return txid(dnsid)
|
||||
}
|
||||
|
||||
offset := headerBytes
|
||||
for i := uint16(0); i < qcount; i++ {
|
||||
// Note: this relies on the fact that names are not compressed in questions,
|
||||
// so they are guaranteed to end with a NUL byte.
|
||||
//
|
||||
// Justification:
|
||||
// RFC 1035 doesn't seem to explicitly prohibit compressing names in questions,
|
||||
// but this is exceedingly unlikely to be done in practice. A DNS request
|
||||
// with multiple questions is ill-defined (which questions do the header flags apply to?)
|
||||
// and a single question would have to contain a pointer to an *answer*,
|
||||
// which would be excessively smart, pointless (an answer can just as well refer to the question)
|
||||
// and perhaps even prohibited: a draft RFC (draft-ietf-dnsind-local-compression-05) states:
|
||||
//
|
||||
// > It is important that these pointers always point backwards.
|
||||
//
|
||||
// This is said in summarizing RFC 1035, although that phrase does not appear in the original RFC.
|
||||
// Additionally, (https://cr.yp.to/djbdns/notes.html) states:
|
||||
//
|
||||
// > The precise rule is that a name can be compressed if it is a response owner name,
|
||||
// > the name in NS data, the name in CNAME data, the name in PTR data, the name in MX data,
|
||||
// > or one of the names in SOA data.
|
||||
namebytes := bytes.IndexByte(packet[offset:], 0)
|
||||
// ... | name | NUL | type | class
|
||||
// ?? 1 2 2
|
||||
offset = offset + namebytes + 5
|
||||
if len(packet) < offset {
|
||||
// Corrupt packet; don't crash.
|
||||
return txid(dnsid)
|
||||
}
|
||||
}
|
||||
|
||||
hash := crc32.ChecksumIEEE(packet[headerBytes:offset])
|
||||
return (txid(hash) << 32) | txid(dnsid)
|
||||
// Previously, we hashed the question and combined it with the original txid
|
||||
// which was useful when concurrent queries were multiplexed on a single
|
||||
// local source port. We encountered some situations where the DNS server
|
||||
// canonicalizes the question in the response (uppercase converted to
|
||||
// lowercase in this case), which resulted in responses that we couldn't
|
||||
// match to the original request due to hash mismatches.
|
||||
return txid(dnsid)
|
||||
}
|
||||
|
||||
// clampEDNSSize attempts to limit the maximum EDNS response size. This is not
|
||||
@@ -108,6 +93,7 @@ func clampEDNSSize(packet []byte, maxSize uint16) {
|
||||
return
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc6891#section-6.1.2
|
||||
opt := packet[len(packet)-optFixedBytes:]
|
||||
|
||||
if opt[0] != 0 {
|
||||
@@ -124,8 +110,8 @@ func clampEDNSSize(packet []byte, maxSize uint16) {
|
||||
// Be conservative and don't touch unknown versions.
|
||||
return
|
||||
}
|
||||
// Ignore flags in opt[7:9]
|
||||
if binary.BigEndian.Uint16(opt[10:12]) != 0 {
|
||||
// Ignore flags in opt[6:9]
|
||||
if binary.BigEndian.Uint16(opt[9:11]) != 0 {
|
||||
// RDLEN must be 0 (no variable length data). We're at the end of the
|
||||
// packet so this should be 0 anyway)..
|
||||
return
|
||||
@@ -141,7 +127,20 @@ func clampEDNSSize(packet []byte, maxSize uint16) {
|
||||
|
||||
type route struct {
|
||||
Suffix dnsname.FQDN
|
||||
Resolvers []netaddr.IPPort
|
||||
Resolvers []resolverAndDelay
|
||||
}
|
||||
|
||||
// resolverAndDelay is an upstream DNS resolver and a delay for how
|
||||
// long to wait before querying it.
|
||||
type resolverAndDelay struct {
|
||||
// ipp is the upstream resolver.
|
||||
ipp netaddr.IPPort
|
||||
|
||||
// startDelay is an amount to delay this resolver at
|
||||
// start. It's used when, say, there are four Google or
|
||||
// Cloudflare DNS IPs (two IPv4 + two IPv6) and we don't want
|
||||
// to race all four at once.
|
||||
startDelay time.Duration
|
||||
}
|
||||
|
||||
// forwarder forwards DNS packets to a number of upstream nameservers.
|
||||
@@ -149,6 +148,7 @@ type forwarder struct {
|
||||
logf logger.Logf
|
||||
linkMon *monitor.Mon
|
||||
linkSel ForwardLinkSelector
|
||||
dohSem chan struct{}
|
||||
|
||||
ctx context.Context // good until Close
|
||||
ctxCancel context.CancelFunc // closes ctx
|
||||
@@ -158,6 +158,8 @@ type forwarder struct {
|
||||
|
||||
mu sync.Mutex // guards following
|
||||
|
||||
dohClient map[netaddr.IP]*http.Client
|
||||
|
||||
// routes are per-suffix resolvers to use, with
|
||||
// the most specific routes first.
|
||||
routes []route
|
||||
@@ -168,11 +170,18 @@ func init() {
|
||||
}
|
||||
|
||||
func newForwarder(logf logger.Logf, responses chan packet, linkMon *monitor.Mon, linkSel ForwardLinkSelector) *forwarder {
|
||||
maxDoHInFlight := 1000 // effectively unlimited
|
||||
if runtime.GOOS == "ios" {
|
||||
// No HTTP/2 on iOS yet (for size reasons), so DoH is
|
||||
// pricier.
|
||||
maxDoHInFlight = 10
|
||||
}
|
||||
f := &forwarder{
|
||||
logf: logger.WithPrefix(logf, "forward: "),
|
||||
linkMon: linkMon,
|
||||
linkSel: linkSel,
|
||||
responses: responses,
|
||||
dohSem: make(chan struct{}, maxDoHInFlight),
|
||||
}
|
||||
f.ctx, f.ctxCancel = context.WithCancel(context.Background())
|
||||
return f
|
||||
@@ -183,7 +192,84 @@ func (f *forwarder) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *forwarder) setRoutes(routes []route) {
|
||||
// resolversWithDelays maps from a set of DNS server ip:ports (currently
|
||||
// the port is always 53) to a slice of a type that included a
|
||||
// startDelay. So if ipps contains e.g. four Google DNS IPs (two IPv4
|
||||
// + twoIPv6), this function partition adds delays to some.
|
||||
func resolversWithDelays(ipps []netaddr.IPPort) []resolverAndDelay {
|
||||
type hostAndFam struct {
|
||||
host string // some arbitrary string representing DNS host (currently the DoH base)
|
||||
bits uint8 // either 32 or 128 for IPv4 vs IPv6s address family
|
||||
}
|
||||
|
||||
// Track how many of each known resolver host are in the list,
|
||||
// per address family.
|
||||
total := map[hostAndFam]int{}
|
||||
|
||||
rr := make([]resolverAndDelay, len(ipps))
|
||||
for _, ipp := range ipps {
|
||||
ip := ipp.IP()
|
||||
if host, ok := knownDoH[ip]; ok {
|
||||
total[hostAndFam{host, ip.BitLen()}]++
|
||||
}
|
||||
}
|
||||
|
||||
done := map[hostAndFam]int{}
|
||||
for i, ipp := range ipps {
|
||||
ip := ipp.IP()
|
||||
var startDelay time.Duration
|
||||
if host, ok := knownDoH[ip]; ok {
|
||||
key4 := hostAndFam{host, 32}
|
||||
key6 := hostAndFam{host, 128}
|
||||
switch {
|
||||
case ip.Is4():
|
||||
if done[key4] > 0 {
|
||||
startDelay += wellKnownHostBackupDelay
|
||||
}
|
||||
case ip.Is6():
|
||||
total4 := total[key4]
|
||||
if total4 >= 2 {
|
||||
// If we have two IPv4 IPs of the same provider
|
||||
// already in the set, delay the IPv6 queries
|
||||
// until halfway through the timeout (so wait
|
||||
// 2.5 seconds). Even the network is IPv6-only,
|
||||
// the DoH dialer will fallback to IPv6
|
||||
// immediately anyway.
|
||||
startDelay = responseTimeout / 2
|
||||
} else if total4 == 1 {
|
||||
startDelay += wellKnownHostBackupDelay
|
||||
}
|
||||
if done[key6] > 0 {
|
||||
startDelay += wellKnownHostBackupDelay
|
||||
}
|
||||
}
|
||||
done[hostAndFam{host, ip.BitLen()}]++
|
||||
}
|
||||
rr[i] = resolverAndDelay{
|
||||
ipp: ipp,
|
||||
startDelay: startDelay,
|
||||
}
|
||||
}
|
||||
return rr
|
||||
}
|
||||
|
||||
// setRoutes sets the routes to use for DNS forwarding. It's called by
|
||||
// Resolver.SetConfig on reconfig.
|
||||
//
|
||||
// The memory referenced by routesBySuffix should not be modified.
|
||||
func (f *forwarder) setRoutes(routesBySuffix map[dnsname.FQDN][]netaddr.IPPort) {
|
||||
routes := make([]route, 0, len(routesBySuffix))
|
||||
for suffix, ipps := range routesBySuffix {
|
||||
routes = append(routes, route{
|
||||
Suffix: suffix,
|
||||
Resolvers: resolversWithDelays(ipps),
|
||||
})
|
||||
}
|
||||
// Sort from longest prefix to shortest.
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
return routes[i].Suffix.NumLabels() > routes[j].Suffix.NumLabels()
|
||||
})
|
||||
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.routes = routes
|
||||
@@ -210,21 +296,104 @@ func (f *forwarder) packetListener(ip netaddr.IP) (packetListener, error) {
|
||||
return lc, nil
|
||||
}
|
||||
|
||||
func (f *forwarder) getDoHClient(ip netaddr.IP) (urlBase string, c *http.Client, ok bool) {
|
||||
urlBase, ok = knownDoH[ip]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if c, ok := f.dohClient[ip]; ok {
|
||||
return urlBase, c, true
|
||||
}
|
||||
if f.dohClient == nil {
|
||||
f.dohClient = map[netaddr.IP]*http.Client{}
|
||||
}
|
||||
nsDialer := netns.NewDialer()
|
||||
c = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
IdleConnTimeout: dohTransportTimeout,
|
||||
DialContext: func(ctx context.Context, netw, addr string) (net.Conn, error) {
|
||||
if !strings.HasPrefix(netw, "tcp") {
|
||||
return nil, fmt.Errorf("unexpected network %q", netw)
|
||||
}
|
||||
c, err := nsDialer.DialContext(ctx, "tcp", net.JoinHostPort(ip.String(), "443"))
|
||||
// If v4 failed, try an equivalent v6 also in the time remaining.
|
||||
if err != nil && ctx.Err() == nil {
|
||||
if ip6, ok := dohV6(urlBase); ok && ip.Is4() {
|
||||
if c6, err := nsDialer.DialContext(ctx, "tcp", net.JoinHostPort(ip6.String(), "443")); err == nil {
|
||||
return c6, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return c, err
|
||||
},
|
||||
},
|
||||
}
|
||||
f.dohClient[ip] = c
|
||||
return urlBase, c, true
|
||||
}
|
||||
|
||||
const dohType = "application/dns-message"
|
||||
|
||||
func (f *forwarder) releaseDoHSem() { <-f.dohSem }
|
||||
|
||||
func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client, packet []byte) ([]byte, error) {
|
||||
// Bound the number of HTTP requests in flight. This primarily
|
||||
// matters for iOS where we're very memory constrained and
|
||||
// HTTP requests are heavier on iOS where we don't include
|
||||
// HTTP/2 for binary size reasons (as binaries on iOS linked
|
||||
// with Go code cost memory proportional to the binary size,
|
||||
// for reasons not fully understood).
|
||||
select {
|
||||
case f.dohSem <- struct{}{}:
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
defer f.releaseDoHSem()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", urlBase, bytes.NewReader(packet))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", dohType)
|
||||
// Note: we don't currently set the Accept header (which is
|
||||
// only a SHOULD in the spec) as iOS doesn't use HTTP/2 and
|
||||
// we'd rather save a few bytes on outgoing requests when
|
||||
// empirically no provider cares about the Accept header's
|
||||
// absence.
|
||||
|
||||
hres, err := c.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer hres.Body.Close()
|
||||
if hres.StatusCode != 200 {
|
||||
return nil, errors.New(hres.Status)
|
||||
}
|
||||
if ct := hres.Header.Get("Content-Type"); ct != dohType {
|
||||
return nil, fmt.Errorf("unexpected response Content-Type %q", ct)
|
||||
}
|
||||
return ioutil.ReadAll(hres.Body)
|
||||
}
|
||||
|
||||
// send sends packet to dst. It is best effort.
|
||||
//
|
||||
// send expects the reply to have the same txid as txidOut.
|
||||
//
|
||||
// The provided closeOnCtxDone lets send register values to Close if
|
||||
// the caller's ctx expires. This avoids send from allocating its own
|
||||
// waiting goroutine to interrupt the ReadFrom, as memory is tight on
|
||||
// iOS and we want the number of pending DNS lookups to be bursty
|
||||
// without too much associated goroutine/memory cost.
|
||||
func (f *forwarder) send(ctx context.Context, txidOut txid, closeOnCtxDone *closePool, packet []byte, dst netaddr.IPPort) ([]byte, error) {
|
||||
// TODO(bradfitz): if dst.IP is 8.8.8.8 or 8.8.4.4 or 1.1.1.1, etc, or
|
||||
// something dynamically probed earlier to support DoH or DoT,
|
||||
// do that here instead.
|
||||
func (f *forwarder) send(ctx context.Context, fq *forwardQuery, dst netaddr.IPPort) ([]byte, error) {
|
||||
ip := dst.IP()
|
||||
|
||||
ln, err := f.packetListener(dst.IP())
|
||||
// Upgrade known DNS IPs to DoH (DNS-over-HTTPs).
|
||||
if urlBase, dc, ok := f.getDoHClient(ip); ok {
|
||||
res, err := f.sendDoH(ctx, urlBase, dc, fq.packet)
|
||||
if err == nil || ctx.Err() != nil {
|
||||
return res, err
|
||||
}
|
||||
f.logf("DoH error from %v: %v", ip, err)
|
||||
}
|
||||
|
||||
ln, err := f.packetListener(ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -235,10 +404,10 @@ func (f *forwarder) send(ctx context.Context, txidOut txid, closeOnCtxDone *clos
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
closeOnCtxDone.Add(conn)
|
||||
defer closeOnCtxDone.Remove(conn)
|
||||
fq.closeOnCtxDone.Add(conn)
|
||||
defer fq.closeOnCtxDone.Remove(conn)
|
||||
|
||||
if _, err := conn.WriteTo(packet, dst.UDPAddr()); err != nil {
|
||||
if _, err := conn.WriteTo(fq.packet, dst.UDPAddr()); err != nil {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -267,7 +436,7 @@ func (f *forwarder) send(ctx context.Context, txidOut txid, closeOnCtxDone *clos
|
||||
}
|
||||
out = out[:n]
|
||||
txid := getTxID(out)
|
||||
if txid != txidOut {
|
||||
if txid != fq.txid {
|
||||
return nil, errors.New("txid doesn't match")
|
||||
}
|
||||
|
||||
@@ -290,7 +459,7 @@ func (f *forwarder) send(ctx context.Context, txidOut txid, closeOnCtxDone *clos
|
||||
}
|
||||
|
||||
// resolvers returns the resolvers to use for domain.
|
||||
func (f *forwarder) resolvers(domain dnsname.FQDN) []netaddr.IPPort {
|
||||
func (f *forwarder) resolvers(domain dnsname.FQDN) []resolverAndDelay {
|
||||
f.mu.Lock()
|
||||
routes := f.routes
|
||||
f.mu.Unlock()
|
||||
@@ -302,6 +471,30 @@ func (f *forwarder) resolvers(domain dnsname.FQDN) []netaddr.IPPort {
|
||||
return nil
|
||||
}
|
||||
|
||||
// forwardQuery is information and state about a forwarded DNS query that's
|
||||
// being sent to 1 or more upstreams.
|
||||
//
|
||||
// In the case of racing against multiple equivalent upstreams
|
||||
// (e.g. Google or CloudFlare's 4 DNS IPs: 2 IPv4 + 2 IPv6), this type
|
||||
// handles racing them more intelligently than just blasting away 4
|
||||
// queries at once.
|
||||
type forwardQuery struct {
|
||||
txid txid
|
||||
packet []byte
|
||||
|
||||
// closeOnCtxDone lets send register values to Close if the
|
||||
// caller's ctx expires. This avoids send from allocating its
|
||||
// own waiting goroutine to interrupt the ReadFrom, as memory
|
||||
// is tight on iOS and we want the number of pending DNS
|
||||
// lookups to be bursty without too much associated
|
||||
// goroutine/memory cost.
|
||||
closeOnCtxDone *closePool
|
||||
|
||||
// TODO(bradfitz): add race delay state:
|
||||
// mu sync.Mutex
|
||||
// ...
|
||||
}
|
||||
|
||||
// forward forwards the query to all upstream nameservers and returns the first response.
|
||||
func (f *forwarder) forward(query packet) error {
|
||||
domain, err := nameFromQuery(query.bs)
|
||||
@@ -309,7 +502,6 @@ func (f *forwarder) forward(query packet) error {
|
||||
return err
|
||||
}
|
||||
|
||||
txid := getTxID(query.bs)
|
||||
clampEDNSSize(query.bs, maxResponseBytes)
|
||||
|
||||
resolvers := f.resolvers(domain)
|
||||
@@ -317,8 +509,12 @@ func (f *forwarder) forward(query packet) error {
|
||||
return errNoUpstreams
|
||||
}
|
||||
|
||||
closeOnCtxDone := new(closePool)
|
||||
defer closeOnCtxDone.Close()
|
||||
fq := &forwardQuery{
|
||||
txid: getTxID(query.bs),
|
||||
packet: query.bs,
|
||||
closeOnCtxDone: new(closePool),
|
||||
}
|
||||
defer fq.closeOnCtxDone.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(f.ctx, responseTimeout)
|
||||
defer cancel()
|
||||
@@ -329,9 +525,18 @@ func (f *forwarder) forward(query packet) error {
|
||||
firstErr error
|
||||
)
|
||||
|
||||
for _, ipp := range resolvers {
|
||||
go func(ipp netaddr.IPPort) {
|
||||
resb, err := f.send(ctx, txid, closeOnCtxDone, query.bs, ipp)
|
||||
for _, rr := range resolvers {
|
||||
go func(rr resolverAndDelay) {
|
||||
if rr.startDelay > 0 {
|
||||
timer := time.NewTimer(rr.startDelay)
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
resb, err := f.send(ctx, fq, rr.ipp)
|
||||
if err != nil {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
@@ -344,7 +549,7 @@ func (f *forwarder) forward(query packet) error {
|
||||
case resc <- resb:
|
||||
default:
|
||||
}
|
||||
}(ipp)
|
||||
}(rr)
|
||||
}
|
||||
|
||||
select {
|
||||
@@ -432,3 +637,63 @@ func (p *closePool) Close() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var knownDoH = map[netaddr.IP]string{}
|
||||
|
||||
var dohIPsOfBase = map[string][]netaddr.IP{}
|
||||
|
||||
func addDoH(ipStr, base string) {
|
||||
ip := netaddr.MustParseIP(ipStr)
|
||||
knownDoH[ip] = base
|
||||
dohIPsOfBase[base] = append(dohIPsOfBase[base], ip)
|
||||
}
|
||||
|
||||
func dohV6(base string) (ip netaddr.IP, ok bool) {
|
||||
for _, ip := range dohIPsOfBase[base] {
|
||||
if ip.Is6() {
|
||||
return ip, true
|
||||
}
|
||||
}
|
||||
return ip, false
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Cloudflare
|
||||
addDoH("1.1.1.1", "https://cloudflare-dns.com/dns-query")
|
||||
addDoH("1.0.0.1", "https://cloudflare-dns.com/dns-query")
|
||||
addDoH("2606:4700:4700::1111", "https://cloudflare-dns.com/dns-query")
|
||||
addDoH("2606:4700:4700::1001", "https://cloudflare-dns.com/dns-query")
|
||||
|
||||
// Cloudflare -Malware
|
||||
addDoH("1.1.1.2", "https://security.cloudflare-dns.com/dns-query")
|
||||
addDoH("1.0.0.2", "https://security.cloudflare-dns.com/dns-query")
|
||||
addDoH("2606:4700:4700::1112", "https://security.cloudflare-dns.com/dns-query")
|
||||
addDoH("2606:4700:4700::1002", "https://security.cloudflare-dns.com/dns-query")
|
||||
|
||||
// Cloudflare -Malware -Adult
|
||||
addDoH("1.1.1.3", "https://family.cloudflare-dns.com/dns-query")
|
||||
addDoH("1.0.0.3", "https://family.cloudflare-dns.com/dns-query")
|
||||
addDoH("2606:4700:4700::1113", "https://family.cloudflare-dns.com/dns-query")
|
||||
addDoH("2606:4700:4700::1003", "https://family.cloudflare-dns.com/dns-query")
|
||||
|
||||
// Google
|
||||
addDoH("8.8.8.8", "https://dns.google/dns-query")
|
||||
addDoH("8.8.4.4", "https://dns.google/dns-query")
|
||||
addDoH("2001:4860:4860::8888", "https://dns.google/dns-query")
|
||||
addDoH("2001:4860:4860::8844", "https://dns.google/dns-query")
|
||||
|
||||
// OpenDNS
|
||||
// TODO(bradfitz): OpenDNS is unique amongst this current set in that
|
||||
// its DoH DNS names resolve to different IPs than its normal DNS
|
||||
// IPs. Support that later. For now we assume that they're the same.
|
||||
// addDoH("208.67.222.222", "https://doh.opendns.com/dns-query")
|
||||
// addDoH("208.67.220.220", "https://doh.opendns.com/dns-query")
|
||||
// addDoH("208.67.222.123", "https://doh.familyshield.opendns.com/dns-query")
|
||||
// addDoH("208.67.220.123", "https://doh.familyshield.opendns.com/dns-query")
|
||||
|
||||
// Quad9
|
||||
addDoH("9.9.9.9", "https://dns.quad9.net/dns-query")
|
||||
addDoH("149.112.112.112", "https://dns.quad9.net/dns-query")
|
||||
addDoH("2620:fe::fe", "https://dns.quad9.net/dns-query")
|
||||
addDoH("2620:fe::fe:9", "https://dns.quad9.net/dns-query")
|
||||
}
|
||||
|
||||
90
net/dns/resolver/forwarder_test.go
Normal file
90
net/dns/resolver/forwarder_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
// 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 resolver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func (rr resolverAndDelay) String() string {
|
||||
return fmt.Sprintf("%v+%v", rr.ipp, rr.startDelay)
|
||||
}
|
||||
|
||||
func TestResolversWithDelays(t *testing.T) {
|
||||
// query
|
||||
q := func(ss ...string) (ipps []netaddr.IPPort) {
|
||||
for _, s := range ss {
|
||||
ipps = append(ipps, netaddr.MustParseIPPort(s))
|
||||
}
|
||||
return
|
||||
}
|
||||
// output
|
||||
o := func(ss ...string) (rr []resolverAndDelay) {
|
||||
for _, s := range ss {
|
||||
var d time.Duration
|
||||
if i := strings.Index(s, "+"); i != -1 {
|
||||
var err error
|
||||
d, err = time.ParseDuration(s[i+1:])
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("parsing duration in %q: %v", s, err))
|
||||
}
|
||||
s = s[:i]
|
||||
}
|
||||
rr = append(rr, resolverAndDelay{
|
||||
ipp: netaddr.MustParseIPPort(s),
|
||||
startDelay: d,
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
in []netaddr.IPPort
|
||||
want []resolverAndDelay
|
||||
}{
|
||||
{
|
||||
name: "unknown-no-delays",
|
||||
in: q("1.2.3.4:53", "2.3.4.5:53"),
|
||||
want: o("1.2.3.4:53", "2.3.4.5:53"),
|
||||
},
|
||||
{
|
||||
name: "google-all-ipv4",
|
||||
in: q("8.8.8.8:53", "8.8.4.4:53"),
|
||||
want: o("8.8.8.8:53", "8.8.4.4:53+200ms"),
|
||||
},
|
||||
{
|
||||
name: "google-only-ipv6",
|
||||
in: q("[2001:4860:4860::8888]:53", "[2001:4860:4860::8844]:53"),
|
||||
want: o("[2001:4860:4860::8888]:53", "[2001:4860:4860::8844]:53+200ms"),
|
||||
},
|
||||
{
|
||||
name: "google-all-four",
|
||||
in: q("8.8.8.8:53", "8.8.4.4:53", "[2001:4860:4860::8888]:53", "[2001:4860:4860::8844]:53"),
|
||||
want: o("8.8.8.8:53", "8.8.4.4:53+200ms", "[2001:4860:4860::8888]:53+2.5s", "[2001:4860:4860::8844]:53+2.7s"),
|
||||
},
|
||||
{
|
||||
name: "quad9-one-v4-one-v6",
|
||||
in: q("9.9.9.9:53", "[2620:fe::fe]:53"),
|
||||
want: o("9.9.9.9:53", "[2620:fe::fe]:53+200ms"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := resolversWithDelays(tt.in)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("got %v; want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,7 +2,9 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin,ts_macext ios,ts_macext
|
||||
//go:build ts_macext && (darwin || ios)
|
||||
// +build ts_macext
|
||||
// +build darwin ios
|
||||
|
||||
package resolver
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !darwin && !windows
|
||||
// +build !darwin,!windows
|
||||
|
||||
package resolver
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -79,6 +81,74 @@ type Config struct {
|
||||
LocalDomains []dnsname.FQDN
|
||||
}
|
||||
|
||||
// WriteToBufioWriter write a debug version of c for logs to w, omitting
|
||||
// spammy stuff like *.arpa entries and replacing it with a total count.
|
||||
func (c *Config) WriteToBufioWriter(w *bufio.Writer) {
|
||||
w.WriteString("{Routes:")
|
||||
WriteRoutes(w, c.Routes)
|
||||
fmt.Fprintf(w, " Hosts:%v LocalDomains:[", len(c.Hosts))
|
||||
space := false
|
||||
arpa := 0
|
||||
for _, d := range c.LocalDomains {
|
||||
if strings.HasSuffix(string(d), ".arpa.") {
|
||||
arpa++
|
||||
continue
|
||||
}
|
||||
if space {
|
||||
w.WriteByte(' ')
|
||||
}
|
||||
w.WriteString(string(d))
|
||||
space = true
|
||||
}
|
||||
w.WriteString("]")
|
||||
if arpa > 0 {
|
||||
fmt.Fprintf(w, "+%darpa", arpa)
|
||||
}
|
||||
w.WriteString("}")
|
||||
}
|
||||
|
||||
// WriteIPPorts writes vv to w.
|
||||
func WriteIPPorts(w *bufio.Writer, vv []netaddr.IPPort) {
|
||||
w.WriteByte('[')
|
||||
var b []byte
|
||||
for i, v := range vv {
|
||||
if i > 0 {
|
||||
w.WriteByte(' ')
|
||||
}
|
||||
b = v.AppendTo(b[:0])
|
||||
w.Write(b)
|
||||
}
|
||||
w.WriteByte(']')
|
||||
}
|
||||
|
||||
// WriteRoutes writes routes to w, omitting *.arpa routes and instead
|
||||
// summarizing how many of them there were.
|
||||
func WriteRoutes(w *bufio.Writer, routes map[dnsname.FQDN][]netaddr.IPPort) {
|
||||
var kk []dnsname.FQDN
|
||||
arpa := 0
|
||||
for k := range routes {
|
||||
if strings.HasSuffix(string(k), ".arpa.") {
|
||||
arpa++
|
||||
continue
|
||||
}
|
||||
kk = append(kk, k)
|
||||
}
|
||||
sort.Slice(kk, func(i, j int) bool { return kk[i] < kk[j] })
|
||||
w.WriteByte('{')
|
||||
for i, k := range kk {
|
||||
if i > 0 {
|
||||
w.WriteByte(' ')
|
||||
}
|
||||
w.WriteString(string(k))
|
||||
w.WriteByte(':')
|
||||
WriteIPPorts(w, routes[k])
|
||||
}
|
||||
w.WriteByte('}')
|
||||
if arpa > 0 {
|
||||
fmt.Fprintf(w, "+%darpa", arpa)
|
||||
}
|
||||
}
|
||||
|
||||
// Resolver is a DNS resolver for nodes on the Tailscale network,
|
||||
// associating them with domain names of the form <mynode>.<mydomain>.<root>.
|
||||
// If it is asked to resolve a domain that is not of that form,
|
||||
@@ -138,7 +208,6 @@ func (r *Resolver) SetConfig(cfg Config) error {
|
||||
r.saveConfigForTests(cfg)
|
||||
}
|
||||
|
||||
routes := make([]route, 0, len(cfg.Routes))
|
||||
reverse := make(map[netaddr.IP]dnsname.FQDN, len(cfg.Hosts))
|
||||
|
||||
for host, ips := range cfg.Hosts {
|
||||
@@ -147,18 +216,7 @@ func (r *Resolver) SetConfig(cfg Config) error {
|
||||
}
|
||||
}
|
||||
|
||||
for suffix, ips := range cfg.Routes {
|
||||
routes = append(routes, route{
|
||||
Suffix: suffix,
|
||||
Resolvers: ips,
|
||||
})
|
||||
}
|
||||
// Sort from longest prefix to shortest.
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
return routes[i].Suffix.NumLabels() > routes[j].Suffix.NumLabels()
|
||||
})
|
||||
|
||||
r.forwarder.setRoutes(routes)
|
||||
r.forwarder.setRoutes(cfg.Routes)
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
@@ -276,7 +334,7 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netaddr.IP,
|
||||
case dns.TypeNS, dns.TypeSOA, dns.TypeAXFR, dns.TypeHINFO:
|
||||
return netaddr.IP{}, dns.RCodeNotImplemented
|
||||
|
||||
// For everything except for the few types above that are explictly not implemented, return no records.
|
||||
// For everything except for the few types above that are explicitly not implemented, return no records.
|
||||
// This is what other DNS systems do: always return NOERROR
|
||||
// without any records whenever the requested record type is unknown.
|
||||
// You can try this with:
|
||||
|
||||
@@ -6,6 +6,7 @@ package resolver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
@@ -66,6 +67,58 @@ func resolveToIP(ipv4, ipv6 netaddr.IP, ns string) dns.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// resolveToIPLowercase returns a handler function which canonicalizes responses
|
||||
// by lowercasing the question and answer names, and responds
|
||||
// to queries of type A it receives with an A record containing ipv4,
|
||||
// to queries of type AAAA with an AAAA record containing ipv6,
|
||||
// to queries of type NS with an NS record containg name.
|
||||
func resolveToIPLowercase(ipv4, ipv6 netaddr.IP, ns string) dns.HandlerFunc {
|
||||
return func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(req)
|
||||
|
||||
if len(req.Question) != 1 {
|
||||
panic("not a single-question request")
|
||||
}
|
||||
m.Question[0].Name = strings.ToLower(m.Question[0].Name)
|
||||
question := req.Question[0]
|
||||
|
||||
var ans dns.RR
|
||||
switch question.Qtype {
|
||||
case dns.TypeA:
|
||||
ans = &dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
A: ipv4.IPAddr().IP,
|
||||
}
|
||||
case dns.TypeAAAA:
|
||||
ans = &dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
AAAA: ipv6.IPAddr().IP,
|
||||
}
|
||||
case dns.TypeNS:
|
||||
ans = &dns.NS{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeNS,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
Ns: ns,
|
||||
}
|
||||
}
|
||||
|
||||
m.Answer = append(m.Answer, ans)
|
||||
w.WriteMsg(m)
|
||||
}
|
||||
}
|
||||
|
||||
// resolveToTXT returns a handler function which responds to queries of type TXT
|
||||
// it receives with the strings in txts.
|
||||
func resolveToTXT(txts []string, ednsMaxSize uint16) dns.HandlerFunc {
|
||||
|
||||
@@ -440,6 +440,8 @@ func TestDelegate(t *testing.T) {
|
||||
records := []interface{}{
|
||||
"test.site.",
|
||||
resolveToIP(testipv4, testipv6, "dns.test.site."),
|
||||
"LCtesT.SiTe.",
|
||||
resolveToIPLowercase(testipv4, testipv6, "dns.test.site."),
|
||||
"nxdomain.site.", resolveToNXDOMAIN,
|
||||
"small.txt.", resolveToTXT(smallTXT, noEdns),
|
||||
"smalledns.txt.", resolveToTXT(smallTXT, 512),
|
||||
@@ -485,6 +487,21 @@ func TestDelegate(t *testing.T) {
|
||||
dnspacket("test.site.", dns.TypeNS, noEdns),
|
||||
dnsResponse{name: "dns.test.site.", rcode: dns.RCodeSuccess},
|
||||
},
|
||||
{
|
||||
"ipv4",
|
||||
dnspacket("LCtesT.SiTe.", dns.TypeA, noEdns),
|
||||
dnsResponse{ip: testipv4, rcode: dns.RCodeSuccess},
|
||||
},
|
||||
{
|
||||
"ipv6",
|
||||
dnspacket("LCtesT.SiTe.", dns.TypeAAAA, noEdns),
|
||||
dnsResponse{ip: testipv6, rcode: dns.RCodeSuccess},
|
||||
},
|
||||
{
|
||||
"ns",
|
||||
dnspacket("LCtesT.SiTe.", dns.TypeNS, noEdns),
|
||||
dnsResponse{name: "dns.test.site.", rcode: dns.RCodeSuccess},
|
||||
},
|
||||
{
|
||||
"nxdomain",
|
||||
dnspacket("nxdomain.site.", dns.TypeA, noEdns),
|
||||
@@ -931,8 +948,11 @@ func TestAllocs(t *testing.T) {
|
||||
query []byte
|
||||
want int
|
||||
}{
|
||||
// Name lowercasing and response slice created by dns.NewBuilder.
|
||||
{"forward", dnspacket("test1.ipn.dev.", dns.TypeA, noEdns), 2},
|
||||
// Name lowercasing, response slice created by dns.NewBuilder,
|
||||
// and closure allocation from go call.
|
||||
// (Closure allocation only happens when using new register ABI,
|
||||
// which is amd64 with Go 1.17, and probably more platforms later.)
|
||||
{"forward", dnspacket("test1.ipn.dev.", dns.TypeA, noEdns), 3},
|
||||
// 3 extra allocs in rdnsNameToIPv4 and one in marshalPTRRecord (dns.NewName).
|
||||
{"reverse", dnspacket("4.3.2.1.in-addr.arpa.", dns.TypePTR, noEdns), 5},
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// TODO(bradfitz): update this code to use netaddr more
|
||||
|
||||
// Package dnscache contains a minimal DNS cache that makes a bunch of
|
||||
// assumptions that are only valid for us. Not recommended for general use.
|
||||
package dnscache
|
||||
@@ -78,8 +80,9 @@ type Resolver struct {
|
||||
}
|
||||
|
||||
type ipCacheEntry struct {
|
||||
ip net.IP // either v4 or v6
|
||||
ip6 net.IP // nil if no v4 or no v6
|
||||
ip net.IP // either v4 or v6
|
||||
ip6 net.IP // nil if no v4 or no v6
|
||||
allIPs []net.IPAddr // 1+ v4 and/or v6
|
||||
expires time.Time
|
||||
}
|
||||
|
||||
@@ -105,81 +108,82 @@ var debug, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_DNS_CACHE"))
|
||||
//
|
||||
// If err is nil, ip will be non-nil. The v6 address may be nil even
|
||||
// with a nil error.
|
||||
func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 net.IP, err error) {
|
||||
func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 net.IP, allIPs []net.IPAddr, err error) {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
return ip4, nil, nil
|
||||
return ip4, nil, []net.IPAddr{{IP: ip4}}, nil
|
||||
}
|
||||
if debug {
|
||||
log.Printf("dnscache: %q is an IP", host)
|
||||
}
|
||||
return ip, nil, nil
|
||||
return ip, nil, []net.IPAddr{{IP: ip}}, nil
|
||||
}
|
||||
|
||||
if ip, ip6, ok := r.lookupIPCache(host); ok {
|
||||
if ip, ip6, allIPs, ok := r.lookupIPCache(host); ok {
|
||||
if debug {
|
||||
log.Printf("dnscache: %q = %v (cached)", host, ip)
|
||||
}
|
||||
return ip, ip6, nil
|
||||
return ip, ip6, allIPs, nil
|
||||
}
|
||||
|
||||
type ipPair struct {
|
||||
type ipRes struct {
|
||||
ip, ip6 net.IP
|
||||
allIPs []net.IPAddr
|
||||
}
|
||||
ch := r.sf.DoChan(host, func() (interface{}, error) {
|
||||
ip, ip6, err := r.lookupIP(host)
|
||||
ip, ip6, allIPs, err := r.lookupIP(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ipPair{ip, ip6}, nil
|
||||
return ipRes{ip, ip6, allIPs}, nil
|
||||
})
|
||||
select {
|
||||
case res := <-ch:
|
||||
if res.Err != nil {
|
||||
if r.UseLastGood {
|
||||
if ip, ip6, ok := r.lookupIPCacheExpired(host); ok {
|
||||
if ip, ip6, allIPs, ok := r.lookupIPCacheExpired(host); ok {
|
||||
if debug {
|
||||
log.Printf("dnscache: %q using %v after error", host, ip)
|
||||
}
|
||||
return ip, ip6, nil
|
||||
return ip, ip6, allIPs, nil
|
||||
}
|
||||
}
|
||||
if debug {
|
||||
log.Printf("dnscache: error resolving %q: %v", host, res.Err)
|
||||
}
|
||||
return nil, nil, res.Err
|
||||
return nil, nil, nil, res.Err
|
||||
}
|
||||
pair := res.Val.(ipPair)
|
||||
return pair.ip, pair.ip6, nil
|
||||
r := res.Val.(ipRes)
|
||||
return r.ip, r.ip6, r.allIPs, nil
|
||||
case <-ctx.Done():
|
||||
if debug {
|
||||
log.Printf("dnscache: context done while resolving %q: %v", host, ctx.Err())
|
||||
}
|
||||
return nil, nil, ctx.Err()
|
||||
return nil, nil, nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupIPCache(host string) (ip, ip6 net.IP, ok bool) {
|
||||
func (r *Resolver) lookupIPCache(host string) (ip, ip6 net.IP, allIPs []net.IPAddr, ok bool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if ent, ok := r.ipCache[host]; ok && ent.expires.After(time.Now()) {
|
||||
return ent.ip, ent.ip6, true
|
||||
return ent.ip, ent.ip6, ent.allIPs, true
|
||||
}
|
||||
return nil, nil, false
|
||||
return nil, nil, nil, false
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupIPCacheExpired(host string) (ip, ip6 net.IP, ok bool) {
|
||||
func (r *Resolver) lookupIPCacheExpired(host string) (ip, ip6 net.IP, allIPs []net.IPAddr, ok bool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if ent, ok := r.ipCache[host]; ok {
|
||||
return ent.ip, ent.ip6, true
|
||||
return ent.ip, ent.ip6, ent.allIPs, true
|
||||
}
|
||||
return nil, nil, false
|
||||
return nil, nil, nil, false
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupTimeoutForHost(host string) time.Duration {
|
||||
if r.UseLastGood {
|
||||
if _, _, ok := r.lookupIPCacheExpired(host); ok {
|
||||
if _, _, _, ok := r.lookupIPCacheExpired(host); ok {
|
||||
// If we have some previous good value for this host,
|
||||
// don't give this DNS lookup much time. If we're in a
|
||||
// situation where the user's DNS server is unreachable
|
||||
@@ -194,12 +198,12 @@ func (r *Resolver) lookupTimeoutForHost(host string) time.Duration {
|
||||
return 10 * time.Second
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupIP(host string) (ip, ip6 net.IP, err error) {
|
||||
if ip, ip6, ok := r.lookupIPCache(host); ok {
|
||||
func (r *Resolver) lookupIP(host string) (ip, ip6 net.IP, allIPs []net.IPAddr, err error) {
|
||||
if ip, ip6, allIPs, ok := r.lookupIPCache(host); ok {
|
||||
if debug {
|
||||
log.Printf("dnscache: %q found in cache as %v", host, ip)
|
||||
}
|
||||
return ip, ip6, nil
|
||||
return ip, ip6, allIPs, nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), r.lookupTimeoutForHost(host))
|
||||
@@ -218,10 +222,10 @@ func (r *Resolver) lookupIP(host string) (ip, ip6 net.IP, err error) {
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return nil, nil, fmt.Errorf("no IPs for %q found", host)
|
||||
return nil, nil, nil, fmt.Errorf("no IPs for %q found", host)
|
||||
}
|
||||
|
||||
have4 := false
|
||||
@@ -240,12 +244,12 @@ func (r *Resolver) lookupIP(host string) (ip, ip6 net.IP, err error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
r.addIPCache(host, ip, ip6, r.ttl())
|
||||
return ip, ip6, nil
|
||||
r.addIPCache(host, ip, ip6, ips, r.ttl())
|
||||
return ip, ip6, ips, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) addIPCache(host string, ip, ip6 net.IP, d time.Duration) {
|
||||
if isPrivateIP(ip) {
|
||||
func (r *Resolver) addIPCache(host string, ip, ip6 net.IP, allIPs []net.IPAddr, d time.Duration) {
|
||||
if naIP, _ := netaddr.FromStdIP(ip); naIP.IsPrivate() {
|
||||
// Don't cache obviously wrong entries from captive portals.
|
||||
// TODO: use DoH or DoT for the forwarding resolver?
|
||||
if debug {
|
||||
@@ -263,27 +267,14 @@ func (r *Resolver) addIPCache(host string, ip, ip6 net.IP, d time.Duration) {
|
||||
if r.ipCache == nil {
|
||||
r.ipCache = make(map[string]ipCacheEntry)
|
||||
}
|
||||
r.ipCache[host] = ipCacheEntry{ip: ip, ip6: ip6, expires: time.Now().Add(d)}
|
||||
}
|
||||
|
||||
func mustCIDR(s string) *net.IPNet {
|
||||
_, ipNet, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
r.ipCache[host] = ipCacheEntry{
|
||||
ip: ip,
|
||||
ip6: ip6,
|
||||
allIPs: allIPs,
|
||||
expires: time.Now().Add(d),
|
||||
}
|
||||
return ipNet
|
||||
}
|
||||
|
||||
func isPrivateIP(ip net.IP) bool {
|
||||
return private1.Contains(ip) || private2.Contains(ip) || private3.Contains(ip)
|
||||
}
|
||||
|
||||
var (
|
||||
private1 = mustCIDR("10.0.0.0/8")
|
||||
private2 = mustCIDR("172.16.0.0/12")
|
||||
private3 = mustCIDR("192.168.0.0/16")
|
||||
)
|
||||
|
||||
type DialContextFunc func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
|
||||
// Dialer returns a wrapped DialContext func that uses the provided dnsCache.
|
||||
@@ -305,41 +296,131 @@ func Dialer(fwd DialContextFunc, dnsCache *Resolver) DialContextFunc {
|
||||
// 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
|
||||
}
|
||||
if c, err := raceDial(ctx, fwd, network, ips, port); err == nil {
|
||||
retConn = c
|
||||
ret = nil
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
ip, ip6, err := dnsCache.LookupIP(ctx, host)
|
||||
ip, ip6, allIPs, err := dnsCache.LookupIP(ctx, host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve %q: %w", host, err)
|
||||
}
|
||||
dst := net.JoinHostPort(ip.String(), port)
|
||||
if debug {
|
||||
log.Printf("dnscache: dialing %s, %s for %s", network, dst, address)
|
||||
i4s := v4addrs(allIPs)
|
||||
if len(i4s) < 2 {
|
||||
dst := net.JoinHostPort(ip.String(), port)
|
||||
if debug {
|
||||
log.Printf("dnscache: dialing %s, %s for %s", network, dst, address)
|
||||
}
|
||||
c, err := fwd(ctx, network, dst)
|
||||
if err == nil || ctx.Err() != nil || ip6 == nil {
|
||||
return c, err
|
||||
}
|
||||
// Fall back to trying IPv6.
|
||||
dst = net.JoinHostPort(ip6.String(), port)
|
||||
return fwd(ctx, network, dst)
|
||||
}
|
||||
c, err := fwd(ctx, network, dst)
|
||||
if err == nil || ctx.Err() != nil || ip6 == nil {
|
||||
return c, err
|
||||
}
|
||||
// Fall back to trying IPv6.
|
||||
// TODO(bradfitz): this is a primarily for IPv6-only
|
||||
// hosts; it's not supposed to be a real Happy
|
||||
// Eyeballs implementation. We should use the net
|
||||
// package's implementation of that by plumbing this
|
||||
// dnscache impl into net.Dialer.Resolver.Dial and
|
||||
// unmarshal/marshal DNS queries/responses to the net
|
||||
// package. This works for v6-only hosts for now.
|
||||
dst = net.JoinHostPort(ip6.String(), port)
|
||||
return fwd(ctx, network, dst)
|
||||
|
||||
// Multiple IPv4 candidates, and 0+ IPv6.
|
||||
ipsToTry := append(i4s, v6addrs(allIPs)...)
|
||||
return raceDial(ctx, fwd, network, ipsToTry, port)
|
||||
}
|
||||
}
|
||||
|
||||
// fallbackDelay is how long to wait between trying subsequent
|
||||
// addresses when multiple options are available.
|
||||
// 300ms is the same as Go's Happy Eyeballs fallbackDelay value.
|
||||
const fallbackDelay = 300 * time.Millisecond
|
||||
|
||||
// raceDial tries to dial port on each ip in ips, starting a new race
|
||||
// dial every fallbackDelay apart, returning whichever completes first.
|
||||
func raceDial(ctx context.Context, fwd DialContextFunc, network string, ips []netaddr.IP, port string) (net.Conn, error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
type res struct {
|
||||
c net.Conn
|
||||
err error
|
||||
}
|
||||
resc := make(chan res) // must be unbuffered
|
||||
failBoost := make(chan struct{}) // best effort send on dial failure
|
||||
|
||||
go func() {
|
||||
for i, ip := range ips {
|
||||
if i != 0 {
|
||||
timer := time.NewTimer(fallbackDelay)
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-failBoost:
|
||||
timer.Stop()
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
go func(ip netaddr.IP) {
|
||||
c, err := fwd(ctx, network, net.JoinHostPort(ip.String(), port))
|
||||
if err != nil {
|
||||
// Best effort wake-up a pending dial.
|
||||
// e.g. IPv4 dials failing quickly on an IPv6-only system.
|
||||
// In that case we don't want to wait 300ms per IPv4 before
|
||||
// we get to the IPv6 addresses.
|
||||
select {
|
||||
case failBoost <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
select {
|
||||
case resc <- res{c, err}:
|
||||
case <-ctx.Done():
|
||||
if c != nil {
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
}(ip)
|
||||
}
|
||||
}()
|
||||
|
||||
var firstErr error
|
||||
var fails int
|
||||
for {
|
||||
select {
|
||||
case r := <-resc:
|
||||
if r.c != nil {
|
||||
return r.c, nil
|
||||
}
|
||||
fails++
|
||||
if firstErr == nil {
|
||||
firstErr = r.err
|
||||
}
|
||||
if fails == len(ips) {
|
||||
return nil, firstErr
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func v4addrs(aa []net.IPAddr) (ret []netaddr.IP) {
|
||||
for _, a := range aa {
|
||||
if ip, ok := netaddr.FromStdIP(a.IP); ok && ip.Is4() {
|
||||
ret = append(ret, ip)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func v6addrs(aa []net.IPAddr) (ret []netaddr.IP) {
|
||||
for _, a := range aa {
|
||||
if ip, ok := netaddr.FromStdIP(a.IP); ok && ip.Is6() {
|
||||
ret = append(ret, ip)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
var errTLSHandshakeTimeout = errors.New("timeout doing TLS handshake")
|
||||
|
||||
// TLSDialer is like Dialer but returns a func suitable for using with net/http.Transport.DialTLSContext.
|
||||
|
||||
@@ -5,24 +5,29 @@
|
||||
package dnscache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestIsPrivateIP(t *testing.T) {
|
||||
tests := []struct {
|
||||
ip string
|
||||
want bool
|
||||
}{
|
||||
{"10.1.2.3", true},
|
||||
{"172.16.1.100", true},
|
||||
{"192.168.1.1", true},
|
||||
{"1.2.3.4", false},
|
||||
}
|
||||
var dialTest = flag.String("dial-test", "", "if non-empty, addr:port to test dial")
|
||||
|
||||
for _, test := range tests {
|
||||
if got := isPrivateIP(net.ParseIP(test.ip)); got != test.want {
|
||||
t.Errorf("isPrivateIP(%q)=%v, want %v", test.ip, got, test.want)
|
||||
}
|
||||
func TestDialer(t *testing.T) {
|
||||
if *dialTest == "" {
|
||||
t.Skip("skipping; --dial-test is blank")
|
||||
}
|
||||
r := new(Resolver)
|
||||
var std net.Dialer
|
||||
dialer := Dialer(std.DialContext, r)
|
||||
t0 := time.Now()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
c, err := dialer(ctx, "tcp", *dialTest)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("dialed in %v", time.Since(t0))
|
||||
c.Close()
|
||||
}
|
||||
|
||||
@@ -90,18 +90,25 @@
|
||||
"RegionName": "r4",
|
||||
"Nodes": [
|
||||
{
|
||||
"Name": "4a",
|
||||
"Name": "4c",
|
||||
"RegionID": 4,
|
||||
"HostName": "derp4.tailscale.com",
|
||||
"IPv4": "167.172.182.26",
|
||||
"IPv6": "2a03:b0c0:3:e0::36e:9001"
|
||||
"HostName": "derp4c.tailscale.com",
|
||||
"IPv4": "134.122.77.138",
|
||||
"IPv6": "2a03:b0c0:3:d0::1501:6001"
|
||||
},
|
||||
{
|
||||
"Name": "4b",
|
||||
"Name": "4d",
|
||||
"RegionID": 4,
|
||||
"HostName": "derp4b.tailscale.com",
|
||||
"IPv4": "157.230.25.0",
|
||||
"IPv6": "2a03:b0c0:3:e0::58f:3001"
|
||||
"HostName": "derp4d.tailscale.com",
|
||||
"IPv4": "134.122.94.167",
|
||||
"IPv6": "2a03:b0c0:3:d0::1501:b001"
|
||||
},
|
||||
{
|
||||
"Name": "4e",
|
||||
"RegionID": 4,
|
||||
"HostName": "derp4e.tailscale.com",
|
||||
"IPv4": "134.122.74.153",
|
||||
"IPv6": "2a03:b0c0:3:d0::29:9001"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -153,11 +160,25 @@
|
||||
"RegionName": "r8",
|
||||
"Nodes": [
|
||||
{
|
||||
"Name": "8a",
|
||||
"Name": "8b",
|
||||
"RegionID": 8,
|
||||
"HostName": "derp8.tailscale.com",
|
||||
"IPv4": "167.71.139.179",
|
||||
"IPv6": "2a03:b0c0:1:e0::3cc:e001"
|
||||
"HostName": "derp8b.tailscale.com",
|
||||
"IPv4": "46.101.74.201",
|
||||
"IPv6": "2a03:b0c0:1:d0::ec1:e001"
|
||||
},
|
||||
{
|
||||
"Name": "8c",
|
||||
"RegionID": 8,
|
||||
"HostName": "derp8c.tailscale.com",
|
||||
"IPv4": "206.189.16.32",
|
||||
"IPv6": "2a03:b0c0:1:d0::e1f:4001"
|
||||
},
|
||||
{
|
||||
"Name": "8d",
|
||||
"RegionID": 8,
|
||||
"HostName": "derp8d.tailscale.com",
|
||||
"IPv4": "178.62.44.132",
|
||||
"IPv6": "2a03:b0c0:1:d0::e08:e001"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build ignore
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
@@ -49,7 +49,7 @@ type entry struct {
|
||||
value interface{}
|
||||
}
|
||||
|
||||
// Add adds a value to the cache, set or updating its assoicated
|
||||
// Add adds a value to the cache, set or updating its associated
|
||||
// value.
|
||||
//
|
||||
// If MaxEntries is non-zero and the length of the cache is greater
|
||||
|
||||
@@ -134,7 +134,7 @@ func LocalAddresses() (regular, loopback []netaddr.IP, err error) {
|
||||
// but their OS supports IPv6 so they have an fe80::
|
||||
// address. We don't want to report all of those
|
||||
// IPv6 LL to Control.
|
||||
} else if ip.Is6() && tsaddr.IsULA(ip) {
|
||||
} else if ip.Is6() && ip.IsPrivate() {
|
||||
// Google Cloud Run uses NAT with IPv6 Unique
|
||||
// Local Addresses to provide IPv6 connectivity.
|
||||
ula6 = append(ula6, ip)
|
||||
@@ -479,7 +479,7 @@ func HTTPOfListener(ln net.Listener) string {
|
||||
var privateIP string
|
||||
ForeachInterfaceAddress(func(i Interface, pfx netaddr.IPPrefix) {
|
||||
ip := pfx.IP()
|
||||
if isPrivateIP(ip) {
|
||||
if ip.IsPrivate() {
|
||||
if privateIP == "" {
|
||||
privateIP = ip.String()
|
||||
}
|
||||
@@ -519,21 +519,15 @@ func LikelyHomeRouterIP() (gateway, myIP netaddr.IP, ok bool) {
|
||||
if !i.IsUp() || ip.IsZero() || !myIP.IsZero() {
|
||||
return
|
||||
}
|
||||
for _, prefix := range privatev4s {
|
||||
if prefix.Contains(gateway) && prefix.Contains(ip) {
|
||||
myIP = ip
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
if gateway.IsPrivate() && ip.IsPrivate() {
|
||||
myIP = ip
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
})
|
||||
return gateway, myIP, !myIP.IsZero()
|
||||
}
|
||||
|
||||
func isPrivateIP(ip netaddr.IP) bool {
|
||||
return private1.Contains(ip) || private2.Contains(ip) || private3.Contains(ip)
|
||||
}
|
||||
|
||||
// isUsableV4 reports whether ip is a usable IPv4 address which could
|
||||
// conceivably be used to get Internet connectivity. Globally routable and
|
||||
// private IPv4 addresses are always Usable, and link local 169.254.x.x
|
||||
@@ -554,23 +548,11 @@ func isUsableV4(ip netaddr.IP) bool {
|
||||
// (fc00::/7) are in some environments used with address translation.
|
||||
func isUsableV6(ip netaddr.IP) bool {
|
||||
return v6Global1.Contains(ip) ||
|
||||
(tsaddr.IsULA(ip) && !tsaddr.TailscaleULARange().Contains(ip))
|
||||
}
|
||||
|
||||
func mustCIDR(s string) netaddr.IPPrefix {
|
||||
prefix, err := netaddr.ParseIPPrefix(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return prefix
|
||||
(ip.Is6() && ip.IsPrivate() && !tsaddr.TailscaleULARange().Contains(ip))
|
||||
}
|
||||
|
||||
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}
|
||||
v6Global1 = mustCIDR("2000::/3")
|
||||
v6Global1 = netaddr.MustParseIPPrefix("2000::/3")
|
||||
)
|
||||
|
||||
// anyInterestingIP reports whether pfxs contains any IP that matches
|
||||
|
||||
@@ -73,7 +73,7 @@ func likelyHomeRouterIPDarwinExec() (ret netaddr.IP, ok bool) {
|
||||
return nil
|
||||
}
|
||||
ip, err := netaddr.ParseIP(string(mem.Append(nil, ipm)))
|
||||
if err == nil && isPrivateIP(ip) {
|
||||
if err == nil && ip.IsPrivate() {
|
||||
ret = ip
|
||||
// We've found what we're looking for.
|
||||
return errStopReadingNetstatTable
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux || (darwin && !ts_macext)
|
||||
// +build linux darwin,!ts_macext
|
||||
|
||||
package interfaces
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !linux && !windows && !darwin
|
||||
// +build !linux,!windows,!darwin
|
||||
|
||||
package interfaces
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user