Compare commits
375 Commits
c22wen/api
...
crawshaw/u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6d31436ec | ||
|
|
c6358f2247 | ||
|
|
0a84359d2d | ||
|
|
c81814e4f8 | ||
|
|
f9f3b67f3a | ||
|
|
bdb91a20eb | ||
|
|
1bc3c03562 | ||
|
|
fa6110e47b | ||
|
|
c576fea60e | ||
|
|
0b66cfe1e0 | ||
|
|
0430c2dd12 | ||
|
|
cc99059fc2 | ||
|
|
bf0740b011 | ||
|
|
a7f12a110a | ||
|
|
d79a2f3809 | ||
|
|
ef7bac2895 | ||
|
|
79d8288f0a | ||
|
|
66480755c2 | ||
|
|
387e83c8fe | ||
|
|
fee74e7ea7 | ||
|
|
d3e56aa979 | ||
|
|
04e72f95cc | ||
|
|
c445e3d327 | ||
|
|
258d0e8d9a | ||
|
|
4c80344e27 | ||
|
|
7325b5a7ba | ||
|
|
43b30e463c | ||
|
|
bcea88da46 | ||
|
|
c8af6bc009 | ||
|
|
f45a9e291b | ||
|
|
e453c7ca57 | ||
|
|
f11cb811cc | ||
|
|
bc159dc689 | ||
|
|
c136f48b79 | ||
|
|
a4b585947d | ||
|
|
1ca3e739f7 | ||
|
|
0d0fad43ed | ||
|
|
602f92ec30 | ||
|
|
b14ea68754 | ||
|
|
affd859121 | ||
|
|
d37b3b02cd | ||
|
|
63a9adeb6c | ||
|
|
82edf94df7 | ||
|
|
a6d098c750 | ||
|
|
829eb8363a | ||
|
|
ad6edf5ecd | ||
|
|
ffa70a617d | ||
|
|
10f48087f4 | ||
|
|
061422affc | ||
|
|
524fb2c190 | ||
|
|
6756f20632 | ||
|
|
2e347d1e10 | ||
|
|
ea49b1e811 | ||
|
|
1cb0ffc3ff | ||
|
|
92cdb30b26 | ||
|
|
f858b0d25f | ||
|
|
d01c60dad5 | ||
|
|
7461dded88 | ||
|
|
8a55d463c8 | ||
|
|
8d77dfdacb | ||
|
|
b4cf837d8a | ||
|
|
c3e5903b91 | ||
|
|
15b6969a95 | ||
|
|
63ed4dd6c9 | ||
|
|
95c03d1ead | ||
|
|
471f0c470a | ||
|
|
be779b3587 | ||
|
|
f304a45481 | ||
|
|
0d0ec7853c | ||
|
|
31721759f3 | ||
|
|
b89c757817 | ||
|
|
c0cdca6d06 | ||
|
|
24fa616e73 | ||
|
|
625c413508 | ||
|
|
487c520109 | ||
|
|
793cb131f0 | ||
|
|
ac3de93d5c | ||
|
|
30a37622b4 | ||
|
|
f647e3daaf | ||
|
|
b46e337cdc | ||
|
|
9df4185c94 | ||
|
|
03c344333e | ||
|
|
e3df29d488 | ||
|
|
a038e8690c | ||
|
|
38dc6fe758 | ||
|
|
d74cddcc56 | ||
|
|
34188d93d4 | ||
|
|
14dc790137 | ||
|
|
a55a03d5ff | ||
|
|
ee6475a44d | ||
|
|
dda03a911e | ||
|
|
0eea490724 | ||
|
|
719de8f0e1 | ||
|
|
2d5db90161 | ||
|
|
e98cdbb8b6 | ||
|
|
fec9dcbda1 | ||
|
|
fe16ef6812 | ||
|
|
f68431fc02 | ||
|
|
c1ae1a3d2d | ||
|
|
99d67493be | ||
|
|
000b80de9d | ||
|
|
3fd00c4a40 | ||
|
|
517c90d7e5 | ||
|
|
daf6de4f14 | ||
|
|
ea3715e3ce | ||
|
|
360095cd34 | ||
|
|
8ee1cb6156 | ||
|
|
54d7070121 | ||
|
|
abfd73f569 | ||
|
|
2404c0ffad | ||
|
|
ebf3f2fd9f | ||
|
|
e9e4f1063d | ||
|
|
f11952ad7f | ||
|
|
c64bd587ae | ||
|
|
d038a5295d | ||
|
|
188bb14269 | ||
|
|
6e42430ad8 | ||
|
|
df5adb2e23 | ||
|
|
b83c273737 | ||
|
|
2c500cee23 | ||
|
|
39f7a61e9c | ||
|
|
87f2e4c12c | ||
|
|
86d3a6c9a6 | ||
|
|
9748c5414e | ||
|
|
826f64e863 | ||
|
|
7ad3af2141 | ||
|
|
76fb27bea7 | ||
|
|
c386496e4f | ||
|
|
fd8e070d01 | ||
|
|
2d96215d97 | ||
|
|
6a2c6541da | ||
|
|
96a488e37e | ||
|
|
38629b62fc | ||
|
|
3e5c3e932c | ||
|
|
d98ef5699d | ||
|
|
7038c09bc9 | ||
|
|
d3efe8caf6 | ||
|
|
65815cc1ac | ||
|
|
4ec01323c1 | ||
|
|
73552eb32e | ||
|
|
dec01ef22b | ||
|
|
7e00100a0a | ||
|
|
fdac0387a7 | ||
|
|
36189e2704 | ||
|
|
bbb4631e04 | ||
|
|
f4ae745b0b | ||
|
|
e923639feb | ||
|
|
d7569863b5 | ||
|
|
52e24aa966 | ||
|
|
4f7d60ad42 | ||
|
|
29b028b9c4 | ||
|
|
54e108ff4e | ||
|
|
20e66c5b92 | ||
|
|
c7e5ab8094 | ||
|
|
ca51529b81 | ||
|
|
741d654aa3 | ||
|
|
1632f9fd6b | ||
|
|
88586ec4a4 | ||
|
|
0c673c1344 | ||
|
|
4cd9218351 | ||
|
|
be906dabd4 | ||
|
|
6680976b50 | ||
|
|
88ab0173a7 | ||
|
|
25321cbd01 | ||
|
|
5378776043 | ||
|
|
6075135e0a | ||
|
|
917307a90c | ||
|
|
34ffd4f7c6 | ||
|
|
de3001bc79 | ||
|
|
11bbfbd8bb | ||
|
|
635e4c7435 | ||
|
|
1ec64bc94d | ||
|
|
7e201806b1 | ||
|
|
1f0fa8b814 | ||
|
|
e101d8396d | ||
|
|
cbd6224ca4 | ||
|
|
4a82e36491 | ||
|
|
9b4e50cec0 | ||
|
|
07c3df13c6 | ||
|
|
e7caad61fb | ||
|
|
6b365b0239 | ||
|
|
e1f773ebba | ||
|
|
6d2b8df06d | ||
|
|
e86b39b73f | ||
|
|
1e7a35b225 | ||
|
|
ddfcc4326c | ||
|
|
a046b48593 | ||
|
|
6064b6ff47 | ||
|
|
138055dd70 | ||
|
|
ace57d7627 | ||
|
|
b9c2231fdf | ||
|
|
fb6b0e247c | ||
|
|
98f9e82c62 | ||
|
|
e8d4afedd1 | ||
|
|
a7562be5e1 | ||
|
|
6f7974b7f2 | ||
|
|
6099ecf7f4 | ||
|
|
7529b74018 | ||
|
|
aa6856a9eb | ||
|
|
d76334d2f0 | ||
|
|
6254efb9ef | ||
|
|
70eb05fd47 | ||
|
|
d37058af72 | ||
|
|
2f0cb98e50 | ||
|
|
f7eed25bb9 | ||
|
|
81466eef81 | ||
|
|
45fe06a89f | ||
|
|
e8cd7bb66f | ||
|
|
9a70789853 | ||
|
|
a2aa6cd2ed | ||
|
|
d139fa9c92 | ||
|
|
267531e4f8 | ||
|
|
717c715c96 | ||
|
|
516e8a4838 | ||
|
|
dd10babaed | ||
|
|
c7d4bf2333 | ||
|
|
2889fabaef | ||
|
|
761188e5d2 | ||
|
|
914a486af6 | ||
|
|
60e189f699 | ||
|
|
006a224f50 | ||
|
|
fe7c3e9c17 | ||
|
|
0bc73f8e4f | ||
|
|
c611d8480b | ||
|
|
c7fc4a06da | ||
|
|
de497358b8 | ||
|
|
1e28207a15 | ||
|
|
7a16ac80b7 | ||
|
|
4d943536f1 | ||
|
|
9f5b0d058f | ||
|
|
4dab0c1702 | ||
|
|
35e10c78fc | ||
|
|
692a011b54 | ||
|
|
e970ed0995 | ||
|
|
a7edcd0872 | ||
|
|
a98538f84a | ||
|
|
c3c59445ff | ||
|
|
0dde8fa0a8 | ||
|
|
4d3c09ced4 | ||
|
|
567c5a6d9e | ||
|
|
4fea604979 | ||
|
|
bf6205d200 | ||
|
|
9f7cbf6cf1 | ||
|
|
9ce92aad3e | ||
|
|
fa3543d629 | ||
|
|
e7bf144c3f | ||
|
|
97496a83af | ||
|
|
eb47cba435 | ||
|
|
daf2c70a08 | ||
|
|
d5baeeed5c | ||
|
|
4306433d1c | ||
|
|
9541886856 | ||
|
|
49d00b6a28 | ||
|
|
54d0d83b67 | ||
|
|
fec9490378 | ||
|
|
c55d26967b | ||
|
|
9f1b02699a | ||
|
|
a905ce5607 | ||
|
|
359055d3fa | ||
|
|
b5628cee4e | ||
|
|
edf64e0901 | ||
|
|
ec77b80c53 | ||
|
|
b5b4992eff | ||
|
|
d3dd7c6270 | ||
|
|
187e22a756 | ||
|
|
ab9cccb292 | ||
|
|
78338ac029 | ||
|
|
b405644f5d | ||
|
|
5fe5402fcd | ||
|
|
e4c075cd95 | ||
|
|
edce91a8a6 | ||
|
|
51bd1feae4 | ||
|
|
da4ec54756 | ||
|
|
5c619882bc | ||
|
|
9936cffc1a | ||
|
|
3fa86a8b23 | ||
|
|
4811236189 | ||
|
|
c78ed5b399 | ||
|
|
013da6660e | ||
|
|
8578b0445d | ||
|
|
7c1a9e8616 | ||
|
|
a64d06f15c | ||
|
|
503db5540f | ||
|
|
ed2169ae99 | ||
|
|
12bb949178 | ||
|
|
63af950d8c | ||
|
|
23c2dc2165 | ||
|
|
e23b4191c4 | ||
|
|
0733c5d2e0 | ||
|
|
57d95dd005 | ||
|
|
a2463e8948 | ||
|
|
d456bfdc6d | ||
|
|
2d837f79dc | ||
|
|
08baa17d9a | ||
|
|
7c76435bf7 | ||
|
|
d2529affa2 | ||
|
|
3ad7c2133a | ||
|
|
b560386c1a | ||
|
|
01e8b7fb7e | ||
|
|
5611f290eb | ||
|
|
a45665426b | ||
|
|
420c7a35e2 | ||
|
|
3ac952d4e9 | ||
|
|
a4b39022e0 | ||
|
|
b00c0e5f60 | ||
|
|
6e4231c03c | ||
|
|
654b5f1570 | ||
|
|
9abcb18061 | ||
|
|
22507adf54 | ||
|
|
017dcd520f | ||
|
|
c1dabd9436 | ||
|
|
b38fa7de29 | ||
|
|
020084e84d | ||
|
|
2bf49ddf90 | ||
|
|
ce058c8280 | ||
|
|
b2a08ddacd | ||
|
|
e4f53e9b6f | ||
|
|
b987b2ab18 | ||
|
|
7acd3397d5 | ||
|
|
9d73f84a71 | ||
|
|
a746ff5de7 | ||
|
|
8d7ddf5e94 | ||
|
|
ac42757cd7 | ||
|
|
43e060b0e5 | ||
|
|
8349e10907 | ||
|
|
b771a1363b | ||
|
|
2e9728023b | ||
|
|
2c328da094 | ||
|
|
0aed59b691 | ||
|
|
07e4009e15 | ||
|
|
0aa55bffce | ||
|
|
85e54af0d7 | ||
|
|
5eeaea9ef9 | ||
|
|
ad3fb6125d | ||
|
|
d6e9fb1df0 | ||
|
|
6b08303b0f | ||
|
|
676b5b7946 | ||
|
|
024671406b | ||
|
|
f85769b1ed | ||
|
|
a80446c026 | ||
|
|
4d15e954bd | ||
|
|
f69e46175d | ||
|
|
8b0112649a | ||
|
|
5aa5db89d6 | ||
|
|
5efb0a8bca | ||
|
|
c09d5a9e28 | ||
|
|
b5b9866ba2 | ||
|
|
a4cc31e7d8 | ||
|
|
1271e135cd | ||
|
|
18471a8792 | ||
|
|
1e4604f60e | ||
|
|
c580d2eab1 | ||
|
|
53f9dcdf05 | ||
|
|
80c33f4fa1 | ||
|
|
e0c4ffa71f | ||
|
|
fa3e8e1a28 | ||
|
|
67ebc7c0e7 | ||
|
|
02c34881b5 | ||
|
|
df64b7abf8 | ||
|
|
414cb4a695 | ||
|
|
b716c76df9 | ||
|
|
2f04f49376 | ||
|
|
e692e3866b | ||
|
|
d12add6e22 | ||
|
|
332759ef73 | ||
|
|
9985b3f1ed | ||
|
|
83fccf9fe5 | ||
|
|
b5129dadfd | ||
|
|
66be052a70 | ||
|
|
560da4884f | ||
|
|
d8a5b3f22f | ||
|
|
3e3bd5f169 | ||
|
|
312646c516 | ||
|
|
e8ae355bb8 | ||
|
|
1ccf997699 |
48
.github/workflows/coverage.yml
vendored
Normal file
48
.github/workflows/coverage.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Code Coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v1
|
||||
|
||||
# https://markphelps.me/2019/11/speed-up-your-go-builds-with-actions-cache/
|
||||
- name: Restore Cache
|
||||
uses: actions/cache@preview
|
||||
id: cache
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/go.sum') }}
|
||||
|
||||
- name: Basic build
|
||||
run: go build ./cmd/...
|
||||
|
||||
- name: Run tests on linux with coverage data
|
||||
run: go test -race -coverprofile=coverage.txt -bench=. -benchtime=1x ./...
|
||||
|
||||
- name: coveralls.io
|
||||
uses: shogo82148/actions-goveralls@v1
|
||||
env:
|
||||
COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.COVERALLS_BOT_PUBLIC_REPO_TOKEN }}
|
||||
with:
|
||||
path-to-profile: ./coverage.txt
|
||||
2
.github/workflows/cross-darwin.yml
vendored
2
.github/workflows/cross-darwin.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
go-version: 1.16
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/cross-freebsd.yml
vendored
2
.github/workflows/cross-freebsd.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
go-version: 1.16
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/cross-openbsd.yml
vendored
2
.github/workflows/cross-openbsd.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
go-version: 1.16
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/cross-windows.yml
vendored
2
.github/workflows/cross-windows.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
go-version: 1.16
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/depaware.yml
vendored
2
.github/workflows/depaware.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
go-version: 1.16
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
2
.github/workflows/license.yml
vendored
2
.github/workflows/license.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
go-version: 1.16
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
go-version: 1.16
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/linux32.yml
vendored
2
.github/workflows/linux32.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
go-version: 1.16
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
|
||||
2
.github/workflows/staticcheck.yml
vendored
2
.github/workflows/staticcheck.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.15
|
||||
go-version: 1.16
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
2
.github/workflows/windows.yml
vendored
2
.github/workflows/windows.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.15.x
|
||||
go-version: 1.16.x
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
33
Dockerfile
33
Dockerfile
@@ -2,6 +2,23 @@
|
||||
# Use of this source code is governed by a BSD-style
|
||||
# license that can be found in the LICENSE file.
|
||||
|
||||
############################################################################
|
||||
#
|
||||
# WARNING: Tailscale is not yet officially supported in Docker,
|
||||
# Kubernetes, etc.
|
||||
#
|
||||
# It might work, but we don't regularly test it, and it's not as polished as
|
||||
# our currently supported platforms. This is provided for people who know
|
||||
# how Tailscale works and what they're doing.
|
||||
#
|
||||
# Our tracking bug for officially support container use cases is:
|
||||
# https://github.com/tailscale/tailscale/issues/504
|
||||
#
|
||||
# Also, see the various bugs tagged "containers":
|
||||
# https://github.com/tailscale/tailscale/labels/containers
|
||||
#
|
||||
############################################################################
|
||||
|
||||
# This Dockerfile includes all the tailscale binaries.
|
||||
#
|
||||
# To build the Dockerfile:
|
||||
@@ -21,7 +38,7 @@
|
||||
# $ docker exec tailscaled tailscale status
|
||||
|
||||
|
||||
FROM golang:1.15-alpine AS build-env
|
||||
FROM golang:1.16-alpine AS build-env
|
||||
|
||||
WORKDIR /go/src/tailscale
|
||||
|
||||
@@ -31,7 +48,19 @@ RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN go install -v ./cmd/...
|
||||
# see build_docker.sh
|
||||
ARG VERSION_LONG=""
|
||||
ENV VERSION_LONG=$VERSION_LONG
|
||||
ARG VERSION_SHORT=""
|
||||
ENV VERSION_SHORT=$VERSION_SHORT
|
||||
ARG VERSION_GIT_HASH=""
|
||||
ENV VERSION_GIT_HASH=$VERSION_GIT_HASH
|
||||
|
||||
RUN go install -tags=xversion -ldflags="\
|
||||
-X tailscale.com/version.Long=$VERSION_LONG \
|
||||
-X tailscale.com/version.Short=$VERSION_SHORT \
|
||||
-X tailscale.com/version.GitCommit=$VERSION_GIT_HASH" \
|
||||
-v ./cmd/...
|
||||
|
||||
FROM alpine:3.11
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2
|
||||
|
||||
46
LICENSE
46
LICENSE
@@ -1,27 +1,29 @@
|
||||
Copyright (c) 2020 Tailscale & AUTHORS. All rights reserved.
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2020 Tailscale & AUTHORS.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Tailscale Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
8
Makefile
8
Makefile
@@ -12,7 +12,13 @@ depaware:
|
||||
go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscaled
|
||||
go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscale
|
||||
|
||||
check: staticcheck vet depaware
|
||||
buildwindows:
|
||||
GOOS=windows GOARCH=amd64 go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
|
||||
|
||||
build386:
|
||||
GOOS=linux GOARCH=386 go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
|
||||
|
||||
check: staticcheck vet depaware buildwindows build386
|
||||
|
||||
staticcheck:
|
||||
go run honnef.co/go/tools/cmd/staticcheck -- $$(go list ./... | grep -v tempfork)
|
||||
|
||||
13
README.md
13
README.md
@@ -43,7 +43,7 @@ If your distro has conventions that preclude the use of
|
||||
distro's way, so that bug reports contain useful version information.
|
||||
|
||||
We only guarantee to support the latest Go release and any Go beta or
|
||||
release candidate builds (currently Go 1.15) in module mode. It might
|
||||
release candidate builds (currently Go 1.16) in module mode. It might
|
||||
work in earlier Go versions or in GOPATH mode, but we're making no
|
||||
effort to keep those working.
|
||||
|
||||
@@ -63,8 +63,13 @@ Origin](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin)
|
||||
|
||||
## About Us
|
||||
|
||||
We are apenwarr, bradfitz, crawshaw, danderson, dfcarney, josharian
|
||||
from Tailscale Inc.
|
||||
You can learn more about us from [our website](https://tailscale.com).
|
||||
[Tailscale](https://tailscale.com/) is primarily developed by the
|
||||
people at https://github.com/orgs/tailscale/people. For other contributors,
|
||||
see:
|
||||
|
||||
* https://github.com/tailscale/tailscale/graphs/contributors
|
||||
* https://github.com/tailscale/tailscale-android/graphs/contributors
|
||||
|
||||
## Legal
|
||||
|
||||
WireGuard is a registered trademark of Jason A. Donenfeld.
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.3.0
|
||||
1.5.0
|
||||
|
||||
774
api.md
Normal file
774
api.md
Normal file
@@ -0,0 +1,774 @@
|
||||
# Tailscale API
|
||||
|
||||
The Tailscale API is a (mostly) RESTful API. Typically, POST bodies should be JSON encoded and responses will be JSON encoded.
|
||||
|
||||
# Authentication
|
||||
Currently based on {some authentication method}. Visit the [admin panel](https://api.tailscale.com/admin) and navigate to the `Keys` page. Generate an API Key and keep it safe. Provide the key as the user key in basic auth when making calls to Tailscale API endpoints.
|
||||
|
||||
# APIs
|
||||
|
||||
* **[Devices](#device)**
|
||||
- [GET device](#device-get)
|
||||
- [DELETE device](#device-delete)
|
||||
- Routes
|
||||
- [GET device routes](#device-routes-get)
|
||||
- [POST device routes](#device-routes-post)
|
||||
* **[Tailnets](#tailnet)**
|
||||
- ACLs
|
||||
- [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
|
||||
- [Devices](#tailnet-devices)
|
||||
- [GET tailnet devices](#tailnet-devices-get)
|
||||
- [DNS](#tailnet-dns)
|
||||
- [GET tailnet DNS nameservers](#tailnet-dns-nameservers-get)
|
||||
- [POST tailnet DNS nameservers](#tailnet-dns-nameservers-post)
|
||||
- [GET tailnet DNS preferences](#tailnet-dns-preferences-get)
|
||||
- [POST tailnet DNS preferences](#tailnet-dns-preferences-post)
|
||||
- [GET tailnet DNS searchpaths](#tailnet-dns-searchpaths-get)
|
||||
- [POST tailnet DNS searchpaths](#tailnet-dns-searchpaths-post)
|
||||
|
||||
## Device
|
||||
<!-- TODO: description about what devices are -->
|
||||
Each Tailscale-connected device has a globally-unique identifier number which we refer as the "deviceID" or sometimes, just "id".
|
||||
You can use the deviceID to specify operations on a specific device, like retrieving its subnet routes.
|
||||
|
||||
To find the deviceID of a particular device, you can use the ["GET /devices"](#getdevices) API call and generate a list of devices on your network.
|
||||
Find the device you're looking for and get the "id" field.
|
||||
This is your deviceID.
|
||||
|
||||
<a name=device-get></div>
|
||||
|
||||
#### `GET /api/v2/device/:deviceid` - lists the details for a device
|
||||
Returns the details for the specified device.
|
||||
Supply the device of interest in the path using its ID.
|
||||
Use the `fields` query parameter to explicitly indicate which fields are returned.
|
||||
|
||||
|
||||
##### Parameters
|
||||
##### Query Parameters
|
||||
`fields` - Controls which fields will be included in the returned response.
|
||||
Currently, supported options are:
|
||||
* `all`: returns all fields in the response.
|
||||
* `default`: return all fields except:
|
||||
* `enabledRoutes`
|
||||
* `advertisedRoutes`
|
||||
* `clientConnectivity` (which contains the following fields: `mappingVariesByDestIP`, `derp`, `endpoints`, `latency`, and `clientSupports`)
|
||||
|
||||
Use commas to separate multiple options.
|
||||
If more than one option is indicated, then the union is used.
|
||||
For example, for `fields=default,all`, all fields are returned.
|
||||
If the `fields` parameter is not provided, then the default option is used.
|
||||
|
||||
##### Example
|
||||
```
|
||||
GET /api/v2/device/12345
|
||||
curl 'https://api.tailscale.com/api/v2/device/12345?fields=all' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
{
|
||||
"addresses":[
|
||||
"100.105.58.116"
|
||||
],
|
||||
"id":"12345",
|
||||
"user":"user1@example.com",
|
||||
"name":"user1-device.example.com",
|
||||
"hostname":"User1-Device",
|
||||
"clientVersion":"date.20201107",
|
||||
"updateAvailable":false,
|
||||
"os":"macOS",
|
||||
"created":"2020-11-20T20:56:49Z",
|
||||
"lastSeen":"2020-11-20T16:15:55-05:00",
|
||||
"keyExpiryDisabled":false,
|
||||
"expires":"2021-05-19T20:56:49Z",
|
||||
"authorized":true,
|
||||
"isExternal":false,
|
||||
"machineKey":"mkey:user1-machine-key",
|
||||
"nodeKey":"nodekey:user1-node-key",
|
||||
"blocksIncomingConnections":false,
|
||||
"enabledRoutes":[
|
||||
|
||||
],
|
||||
"advertisedRoutes":[
|
||||
|
||||
],
|
||||
"clientConnectivity": {
|
||||
"endpoints":[
|
||||
"209.195.87.231:59128",
|
||||
"192.168.0.173:59128"
|
||||
],
|
||||
"derp":"",
|
||||
"mappingVariesByDestIP":false,
|
||||
"latency":{
|
||||
"Dallas":{
|
||||
"latencyMs":60.463043
|
||||
},
|
||||
"New York City":{
|
||||
"preferred":true,
|
||||
"latencyMs":31.323811
|
||||
},
|
||||
"San Francisco":{
|
||||
"latencyMs":81.313389
|
||||
}
|
||||
},
|
||||
"clientSupports":{
|
||||
"hairPinning":false,
|
||||
"ipv6":false,
|
||||
"pcp":false,
|
||||
"pmp":false,
|
||||
"udp":true,
|
||||
"upnp":false
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<a name=device-delete></div>
|
||||
|
||||
#### `DELETE /api/v2/device/:deviceID` - deletes the device from its tailnet
|
||||
Deletes the provided device from its tailnet.
|
||||
The device must belong to the user's tailnet.
|
||||
Deleting shared/external devices is not supported.
|
||||
Supply the device of interest in the path using its ID.
|
||||
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
```
|
||||
DELETE /api/v2/device/12345
|
||||
curl -X DELETE 'https://api.tailscale.com/api/v2/device/12345' \
|
||||
-u "tskey-yourapikey123:" -v
|
||||
```
|
||||
|
||||
Response
|
||||
|
||||
If successful, the response should be empty:
|
||||
```
|
||||
< HTTP/1.1 200 OK
|
||||
...
|
||||
* Connection #0 to host left intact
|
||||
* Closing connection 0
|
||||
```
|
||||
|
||||
If the device is not owned by your tailnet:
|
||||
```
|
||||
< HTTP/1.1 501 Not Implemented
|
||||
...
|
||||
{"message":"cannot delete devices outside of your tailnet"}
|
||||
```
|
||||
|
||||
|
||||
<a name=device-routes-get></div>
|
||||
|
||||
#### `GET /api/v2/device/:deviceID/routes` - fetch subnet routes that are advertised and enabled for a device
|
||||
|
||||
Retrieves the list of subnet routes that a device is advertising, as well as those that are enabled for it. Enabled routes are not necessarily advertised (e.g. for pre-enabling), and likewise, advertised routes are not necessarily enabled.
|
||||
|
||||
##### Parameters
|
||||
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/device/11055/routes' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
{
|
||||
"advertisedRoutes" : [
|
||||
"10.0.1.0/24",
|
||||
"1.2.0.0/16",
|
||||
"2.0.0.0/24"
|
||||
],
|
||||
"enabledRoutes" : []
|
||||
}
|
||||
```
|
||||
|
||||
<a name=device-routes-post></div>
|
||||
|
||||
#### `POST /api/v2/device/:deviceID/routes` - set the subnet routes that are enabled for a device
|
||||
|
||||
Sets which subnet routes are enabled to be routed by a device by replacing the existing list of subnet routes with the supplied parameters. Routes can be enabled without a device advertising them (e.g. for preauth). Returns a list of enabled subnet routes and a list of advertised subnet routes for a device.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
`routes` - The new list of enabled subnet routes in JSON.
|
||||
```
|
||||
{
|
||||
"routes": ["10.0.1.0/24", "1.2.0.0/16", "2.0.0.0/24"]
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/device/11055/routes' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"routes": ["10.0.1.0/24", "1.2.0.0/16", "2.0.0.0/24"]}'
|
||||
```
|
||||
|
||||
Response
|
||||
|
||||
```
|
||||
{
|
||||
"advertisedRoutes" : [
|
||||
"10.0.1.0/24",
|
||||
"1.2.0.0/16",
|
||||
"2.0.0.0/24"
|
||||
],
|
||||
"enabledRoutes" : [
|
||||
"10.0.1.0/24",
|
||||
"1.2.0.0/16",
|
||||
"2.0.0.0/24"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Tailnet
|
||||
A tailnet is the name of your Tailscale network.
|
||||
You can find it in the top left corner of the [Admin Panel](https://login.tailscale.com/admin) beside the Tailscale logo.
|
||||
|
||||
|
||||
`alice@example.com` belongs to the `example.com` tailnet and would use the following format for API calls:
|
||||
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/...
|
||||
curl https://api.tailscale.com/api/v2/tailnet/example.com/...
|
||||
```
|
||||
|
||||
|
||||
For solo plans, the tailnet is the email you signed up with.
|
||||
So `alice@gmail.com` has the tailnet `alice@gmail.com` since `@gmail.com` is a shared email host.
|
||||
Her API calls would have the following format:
|
||||
```
|
||||
GET /api/v2/tailnet/alice@gmail.com/...
|
||||
curl https://api.tailscale.com/api/v2/tailnet/alice@gmail.com/...
|
||||
```
|
||||
|
||||
Tailnets are a top-level resource. ACL is an example of a resource that is tied to a top-level tailnet.
|
||||
|
||||
For more information on Tailscale networks/tailnets, click [here](https://tailscale.com/kb/1064/invite-team-members).
|
||||
|
||||
### ACL
|
||||
|
||||
<a name=tailnet-acl-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/acl` - fetch ACL for a tailnet
|
||||
|
||||
Retrieves the ACL that is currently set for the given tailnet. Supply the tailnet of interest in the path. This endpoint can send back either the HuJSON of the ACL or a parsed JSON, depending on the `Accept` header.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### Headers
|
||||
`Accept` - Response is parsed `JSON` if `application/json` is explicitly named, otherwise HuJSON will be returned.
|
||||
|
||||
##### Returns
|
||||
Returns the ACL HuJSON by default. Returns a parsed JSON of the ACL (sans comments) if the `Accept` type is explicitly set to `application/json`. An `ETag` header is also sent in the response, which can be optionally used in POST requests to avoid missed updates.
|
||||
<!-- TODO (chungdaniel): define error types and a set of docs for them -->
|
||||
|
||||
##### Example
|
||||
|
||||
###### Requesting a HuJSON response:
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/acl
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
-H "Accept: application/hujson" \
|
||||
-v
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
...
|
||||
Content-Type: application/hujson
|
||||
Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
|
||||
...
|
||||
|
||||
// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
"Tests": [],
|
||||
// Declare static groups of users beyond those in the identity service.
|
||||
"Groups": {
|
||||
"group:example": [
|
||||
"user1@example.com",
|
||||
"user2@example.com"
|
||||
],
|
||||
},
|
||||
// Declare convenient hostname aliases to use in place of IP addresses.
|
||||
"Hosts": {
|
||||
"example-host-1": "100.100.100.100",
|
||||
},
|
||||
// Access control lists.
|
||||
"ACLs": [
|
||||
// Match absolutely everything. Comment out this section if you want
|
||||
// to define specific ACL restrictions.
|
||||
{
|
||||
"Action": "accept",
|
||||
"Users": [
|
||||
"*"
|
||||
],
|
||||
"Ports": [
|
||||
"*:*"
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
###### Requesting a JSON response:
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/acl
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
-H "Accept: application/json" \
|
||||
-v
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
...
|
||||
Content-Type: application/json
|
||||
Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
|
||||
...
|
||||
{
|
||||
"acls" : [
|
||||
{
|
||||
"action" : "accept",
|
||||
"ports" : [
|
||||
"*:*"
|
||||
],
|
||||
"users" : [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
],
|
||||
"groups" : {
|
||||
"group:example" : [
|
||||
"user1@example.com",
|
||||
"user2@example.com"
|
||||
]
|
||||
},
|
||||
"hosts" : {
|
||||
"example-host-1" : "100.100.100.100"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-acl-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/acl` - set ACL for a tailnet
|
||||
|
||||
Sets the ACL for the given tailnet. HuJSON and JSON are both accepted inputs. An `If-Match` header can be set to avoid missed updates.
|
||||
|
||||
Returns error for invalid ACLs.
|
||||
Returns error if using an `If-Match` header and the ETag does not match.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### Headers
|
||||
`If-Match` - A request header. Set this value to the ETag header provided in an `ACL GET` request to avoid missed updates.
|
||||
|
||||
`Accept` - Sets the return type of the updated ACL. Response is parsed `JSON` if `application/json` is explicitly named, otherwise HuJSON will be returned.
|
||||
|
||||
###### POST Body
|
||||
ACL JSON or HuJSON (see https://tailscale.com/kb/1018/acls)
|
||||
|
||||
##### Example
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/acl
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
-H "If-Match: \"e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c\""
|
||||
--data-binary '// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
// Declare tests to check functionality of ACL rules. User must be a valid user with registered machines.
|
||||
"Tests": [
|
||||
// {"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]},
|
||||
],
|
||||
// Declare static groups of users beyond those in the identity service.
|
||||
"Groups": {
|
||||
"group:example": [ "user1@example.com", "user2@example.com" ],
|
||||
},
|
||||
// Declare convenient hostname aliases to use in place of IP addresses.
|
||||
"Hosts": {
|
||||
"example-host-1": "100.100.100.100",
|
||||
},
|
||||
// Access control lists.
|
||||
"ACLs": [
|
||||
// Match absolutely everything. Comment out this section if you want
|
||||
// to define specific ACL restrictions.
|
||||
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
// Declare tests to check functionality of ACL rules. User must be a valid user with registered machines.
|
||||
"Tests": [
|
||||
// {"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]},
|
||||
],
|
||||
// Declare static groups of users beyond those in the identity service.
|
||||
"Groups": {
|
||||
"group:example": [ "user1@example.com", "user2@example.com" ],
|
||||
},
|
||||
// Declare convenient hostname aliases to use in place of IP addresses.
|
||||
"Hosts": {
|
||||
"example-host-1": "100.100.100.100",
|
||||
},
|
||||
// Access control lists.
|
||||
"ACLs": [
|
||||
// Match absolutely everything. Comment out this section if you want
|
||||
// to define specific ACL restrictions.
|
||||
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-acl-preview-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/acl/preview` - preview rule matches on an ACL for a resource
|
||||
Determines what rules match for a user on an ACL without saving the ACL to the server.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### Query Parameters
|
||||
`user` - A user's email. The provided ACL is queried with this user to determine which rules match.
|
||||
|
||||
###### POST Body
|
||||
ACL JSON or HuJSON (see https://tailscale.com/kb/1018/acls)
|
||||
|
||||
##### Example
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/acl/preiew
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl?user=user1@example.com' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '// Example/default ACLs for unrestricted connections.
|
||||
{
|
||||
// Declare tests to check functionality of ACL rules. User must be a valid user with registered machines.
|
||||
"Tests": [
|
||||
// {"User": "user1@example.com", "Allow": ["example-host-1:22"], "Deny": ["example-host-2:100"]},
|
||||
],
|
||||
// Declare static groups of users beyond those in the identity service.
|
||||
"Groups": {
|
||||
"group:example": [ "user1@example.com", "user2@example.com" ],
|
||||
},
|
||||
// Declare convenient hostname aliases to use in place of IP addresses.
|
||||
"Hosts": {
|
||||
"example-host-1": "100.100.100.100",
|
||||
},
|
||||
// Access control lists.
|
||||
"ACLs": [
|
||||
// Match absolutely everything. Comment out this section if you want
|
||||
// to define specific ACL restrictions.
|
||||
{ "Action": "accept", "Users": ["*"], "Ports": ["*:*"] },
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
{"matches":[{"users":["*"],"ports":["*:*"],"lineNumber":19}],"user":"user1@example.com"}
|
||||
```
|
||||
|
||||
<a name=tailnet-devices></a>
|
||||
|
||||
### Devices
|
||||
|
||||
<a name=tailnet-devices-get></a>
|
||||
|
||||
#### <a name="getdevices"></a> `GET /api/v2/tailnet/:tailnet/devices` - list the devices for a tailnet
|
||||
Lists the devices in a tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
Use the `fields` query parameter to explicitly indicate which fields are returned.
|
||||
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### Query Parameters
|
||||
`fields` - Controls which fields will be included in the returned response.
|
||||
Currently, supported options are:
|
||||
* `all`: Returns all fields in the response.
|
||||
* `default`: return all fields except:
|
||||
* `enabledRoutes`
|
||||
* `advertisedRoutes`
|
||||
* `clientConnectivity` (which contains the following fields: `mappingVariesByDestIP`, `derp`, `endpoints`, `latency`, and `clientSupports`)
|
||||
|
||||
Use commas to separate multiple options.
|
||||
If more than one option is indicated, then the union is used.
|
||||
For example, for `fields=default,all`, all fields are returned.
|
||||
If the `fields` parameter is not provided, then the default option is used.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/devices
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/devices' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
{
|
||||
"devices":[
|
||||
{
|
||||
"addresses":[
|
||||
"100.68.203.125"
|
||||
],
|
||||
"clientVersion":"date.20201107",
|
||||
"os":"macOS",
|
||||
"name":"user1-device.example.com",
|
||||
"created":"2020-11-30T22:20:04Z",
|
||||
"lastSeen":"2020-11-30T17:20:04-05:00",
|
||||
"hostname":"User1-Device",
|
||||
"machineKey":"mkey:user1-node-key",
|
||||
"nodeKey":"nodekey:user1-node-key",
|
||||
"id":"12345",
|
||||
"user":"user1@example.com",
|
||||
"expires":"2021-05-29T22:20:04Z",
|
||||
"keyExpiryDisabled":false,
|
||||
"authorized":false,
|
||||
"isExternal":false,
|
||||
"updateAvailable":false,
|
||||
"blocksIncomingConnections":false,
|
||||
},
|
||||
{
|
||||
"addresses":[
|
||||
"100.111.63.90"
|
||||
],
|
||||
"clientVersion":"date.20201107",
|
||||
"os":"macOS",
|
||||
"name":"user2-device.example.com",
|
||||
"created":"2020-11-30T22:21:03Z",
|
||||
"lastSeen":"2020-11-30T17:21:03-05:00",
|
||||
"hostname":"User2-Device",
|
||||
"machineKey":"mkey:user2-machine-key",
|
||||
"nodeKey":"nodekey:user2-node-key",
|
||||
"id":"48810",
|
||||
"user":"user2@example.com",
|
||||
"expires":"2021-05-29T22:21:03Z",
|
||||
"keyExpiryDisabled":false,
|
||||
"authorized":false,
|
||||
"isExternal":false,
|
||||
"updateAvailable":false,
|
||||
"blocksIncomingConnections":false,
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns></a>
|
||||
|
||||
### DNS
|
||||
|
||||
<a name=tailnet-dns-nameservers-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/dns/nameservers` - list the DNS nameservers for a tailnet
|
||||
Lists the DNS nameservers for a tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/dns/nameservers
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameservers' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response
|
||||
```
|
||||
{
|
||||
"dns": ["8.8.8.8"],
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns-nameservers-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/dns/nameservers` - replaces the list of DNS nameservers for a tailnet
|
||||
Replaces the list of DNS nameservers for the given tailnet with the list supplied by the user.
|
||||
Supply the tailnet of interest in the path.
|
||||
Note that changing the list of DNS nameservers may also affect the status of MagicDNS (if MagicDNS is on).
|
||||
|
||||
##### Parameters
|
||||
###### POST Body
|
||||
`dns` - The new list of DNS nameservers in JSON.
|
||||
```
|
||||
{
|
||||
"dns":["8.8.8.8"]
|
||||
}
|
||||
```
|
||||
|
||||
##### Returns
|
||||
Returns the new list of nameservers and the status of MagicDNS.
|
||||
|
||||
If all nameservers have been removed, MagicDNS will be automatically disabled (until explicitly turned back on by the user).
|
||||
|
||||
##### Example
|
||||
###### Adding DNS nameservers with the MagicDNS on:
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/dns/nameservers
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameservers' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"dns": ["8.8.8.8"]}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"dns":["8.8.8.8"],
|
||||
"magicDNS":true,
|
||||
}
|
||||
```
|
||||
|
||||
###### Removing all DNS nameservers with the MagicDNS on:
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/dns/nameservers
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/nameservers' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"dns": []}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"dns":[],
|
||||
"magicDNS": false,
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns-preferences-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/dns/preferences` - retrieves the DNS preferences for a tailnet
|
||||
Retrieves the DNS preferences that are currently set for the given tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/dns/preferences
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/preferences' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"magicDNS":false,
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns-preferences-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/dns/preferences` - replaces the DNS preferences for a tailnet
|
||||
Replaces the DNS preferences for a tailnet, specifically, the MagicDNS setting.
|
||||
Note that MagicDNS is dependent on DNS servers.
|
||||
|
||||
If there is at least one DNS server, then MagicDNS can be enabled.
|
||||
Otherwise, it returns an error.
|
||||
Note that removing all nameservers will turn off MagicDNS.
|
||||
To reenable it, nameservers must be added back, and MagicDNS must be explicitly turned on.
|
||||
|
||||
##### Parameters
|
||||
###### POST Body
|
||||
The DNS preferences in JSON. Currently, MagicDNS is the only setting available.
|
||||
`magicDNS` - Automatically registers DNS names for devices in your tailnet.
|
||||
```
|
||||
{
|
||||
"magicDNS": true
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/dns/preferences
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/preferences' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"magicDNS": true}'
|
||||
```
|
||||
|
||||
|
||||
Response:
|
||||
|
||||
If there are no DNS servers, it returns an error message:
|
||||
```
|
||||
{
|
||||
"message":"need at least one nameserver to enable MagicDNS"
|
||||
}
|
||||
```
|
||||
|
||||
If there are DNS servers:
|
||||
```
|
||||
{
|
||||
"magicDNS":true,
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns-searchpaths-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/dns/searchpaths` - retrieves the search paths for a tailnet
|
||||
Retrieves the list of search paths that is currently set for the given tailnet.
|
||||
Supply the tailnet of interest in the path.
|
||||
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Example
|
||||
```
|
||||
GET /api/v2/tailnet/example.com/dns/searchpaths
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/searchpaths' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"searchPaths": ["user1.example.com"],
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-dns-searchpaths-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/dns/searchpaths` - replaces the search paths for a tailnet
|
||||
Replaces the list of searchpaths with the list supplied by the user and returns an error otherwise.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
`searchPaths` - A list of searchpaths in JSON.
|
||||
```
|
||||
{
|
||||
"searchPaths: ["user1.example.com", "user2.example.com"]
|
||||
}
|
||||
```
|
||||
|
||||
##### Example
|
||||
```
|
||||
POST /api/v2/tailnet/example.com/dns/searchpaths
|
||||
curl -X POST 'https://api.tailscale.com/api/v2/tailnet/example.com/dns/searchpaths' \
|
||||
-u "tskey-yourapikey123:" \
|
||||
--data-binary '{"searchPaths": ["user1.example.com", "user2.example.com"]}'
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"searchPaths": ["user1.example.com", "user2.example.com"],
|
||||
}
|
||||
```
|
||||
34
build_docker.sh
Executable file
34
build_docker.sh
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
#
|
||||
# Runs `go build` with flags configured for docker distribution. All
|
||||
# it does differently from `go build` is burn git commit and version
|
||||
# information into the binaries inside docker, so that we can track down user
|
||||
# issues.
|
||||
#
|
||||
############################################################################
|
||||
#
|
||||
# WARNING: Tailscale is not yet officially supported in Docker,
|
||||
# Kubernetes, etc.
|
||||
#
|
||||
# It might work, but we don't regularly test it, and it's not as polished as
|
||||
# our currently supported platforms. This is provided for people who know
|
||||
# how Tailscale works and what they're doing.
|
||||
#
|
||||
# Our tracking bug for officially support container use cases is:
|
||||
# https://github.com/tailscale/tailscale/issues/504
|
||||
#
|
||||
# Also, see the various bugs tagged "containers":
|
||||
# https://github.com/tailscale/tailscale/labels/containers
|
||||
#
|
||||
############################################################################
|
||||
|
||||
set -eu
|
||||
|
||||
eval $(./version/version.sh)
|
||||
|
||||
docker build \
|
||||
--build-arg VERSION_LONG=$VERSION_LONG \
|
||||
--build-arg VERSION_SHORT=$VERSION_SHORT \
|
||||
--build-arg VERSION_GIT_HASH=$VERSION_GIT_HASH \
|
||||
-t tailscale:tailscale .
|
||||
110
client/tailscale/tailscale.go
Normal file
110
client/tailscale/tailscale.go
Normal file
@@ -0,0 +1,110 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package tailscale contains Tailscale client code.
|
||||
package tailscale
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
// tsClient does HTTP requests to the local Tailscale daemon.
|
||||
var tsClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
if addr != "local-tailscaled.sock:80" {
|
||||
return nil, fmt.Errorf("unexpected URL address %q", addr)
|
||||
}
|
||||
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
|
||||
// a TCP server on a random port, find the random port. For HTTP connections,
|
||||
// we don't send the token. It gets added in an HTTP Basic-Auth header.
|
||||
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
|
||||
}
|
||||
return safesocket.ConnectDefault()
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// DoLocalRequest makes an HTTP request to the local machine's Tailscale daemon.
|
||||
//
|
||||
// URLs are of the form http://local-tailscaled.sock/localapi/v0/whois?ip=1.2.3.4.
|
||||
//
|
||||
// The hostname must be "local-tailscaled.sock", even though it
|
||||
// doesn't actually do any DNS lookup. The actual means of connecting to and
|
||||
// authenticating to the local Tailscale daemon vary by platform.
|
||||
//
|
||||
// DoLocalRequest may mutate the request to add Authorization headers.
|
||||
func DoLocalRequest(req *http.Request) (*http.Response, error) {
|
||||
if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil {
|
||||
req.SetBasicAuth("", token)
|
||||
}
|
||||
return tsClient.Do(req)
|
||||
}
|
||||
|
||||
// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port.
|
||||
func WhoIs(ctx context.Context, remoteAddr string) (*tailcfg.WhoIsResponse, error) {
|
||||
var ip string
|
||||
if net.ParseIP(remoteAddr) != nil {
|
||||
ip = remoteAddr
|
||||
} else {
|
||||
var err error
|
||||
ip, _, err = net.SplitHostPort(remoteAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid remoteAddr %q", remoteAddr)
|
||||
}
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/whois?ip="+url.QueryEscape(ip), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := DoLocalRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
slurp, _ := ioutil.ReadAll(res.Body)
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("HTTP %s: %s", res.Status, slurp)
|
||||
}
|
||||
r := new(tailcfg.WhoIsResponse)
|
||||
if err := json.Unmarshal(slurp, r); err != nil {
|
||||
if max := 200; len(slurp) > max {
|
||||
slurp = slurp[:max]
|
||||
}
|
||||
return nil, fmt.Errorf("failed to parse JSON WhoIsResponse from %q", slurp)
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func Goroutines(ctx context.Context) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/goroutines", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := DoLocalRequest(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("HTTP %s: %s", res.Status, body)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
@@ -140,7 +140,7 @@ func main() {
|
||||
flag.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
if err := ioutil.WriteFile(output, out, 0666); err != nil {
|
||||
if err := ioutil.WriteFile(output, out, 0644); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
69
cmd/derper/bootstrap_dns.go
Normal file
69
cmd/derper/bootstrap_dns.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"expvar"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
dnsMu sync.Mutex
|
||||
dnsCache = map[string][]net.IP{}
|
||||
)
|
||||
|
||||
var bootstrapDNSRequests = expvar.NewInt("counter_bootstrap_dns_requests")
|
||||
|
||||
func refreshBootstrapDNSLoop() {
|
||||
if *bootstrapDNS == "" {
|
||||
return
|
||||
}
|
||||
for {
|
||||
refreshBootstrapDNS()
|
||||
time.Sleep(10 * time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
func refreshBootstrapDNS() {
|
||||
if *bootstrapDNS == "" {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
names := strings.Split(*bootstrapDNS, ",")
|
||||
var r net.Resolver
|
||||
for _, name := range names {
|
||||
addrs, err := r.LookupIP(ctx, "ip", name)
|
||||
if err != nil {
|
||||
log.Printf("bootstrap DNS lookup %q: %v", name, err)
|
||||
continue
|
||||
}
|
||||
dnsMu.Lock()
|
||||
dnsCache[name] = addrs
|
||||
dnsMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func handleBootstrapDNS(w http.ResponseWriter, r *http.Request) {
|
||||
bootstrapDNSRequests.Add(1)
|
||||
dnsMu.Lock()
|
||||
j, err := json.MarshalIndent(dnsCache, "", "\t")
|
||||
dnsMu.Unlock()
|
||||
if err != nil {
|
||||
log.Printf("bootstrap DNS JSON: %v", err)
|
||||
http.Error(w, "JSON marshal error", 500)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(j)
|
||||
}
|
||||
@@ -48,6 +48,7 @@ var (
|
||||
runSTUN = flag.Bool("stun", false, "also run a STUN server")
|
||||
meshPSKFile = flag.String("mesh-psk-file", defaultMeshPSKFile(), "if non-empty, path to file containing the mesh pre-shared key file. It should contain some hex string; whitespace is trimmed.")
|
||||
meshWith = flag.String("mesh-with", "", "optional comma-separated list of hostnames to mesh with; the server's own hostname can be in the list")
|
||||
bootstrapDNS = flag.String("bootstrap-dns-names", "", "optional comma-separated list of hostnames to make available at /bootstrap-dns")
|
||||
)
|
||||
|
||||
type config struct {
|
||||
@@ -97,7 +98,7 @@ func writeNewConfig() config {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if err := atomicfile.WriteFile(*configPath, b, 0666); err != nil {
|
||||
if err := atomicfile.WriteFile(*configPath, b, 0600); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
return cfg
|
||||
@@ -145,6 +146,8 @@ func main() {
|
||||
// Create our own mux so we don't expose /debug/ stuff to the world.
|
||||
mux := tsweb.NewMux(debugHandler(s))
|
||||
mux.Handle("/derp", derphttp.Handler(s))
|
||||
go refreshBootstrapDNSLoop()
|
||||
mux.HandleFunc("/bootstrap-dns", handleBootstrapDNS)
|
||||
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
w.WriteHeader(200)
|
||||
@@ -153,7 +156,7 @@ func main() {
|
||||
<p>
|
||||
This is a
|
||||
<a href="https://tailscale.com/">Tailscale</a>
|
||||
<a href="https://godoc.org/tailscale.com/derp">DERP</a>
|
||||
<a href="https://pkg.go.dev/tailscale.com/derp">DERP</a>
|
||||
server.
|
||||
</p>
|
||||
`)
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -40,6 +41,6 @@ func startMeshWithHost(s *derp.Server, host string) error {
|
||||
c.MeshKey = s.MeshKey()
|
||||
add := func(k key.Public) { s.AddPacketForwarder(k, c) }
|
||||
remove := func(k key.Public) { s.RemovePacketForwarder(k, c) }
|
||||
go c.RunWatchConnectionLoop(s.PublicKey(), add, remove)
|
||||
go c.RunWatchConnectionLoop(context.Background(), s.PublicKey(), logf, add, remove)
|
||||
return nil
|
||||
}
|
||||
|
||||
169
cmd/hello/hello.go
Normal file
169
cmd/hello/hello.go
Normal file
@@ -0,0 +1,169 @@
|
||||
// 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 hello binary runs hello.ipn.dev.
|
||||
package main // import "tailscale.com/cmd/hello"
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
var (
|
||||
httpAddr = flag.String("http", ":80", "address to run an HTTP server on, or empty for none")
|
||||
httpsAddr = flag.String("https", ":443", "address to run an HTTPS server on, or empty for none")
|
||||
testIP = flag.String("test-ip", "", "if non-empty, look up IP and exit before running a server")
|
||||
)
|
||||
|
||||
//go:embed hello.tmpl.html
|
||||
var embeddedTemplate string
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *testIP != "" {
|
||||
res, err := tailscale.WhoIs(context.Background(), *testIP)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
e := json.NewEncoder(os.Stdout)
|
||||
e.SetIndent("", "\t")
|
||||
e.Encode(res)
|
||||
return
|
||||
}
|
||||
if devMode() {
|
||||
// Parse it optimistically
|
||||
var err error
|
||||
tmpl, err = template.New("home").Parse(embeddedTemplate)
|
||||
if err != nil {
|
||||
log.Printf("ignoring template error in dev mode: %v", err)
|
||||
}
|
||||
} else {
|
||||
if embeddedTemplate == "" {
|
||||
log.Fatalf("embeddedTemplate is empty; must be build with Go 1.16+")
|
||||
}
|
||||
tmpl = template.Must(template.New("home").Parse(embeddedTemplate))
|
||||
}
|
||||
|
||||
http.HandleFunc("/", root)
|
||||
log.Printf("Starting hello server.")
|
||||
|
||||
errc := make(chan error, 1)
|
||||
if *httpAddr != "" {
|
||||
log.Printf("running HTTP server on %s", *httpAddr)
|
||||
go func() {
|
||||
errc <- http.ListenAndServe(*httpAddr, nil)
|
||||
}()
|
||||
}
|
||||
if *httpsAddr != "" {
|
||||
log.Printf("running HTTPS server on %s", *httpsAddr)
|
||||
go func() {
|
||||
errc <- http.ListenAndServeTLS(*httpsAddr,
|
||||
"/etc/hello/hello.ipn.dev.crt",
|
||||
"/etc/hello/hello.ipn.dev.key",
|
||||
nil,
|
||||
)
|
||||
}()
|
||||
}
|
||||
log.Fatal(<-errc)
|
||||
}
|
||||
|
||||
func devMode() bool { return *httpsAddr == "" && *httpAddr != "" }
|
||||
|
||||
func getTmpl() (*template.Template, error) {
|
||||
if devMode() {
|
||||
tmplData, err := ioutil.ReadFile("hello.tmpl.html")
|
||||
if os.IsNotExist(err) {
|
||||
log.Printf("using baked-in template in dev mode; can't find hello.tmpl.html in current directory")
|
||||
return tmpl, nil
|
||||
}
|
||||
return template.New("home").Parse(string(tmplData))
|
||||
}
|
||||
return tmpl, nil
|
||||
}
|
||||
|
||||
// tmpl is the template used in prod mode.
|
||||
// In dev mode it's only used if the template file doesn't exist on disk.
|
||||
// It's initialized by main after flag parsing.
|
||||
var tmpl *template.Template
|
||||
|
||||
type tmplData struct {
|
||||
DisplayName string // "Foo Barberson"
|
||||
LoginName string // "foo@bar.com"
|
||||
ProfilePicURL string // "https://..."
|
||||
MachineName string // "imac5k"
|
||||
MachineOS string // "Linux"
|
||||
IP string // "100.2.3.4"
|
||||
}
|
||||
|
||||
func root(w http.ResponseWriter, r *http.Request) {
|
||||
if r.TLS == nil && *httpsAddr != "" {
|
||||
host := r.Host
|
||||
if strings.Contains(r.Host, "100.101.102.103") {
|
||||
host = "hello.ipn.dev"
|
||||
}
|
||||
http.Redirect(w, r, "https://"+host, http.StatusFound)
|
||||
return
|
||||
}
|
||||
if r.RequestURI != "/" {
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
tmpl, err := getTmpl()
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
http.Error(w, "template error: "+err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
who, err := tailscale.WhoIs(r.Context(), r.RemoteAddr)
|
||||
var data tmplData
|
||||
if err != nil {
|
||||
if devMode() {
|
||||
log.Printf("warning: using fake data in dev mode due to whois lookup error: %v", err)
|
||||
data = tmplData{
|
||||
DisplayName: "Taily Scalerson",
|
||||
LoginName: "taily@scaler.son",
|
||||
ProfilePicURL: "https://placekitten.com/200/200",
|
||||
MachineName: "scaled",
|
||||
MachineOS: "Linux",
|
||||
IP: "100.1.2.3",
|
||||
}
|
||||
} else {
|
||||
log.Printf("whois(%q) error: %v", r.RemoteAddr, err)
|
||||
http.Error(w, "Your Tailscale works, but we failed to look you up.", 500)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
|
||||
data = tmplData{
|
||||
DisplayName: who.UserProfile.DisplayName,
|
||||
LoginName: who.UserProfile.LoginName,
|
||||
ProfilePicURL: who.UserProfile.ProfilePicURL,
|
||||
MachineName: firstLabel(who.Node.ComputedName),
|
||||
MachineOS: who.Node.Hostinfo.OS,
|
||||
IP: ip,
|
||||
}
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
tmpl.Execute(w, data)
|
||||
}
|
||||
|
||||
// firstLabel s up until the first period, if any.
|
||||
func firstLabel(s string) string {
|
||||
if i := strings.Index(s, "."); i != -1 {
|
||||
return s[:i]
|
||||
}
|
||||
return s
|
||||
}
|
||||
436
cmd/hello/hello.tmpl.html
Normal file
436
cmd/hello/hello.tmpl.html
Normal file
@@ -0,0 +1,436 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
|
||||
<title>Hello from Tailscale</title>
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 100%;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
main {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
border-width: 0;
|
||||
border-style: solid;
|
||||
border-color: #dad6d5;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: inherit;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
max-width: 24rem;
|
||||
width: 95%;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.p-2 {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.p-4 {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.px-2 {
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.pl-3 {
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
|
||||
.pr-3 {
|
||||
padding-right: 0.75rem;
|
||||
}
|
||||
|
||||
.pt-4 {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.mr-2 {
|
||||
margin-right: 0.5rem;
|
||||
;
|
||||
}
|
||||
|
||||
.mb-1 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.mb-2 {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.mb-4 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.mb-6 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.mb-8 {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.mb-12 {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.width-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.min-width-0 {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.rounded-lg {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.border {
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
.border-t-1 {
|
||||
border-top-width: 1px;
|
||||
}
|
||||
|
||||
.border-gray-100 {
|
||||
border-color: #f7f5f4;
|
||||
}
|
||||
|
||||
.border-gray-200 {
|
||||
border-color: #eeebea;
|
||||
}
|
||||
|
||||
.border-gray-300 {
|
||||
border-color: #dad6d5;
|
||||
}
|
||||
|
||||
.bg-white {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.bg-gray-0 {
|
||||
background-color: #faf9f8;
|
||||
}
|
||||
|
||||
.bg-gray-100 {
|
||||
background-color: #f7f5f4;
|
||||
}
|
||||
|
||||
.text-green-600 {
|
||||
color: #0d4b3b;
|
||||
}
|
||||
|
||||
.text-blue-600 {
|
||||
color: #3f5db3;
|
||||
}
|
||||
|
||||
.hover\:text-blue-800:hover {
|
||||
color: #253570;
|
||||
}
|
||||
|
||||
.text-gray-600 {
|
||||
color: #444342;
|
||||
}
|
||||
|
||||
.text-gray-700 {
|
||||
color: #2e2d2d;
|
||||
}
|
||||
|
||||
.text-gray-800 {
|
||||
color: #232222;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.font-title {
|
||||
font-size: 1.25rem;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.font-semibold {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.font-medium {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.font-regular {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.truncate {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.overflow-hidden {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.profile-pic {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 9999px;
|
||||
background-size: cover;
|
||||
margin-right: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.panel {
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.animate .panel {
|
||||
transform: translateY(10%);
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.0), 0 10px 10px -5px rgba(0, 0, 0, 0.0);
|
||||
transition: transform 1200ms ease, opacity 1200ms ease, box-shadow 1200ms ease;
|
||||
}
|
||||
|
||||
.animate .panel-interior {
|
||||
opacity: 0.0;
|
||||
transition: opacity 1200ms ease;
|
||||
}
|
||||
|
||||
.animate .logo {
|
||||
transform: translateY(2rem);
|
||||
opacity: 0.0;
|
||||
transition: transform 1200ms ease, opacity 1200ms ease;
|
||||
}
|
||||
|
||||
.animate .header-title {
|
||||
transform: translateY(1.6rem);
|
||||
opacity: 0.0;
|
||||
transition: transform 1200ms ease, opacity 1200ms ease;
|
||||
}
|
||||
|
||||
.animate .header-text {
|
||||
transform: translateY(1.2rem);
|
||||
opacity: 0.0;
|
||||
transition: transform 1200ms ease, opacity 1200ms ease;
|
||||
}
|
||||
|
||||
.animate .footer {
|
||||
transform: translateY(-0.5rem);
|
||||
opacity: 0.0;
|
||||
transition: transform 1200ms ease, opacity 1200ms ease;
|
||||
}
|
||||
|
||||
.animating .panel {
|
||||
transform: translateY(0);
|
||||
opacity: 1.0;
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.animating .panel-interior {
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.animating .spinner {
|
||||
opacity: 0.0;
|
||||
}
|
||||
|
||||
.animating .logo,
|
||||
.animating .header-title,
|
||||
.animating .header-text,
|
||||
.animating .footer {
|
||||
transform: translateY(0);
|
||||
opacity: 1.0;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: inline-flex;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
align-items: center;
|
||||
transition: opacity 200ms ease;
|
||||
}
|
||||
|
||||
.spinner span {
|
||||
display: inline-block;
|
||||
background-color: currentColor;
|
||||
border-radius: 9999px;
|
||||
animation-name: loading-dots-blink;
|
||||
animation-duration: 1.4s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-fill-mode: both;
|
||||
width: 0.35em;
|
||||
height: 0.35em;
|
||||
margin: 0 0.15em;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(2) {
|
||||
animation-delay: 200ms;
|
||||
}
|
||||
|
||||
.spinner span:nth-child(3) {
|
||||
animation-delay: 400ms;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.animate .spinner {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
@keyframes loading-dots-blink {
|
||||
0% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
20% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion) {
|
||||
* {
|
||||
animation-duration: 0ms !important;
|
||||
transition-duration: 0ms !important;
|
||||
transition-delay: 0ms !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<script>
|
||||
(function() {
|
||||
var lastSeen = localStorage.getItem("lastSeen");
|
||||
if (!lastSeen) {
|
||||
document.body.classList.add("animate");
|
||||
window.addEventListener("load", function () {
|
||||
setTimeout(function () {
|
||||
document.body.classList.add("animating");
|
||||
localStorage.setItem("lastSeen", Date.now());
|
||||
}, 100);
|
||||
});
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<main class="text-gray-800">
|
||||
<svg class="logo mb-6" width="28" height="28" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle opacity="0.2" cx="3.4" cy="3.25" r="2.7" fill="currentColor" />
|
||||
<circle cx="3.4" cy="11.3" r="2.7" fill="currentColor" />
|
||||
<circle opacity="0.2" cx="3.4" cy="19.5" r="2.7" fill="currentColor" />
|
||||
<circle cx="11.5" cy="11.3" r="2.7" fill="currentColor" />
|
||||
<circle cx="11.5" cy="19.5" r="2.7" fill="currentColor" />
|
||||
<circle opacity="0.2" cx="11.5" cy="3.25" r="2.7" fill="currentColor" />
|
||||
<circle opacity="0.2" cx="19.5" cy="3.25" r="2.7" fill="currentColor" />
|
||||
<circle cx="19.5" cy="11.3" r="2.7" fill="currentColor" />
|
||||
<circle opacity="0.2" cx="19.5" cy="19.5" r="2.7" fill="currentColor" />
|
||||
</svg>
|
||||
<header class="mb-8 text-center">
|
||||
<h1 class="header-title font-title font-semibold mb-2">You're connected over Tailscale!</h1>
|
||||
<p class="header-text">This device is signed in as…</p>
|
||||
</header>
|
||||
<div class="panel relative bg-white rounded-lg width-full shadow-xl mb-8 p-4">
|
||||
<div class="spinner text-gray-600">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
<div class="panel-interior flex items-center width-full min-width-0 p-2 mb-4">
|
||||
<div class="profile-pic bg-gray-100" style="background-image: url({{.ProfilePicURL}});"></div>
|
||||
<div class="overflow-hidden">
|
||||
{{ with .DisplayName }}
|
||||
<h4 class="font-semibold truncate">{{.}}</h4>
|
||||
{{ end }}
|
||||
<h5 class="text-gray-600 truncate">{{.LoginName}}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="panel-interior border border-gray-200 bg-gray-0 rounded-lg p-2 pl-3 pr-3 mb-2 width-full flex justify-between items-center">
|
||||
<div class="flex items-center min-width-0">
|
||||
<svg class="text-gray-600 mr-2" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
|
||||
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
|
||||
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
||||
<line x1="6" y1="18" x2="6.01" y2="18"></line>
|
||||
</svg>
|
||||
<h4 class="font-semibold truncate mr-2">{{.MachineName}}</h4>
|
||||
</div>
|
||||
<h5>{{.IP}}</h5>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="footer text-gray-600 text-center mb-12">
|
||||
<p>Read about <a href="https://tailscale.com/kb/1017/install#advanced-features" class="text-blue-600 hover:text-blue-800"
|
||||
target="_blank">what you can do next →</a></p>
|
||||
</footer>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -50,9 +50,13 @@ func Run(args []string) error {
|
||||
|
||||
rootCmd := &ffcli.Command{
|
||||
Name: "tailscale",
|
||||
ShortUsage: "tailscale subcommand [flags]",
|
||||
ShortUsage: "tailscale [flags] <subcommand> [command flags]",
|
||||
ShortHelp: "The easiest, most secure way to use WireGuard.",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
For help on subcommands, add -help after: "tailscale status -help".
|
||||
|
||||
All flags can use single or double hyphen prefixes (-help or --help).
|
||||
|
||||
This CLI is still under active development. Commands and flags will
|
||||
change in the future.
|
||||
`),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// 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.
|
||||
|
||||
@@ -6,26 +6,12 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/derp/derpmap"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
var debugCmd = &ffcli.Command{
|
||||
@@ -33,143 +19,25 @@ var debugCmd = &ffcli.Command{
|
||||
Exec: runDebug,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("debug", flag.ExitOnError)
|
||||
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
|
||||
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
|
||||
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
|
||||
fs.BoolVar(&debugArgs.goroutines, "daemon-goroutines", false, "If true, dump the tailscaled daemon's goroutines")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var debugArgs struct {
|
||||
monitor bool
|
||||
getURL string
|
||||
derpCheck string
|
||||
goroutines bool
|
||||
}
|
||||
|
||||
func runDebug(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("unknown arguments")
|
||||
}
|
||||
if debugArgs.derpCheck != "" {
|
||||
return checkDerp(ctx, debugArgs.derpCheck)
|
||||
}
|
||||
if debugArgs.monitor {
|
||||
return runMonitor(ctx)
|
||||
}
|
||||
if debugArgs.getURL != "" {
|
||||
return getURL(ctx, debugArgs.getURL)
|
||||
}
|
||||
return errors.New("only --monitor is available at the moment")
|
||||
}
|
||||
|
||||
func runMonitor(ctx context.Context) error {
|
||||
dump := func() {
|
||||
st, err := interfaces.GetState()
|
||||
if debugArgs.goroutines {
|
||||
goroutines, err := tailscale.Goroutines(ctx)
|
||||
if err != nil {
|
||||
log.Printf("error getting state: %v", err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
j, _ := json.MarshalIndent(st, "", " ")
|
||||
os.Stderr.Write(j)
|
||||
os.Stdout.Write(goroutines)
|
||||
}
|
||||
mon, err := monitor.New(log.Printf, func() {
|
||||
log.Printf("Link monitor fired. State:")
|
||||
dump()
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Starting link change monitor; initial state:")
|
||||
dump()
|
||||
mon.Start()
|
||||
log.Printf("Started link change monitor; waiting...")
|
||||
select {}
|
||||
}
|
||||
|
||||
func getURL(ctx context.Context, urlStr string) error {
|
||||
if urlStr == "login" {
|
||||
urlStr = "https://login.tailscale.com"
|
||||
}
|
||||
log.SetOutput(os.Stdout)
|
||||
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
|
||||
GetConn: func(hostPort string) { log.Printf("GetConn(%q)", hostPort) },
|
||||
GotConn: func(info httptrace.GotConnInfo) { log.Printf("GotConn: %+v", info) },
|
||||
DNSStart: func(info httptrace.DNSStartInfo) { log.Printf("DNSStart: %+v", info) },
|
||||
DNSDone: func(info httptrace.DNSDoneInfo) { log.Printf("DNSDoneInfo: %+v", info) },
|
||||
TLSHandshakeStart: func() { log.Printf("TLSHandshakeStart") },
|
||||
TLSHandshakeDone: func(cs tls.ConnectionState, err error) { log.Printf("TLSHandshakeDone: %+v, %v", cs, err) },
|
||||
WroteRequest: func(info httptrace.WroteRequestInfo) { log.Printf("WroteRequest: %+v", info) },
|
||||
})
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("http.NewRequestWithContext: %v", err)
|
||||
}
|
||||
proxyURL, err := tshttpproxy.ProxyFromEnvironment(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tshttpproxy.ProxyFromEnvironment: %v", err)
|
||||
}
|
||||
log.Printf("proxy: %v", proxyURL)
|
||||
tr := &http.Transport{
|
||||
Proxy: func(*http.Request) (*url.URL, error) { return proxyURL, nil },
|
||||
ProxyConnectHeader: http.Header{},
|
||||
DisableKeepAlives: true,
|
||||
}
|
||||
if proxyURL != nil {
|
||||
auth, err := tshttpproxy.GetAuthHeader(proxyURL)
|
||||
if err == nil && auth != "" {
|
||||
tr.ProxyConnectHeader.Set("Proxy-Authorization", auth)
|
||||
}
|
||||
const truncLen = 20
|
||||
if len(auth) > truncLen {
|
||||
auth = fmt.Sprintf("%s...(%d total bytes)", auth[:truncLen], len(auth))
|
||||
}
|
||||
log.Printf("tshttpproxy.GetAuthHeader(%v) for Proxy-Auth: = %q, %v", proxyURL, auth, err)
|
||||
}
|
||||
res, err := tr.RoundTrip(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Transport.RoundTrip: %v", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
return res.Write(os.Stdout)
|
||||
}
|
||||
|
||||
func checkDerp(ctx context.Context, derpRegion string) error {
|
||||
dmap := derpmap.Prod()
|
||||
getRegion := func() *tailcfg.DERPRegion {
|
||||
for _, r := range dmap.Regions {
|
||||
if r.RegionCode == derpRegion {
|
||||
return r
|
||||
}
|
||||
}
|
||||
for _, r := range dmap.Regions {
|
||||
log.Printf("Known region: %q", r.RegionCode)
|
||||
}
|
||||
log.Fatalf("unknown region %q", derpRegion)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
priv1 := key.NewPrivate()
|
||||
priv2 := key.NewPrivate()
|
||||
|
||||
c1 := derphttp.NewRegionClient(priv1, log.Printf, getRegion)
|
||||
c2 := derphttp.NewRegionClient(priv2, log.Printf, getRegion)
|
||||
|
||||
c2.NotePreferred(true) // just to open it
|
||||
|
||||
m, err := c2.Recv()
|
||||
log.Printf("c2 got %T, %v", m, err)
|
||||
|
||||
t0 := time.Now()
|
||||
if err := c1.Send(priv2.Public(), []byte("hello")); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(time.Since(t0))
|
||||
|
||||
m, err = c2.Recv()
|
||||
log.Printf("c2 got %T, %v", m, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("ok")
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"tailscale.com/derp/derpmap"
|
||||
"tailscale.com/net/netcheck"
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
@@ -43,7 +44,10 @@ var netcheckArgs struct {
|
||||
}
|
||||
|
||||
func runNetcheck(ctx context.Context, args []string) error {
|
||||
c := &netcheck.Client{}
|
||||
c := &netcheck.Client{
|
||||
UDPBindAddr: os.Getenv("TS_DEBUG_NETCHECK_UDP_BIND"),
|
||||
PortMapper: portmapper.NewClient(logger.WithPrefix(log.Printf, "portmap: ")),
|
||||
}
|
||||
if netcheckArgs.verbose {
|
||||
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
|
||||
c.Verbose = true
|
||||
|
||||
@@ -64,34 +64,63 @@ func runPing(ctx context.Context, args []string) error {
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
if len(args) != 1 {
|
||||
if len(args) != 1 || args[0] == "" {
|
||||
return errors.New("usage: ping <hostname-or-IP>")
|
||||
}
|
||||
hostOrIP := args[0]
|
||||
var ip string
|
||||
var res net.Resolver
|
||||
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
|
||||
return fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
|
||||
} else if len(addrs) == 0 {
|
||||
return fmt.Errorf("no IPs found for %q", hostOrIP)
|
||||
} else {
|
||||
ip = addrs[0]
|
||||
}
|
||||
if pingArgs.verbose && ip != hostOrIP {
|
||||
log.Printf("lookup %q => %q", hostOrIP, ip)
|
||||
}
|
||||
|
||||
ch := make(chan *ipnstate.PingResult, 1)
|
||||
prc := make(chan *ipnstate.PingResult, 1)
|
||||
stc := make(chan *ipnstate.Status, 1)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if pr := n.PingResult; pr != nil && pr.IP == ip {
|
||||
ch <- pr
|
||||
prc <- pr
|
||||
}
|
||||
if n.Status != nil {
|
||||
stc <- n.Status
|
||||
}
|
||||
})
|
||||
go pump(ctx, bc, c)
|
||||
|
||||
hostOrIP := args[0]
|
||||
|
||||
// If the argument is an IP address, use it directly without any resolution.
|
||||
if net.ParseIP(hostOrIP) != nil {
|
||||
ip = hostOrIP
|
||||
}
|
||||
|
||||
// Otherwise, try to resolve it first from the network peer list.
|
||||
if ip == "" {
|
||||
bc.RequestStatus()
|
||||
select {
|
||||
case st := <-stc:
|
||||
for _, ps := range st.Peer {
|
||||
if hostOrIP == dnsOrQuoteHostname(st, ps) || hostOrIP == ps.DNSName {
|
||||
ip = ps.TailAddr
|
||||
break
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, use DNS.
|
||||
if ip == "" {
|
||||
var res net.Resolver
|
||||
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
|
||||
return fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
|
||||
} else if len(addrs) == 0 {
|
||||
return fmt.Errorf("no IPs found for %q", hostOrIP)
|
||||
} else {
|
||||
ip = addrs[0]
|
||||
}
|
||||
}
|
||||
if pingArgs.verbose && ip != hostOrIP {
|
||||
log.Printf("lookup %q => %q", hostOrIP, ip)
|
||||
}
|
||||
|
||||
n := 0
|
||||
anyPong := false
|
||||
for {
|
||||
@@ -101,7 +130,7 @@ func runPing(ctx context.Context, args []string) error {
|
||||
select {
|
||||
case <-timer.C:
|
||||
fmt.Printf("timeout waiting for ping reply\n")
|
||||
case pr := <-ch:
|
||||
case pr := <-prc:
|
||||
timer.Stop()
|
||||
if pr.Err != "" {
|
||||
return errors.New(pr.Err)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
var statusCmd = &ffcli.Command{
|
||||
@@ -34,7 +36,8 @@ var statusCmd = &ffcli.Command{
|
||||
fs.BoolVar(&statusArgs.web, "web", false, "run webserver with HTML showing status")
|
||||
fs.BoolVar(&statusArgs.active, "active", false, "filter output to only peers with active sessions (not applicable to web mode)")
|
||||
fs.BoolVar(&statusArgs.self, "self", true, "show status of local machine")
|
||||
fs.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address; use port 0 for automatic")
|
||||
fs.BoolVar(&statusArgs.peers, "peers", true, "show status of peers")
|
||||
fs.StringVar(&statusArgs.listen, "listen", "127.0.0.1:8384", "listen address for web mode; use port 0 for automatic")
|
||||
fs.BoolVar(&statusArgs.browser, "browser", true, "Open a browser in web mode")
|
||||
return fs
|
||||
})(),
|
||||
@@ -47,6 +50,7 @@ var statusArgs struct {
|
||||
browser bool // in web mode, whether to open browser
|
||||
active bool // in CLI mode, filter output to only peers with active sessions
|
||||
self bool // in CLI mode, show status of local machine
|
||||
peers bool // in CLI mode, show status of peer machines
|
||||
}
|
||||
|
||||
func runStatus(ctx context.Context, args []string) error {
|
||||
@@ -61,7 +65,17 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if n.Status != nil {
|
||||
ch <- n.Status
|
||||
select {
|
||||
case ch <- n.Status:
|
||||
default:
|
||||
// A status update from somebody else's request.
|
||||
// Ignoring this matters mostly for "tailscale status -web"
|
||||
// mode, otherwise the channel send would block forever
|
||||
// and pump would stop reading from tailscaled, which
|
||||
// previously caused tailscaled to block (while holding
|
||||
// a mutex), backing up unrelated clients.
|
||||
// See https://github.com/tailscale/tailscale/issues/1234
|
||||
}
|
||||
}
|
||||
})
|
||||
go pump(ctx, bc, c)
|
||||
@@ -136,30 +150,35 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
f := func(format string, a ...interface{}) { fmt.Fprintf(&buf, format, a...) }
|
||||
printPS := func(ps *ipnstate.PeerStatus) {
|
||||
active := peerActive(ps)
|
||||
f("%s %-7s %-15s %-18s tx=%8d rx=%8d ",
|
||||
ps.PublicKey.ShortString(),
|
||||
ps.OS,
|
||||
f("%-15s %-20s %-12s %-7s ",
|
||||
ps.TailAddr,
|
||||
ps.SimpleHostName(),
|
||||
ps.TxBytes,
|
||||
ps.RxBytes,
|
||||
dnsOrQuoteHostname(st, ps),
|
||||
ownerLogin(st, ps),
|
||||
ps.OS,
|
||||
)
|
||||
relay := ps.Relay
|
||||
if active && relay != "" && ps.CurAddr == "" {
|
||||
relay = "*" + relay + "*"
|
||||
} else {
|
||||
relay = " " + relay
|
||||
}
|
||||
f("%-6s", relay)
|
||||
for i, addr := range ps.Addrs {
|
||||
if i != 0 {
|
||||
f(", ")
|
||||
}
|
||||
if addr == ps.CurAddr {
|
||||
f("*%s*", addr)
|
||||
anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
|
||||
if !active {
|
||||
if ps.ExitNode {
|
||||
f("idle; exit node")
|
||||
} else if anyTraffic {
|
||||
f("idle")
|
||||
} else {
|
||||
f("%s", addr)
|
||||
f("-")
|
||||
}
|
||||
} else {
|
||||
f("active; ")
|
||||
if ps.ExitNode {
|
||||
f("exit node; ")
|
||||
}
|
||||
if relay != "" && ps.CurAddr == "" {
|
||||
f("relay %q", relay)
|
||||
} else if ps.CurAddr != "" {
|
||||
f("direct %s", ps.CurAddr)
|
||||
}
|
||||
}
|
||||
if anyTraffic {
|
||||
f(", tx %d rx %d", ps.TxBytes, ps.RxBytes)
|
||||
}
|
||||
f("\n")
|
||||
}
|
||||
@@ -167,16 +186,23 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
if statusArgs.self && st.Self != nil {
|
||||
printPS(st.Self)
|
||||
}
|
||||
for _, peer := range st.Peers() {
|
||||
ps := st.Peer[peer]
|
||||
if ps.ShareeNode {
|
||||
continue
|
||||
if statusArgs.peers {
|
||||
var peers []*ipnstate.PeerStatus
|
||||
for _, peer := range st.Peers() {
|
||||
ps := st.Peer[peer]
|
||||
if ps.ShareeNode {
|
||||
continue
|
||||
}
|
||||
peers = append(peers, ps)
|
||||
}
|
||||
active := peerActive(ps)
|
||||
if statusArgs.active && !active {
|
||||
continue
|
||||
ipnstate.SortPeers(peers)
|
||||
for _, ps := range peers {
|
||||
active := peerActive(ps)
|
||||
if statusArgs.active && !active {
|
||||
continue
|
||||
}
|
||||
printPS(ps)
|
||||
}
|
||||
printPS(ps)
|
||||
}
|
||||
os.Stdout.Write(buf.Bytes())
|
||||
return nil
|
||||
@@ -188,3 +214,25 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
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 != "" {
|
||||
return baseName
|
||||
}
|
||||
return fmt.Sprintf("(%q)", dnsname.SanitizeHostname(ps.HostName))
|
||||
}
|
||||
|
||||
func ownerLogin(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||
if ps.UserID.IsZero() {
|
||||
return "-"
|
||||
}
|
||||
u, ok := st.User[ps.UserID]
|
||||
if !ok {
|
||||
return fmt.Sprint(ps.UserID)
|
||||
}
|
||||
if i := strings.Index(u.LoginName, "@"); i != -1 {
|
||||
return u.LoginName[:i+1]
|
||||
}
|
||||
return u.LoginName
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -22,9 +23,8 @@ import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
var upCmd = &ffcli.Command{
|
||||
@@ -45,13 +45,16 @@ specify any flags, options are reset to their default.
|
||||
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.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
||||
upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication")
|
||||
upf.BoolVar(&upArgs.json, "json", false, "print output as JSON and exit when control server provides instructions")
|
||||
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "ACL tags to request (comma-separated, e.g. eng,montreal,ssh)")
|
||||
upf.StringVar(&upArgs.authKey, "authkey", "", "node authorization key")
|
||||
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
|
||||
if runtime.GOOS == "linux" || isBSD(runtime.GOOS) || version.OS() == "macOS" {
|
||||
if runtime.GOOS == "linux" || isBSD(runtime.GOOS) {
|
||||
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. 10.0.0.0/8,192.168.0.0/24)")
|
||||
upf.BoolVar(&upArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
|
||||
}
|
||||
if runtime.GOOS == "linux" {
|
||||
upf.BoolVar(&upArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
|
||||
@@ -70,18 +73,21 @@ func defaultNetfilterMode() string {
|
||||
}
|
||||
|
||||
var upArgs struct {
|
||||
server string
|
||||
acceptRoutes bool
|
||||
acceptDNS bool
|
||||
singleRoutes bool
|
||||
shieldsUp bool
|
||||
forceReauth bool
|
||||
advertiseRoutes string
|
||||
advertiseTags string
|
||||
snat bool
|
||||
netfilterMode string
|
||||
authKey string
|
||||
hostname string
|
||||
server string
|
||||
acceptRoutes bool
|
||||
acceptDNS bool
|
||||
singleRoutes bool
|
||||
exitNodeIP string
|
||||
shieldsUp bool
|
||||
forceReauth bool
|
||||
json bool
|
||||
advertiseRoutes string
|
||||
advertiseDefaultRoute bool
|
||||
advertiseTags string
|
||||
snat bool
|
||||
netfilterMode string
|
||||
authKey string
|
||||
hostname string
|
||||
}
|
||||
|
||||
func isBSD(s string) bool {
|
||||
@@ -92,14 +98,14 @@ func warnf(format string, args ...interface{}) {
|
||||
fmt.Printf("Warning: "+format+"\n", args...)
|
||||
}
|
||||
|
||||
// checkIPForwarding prints warnings if IP forwarding is not
|
||||
// checkIPForwarding prints warnings on linux if IP forwarding is not
|
||||
// enabled, or if we were unable to verify the state of IP forwarding.
|
||||
func checkIPForwarding() {
|
||||
var key string
|
||||
|
||||
if runtime.GOOS == "linux" {
|
||||
key = "net.ipv4.ip_forward"
|
||||
} else if isBSD(runtime.GOOS) || version.OS() == "macOS" {
|
||||
} else if isBSD(runtime.GOOS) {
|
||||
key = "net.inet.ip.forwarding"
|
||||
} else {
|
||||
return
|
||||
@@ -120,6 +126,11 @@ func checkIPForwarding() {
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ipv4default = netaddr.MustParseIPPrefix("0.0.0.0/0")
|
||||
ipv6default = netaddr.MustParseIPPrefix("::/0")
|
||||
)
|
||||
|
||||
func runUp(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
log.Fatalf("too many non-flag arguments: %q", args)
|
||||
@@ -133,12 +144,16 @@ func runUp(ctx context.Context, args []string) error {
|
||||
if upArgs.acceptRoutes {
|
||||
return errors.New("--accept-routes is " + notSupported)
|
||||
}
|
||||
if upArgs.exitNodeIP != "" {
|
||||
return errors.New("--exit-node is " + notSupported)
|
||||
}
|
||||
if upArgs.netfilterMode != "off" {
|
||||
return errors.New("--netfilter-mode values besides \"off\" " + notSupported)
|
||||
}
|
||||
}
|
||||
|
||||
var routes []netaddr.IPPrefix
|
||||
routeMap := map[netaddr.IPPrefix]bool{}
|
||||
var default4, default6 bool
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
|
||||
for _, s := range advroutes {
|
||||
@@ -149,10 +164,45 @@ func runUp(ctx context.Context, args []string) error {
|
||||
if ipp != ipp.Masked() {
|
||||
fatalf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
|
||||
}
|
||||
routes = append(routes, ipp)
|
||||
if ipp == ipv4default {
|
||||
default4 = true
|
||||
} else if ipp == ipv6default {
|
||||
default6 = true
|
||||
}
|
||||
routeMap[ipp] = true
|
||||
}
|
||||
if default4 && !default6 {
|
||||
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv4default, ipv6default)
|
||||
} else if default6 && !default4 {
|
||||
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv6default, ipv4default)
|
||||
}
|
||||
}
|
||||
if upArgs.advertiseDefaultRoute {
|
||||
routeMap[netaddr.MustParseIPPrefix("0.0.0.0/0")] = true
|
||||
routeMap[netaddr.MustParseIPPrefix("::/0")] = true
|
||||
}
|
||||
if len(routeMap) > 0 {
|
||||
checkIPForwarding()
|
||||
}
|
||||
routes := make([]netaddr.IPPrefix, 0, len(routeMap))
|
||||
for r := range routeMap {
|
||||
routes = append(routes, r)
|
||||
}
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
if routes[i].Bits != routes[j].Bits {
|
||||
return routes[i].Bits < routes[j].Bits
|
||||
}
|
||||
return routes[i].IP.Less(routes[j].IP)
|
||||
})
|
||||
|
||||
var exitNodeIP netaddr.IP
|
||||
if upArgs.exitNodeIP != "" {
|
||||
var err error
|
||||
exitNodeIP, err = netaddr.ParseIP(upArgs.exitNodeIP)
|
||||
if err != nil {
|
||||
fatalf("invalid IP address %q for --exit-node: %v", upArgs.exitNodeIP, err)
|
||||
}
|
||||
}
|
||||
|
||||
var tags []string
|
||||
if upArgs.advertiseTags != "" {
|
||||
@@ -169,11 +219,11 @@ func runUp(ctx context.Context, args []string) error {
|
||||
fatalf("hostname too long: %d bytes (max 256)", len(upArgs.hostname))
|
||||
}
|
||||
|
||||
// TODO(apenwarr): fix different semantics between prefs and uflags
|
||||
prefs := ipn.NewPrefs()
|
||||
prefs.ControlURL = upArgs.server
|
||||
prefs.WantRunning = true
|
||||
prefs.RouteAll = upArgs.acceptRoutes
|
||||
prefs.ExitNodeIP = exitNodeIP
|
||||
prefs.CorpDNS = upArgs.acceptDNS
|
||||
prefs.AllowSingleHosts = upArgs.singleRoutes
|
||||
prefs.ShieldsUp = upArgs.shieldsUp
|
||||
@@ -186,12 +236,12 @@ func runUp(ctx context.Context, args []string) error {
|
||||
if runtime.GOOS == "linux" {
|
||||
switch upArgs.netfilterMode {
|
||||
case "on":
|
||||
prefs.NetfilterMode = router.NetfilterOn
|
||||
prefs.NetfilterMode = preftype.NetfilterOn
|
||||
case "nodivert":
|
||||
prefs.NetfilterMode = router.NetfilterNoDivert
|
||||
prefs.NetfilterMode = preftype.NetfilterNoDivert
|
||||
warnf("netfilter=nodivert; add iptables calls to ts-* chains manually.")
|
||||
case "off":
|
||||
prefs.NetfilterMode = router.NetfilterOff
|
||||
prefs.NetfilterMode = preftype.NetfilterOff
|
||||
warnf("netfilter=off; configure iptables yourself.")
|
||||
default:
|
||||
fatalf("invalid value --netfilter-mode: %q", upArgs.netfilterMode)
|
||||
@@ -212,7 +262,16 @@ func runUp(ctx context.Context, args []string) error {
|
||||
AuthKey: upArgs.authKey,
|
||||
Notify: func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
fatalf("backend error: %v\n", *n.ErrMessage)
|
||||
msg := *n.ErrMessage
|
||||
if msg == ipn.ErrMsgPermissionDenied {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
msg += " (Tailscale service in use by other user?)"
|
||||
default:
|
||||
msg += " (try 'sudo tailscale up [...]')"
|
||||
}
|
||||
}
|
||||
fatalf("backend error: %v\n", msg)
|
||||
}
|
||||
if s := n.State; s != nil {
|
||||
switch *s {
|
||||
@@ -237,6 +296,29 @@ func runUp(ctx context.Context, args []string) error {
|
||||
},
|
||||
}
|
||||
|
||||
if upArgs.json {
|
||||
opts.Notify = func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
fmt.Fprintf(os.Stdout, `{"error":%q}`, *n.ErrMessage)
|
||||
os.Exit(1)
|
||||
}
|
||||
state := ""
|
||||
if n.State != nil {
|
||||
state = n.State.String()
|
||||
}
|
||||
if url := n.BrowseToURL; url != nil {
|
||||
fmt.Fprintf(os.Stdout, `{"state":%q,"auth_url":%q}`, state, *url)
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
if n.State != nil && *n.State == ipn.Running {
|
||||
fmt.Fprintf(os.Stdout, `{"state":%q}`, state)
|
||||
cancel()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// On Windows, we still run in mostly the "legacy" way that
|
||||
// predated the server's StateStore. That is, we send an empty
|
||||
// StateKey and send the prefs directly. Although the Windows
|
||||
|
||||
@@ -2,123 +2,85 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscale
|
||||
W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
|
||||
LW github.com/go-multierror/multierror from tailscale.com/wgengine/router
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/wgengine/router/dns
|
||||
github.com/golang/groupcache/lru from tailscale.com/wgengine/filter+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
|
||||
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/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
|
||||
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
|
||||
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
|
||||
W 💣 github.com/tailscale/wireguard-go/ipc/winpipe from github.com/tailscale/wireguard-go/ipc
|
||||
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/replay from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
|
||||
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun
|
||||
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/conn+
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
|
||||
💣 go4.org/intern from inet.af/netaddr
|
||||
💣 go4.org/mem from tailscale.com/control/controlclient+
|
||||
💣 go4.org/mem from tailscale.com/derp+
|
||||
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/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||
tailscale.com/control/controlclient from tailscale.com/ipn+
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp+
|
||||
tailscale.com/derp/derphttp from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
|
||||
tailscale.com/derp/derpmap from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/disco from tailscale.com/derp+
|
||||
tailscale.com/internal/deepprint from tailscale.com/ipn+
|
||||
tailscale.com/disco from tailscale.com/derp
|
||||
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn
|
||||
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
||||
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
|
||||
tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/dnscache from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netns from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/packet from tailscale.com/wgengine+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/portlist from tailscale.com/ipn
|
||||
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli
|
||||
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/filter
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/tsaddr from tailscale.com/net/interfaces
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/types/empty from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/key from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/empty from tailscale.com/ipn
|
||||
tailscale.com/types/key from tailscale.com/derp+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/opt from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/netmap from tailscale.com/ipn
|
||||
tailscale.com/types/opt from tailscale.com/net/netcheck+
|
||||
tailscale.com/types/persist from tailscale.com/ipn
|
||||
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/structs from tailscale.com/ipn+
|
||||
tailscale.com/types/wgkey from tailscale.com/types/netmap+
|
||||
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
||||
W tailscale.com/util/endian from tailscale.com/net/netns
|
||||
tailscale.com/util/lineread from tailscale.com/net/interfaces
|
||||
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/wgengine from tailscale.com/ipn
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/wgengine
|
||||
💣 tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/wgengine/router from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/wgengine/router/dns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tstun from tailscale.com/wgengine
|
||||
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/wgengine/filter from tailscale.com/types/netmap
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
golang.org/x/crypto/curve25519 from crypto/tls+
|
||||
golang.org/x/crypto/hkdf from crypto/tls
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/control/controlclient+
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/derp
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305+
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
golang.org/x/net/context/ctxhttp from golang.org/x/oauth2/internal
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/dns/dnsmessage from net
|
||||
golang.org/x/net/http/httpguts from net/http
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device
|
||||
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net
|
||||
golang.org/x/oauth2 from tailscale.com/control/controlclient+
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/oauth2 from tailscale.com/ipn+
|
||||
golang.org/x/oauth2/internal from golang.org/x/oauth2
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp
|
||||
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
|
||||
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
LD golang.org/x/sys/unix from tailscale.com/net/netns+
|
||||
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
@@ -127,7 +89,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from net/http+
|
||||
compress/gzip from net/http
|
||||
compress/zlib from debug/elf+
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
@@ -155,7 +117,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
debug/elf from rsc.io/goversion/version
|
||||
debug/macho from rsc.io/goversion/version
|
||||
debug/pe from rsc.io/goversion/version
|
||||
encoding from encoding/json+
|
||||
encoding from encoding/json
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base64 from encoding/json+
|
||||
encoding/binary from compress/gzip+
|
||||
@@ -169,16 +131,16 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
hash from compress/zlib+
|
||||
hash/adler32 from compress/zlib
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from tailscale.com/wgengine/magicsock
|
||||
hash/maphash from go4.org/mem
|
||||
html from tailscale.com/ipn/ipnstate
|
||||
io from bufio+
|
||||
io/ioutil from crypto/tls+
|
||||
io/fs from crypto/rand+
|
||||
io/ioutil from golang.org/x/oauth2/internal+
|
||||
log from expvar+
|
||||
math from compress/flate+
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
math/rand from github.com/mdlayher/netlink+
|
||||
math/rand from math/big+
|
||||
mime from golang.org/x/oauth2/internal+
|
||||
mime/multipart from net/http
|
||||
mime/quotedprintable from mime/multipart
|
||||
@@ -189,23 +151,21 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
net/textproto from golang.org/x/net/http/httpguts+
|
||||
net/url from crypto/x509+
|
||||
os from crypto/rand+
|
||||
os/exec from github.com/coreos/go-iptables/iptables+
|
||||
os/exec from github.com/toqueteos/webbrowser+
|
||||
os/signal from tailscale.com/cmd/tailscale/cli
|
||||
L os/user from github.com/godbus/dbus/v5
|
||||
path from debug/dwarf+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from github.com/coreos/go-iptables/iptables+
|
||||
regexp from rsc.io/goversion/version
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from golang.org/x/sync/singleflight
|
||||
runtime/pprof from tailscale.com/log/logheap+
|
||||
sort from compress/flate+
|
||||
strconv from compress/flate+
|
||||
strings from bufio+
|
||||
sync from compress/flate+
|
||||
sync/atomic from context+
|
||||
syscall from crypto/rand+
|
||||
text/tabwriter from github.com/peterbourgon/ff/v2/ffcli+
|
||||
text/tabwriter from github.com/peterbourgon/ff/v2/ffcli
|
||||
time from compress/gzip+
|
||||
unicode from bytes+
|
||||
unicode/utf16 from encoding/asn1+
|
||||
|
||||
@@ -8,19 +8,12 @@ package main // import "tailscale.com/cmd/tailscale"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/apenwarr/fixconsole"
|
||||
"tailscale.com/cmd/tailscale/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := fixconsole.FixConsoleIfNeeded()
|
||||
if err != nil {
|
||||
log.Printf("fixConsoleOutput: %v\n", err)
|
||||
}
|
||||
|
||||
if err := cli.Run(os.Args[1:]); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
|
||||
172
cmd/tailscaled/debug.go
Normal file
172
cmd/tailscaled/debug.go
Normal file
@@ -0,0 +1,172 @@
|
||||
// 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 main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/url"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/derp/derpmap"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
var debugArgs struct {
|
||||
monitor bool
|
||||
getURL string
|
||||
derpCheck string
|
||||
}
|
||||
|
||||
var debugModeFunc = debugMode // so it can be addressable
|
||||
|
||||
func debugMode(args []string) error {
|
||||
fs := flag.NewFlagSet("debug", flag.ExitOnError)
|
||||
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
|
||||
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
|
||||
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(fs.Args()) > 0 {
|
||||
return errors.New("unknown non-flag debug subcommand arguments")
|
||||
}
|
||||
ctx := context.Background()
|
||||
if debugArgs.derpCheck != "" {
|
||||
return checkDerp(ctx, debugArgs.derpCheck)
|
||||
}
|
||||
if debugArgs.monitor {
|
||||
return runMonitor(ctx)
|
||||
}
|
||||
if debugArgs.getURL != "" {
|
||||
return getURL(ctx, debugArgs.getURL)
|
||||
}
|
||||
return errors.New("only --monitor is available at the moment")
|
||||
}
|
||||
|
||||
func runMonitor(ctx context.Context) error {
|
||||
dump := func(st *interfaces.State) {
|
||||
j, _ := json.MarshalIndent(st, "", " ")
|
||||
os.Stderr.Write(j)
|
||||
}
|
||||
mon, err := monitor.New(log.Printf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mon.RegisterChangeCallback(func(changed bool, st *interfaces.State) {
|
||||
if !changed {
|
||||
log.Printf("Link monitor fired; no change")
|
||||
return
|
||||
}
|
||||
log.Printf("Link monitor fired. New state:")
|
||||
dump(st)
|
||||
})
|
||||
log.Printf("Starting link change monitor; initial state:")
|
||||
dump(mon.InterfaceState())
|
||||
mon.Start()
|
||||
log.Printf("Started link change monitor; waiting...")
|
||||
select {}
|
||||
}
|
||||
|
||||
func getURL(ctx context.Context, urlStr string) error {
|
||||
if urlStr == "login" {
|
||||
urlStr = "https://login.tailscale.com"
|
||||
}
|
||||
log.SetOutput(os.Stdout)
|
||||
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
|
||||
GetConn: func(hostPort string) { log.Printf("GetConn(%q)", hostPort) },
|
||||
GotConn: func(info httptrace.GotConnInfo) { log.Printf("GotConn: %+v", info) },
|
||||
DNSStart: func(info httptrace.DNSStartInfo) { log.Printf("DNSStart: %+v", info) },
|
||||
DNSDone: func(info httptrace.DNSDoneInfo) { log.Printf("DNSDoneInfo: %+v", info) },
|
||||
TLSHandshakeStart: func() { log.Printf("TLSHandshakeStart") },
|
||||
TLSHandshakeDone: func(cs tls.ConnectionState, err error) { log.Printf("TLSHandshakeDone: %+v, %v", cs, err) },
|
||||
WroteRequest: func(info httptrace.WroteRequestInfo) { log.Printf("WroteRequest: %+v", info) },
|
||||
})
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", urlStr, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("http.NewRequestWithContext: %v", err)
|
||||
}
|
||||
proxyURL, err := tshttpproxy.ProxyFromEnvironment(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tshttpproxy.ProxyFromEnvironment: %v", err)
|
||||
}
|
||||
log.Printf("proxy: %v", proxyURL)
|
||||
tr := &http.Transport{
|
||||
Proxy: func(*http.Request) (*url.URL, error) { return proxyURL, nil },
|
||||
ProxyConnectHeader: http.Header{},
|
||||
DisableKeepAlives: true,
|
||||
}
|
||||
if proxyURL != nil {
|
||||
auth, err := tshttpproxy.GetAuthHeader(proxyURL)
|
||||
if err == nil && auth != "" {
|
||||
tr.ProxyConnectHeader.Set("Proxy-Authorization", auth)
|
||||
}
|
||||
const truncLen = 20
|
||||
if len(auth) > truncLen {
|
||||
auth = fmt.Sprintf("%s...(%d total bytes)", auth[:truncLen], len(auth))
|
||||
}
|
||||
log.Printf("tshttpproxy.GetAuthHeader(%v) for Proxy-Auth: = %q, %v", proxyURL, auth, err)
|
||||
}
|
||||
res, err := tr.RoundTrip(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Transport.RoundTrip: %v", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
return res.Write(os.Stdout)
|
||||
}
|
||||
|
||||
func checkDerp(ctx context.Context, derpRegion string) error {
|
||||
dmap := derpmap.Prod()
|
||||
getRegion := func() *tailcfg.DERPRegion {
|
||||
for _, r := range dmap.Regions {
|
||||
if r.RegionCode == derpRegion {
|
||||
return r
|
||||
}
|
||||
}
|
||||
for _, r := range dmap.Regions {
|
||||
log.Printf("Known region: %q", r.RegionCode)
|
||||
}
|
||||
log.Fatalf("unknown region %q", derpRegion)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
priv1 := key.NewPrivate()
|
||||
priv2 := key.NewPrivate()
|
||||
|
||||
c1 := derphttp.NewRegionClient(priv1, log.Printf, getRegion)
|
||||
c2 := derphttp.NewRegionClient(priv2, log.Printf, getRegion)
|
||||
|
||||
c2.NotePreferred(true) // just to open it
|
||||
|
||||
m, err := c2.Recv()
|
||||
log.Printf("c2 got %T, %v", m, err)
|
||||
|
||||
t0 := time.Now()
|
||||
if err := c1.Send(priv2.Public(), []byte("hello")); err != nil {
|
||||
return err
|
||||
}
|
||||
fmt.Println(time.Since(t0))
|
||||
|
||||
m, err = c2.Recv()
|
||||
log.Printf("c2 got %T, %v", m, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("ok")
|
||||
return err
|
||||
}
|
||||
@@ -2,14 +2,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscaled
|
||||
W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole
|
||||
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
|
||||
LW github.com/go-multierror/multierror from tailscale.com/wgengine/router
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/wgengine/router/dns
|
||||
github.com/golang/groupcache/lru from tailscale.com/wgengine/filter+
|
||||
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
|
||||
@@ -22,33 +21,66 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
|
||||
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
|
||||
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
|
||||
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
|
||||
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
|
||||
W 💣 github.com/tailscale/wireguard-go/ipc/winpipe from github.com/tailscale/wireguard-go/ipc
|
||||
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/replay from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device
|
||||
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device+
|
||||
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun
|
||||
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/conn+
|
||||
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun+
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
💣 go4.org/intern from inet.af/netaddr
|
||||
💣 go4.org/mem from tailscale.com/control/controlclient+
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire
|
||||
gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
|
||||
gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/rand from gvisor.dev/gvisor/pkg/tcpip/network/hash+
|
||||
💣 gvisor.dev/gvisor/pkg/sleep from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/tcpip+
|
||||
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
|
||||
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/linewriter+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/buffer from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/hash/jenkins from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header from gvisor.dev/gvisor/pkg/tcpip/link/channel+
|
||||
gvisor.dev/gvisor/pkg/tcpip/header/parse from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/link/channel from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/fragmentation from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/hash from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/ip from gvisor.dev/gvisor/pkg/tcpip/network/ipv4+
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/ipv4 from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/network/ipv6 from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/ports from gvisor.dev/gvisor/pkg/tcpip/stack+
|
||||
gvisor.dev/gvisor/pkg/tcpip/seqnum from gvisor.dev/gvisor/pkg/tcpip/header+
|
||||
gvisor.dev/gvisor/pkg/tcpip/stack from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/icmp from tailscale.com/wgengine/netstack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/packet from gvisor.dev/gvisor/pkg/tcpip/transport/raw
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/raw from gvisor.dev/gvisor/pkg/tcpip/transport/icmp+
|
||||
💣 gvisor.dev/gvisor/pkg/tcpip/transport/tcp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/tcpconntrack from gvisor.dev/gvisor/pkg/tcpip/stack
|
||||
gvisor.dev/gvisor/pkg/tcpip/transport/udp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
|
||||
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/tcpip+
|
||||
inet.af/netaddr from tailscale.com/control/controlclient+
|
||||
inet.af/peercred from tailscale.com/ipn/ipnserver
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/control/controlclient from tailscale.com/ipn+
|
||||
tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp+
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
|
||||
tailscale.com/derp/derpmap from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/disco from tailscale.com/derp+
|
||||
tailscale.com/internal/deepprint from tailscale.com/ipn+
|
||||
tailscale.com/ipn from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/internal/deepprint from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/ipn from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/ipn/ipnlocal from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/ipn+
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn
|
||||
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
||||
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled
|
||||
@@ -57,31 +89,42 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/logtail/filch from tailscale.com/logpolicy
|
||||
tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/ipn+
|
||||
tailscale.com/net/dnsfallback from tailscale.com/control/controlclient
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/net/netns from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/net/packet from tailscale.com/wgengine+
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn/ipnlocal+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/portlist from tailscale.com/ipn
|
||||
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/tempfork/wireguard-windows/firewall from tailscale.com/cmd/tailscaled
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/empty from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/types/key from tailscale.com/derp+
|
||||
tailscale.com/types/logger from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/types/netmap from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/opt from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/pad32 from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/types/persist from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/preftype from tailscale.com/ipn+
|
||||
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/dnsname from tailscale.com/wgengine/tsdns+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
||||
tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
||||
@@ -92,14 +135,18 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/wgengine/monitor from tailscale.com/wgengine
|
||||
tailscale.com/wgengine/monitor from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/router/dns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
|
||||
tailscale.com/wgengine/tstun from tailscale.com/wgengine
|
||||
tailscale.com/wgengine/router/dns from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/tsdns from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/tstun from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/wgengine/wglog from tailscale.com/wgengine
|
||||
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device
|
||||
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
@@ -120,15 +167,16 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device
|
||||
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
|
||||
golang.org/x/net/proxy from tailscale.com/net/netns
|
||||
D golang.org/x/net/route from net
|
||||
D golang.org/x/net/route from net+
|
||||
golang.org/x/oauth2 from tailscale.com/control/controlclient+
|
||||
golang.org/x/oauth2/internal from golang.org/x/oauth2
|
||||
golang.org/x/sync/errgroup from tailscale.com/derp
|
||||
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
|
||||
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
|
||||
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
|
||||
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
|
||||
W golang.org/x/sys/windows from github.com/tailscale/wireguard-go/conn+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
W golang.org/x/sys/windows/svc from tailscale.com/cmd/tailscaled
|
||||
golang.org/x/term from tailscale.com/logpolicy
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
@@ -140,6 +188,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from internal/profile+
|
||||
compress/zlib from debug/elf+
|
||||
container/heap from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdsa+
|
||||
@@ -180,12 +229,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
hash from compress/zlib+
|
||||
hash/adler32 from compress/zlib
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from tailscale.com/wgengine/magicsock
|
||||
hash/fnv from tailscale.com/wgengine/magicsock+
|
||||
hash/maphash from go4.org/mem
|
||||
html from html/template+
|
||||
html/template from net/http/pprof
|
||||
html from net/http/pprof+
|
||||
io from bufio+
|
||||
io/ioutil from crypto/tls+
|
||||
io/fs from crypto/rand+
|
||||
io/ioutil from github.com/godbus/dbus/v5+
|
||||
log from expvar+
|
||||
math from compress/flate+
|
||||
math/big from crypto/dsa+
|
||||
@@ -220,8 +269,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
sync/atomic from context+
|
||||
syscall from crypto/rand+
|
||||
text/tabwriter from runtime/pprof
|
||||
text/template from html/template
|
||||
text/template/parse from html/template+
|
||||
time from compress/gzip+
|
||||
unicode from bytes+
|
||||
unicode/utf16 from encoding/asn1+
|
||||
|
||||
146
cmd/tailscaled/install_darwin.go
Normal file
146
cmd/tailscaled/install_darwin.go
Normal file
@@ -0,0 +1,146 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func init() {
|
||||
installSystemDaemon = installSystemDaemonDarwin
|
||||
uninstallSystemDaemon = uninstallSystemDaemonDarwin
|
||||
}
|
||||
|
||||
// darwinLaunchdPlist is the launchd.plist that's written to
|
||||
// /Library/LaunchDaemons/com.tailscale.tailscaled.plist or (in the
|
||||
// future) a user-specific location.
|
||||
//
|
||||
// See man launchd.plist.
|
||||
const darwinLaunchdPlist = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
|
||||
<key>Label</key>
|
||||
<string>com.tailscale.tailscaled</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/usr/local/bin/tailscaled</string>
|
||||
</array>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
</dict>
|
||||
</plist>
|
||||
`
|
||||
|
||||
const sysPlist = "/Library/LaunchDaemons/com.tailscale.tailscaled.plist"
|
||||
const targetBin = "/usr/local/bin/tailscaled"
|
||||
const service = "com.tailscale.tailscaled"
|
||||
|
||||
func uninstallSystemDaemonDarwin(args []string) (ret error) {
|
||||
if len(args) > 0 {
|
||||
return errors.New("uninstall subcommand takes no arguments")
|
||||
}
|
||||
|
||||
plist, err := exec.Command("launchctl", "list", "com.tailscale.tailscaled").Output()
|
||||
_ = plist // parse it? https://github.com/DHowett/go-plist if we need something.
|
||||
running := err == nil
|
||||
|
||||
if running {
|
||||
out, err := exec.Command("launchctl", "stop", "com.tailscale.tailscaled").CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("launchctl stop com.tailscale.tailscaled: %v, %s\n", err, out)
|
||||
ret = err
|
||||
}
|
||||
out, err = exec.Command("launchctl", "unload", sysPlist).CombinedOutput()
|
||||
if err != nil {
|
||||
fmt.Printf("launchctl unload %s: %v, %s\n", sysPlist, err, out)
|
||||
if ret == nil {
|
||||
ret = err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
err = os.Remove(sysPlist)
|
||||
if os.IsNotExist(err) {
|
||||
err = nil
|
||||
if ret == nil {
|
||||
ret = err
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func installSystemDaemonDarwin(args []string) (err error) {
|
||||
if len(args) > 0 {
|
||||
return errors.New("install subcommand takes no arguments")
|
||||
}
|
||||
defer func() {
|
||||
if err != nil && os.Getuid() != 0 {
|
||||
err = fmt.Errorf("%w; try running tailscaled with sudo", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Copy ourselves to /usr/local/bin/tailscaled.
|
||||
if err := os.MkdirAll(filepath.Dir(targetBin), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find our own executable path: %w", err)
|
||||
}
|
||||
tmpBin := targetBin + ".tmp"
|
||||
f, err := os.Create(tmpBin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
self, err := os.Open(exe)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(f, self)
|
||||
self.Close()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chmod(tmpBin, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmpBin, targetBin); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Best effort:
|
||||
uninstallSystemDaemonDarwin(nil)
|
||||
|
||||
if err := ioutil.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if out, err := exec.Command("launchctl", "load", sysPlist).CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("error running launchctl load %s: %v, %s", sysPlist, err, out)
|
||||
}
|
||||
|
||||
if out, err := exec.Command("launchctl", "start", service).CombinedOutput(); err != nil {
|
||||
return fmt.Errorf("error running launchctl start %s: %v, %s", service, err, out)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/pprof"
|
||||
"os"
|
||||
@@ -21,19 +22,24 @@ import (
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/apenwarr/fixconsole"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/net/socks5"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/types/flagtype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/tstun"
|
||||
)
|
||||
|
||||
// globalStateKey is the ipn.StateKey that tailscaled loads on
|
||||
@@ -52,19 +58,34 @@ func defaultTunName() string {
|
||||
return "tun"
|
||||
case "windows":
|
||||
return "Tailscale"
|
||||
case "darwin":
|
||||
// "utun" is recognized by wireguard-go/tun/tun_darwin.go
|
||||
// as a magic value that uses/creates any free number.
|
||||
return "utun"
|
||||
}
|
||||
return "tailscale0"
|
||||
}
|
||||
|
||||
var args struct {
|
||||
cleanup bool
|
||||
fake bool
|
||||
debug string
|
||||
tunname string
|
||||
port uint16
|
||||
statepath string
|
||||
socketpath string
|
||||
verbose int
|
||||
socksAddr string // listen address for SOCKS5 server
|
||||
}
|
||||
|
||||
var (
|
||||
installSystemDaemon func([]string) error // non-nil on some platforms
|
||||
uninstallSystemDaemon func([]string) error // non-nil on some platforms
|
||||
)
|
||||
|
||||
var subCommands = map[string]*func([]string) error{
|
||||
"install-system-daemon": &installSystemDaemon,
|
||||
"uninstall-system-daemon": &uninstallSystemDaemon,
|
||||
"debug": &debugModeFunc,
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -79,17 +100,31 @@ func main() {
|
||||
printVersion := false
|
||||
flag.IntVar(&args.verbose, "verbose", 0, "log verbosity level; 0 is default, 1 or higher are increasingly verbose")
|
||||
flag.BoolVar(&args.cleanup, "cleanup", false, "clean up system state and exit")
|
||||
flag.BoolVar(&args.fake, "fake", false, "use userspace fake tunnel+routing instead of kernel TUN interface")
|
||||
flag.StringVar(&args.debug, "debug", "", "listen address ([ip]:port) of optional debug server")
|
||||
flag.StringVar(&args.tunname, "tun", defaultTunName(), "tunnel interface name")
|
||||
flag.StringVar(&args.socksAddr, "socks5-server", "", `optional [ip]:port to run a SOCK5 server (e.g. "localhost:1080")`)
|
||||
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
|
||||
flag.Var(flagtype.PortValue(&args.port, magicsock.DefaultPort), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
||||
flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "path of state file")
|
||||
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
|
||||
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
|
||||
|
||||
err := fixconsole.FixConsoleIfNeeded()
|
||||
if err != nil {
|
||||
log.Fatalf("fixConsoleOutput: %v", err)
|
||||
if len(os.Args) > 1 {
|
||||
sub := os.Args[1]
|
||||
if fp, ok := subCommands[sub]; ok {
|
||||
if *fp == nil {
|
||||
log.SetFlags(0)
|
||||
log.Fatalf("%s not available on %v", sub, runtime.GOOS)
|
||||
}
|
||||
if err := (*fp)(os.Args[2:]); err != nil {
|
||||
log.SetFlags(0)
|
||||
log.Fatal(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if beWindowsSubprocess() {
|
||||
return
|
||||
}
|
||||
|
||||
flag.Parse()
|
||||
@@ -102,11 +137,13 @@ func main() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if args.statepath == "" {
|
||||
log.Fatalf("--state is required")
|
||||
if runtime.GOOS == "darwin" && os.Getuid() != 0 && useTUN() {
|
||||
log.SetFlags(0)
|
||||
log.Fatalf("tailscaled requires root; use sudo tailscaled (or use --tun=userspace-networking)")
|
||||
}
|
||||
|
||||
if args.socketpath == "" && runtime.GOOS != "windows" {
|
||||
log.SetFlags(0)
|
||||
log.Fatalf("--socket is required")
|
||||
}
|
||||
|
||||
@@ -128,6 +165,16 @@ func run() error {
|
||||
pol.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
if isWindowsService() {
|
||||
// Run the IPN server from the Windows service manager.
|
||||
log.Printf("Running service...")
|
||||
if err := runWindowsService(pol); err != nil {
|
||||
log.Printf("runservice: %v", err)
|
||||
}
|
||||
log.Printf("Service ended.")
|
||||
return nil
|
||||
}
|
||||
|
||||
var logf logger.Logf = log.Printf
|
||||
if v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_MEMORY")); v {
|
||||
logf = logger.RusagePrefixLog(logf)
|
||||
@@ -139,22 +186,90 @@ func run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if args.statepath == "" {
|
||||
log.Fatalf("--state is required")
|
||||
}
|
||||
|
||||
var debugMux *http.ServeMux
|
||||
if args.debug != "" {
|
||||
debugMux = newDebugMux()
|
||||
go runDebugServer(debugMux, args.debug)
|
||||
}
|
||||
|
||||
var e wgengine.Engine
|
||||
if args.fake {
|
||||
e, err = wgengine.NewFakeUserspaceEngine(logf, args.port)
|
||||
} else {
|
||||
e, err = wgengine.NewUserspaceEngine(logf, args.tunname, args.port)
|
||||
linkMon, err := monitor.New(logf)
|
||||
if err != nil {
|
||||
log.Fatalf("creating link monitor: %v", err)
|
||||
}
|
||||
pol.Logtail.SetLinkMonitor(linkMon)
|
||||
|
||||
var socksListener net.Listener
|
||||
if args.socksAddr != "" {
|
||||
var err error
|
||||
socksListener, err = net.Listen("tcp", args.socksAddr)
|
||||
if err != nil {
|
||||
log.Fatalf("SOCKS5 listener: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
conf := wgengine.Config{
|
||||
ListenPort: args.port,
|
||||
LinkMonitor: linkMon,
|
||||
}
|
||||
if useTUN() {
|
||||
conf.TUNName = args.tunname
|
||||
} else {
|
||||
conf.TUN = tstun.NewFakeTUN()
|
||||
conf.RouterGen = router.NewFake
|
||||
}
|
||||
|
||||
e, err := wgengine.NewUserspaceEngine(logf, conf)
|
||||
if err != nil {
|
||||
logf("wgengine.New: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
var ns *netstack.Impl
|
||||
if useNetstack() {
|
||||
tunDev, magicConn := e.(wgengine.InternalsGetter).GetInternals()
|
||||
ns, err = netstack.Create(logf, tunDev, e, magicConn)
|
||||
if err != nil {
|
||||
log.Fatalf("netstack.Create: %v", err)
|
||||
}
|
||||
if err := ns.Start(); err != nil {
|
||||
log.Fatalf("failed to start netstack: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if socksListener != nil {
|
||||
srv := &socks5.Server{
|
||||
Logf: logger.WithPrefix(logf, "socks5: "),
|
||||
}
|
||||
if useNetstack() {
|
||||
srv.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return ns.DialContextTCP(ctx, addr)
|
||||
}
|
||||
} else {
|
||||
var mu sync.Mutex
|
||||
var dns netstack.DNSMap
|
||||
e.AddNetworkMapCallback(func(nm *netmap.NetworkMap) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
dns = netstack.DNSMapFromNetworkMap(nm)
|
||||
})
|
||||
srv.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
ipp, err := dns.Resolve(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, network, ipp.String())
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
log.Fatalf("SOCKS5 server exited: %v", srv.Serve(socksListener))
|
||||
}()
|
||||
}
|
||||
|
||||
e = wgengine.NewWatchdog(e)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
@@ -214,3 +329,6 @@ func runDebugServer(mux *http.ServeMux, addr string) {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func useTUN() bool { return args.tunname != "userspace-networking" }
|
||||
func useNetstack() bool { return !useTUN() }
|
||||
|
||||
@@ -20,16 +20,5 @@ CacheDirectory=tailscale
|
||||
CacheDirectoryMode=0750
|
||||
Type=notify
|
||||
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
PrivateTmp=true
|
||||
ProtectControlGroups=true
|
||||
ProtectHome=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectSystem=strict
|
||||
ReadWritePaths=/etc/
|
||||
RestrictSUIDSGID=true
|
||||
SystemCallArchitectures=native
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
||||
15
cmd/tailscaled/tailscaled_notwindows.go
Normal file
15
cmd/tailscaled/tailscaled_notwindows.go
Normal file
@@ -0,0 +1,15 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package main // import "tailscale.com/cmd/tailscaled"
|
||||
|
||||
import "tailscale.com/logpolicy"
|
||||
|
||||
func isWindowsService() bool { return false }
|
||||
|
||||
func runWindowsService(pol *logpolicy.Policy) error { panic("unreachable") }
|
||||
|
||||
func beWindowsSubprocess() bool { return false }
|
||||
228
cmd/tailscaled/tailscaled_windows.go
Normal file
228
cmd/tailscaled/tailscaled_windows.go
Normal file
@@ -0,0 +1,228 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main // import "tailscale.com/cmd/tailscaled"
|
||||
|
||||
// TODO: check if administrator, like tswin does.
|
||||
//
|
||||
// TODO: try to load wintun.dll early at startup, before wireguard/tun
|
||||
// does (which panics) and if we'd fail (e.g. due to access
|
||||
// denied, even if administrator), use 'tasklist /m wintun.dll'
|
||||
// to see if something else is currently using it and tell user.
|
||||
//
|
||||
// TODO: check if Tailscale service is already running, and fail early
|
||||
// like tswin does.
|
||||
//
|
||||
// TODO: on failure, check if on a UNC drive and recommend copying it
|
||||
// to C:\ to run it, like tswin does.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/tempfork/wireguard-windows/firewall"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
const serviceName = "Tailscale"
|
||||
|
||||
func isWindowsService() bool {
|
||||
v, err := svc.IsWindowsService()
|
||||
if err != nil {
|
||||
log.Fatalf("svc.IsWindowsService failed: %v", err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func runWindowsService(pol *logpolicy.Policy) error {
|
||||
return svc.Run(serviceName, &ipnService{Policy: pol})
|
||||
}
|
||||
|
||||
type ipnService struct {
|
||||
Policy *logpolicy.Policy
|
||||
}
|
||||
|
||||
// Called by Windows to execute the windows service.
|
||||
func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
|
||||
changes <- svc.Status{State: svc.StartPending}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
doneCh := make(chan struct{})
|
||||
go func() {
|
||||
defer close(doneCh)
|
||||
args := []string{"/subproc", service.Policy.PublicID.String()}
|
||||
ipnserver.BabysitProc(ctx, args, log.Printf)
|
||||
}()
|
||||
|
||||
changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop}
|
||||
|
||||
for ctx.Err() == nil {
|
||||
select {
|
||||
case <-doneCh:
|
||||
case cmd := <-r:
|
||||
switch cmd.Cmd {
|
||||
case svc.Stop:
|
||||
cancel()
|
||||
case svc.Interrogate:
|
||||
changes <- cmd.CurrentStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changes <- svc.Status{State: svc.StopPending}
|
||||
return false, windows.NO_ERROR
|
||||
}
|
||||
|
||||
func beWindowsSubprocess() bool {
|
||||
if beFirewallKillswitch() {
|
||||
return true
|
||||
}
|
||||
|
||||
if len(os.Args) != 3 || os.Args[1] != "/subproc" {
|
||||
return false
|
||||
}
|
||||
logid := os.Args[2]
|
||||
|
||||
log.Printf("Program starting: v%v: %#v", version.Long, os.Args)
|
||||
log.Printf("subproc mode: logid=%v", logid)
|
||||
|
||||
go func() {
|
||||
b := make([]byte, 16)
|
||||
for {
|
||||
_, err := os.Stdin.Read(b)
|
||||
if err != nil {
|
||||
log.Fatalf("stdin err (parent process died): %v", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err := startIPNServer(context.Background(), logid)
|
||||
if err != nil {
|
||||
log.Fatalf("ipnserver: %v", err)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func beFirewallKillswitch() bool {
|
||||
if len(os.Args) != 3 || os.Args[1] != "/firewall" {
|
||||
return false
|
||||
}
|
||||
|
||||
log.SetFlags(0)
|
||||
log.Printf("killswitch subprocess starting, tailscale GUID is %s", os.Args[2])
|
||||
|
||||
go func() {
|
||||
b := make([]byte, 16)
|
||||
for {
|
||||
_, err := os.Stdin.Read(b)
|
||||
if err != nil {
|
||||
log.Fatalf("parent process died or requested exit, exiting (%v)", err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
guid, err := windows.GUIDFromString(os.Args[2])
|
||||
if err != nil {
|
||||
log.Fatalf("invalid GUID %q: %v", os.Args[2], err)
|
||||
}
|
||||
|
||||
luid, err := winipcfg.LUIDFromGUID(&guid)
|
||||
if err != nil {
|
||||
log.Fatalf("no interface with GUID %q", guid)
|
||||
}
|
||||
|
||||
noProtection := false
|
||||
var dnsIPs []net.IP // unused in called code.
|
||||
start := time.Now()
|
||||
firewall.EnableFirewall(uint64(luid), noProtection, dnsIPs)
|
||||
log.Printf("killswitch enabled, took %s", time.Since(start))
|
||||
|
||||
// Block until the monitor goroutine shuts us down.
|
||||
select {}
|
||||
}
|
||||
|
||||
func startIPNServer(ctx context.Context, logid string) error {
|
||||
var logf logger.Logf = log.Printf
|
||||
var eng wgengine.Engine
|
||||
var err error
|
||||
|
||||
getEngine := func() (wgengine.Engine, error) {
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
TUNName: "Tailscale",
|
||||
ListenPort: 41641,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wgengine.NewWatchdog(eng), nil
|
||||
}
|
||||
|
||||
if msg := os.Getenv("TS_DEBUG_WIN_FAIL"); msg != "" {
|
||||
err = fmt.Errorf("pretending to be a service failure: %v", msg)
|
||||
} else {
|
||||
// We have a bunch of bug reports of wgengine.NewUserspaceEngine returning a few different errors,
|
||||
// all intermittently. A few times I (Brad) have also seen sporadic failures that simply
|
||||
// restarting fixed. So try a few times.
|
||||
for try := 1; try <= 5; try++ {
|
||||
if try > 1 {
|
||||
// Only sleep a bit. Don't do some massive backoff because
|
||||
// the frontend GUI has a 30 second timeout on connecting to us,
|
||||
// but even 5 seconds is too long for them to get any results.
|
||||
// 5 tries * 1 second each seems fine.
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
eng, err = getEngine()
|
||||
if err != nil {
|
||||
logf("wgengine.NewUserspaceEngine: (try %v) %v", try, err)
|
||||
continue
|
||||
}
|
||||
if try > 1 {
|
||||
logf("wgengine.NewUserspaceEngine: ended up working on try %v", try)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
// Log the error, but don't fatalf. We want to
|
||||
// propagate the error message to the UI frontend. So
|
||||
// we continue and tell the ipnserver to return that
|
||||
// in a Notify message.
|
||||
logf("wgengine.NewUserspaceEngine: %v", err)
|
||||
}
|
||||
opts := ipnserver.Options{
|
||||
Port: 41112,
|
||||
SurviveDisconnects: false,
|
||||
StatePath: args.statepath,
|
||||
}
|
||||
if err != nil {
|
||||
// Return nicer errors to users, annotated with logids, which helps
|
||||
// when they file bugs.
|
||||
rawGetEngine := getEngine // raw == without verbose logid-containing error
|
||||
getEngine = func() (wgengine.Engine, error) {
|
||||
eng, err := rawGetEngine()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wgengine.NewUserspaceEngine: %v\n\nlogid: %v", err, logid)
|
||||
}
|
||||
return eng, nil
|
||||
}
|
||||
} else {
|
||||
getEngine = ipnserver.FixedEngine(eng)
|
||||
}
|
||||
err = ipnserver.Run(ctx, logf, logid, getEngine, opts)
|
||||
if err != nil {
|
||||
logf("ipnserver.Run: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -32,7 +32,9 @@ import (
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/kr/pty"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tsaddr"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -96,7 +98,13 @@ func handleSSH(s ssh.Session) {
|
||||
s.Exit(1)
|
||||
return
|
||||
}
|
||||
if !interfaces.IsTailscaleIP(ta.IP) {
|
||||
tanetaddr, ok := netaddr.FromStdIP(ta.IP)
|
||||
if !ok {
|
||||
log.Printf("tsshd: rejecting unparseable addr %v", ta.IP)
|
||||
s.Exit(1)
|
||||
return
|
||||
}
|
||||
if !tsaddr.IsTailscaleIP(tanetaddr) {
|
||||
log.Printf("tsshd: rejecting non-Tailscale addr %v", ta.IP)
|
||||
s.Exit(1)
|
||||
return
|
||||
|
||||
@@ -18,10 +18,13 @@ import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
@@ -68,9 +71,9 @@ type Status struct {
|
||||
LoginFinished *empty.Message
|
||||
Err string
|
||||
URL string
|
||||
Persist *Persist // locally persisted configuration
|
||||
NetMap *NetworkMap // server-pushed configuration
|
||||
Hostinfo *tailcfg.Hostinfo // current Hostinfo data
|
||||
Persist *persist.Persist // locally persisted configuration
|
||||
NetMap *netmap.NetworkMap // server-pushed configuration
|
||||
Hostinfo *tailcfg.Hostinfo // current Hostinfo data
|
||||
State State
|
||||
}
|
||||
|
||||
@@ -114,18 +117,21 @@ type Client struct {
|
||||
closed bool
|
||||
newMapCh chan struct{} // readable when we must restart a map request
|
||||
|
||||
unregisterHealthWatch func()
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
statusFunc func(Status) // called to update Client status
|
||||
|
||||
paused bool // whether we should stop making HTTP requests
|
||||
unpauseWaiters []chan struct{}
|
||||
loggedIn bool // true if currently logged in
|
||||
loginGoal *LoginGoal // non-nil if some login activity is desired
|
||||
synced bool // true if our netmap is up-to-date
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
inPollNetMap bool // true if currently running a PollNetMap
|
||||
inSendStatus int // number of sendStatus calls currently in progress
|
||||
state State
|
||||
paused bool // whether we should stop making HTTP requests
|
||||
unpauseWaiters []chan struct{}
|
||||
loggedIn bool // true if currently logged in
|
||||
loginGoal *LoginGoal // non-nil if some login activity is desired
|
||||
synced bool // true if our netmap is up-to-date
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
inPollNetMap bool // true if currently running a PollNetMap
|
||||
inLiteMapUpdate bool // true if a lite (non-streaming) map request is outstanding
|
||||
inSendStatus int // number of sendStatus calls currently in progress
|
||||
state State
|
||||
|
||||
authCtx context.Context // context used for auth requests
|
||||
mapCtx context.Context // context used for netmap requests
|
||||
@@ -168,7 +174,14 @@ func NewNoStart(opts Options) (*Client, error) {
|
||||
}
|
||||
c.authCtx, c.authCancel = context.WithCancel(context.Background())
|
||||
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
|
||||
c.unregisterHealthWatch = health.RegisterWatcher(c.onHealthChange)
|
||||
return c, nil
|
||||
|
||||
}
|
||||
|
||||
func (c *Client) onHealthChange(key string, err error) {
|
||||
c.logf("controlclient: restarting map request for %q health change to new state: %v", key, err)
|
||||
c.cancelMapSafely()
|
||||
}
|
||||
|
||||
// SetPaused controls whether HTTP activity should be paused.
|
||||
@@ -201,6 +214,50 @@ func (c *Client) Start() {
|
||||
go c.mapRoutine()
|
||||
}
|
||||
|
||||
// sendNewMapRequest either sends a new OmitPeers, non-streaming map request
|
||||
// (to just send Hostinfo/Netinfo/Endpoints info, while keeping an existing
|
||||
// streaming response open), or start a new streaming one if necessary.
|
||||
//
|
||||
// It should be called whenever there's something new to tell the server.
|
||||
func (c *Client) sendNewMapRequest() {
|
||||
c.mu.Lock()
|
||||
|
||||
// If we're not already streaming a netmap, or if we're already stuck
|
||||
// in a lite update, then tear down everything and start a new stream
|
||||
// (which starts by sending a new map request)
|
||||
if !c.inPollNetMap || c.inLiteMapUpdate || !c.loggedIn {
|
||||
c.mu.Unlock()
|
||||
c.cancelMapSafely()
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, send a lite update that doesn't keep a
|
||||
// long-running stream response.
|
||||
defer c.mu.Unlock()
|
||||
c.inLiteMapUpdate = true
|
||||
ctx, cancel := context.WithTimeout(c.mapCtx, 10*time.Second)
|
||||
go func() {
|
||||
defer cancel()
|
||||
t0 := time.Now()
|
||||
err := c.direct.SendLiteMapUpdate(ctx)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
c.mu.Lock()
|
||||
c.inLiteMapUpdate = false
|
||||
c.mu.Unlock()
|
||||
if err == nil {
|
||||
c.logf("[v1] successful lite map update in %v", d)
|
||||
return
|
||||
}
|
||||
if ctx.Err() == nil {
|
||||
c.logf("lite map update after %v: %v", d, err)
|
||||
}
|
||||
// Fall back to restarting the long-polling map
|
||||
// request (the old heavy way) if the lite update
|
||||
// failed for any reason.
|
||||
c.cancelMapSafely()
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *Client) cancelAuth() {
|
||||
c.mu.Lock()
|
||||
if c.authCancel != nil {
|
||||
@@ -464,7 +521,7 @@ func (c *Client) mapRoutine() {
|
||||
c.inPollNetMap = false
|
||||
c.mu.Unlock()
|
||||
|
||||
err := c.direct.PollNetMap(ctx, -1, func(nm *NetworkMap) {
|
||||
err := c.direct.PollNetMap(ctx, -1, func(nm *netmap.NetworkMap) {
|
||||
c.mu.Lock()
|
||||
|
||||
select {
|
||||
@@ -545,7 +602,7 @@ func (c *Client) SetHostinfo(hi *tailcfg.Hostinfo) {
|
||||
c.logf("Hostinfo: %v", hi)
|
||||
|
||||
// Send new Hostinfo to server
|
||||
c.cancelMapSafely()
|
||||
c.sendNewMapRequest()
|
||||
}
|
||||
|
||||
func (c *Client) SetNetInfo(ni *tailcfg.NetInfo) {
|
||||
@@ -553,16 +610,15 @@ func (c *Client) SetNetInfo(ni *tailcfg.NetInfo) {
|
||||
panic("nil NetInfo")
|
||||
}
|
||||
if !c.direct.SetNetInfo(ni) {
|
||||
c.logf("[unexpected] duplicate NetInfo: %v", ni)
|
||||
return
|
||||
}
|
||||
c.logf("NetInfo: %v", ni)
|
||||
|
||||
// Send new Hostinfo (which includes NetInfo) to server
|
||||
c.cancelMapSafely()
|
||||
c.sendNewMapRequest()
|
||||
}
|
||||
|
||||
func (c *Client) sendStatus(who string, err error, url string, nm *NetworkMap) {
|
||||
func (c *Client) sendStatus(who string, err error, url string, nm *netmap.NetworkMap) {
|
||||
c.mu.Lock()
|
||||
state := c.state
|
||||
loggedIn := c.loggedIn
|
||||
@@ -574,7 +630,7 @@ func (c *Client) sendStatus(who string, err error, url string, nm *NetworkMap) {
|
||||
|
||||
c.logf("[v1] sendStatus: %s: %v", who, state)
|
||||
|
||||
var p *Persist
|
||||
var p *persist.Persist
|
||||
var fin *empty.Message
|
||||
if state == StateAuthenticated {
|
||||
fin = new(empty.Message)
|
||||
@@ -636,7 +692,7 @@ func (c *Client) Logout() {
|
||||
func (c *Client) UpdateEndpoints(localPort uint16, endpoints []string) {
|
||||
changed := c.direct.SetEndpoints(localPort, endpoints)
|
||||
if changed {
|
||||
c.cancelMapSafely()
|
||||
c.sendNewMapRequest()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -654,6 +710,7 @@ func (c *Client) Shutdown() {
|
||||
|
||||
c.logf("client.Shutdown: inSendStatus=%v", inSendStatus)
|
||||
if !closed {
|
||||
c.unregisterHealthWatch()
|
||||
close(c.quit)
|
||||
c.cancelAuth()
|
||||
<-c.authDone
|
||||
|
||||
@@ -42,6 +42,11 @@ func TestStatusEqual(t *testing.T) {
|
||||
&Status{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
nil,
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Status{},
|
||||
&Status{},
|
||||
|
||||
69
control/controlclient/debug.go
Normal file
69
control/controlclient/debug.go
Normal file
@@ -0,0 +1,69 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func dumpGoroutinesToURL(c *http.Client, targetURL string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
zbuf := new(bytes.Buffer)
|
||||
zw := gzip.NewWriter(zbuf)
|
||||
zw.Write(scrubbedGoroutineDump())
|
||||
zw.Close()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", targetURL, zbuf)
|
||||
if err != nil {
|
||||
log.Printf("dumpGoroutinesToURL: %v", err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Encoding", "gzip")
|
||||
t0 := time.Now()
|
||||
_, err = c.Do(req)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err != nil {
|
||||
log.Printf("dumpGoroutinesToURL error: %v to %v (after %v)", err, targetURL, d)
|
||||
} else {
|
||||
log.Printf("dumpGoroutinesToURL complete to %v (after %v)", targetURL, d)
|
||||
}
|
||||
}
|
||||
|
||||
var reHexArgs = regexp.MustCompile(`\b0x[0-9a-f]+\b`)
|
||||
|
||||
// scrubbedGoroutineDump returns the list of all current goroutines, but with the actual
|
||||
// values of arguments scrubbed out, lest it contain some private key material.
|
||||
func scrubbedGoroutineDump() []byte {
|
||||
buf := make([]byte, 1<<20)
|
||||
buf = buf[:runtime.Stack(buf, true)]
|
||||
|
||||
saw := map[string][]byte{} // "0x123" => "v1%3" (unique value 1 and its value mod 8)
|
||||
return reHexArgs.ReplaceAllFunc(buf, func(in []byte) []byte {
|
||||
if string(in) == "0x0" {
|
||||
return in
|
||||
}
|
||||
if v, ok := saw[string(in)]; ok {
|
||||
return v
|
||||
}
|
||||
u64, err := strconv.ParseUint(string(in[2:]), 16, 64)
|
||||
if err != nil {
|
||||
return []byte("??")
|
||||
}
|
||||
v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8))
|
||||
saw[string(in)] = v
|
||||
return v
|
||||
})
|
||||
}
|
||||
11
control/controlclient/debug_test.go
Normal file
11
control/controlclient/debug_test.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controlclient
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestScrubbedGoroutineDump(t *testing.T) {
|
||||
t.Logf("Got:\n%s\n", scrubbedGoroutineDump())
|
||||
}
|
||||
@@ -4,8 +4,6 @@
|
||||
|
||||
package controlclient
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=Persist -output=direct_clone.go
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
@@ -22,6 +20,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
@@ -34,93 +33,44 @@ import (
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/oauth2"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/log/logheap"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
type Persist struct {
|
||||
_ structs.Incomparable
|
||||
|
||||
// LegacyFrontendPrivateMachineKey is here temporarily
|
||||
// (starting 2020-09-28) during migration of Windows users'
|
||||
// machine keys from frontend storage to the backend. On the
|
||||
// first LocalBackend.Start call, the backend will initialize
|
||||
// the real (backend-owned) machine key from the frontend's
|
||||
// provided value (if non-zero), picking a new random one if
|
||||
// needed. This field should be considered read-only from GUI
|
||||
// frontends. The real value should not be written back in
|
||||
// this field, lest the frontend persist it to disk.
|
||||
LegacyFrontendPrivateMachineKey wgkey.Private `json:"PrivateMachineKey"`
|
||||
|
||||
PrivateNodeKey wgkey.Private
|
||||
OldPrivateNodeKey wgkey.Private // needed to request key rotation
|
||||
Provider string
|
||||
LoginName string
|
||||
}
|
||||
|
||||
func (p *Persist) Equals(p2 *Persist) bool {
|
||||
if p == nil && p2 == nil {
|
||||
return true
|
||||
}
|
||||
if p == nil || p2 == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return p.LegacyFrontendPrivateMachineKey.Equal(p2.LegacyFrontendPrivateMachineKey) &&
|
||||
p.PrivateNodeKey.Equal(p2.PrivateNodeKey) &&
|
||||
p.OldPrivateNodeKey.Equal(p2.OldPrivateNodeKey) &&
|
||||
p.Provider == p2.Provider &&
|
||||
p.LoginName == p2.LoginName
|
||||
}
|
||||
|
||||
func (p *Persist) Pretty() string {
|
||||
var mk, ok, nk wgkey.Key
|
||||
if !p.LegacyFrontendPrivateMachineKey.IsZero() {
|
||||
mk = p.LegacyFrontendPrivateMachineKey.Public()
|
||||
}
|
||||
if !p.OldPrivateNodeKey.IsZero() {
|
||||
ok = p.OldPrivateNodeKey.Public()
|
||||
}
|
||||
if !p.PrivateNodeKey.IsZero() {
|
||||
nk = p.PrivateNodeKey.Public()
|
||||
}
|
||||
ss := func(k wgkey.Key) string {
|
||||
if k.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return k.ShortString()
|
||||
}
|
||||
return fmt.Sprintf("Persist{lm=%v, o=%v, n=%v u=%#v}",
|
||||
ss(mk), ss(ok), ss(nk), p.LoginName)
|
||||
}
|
||||
|
||||
// Direct is the client that connects to a tailcontrol server for a node.
|
||||
type Direct struct {
|
||||
httpc *http.Client // HTTP client used to talk to tailcontrol
|
||||
serverURL string // URL of the tailcontrol server
|
||||
timeNow func() time.Time
|
||||
lastPrintMap time.Time
|
||||
newDecompressor func() (Decompressor, error)
|
||||
keepAlive bool
|
||||
logf logger.Logf
|
||||
discoPubKey tailcfg.DiscoKey
|
||||
machinePrivKey wgkey.Private
|
||||
debugFlags []string
|
||||
httpc *http.Client // HTTP client used to talk to tailcontrol
|
||||
serverURL string // URL of the tailcontrol server
|
||||
timeNow func() time.Time
|
||||
lastPrintMap time.Time
|
||||
newDecompressor func() (Decompressor, error)
|
||||
keepAlive bool
|
||||
logf logger.Logf
|
||||
linkMon *monitor.Mon // or nil
|
||||
discoPubKey tailcfg.DiscoKey
|
||||
machinePrivKey wgkey.Private
|
||||
debugFlags []string
|
||||
keepSharerAndUserSplit bool
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
serverKey wgkey.Key
|
||||
persist Persist
|
||||
persist persist.Persist
|
||||
authKey string
|
||||
tryingNewKey wgkey.Private
|
||||
expiry *time.Time
|
||||
@@ -132,7 +82,7 @@ type Direct struct {
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
Persist Persist // initial persistent data
|
||||
Persist persist.Persist // initial persistent data
|
||||
MachinePrivateKey wgkey.Private // the machine key to use
|
||||
ServerURL string // URL of the tailcontrol server
|
||||
AuthKey string // optional node auth key for auto registration
|
||||
@@ -144,6 +94,11 @@ type Options struct {
|
||||
Logf logger.Logf
|
||||
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
|
||||
DebugFlags []string // debug settings to send to control
|
||||
LinkMonitor *monitor.Mon // optional link monitor
|
||||
|
||||
// KeepSharerAndUserSplit controls whether the client
|
||||
// understands Node.Sharer. If false, the Sharer is mapped to the User.
|
||||
KeepSharerAndUserSplit bool
|
||||
}
|
||||
|
||||
type Decompressor interface {
|
||||
@@ -176,31 +131,35 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
httpc := opts.HTTPTestClient
|
||||
if httpc == nil {
|
||||
dnsCache := &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward, // use default cache's forwarder
|
||||
UseLastGood: true,
|
||||
Forward: dnscache.Get().Forward, // use default cache's forwarder
|
||||
UseLastGood: true,
|
||||
LookupIPFallback: dnsfallback.Lookup,
|
||||
}
|
||||
dialer := netns.NewDialer()
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
tr.DialContext = dnscache.Dialer(dialer.DialContext, dnsCache)
|
||||
tr.ForceAttemptHTTP2 = true
|
||||
tr.TLSClientConfig = tlsdial.Config(serverURL.Host, tr.TLSClientConfig)
|
||||
tr.DialContext = dnscache.Dialer(dialer.DialContext, dnsCache)
|
||||
tr.DialTLSContext = dnscache.TLSDialer(dialer.DialContext, dnsCache, tr.TLSClientConfig)
|
||||
tr.ForceAttemptHTTP2 = true
|
||||
httpc = &http.Client{Transport: tr}
|
||||
}
|
||||
|
||||
c := &Direct{
|
||||
httpc: httpc,
|
||||
machinePrivKey: opts.MachinePrivateKey,
|
||||
serverURL: opts.ServerURL,
|
||||
timeNow: opts.TimeNow,
|
||||
logf: opts.Logf,
|
||||
newDecompressor: opts.NewDecompressor,
|
||||
keepAlive: opts.KeepAlive,
|
||||
persist: opts.Persist,
|
||||
authKey: opts.AuthKey,
|
||||
discoPubKey: opts.DiscoPublicKey,
|
||||
debugFlags: opts.DebugFlags,
|
||||
httpc: httpc,
|
||||
machinePrivKey: opts.MachinePrivateKey,
|
||||
serverURL: opts.ServerURL,
|
||||
timeNow: opts.TimeNow,
|
||||
logf: opts.Logf,
|
||||
newDecompressor: opts.NewDecompressor,
|
||||
keepAlive: opts.KeepAlive,
|
||||
persist: opts.Persist,
|
||||
authKey: opts.AuthKey,
|
||||
discoPubKey: opts.DiscoPublicKey,
|
||||
debugFlags: opts.DebugFlags,
|
||||
keepSharerAndUserSplit: opts.KeepSharerAndUserSplit,
|
||||
linkMon: opts.LinkMonitor,
|
||||
}
|
||||
if opts.Hostinfo == nil {
|
||||
c.SetHostinfo(NewHostinfo())
|
||||
@@ -223,10 +182,25 @@ func NewHostinfo() *tailcfg.Hostinfo {
|
||||
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 {
|
||||
@@ -265,7 +239,7 @@ func (c *Direct) SetNetInfo(ni *tailcfg.NetInfo) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *Direct) GetPersist() Persist {
|
||||
func (c *Direct) GetPersist() persist.Persist {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.persist
|
||||
@@ -288,7 +262,7 @@ func (c *Direct) TryLogout(ctx context.Context) error {
|
||||
// immediately invalidated.
|
||||
//if !c.persist.PrivateNodeKey.IsZero() {
|
||||
//}
|
||||
c.persist = Persist{}
|
||||
c.persist = persist.Persist{}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -520,7 +494,19 @@ func inTest() bool { return flag.Lookup("test.v") != nil }
|
||||
//
|
||||
// maxPolls is how many network maps to download; common values are 1
|
||||
// or -1 (to keep a long-poll query open to the server).
|
||||
func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkMap)) error {
|
||||
func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*netmap.NetworkMap)) error {
|
||||
return c.sendMapRequest(ctx, maxPolls, cb)
|
||||
}
|
||||
|
||||
// SendLiteMapUpdate makes a /map request to update the server of our latest state,
|
||||
// but does not fetch anything. It returns an error if the server did not return a
|
||||
// successful 200 OK response.
|
||||
func (c *Direct) SendLiteMapUpdate(ctx context.Context) error {
|
||||
return c.sendMapRequest(ctx, 1, nil)
|
||||
}
|
||||
|
||||
// cb nil means to omit peers.
|
||||
func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netmap.NetworkMap)) error {
|
||||
c.mu.Lock()
|
||||
persist := c.persist
|
||||
serverURL := c.serverURL
|
||||
@@ -532,6 +518,9 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
everEndpoints := c.everEndpoints
|
||||
c.mu.Unlock()
|
||||
|
||||
if persist.PrivateNodeKey.IsZero() {
|
||||
return errors.New("privateNodeKey is zero")
|
||||
}
|
||||
if backendLogID == "" {
|
||||
return errors.New("hostinfo: BackendLogID missing")
|
||||
}
|
||||
@@ -547,7 +536,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
}
|
||||
|
||||
request := tailcfg.MapRequest{
|
||||
Version: 8,
|
||||
Version: tailcfg.CurrentMapRequestVersion,
|
||||
KeepAlive: c.keepAlive,
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
DiscoKey: c.discoPubKey,
|
||||
@@ -555,10 +544,18 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
Stream: allowStream,
|
||||
Hostinfo: hostinfo,
|
||||
DebugFlags: c.debugFlags,
|
||||
OmitPeers: cb == nil,
|
||||
}
|
||||
if hostinfo != nil && ipForwardingBroken(hostinfo.RoutableIPs) {
|
||||
var extraDebugFlags []string
|
||||
if hostinfo != nil && c.linkMon != nil && ipForwardingBroken(hostinfo.RoutableIPs, c.linkMon.InterfaceState()) {
|
||||
extraDebugFlags = append(extraDebugFlags, "warn-ip-forwarding-off")
|
||||
}
|
||||
if health.RouterHealth() != nil {
|
||||
extraDebugFlags = append(extraDebugFlags, "warn-router-unhealthy")
|
||||
}
|
||||
if len(extraDebugFlags) > 0 {
|
||||
old := request.DebugFlags
|
||||
request.DebugFlags = append(old[:len(old):len(old)], "warn-ip-forwarding-off")
|
||||
request.DebugFlags = append(old[:len(old):len(old)], extraDebugFlags...)
|
||||
}
|
||||
if c.newDecompressor != nil {
|
||||
request.Compress = "zstd"
|
||||
@@ -607,6 +604,11 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if cb == nil {
|
||||
io.Copy(ioutil.Discard, res.Body)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If we go more than pollTimeout without hearing from the server,
|
||||
// end the long poll. We should be receiving a keep alive ping
|
||||
// every minute.
|
||||
@@ -643,6 +645,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
var lastDERPMap *tailcfg.DERPMap
|
||||
var lastUserProfile = map[tailcfg.UserID]tailcfg.UserProfile{}
|
||||
var lastParsedPacketFilter []filter.Match
|
||||
var collectServices bool
|
||||
|
||||
// If allowStream, then the server will use an HTTP long poll to
|
||||
// return incremental results. There is always one response right
|
||||
@@ -674,6 +677,10 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
return err
|
||||
}
|
||||
|
||||
if pr := resp.PingRequest; pr != nil {
|
||||
go answerPing(c.logf, c.httpc, pr)
|
||||
}
|
||||
|
||||
if resp.KeepAlive {
|
||||
vlogf("netmap: got keep-alive")
|
||||
} else {
|
||||
@@ -704,6 +711,9 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
if resp.Debug.LogHeapPprof {
|
||||
go logheap.LogHeap(resp.Debug.LogHeapURL)
|
||||
}
|
||||
if resp.Debug.GoroutineDumpURL != "" {
|
||||
go dumpGoroutinesToURL(c.httpc, resp.Debug.GoroutineDumpURL)
|
||||
}
|
||||
setControlAtomic(&controlUseDERPRoute, resp.Debug.DERPRoute)
|
||||
setControlAtomic(&controlTrimWGConfig, resp.Debug.TrimWGConfig)
|
||||
}
|
||||
@@ -719,28 +729,50 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
}
|
||||
resp.Peers = filtered
|
||||
}
|
||||
if Debug.StripEndpoints {
|
||||
for _, p := range resp.Peers {
|
||||
// We need at least one endpoint here for now else
|
||||
// other code doesn't even create the discoEndpoint.
|
||||
// TODO(bradfitz): fix that and then just nil this out.
|
||||
p.Endpoints = []string{"127.9.9.9:456"}
|
||||
}
|
||||
}
|
||||
|
||||
if pf := resp.PacketFilter; pf != nil {
|
||||
lastParsedPacketFilter = c.parsePacketFilter(pf)
|
||||
}
|
||||
|
||||
nm := &NetworkMap{
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
PrivateKey: persist.PrivateNodeKey,
|
||||
MachineKey: machinePubKey,
|
||||
Expiry: resp.Node.KeyExpiry,
|
||||
Name: resp.Node.Name,
|
||||
Addresses: resp.Node.Addresses,
|
||||
Peers: resp.Peers,
|
||||
LocalPort: localPort,
|
||||
User: resp.Node.User,
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
|
||||
Domain: resp.Domain,
|
||||
DNS: resp.DNSConfig,
|
||||
Hostinfo: resp.Node.Hostinfo,
|
||||
PacketFilter: lastParsedPacketFilter,
|
||||
DERPMap: lastDERPMap,
|
||||
Debug: resp.Debug,
|
||||
if v, ok := resp.CollectServices.Get(); ok {
|
||||
collectServices = v
|
||||
}
|
||||
|
||||
// Get latest localPort. This might've changed if
|
||||
// a lite map update occured meanwhile. This only affects
|
||||
// the end-to-end test.
|
||||
// TODO(bradfitz): remove the NetworkMap.LocalPort field entirely.
|
||||
c.mu.Lock()
|
||||
localPort = c.localPort
|
||||
c.mu.Unlock()
|
||||
|
||||
nm := &netmap.NetworkMap{
|
||||
SelfNode: resp.Node,
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
PrivateKey: persist.PrivateNodeKey,
|
||||
MachineKey: machinePubKey,
|
||||
Expiry: resp.Node.KeyExpiry,
|
||||
Name: resp.Node.Name,
|
||||
Addresses: resp.Node.Addresses,
|
||||
Peers: resp.Peers,
|
||||
LocalPort: localPort,
|
||||
User: resp.Node.User,
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
|
||||
Domain: resp.Domain,
|
||||
DNS: resp.DNSConfig,
|
||||
Hostinfo: resp.Node.Hostinfo,
|
||||
PacketFilter: lastParsedPacketFilter,
|
||||
CollectServices: collectServices,
|
||||
DERPMap: lastDERPMap,
|
||||
Debug: resp.Debug,
|
||||
}
|
||||
addUserProfile := func(userID tailcfg.UserID) {
|
||||
if _, dup := nm.UserProfiles[userID]; dup {
|
||||
@@ -752,7 +784,17 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
|
||||
}
|
||||
}
|
||||
addUserProfile(nm.User)
|
||||
magicDNSSuffix := nm.MagicDNSSuffix()
|
||||
nm.SelfNode.InitDisplayNames(magicDNSSuffix)
|
||||
for _, peer := range resp.Peers {
|
||||
peer.InitDisplayNames(magicDNSSuffix)
|
||||
if !peer.Sharer.IsZero() {
|
||||
if c.keepSharerAndUserSplit {
|
||||
addUserProfile(peer.Sharer)
|
||||
} else {
|
||||
peer.User = peer.Sharer
|
||||
}
|
||||
}
|
||||
addUserProfile(peer.User)
|
||||
}
|
||||
if resp.Node.MachineAuthorized {
|
||||
@@ -927,19 +969,21 @@ func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (w
|
||||
var Debug = initDebug()
|
||||
|
||||
type debug struct {
|
||||
NetMap bool
|
||||
ProxyDNS bool
|
||||
OnlyDisco bool
|
||||
Disco bool
|
||||
NetMap bool
|
||||
ProxyDNS bool
|
||||
OnlyDisco bool
|
||||
Disco bool
|
||||
StripEndpoints bool // strip endpoints from control (only use disco messages)
|
||||
}
|
||||
|
||||
func initDebug() debug {
|
||||
use := os.Getenv("TS_DEBUG_USE_DISCO")
|
||||
return debug{
|
||||
NetMap: envBool("TS_DEBUG_NETMAP"),
|
||||
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
|
||||
OnlyDisco: use == "only",
|
||||
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
|
||||
NetMap: envBool("TS_DEBUG_NETMAP"),
|
||||
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
|
||||
StripEndpoints: envBool("TS_DEBUG_STRIP_ENDPOINTS"),
|
||||
OnlyDisco: use == "only",
|
||||
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1020,6 +1064,24 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
|
||||
}
|
||||
}
|
||||
sortNodes(newFull)
|
||||
|
||||
if mapRes.PeerSeenChange != nil {
|
||||
peerByID := make(map[tailcfg.NodeID]*tailcfg.Node, len(newFull))
|
||||
for _, n := range newFull {
|
||||
peerByID[n.ID] = n
|
||||
}
|
||||
now := time.Now()
|
||||
for nodeID, seen := range mapRes.PeerSeenChange {
|
||||
if n, ok := peerByID[nodeID]; ok {
|
||||
if seen {
|
||||
n.LastSeen = &now
|
||||
} else {
|
||||
n.LastSeen = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mapRes.Peers = newFull
|
||||
mapRes.PeersChanged = nil
|
||||
mapRes.PeersRemoved = nil
|
||||
@@ -1080,7 +1142,7 @@ func TrimWGConfig() opt.Bool {
|
||||
// and will definitely not work for the routes provided.
|
||||
//
|
||||
// It should not return false positives.
|
||||
func ipForwardingBroken(routes []netaddr.IPPrefix) bool {
|
||||
func ipForwardingBroken(routes []netaddr.IPPrefix, state *interfaces.State) bool {
|
||||
if len(routes) == 0 {
|
||||
// Nothing to route, so no need to warn.
|
||||
return false
|
||||
@@ -1094,8 +1156,21 @@ func ipForwardingBroken(routes []netaddr.IPPrefix) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
localIPs := map[netaddr.IP]bool{}
|
||||
for _, addrs := range state.InterfaceIPs {
|
||||
for _, pfx := range addrs {
|
||||
localIPs[pfx.IP] = true
|
||||
}
|
||||
}
|
||||
|
||||
v4Routes, v6Routes := false, false
|
||||
for _, r := range routes {
|
||||
// It's possible to advertise a route to one of the local
|
||||
// machine's local IPs. IP forwarding isn't required for this
|
||||
// to work, so we shouldn't warn for such exports.
|
||||
if r.IsSingleIP() && localIPs[r.IP] {
|
||||
continue
|
||||
}
|
||||
if r.IP.Is4() {
|
||||
v4Routes = true
|
||||
} else {
|
||||
@@ -1147,3 +1222,29 @@ func ipForwardingBroken(routes []netaddr.IPPrefix) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func answerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest) {
|
||||
if pr.URL == "" {
|
||||
logf("invalid PingRequest with no URL")
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "HEAD", pr.URL, nil)
|
||||
if err != nil {
|
||||
logf("http.NewRequestWithContext(%q): %v", pr.URL, err)
|
||||
return
|
||||
}
|
||||
if pr.Log {
|
||||
logf("answerPing: sending ping to %v ...", pr.URL)
|
||||
}
|
||||
t0 := time.Now()
|
||||
_, err = c.Do(req)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err != nil {
|
||||
logf("answerPing error: %v to %v (after %v)", err, pr.URL, d)
|
||||
} else if pr.Log {
|
||||
logf("answerPing complete to %v (after %v)", pr.URL, d)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner -type Persist; DO NOT EDIT.
|
||||
|
||||
package controlclient
|
||||
|
||||
import ()
|
||||
|
||||
// Clone makes a deep copy of Persist.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Persist) Clone() *Persist {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Persist)
|
||||
*dst = *src
|
||||
return dst
|
||||
}
|
||||
@@ -5,12 +5,14 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func TestUndeltaPeers(t *testing.T) {
|
||||
@@ -91,3 +93,79 @@ func formatNodes(nodes []*tailcfg.Node) string {
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func TestNewDirect(t *testing.T) {
|
||||
hi := NewHostinfo()
|
||||
ni := tailcfg.NetInfo{LinkType: "wired"}
|
||||
hi.NetInfo = &ni
|
||||
|
||||
key, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
opts := Options{ServerURL: "https://example.com", MachinePrivateKey: key, Hostinfo: hi}
|
||||
c, err := NewDirect(opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if c.serverURL != opts.ServerURL {
|
||||
t.Errorf("c.serverURL got %v want %v", c.serverURL, opts.ServerURL)
|
||||
}
|
||||
|
||||
if !hi.Equal(c.hostinfo) {
|
||||
t.Errorf("c.hostinfo got %v want %v", c.hostinfo, hi)
|
||||
}
|
||||
|
||||
changed := c.SetNetInfo(&ni)
|
||||
if changed {
|
||||
t.Errorf("c.SetNetInfo(ni) want false got %v", changed)
|
||||
}
|
||||
ni = tailcfg.NetInfo{LinkType: "wifi"}
|
||||
changed = c.SetNetInfo(&ni)
|
||||
if !changed {
|
||||
t.Errorf("c.SetNetInfo(ni) want true got %v", changed)
|
||||
}
|
||||
|
||||
changed = c.SetHostinfo(hi)
|
||||
if changed {
|
||||
t.Errorf("c.SetHostinfo(hi) want false got %v", changed)
|
||||
}
|
||||
hi = NewHostinfo()
|
||||
hi.Hostname = "different host name"
|
||||
changed = c.SetHostinfo(hi)
|
||||
if !changed {
|
||||
t.Errorf("c.SetHostinfo(hi) want true got %v", changed)
|
||||
}
|
||||
|
||||
endpoints := []string{"1", "2", "3"}
|
||||
changed = c.newEndpoints(12, endpoints)
|
||||
if !changed {
|
||||
t.Errorf("c.newEndpoints(12) want true got %v", changed)
|
||||
}
|
||||
changed = c.newEndpoints(12, endpoints)
|
||||
if changed {
|
||||
t.Errorf("c.newEndpoints(12) want false got %v", changed)
|
||||
}
|
||||
changed = c.newEndpoints(13, endpoints)
|
||||
if !changed {
|
||||
t.Errorf("c.newEndpoints(13) want true got %v", changed)
|
||||
}
|
||||
endpoints = []string{"4", "5", "6"}
|
||||
changed = c.newEndpoints(13, endpoints)
|
||||
if !changed {
|
||||
t.Errorf("c.newEndpoints(13) want true got %v", changed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewHostinfo(t *testing.T) {
|
||||
hi := NewHostinfo()
|
||||
if hi == nil {
|
||||
t.Fatal("no Hostinfo")
|
||||
}
|
||||
j, err := json.MarshalIndent(hi, " ", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Got: %s", j)
|
||||
}
|
||||
|
||||
@@ -59,7 +59,8 @@ Login:
|
||||
* server sends frameServerInfo
|
||||
|
||||
Steady state:
|
||||
* server occasionally sends frameKeepAlive
|
||||
* server occasionally sends frameKeepAlive (or framePing)
|
||||
* client responds to any framePing with a framePong
|
||||
* client sends frameSendPacket
|
||||
* server then sends frameRecvPacket to recipient
|
||||
*/
|
||||
@@ -97,6 +98,9 @@ const (
|
||||
// connection. (To be used for cluster load balancing
|
||||
// purposes, when clients end up on a non-ideal node)
|
||||
frameClosePeer = frameType(0x11) // 32B pub key of peer to close.
|
||||
|
||||
framePing = frameType(0x12) // 8 byte ping payload, to be echoed back in framePong
|
||||
framePong = frameType(0x13) // 8 byte payload, the contents of the ping being replied to
|
||||
)
|
||||
|
||||
var bin = binary.BigEndian
|
||||
|
||||
@@ -21,13 +21,14 @@ import (
|
||||
|
||||
// Client is a DERP client.
|
||||
type Client struct {
|
||||
serverKey key.Public // of the DERP server; not a machine or node key
|
||||
privateKey key.Private
|
||||
publicKey key.Public // of privateKey
|
||||
logf logger.Logf
|
||||
nc Conn
|
||||
br *bufio.Reader
|
||||
meshKey string
|
||||
serverKey key.Public // of the DERP server; not a machine or node key
|
||||
privateKey key.Private
|
||||
publicKey key.Public // of privateKey
|
||||
logf logger.Logf
|
||||
nc Conn
|
||||
br *bufio.Reader
|
||||
meshKey string
|
||||
canAckPings bool
|
||||
|
||||
wmu sync.Mutex // hold while writing to bw
|
||||
bw *bufio.Writer
|
||||
@@ -48,8 +49,9 @@ func (f clientOptFunc) update(o *clientOpt) { f(o) }
|
||||
|
||||
// clientOpt are the options passed to newClient.
|
||||
type clientOpt struct {
|
||||
MeshKey string
|
||||
ServerPub key.Public
|
||||
MeshKey string
|
||||
ServerPub key.Public
|
||||
CanAckPings bool
|
||||
}
|
||||
|
||||
// MeshKey returns a ClientOpt to pass to the DERP server during connect to get
|
||||
@@ -64,6 +66,12 @@ func ServerPublicKey(key key.Public) ClientOpt {
|
||||
return clientOptFunc(func(o *clientOpt) { o.ServerPub = key })
|
||||
}
|
||||
|
||||
// CanAckPings returns a ClientOpt to set whether it advertises to the
|
||||
// server that it's capable of acknowledging ping requests.
|
||||
func CanAckPings(v bool) ClientOpt {
|
||||
return clientOptFunc(func(o *clientOpt) { o.CanAckPings = v })
|
||||
}
|
||||
|
||||
func NewClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opts ...ClientOpt) (*Client, error) {
|
||||
var opt clientOpt
|
||||
for _, o := range opts {
|
||||
@@ -77,13 +85,14 @@ func NewClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logg
|
||||
|
||||
func newClient(privateKey key.Private, nc Conn, brw *bufio.ReadWriter, logf logger.Logf, opt clientOpt) (*Client, error) {
|
||||
c := &Client{
|
||||
privateKey: privateKey,
|
||||
publicKey: privateKey.Public(),
|
||||
logf: logf,
|
||||
nc: nc,
|
||||
br: brw.Reader,
|
||||
bw: brw.Writer,
|
||||
meshKey: opt.MeshKey,
|
||||
privateKey: privateKey,
|
||||
publicKey: privateKey.Public(),
|
||||
logf: logf,
|
||||
nc: nc,
|
||||
br: brw.Reader,
|
||||
bw: brw.Writer,
|
||||
meshKey: opt.MeshKey,
|
||||
canAckPings: opt.CanAckPings,
|
||||
}
|
||||
if opt.ServerPub.IsZero() {
|
||||
if err := c.recvServerKey(); err != nil {
|
||||
@@ -147,6 +156,10 @@ type clientInfo struct {
|
||||
// connection list & forward packets. It's empty for regular
|
||||
// users.
|
||||
MeshKey string `json:"meshKey,omitempty"`
|
||||
|
||||
// CanAckPings is whether the client declares it's able to ack
|
||||
// pings.
|
||||
CanAckPings bool
|
||||
}
|
||||
|
||||
func (c *Client) sendClientKey() error {
|
||||
@@ -155,8 +168,9 @@ func (c *Client) sendClientKey() error {
|
||||
return err
|
||||
}
|
||||
msg, err := json.Marshal(clientInfo{
|
||||
Version: ProtocolVersion,
|
||||
MeshKey: c.meshKey,
|
||||
Version: ProtocolVersion,
|
||||
MeshKey: c.meshKey,
|
||||
CanAckPings: c.canAckPings,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -238,6 +252,18 @@ func (c *Client) ForwardPacket(srcKey, dstKey key.Public, pkt []byte) (err error
|
||||
|
||||
func (c *Client) writeTimeoutFired() { c.nc.Close() }
|
||||
|
||||
func (c *Client) SendPong(data [8]byte) error {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
if err := writeFrameHeader(c.bw, framePong, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(data[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.bw.Flush()
|
||||
}
|
||||
|
||||
// NotePreferred sends a packet that tells the server whether this
|
||||
// client is the user's preferred server. This is only used in the
|
||||
// server for stats.
|
||||
@@ -319,6 +345,12 @@ type ServerInfoMessage struct{}
|
||||
|
||||
func (ServerInfoMessage) msg() {}
|
||||
|
||||
// PingMessage is a request from a client or server to reply to the
|
||||
// other side with a PongMessage with the given payload.
|
||||
type PingMessage [8]byte
|
||||
|
||||
func (PingMessage) msg() {}
|
||||
|
||||
// Recv reads a message from the DERP server.
|
||||
//
|
||||
// The returned message may alias memory owned by the Client; it
|
||||
@@ -397,8 +429,8 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
// TODO: add the results of parseServerInfo to ServerInfoMessage if we ever need it.
|
||||
return ServerInfoMessage{}, nil
|
||||
case frameKeepAlive:
|
||||
// TODO: eventually we'll have server->client pings that
|
||||
// require ack pongs.
|
||||
// A one-way keep-alive message that doesn't require an acknowledgement.
|
||||
// This predated framePing/framePong.
|
||||
continue
|
||||
case framePeerGone:
|
||||
if n < keyLen {
|
||||
@@ -427,6 +459,15 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
copy(rp.Source[:], b[:keyLen])
|
||||
rp.Data = b[keyLen:n]
|
||||
return rp, nil
|
||||
|
||||
case framePing:
|
||||
var pm PingMessage
|
||||
if n < 8 {
|
||||
c.logf("[unexpected] dropping short ping frame")
|
||||
continue
|
||||
}
|
||||
copy(pm[:], b[:])
|
||||
return pm, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package derp
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"crypto/x509"
|
||||
@@ -791,6 +792,63 @@ func TestMetaCert(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
type dummyNetConn struct {
|
||||
net.Conn
|
||||
}
|
||||
|
||||
func (dummyNetConn) SetReadDeadline(time.Time) error { return nil }
|
||||
|
||||
func TestClientRecv(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input []byte
|
||||
want interface{}
|
||||
}{
|
||||
{
|
||||
name: "ping",
|
||||
input: []byte{
|
||||
byte(framePing), 0, 0, 0, 8,
|
||||
1, 2, 3, 4, 5, 6, 7, 8,
|
||||
},
|
||||
want: PingMessage{1, 2, 3, 4, 5, 6, 7, 8},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &Client{
|
||||
nc: dummyNetConn{},
|
||||
br: bufio.NewReader(bytes.NewReader(tt.input)),
|
||||
logf: t.Logf,
|
||||
}
|
||||
got, err := c.Recv()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("got %#v; want %#v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientSendPong(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
c := &Client{
|
||||
bw: bufio.NewWriter(&buf),
|
||||
}
|
||||
if err := c.SendPong([8]byte{1, 2, 3, 4, 5, 6, 7, 8}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []byte{
|
||||
byte(framePong), 0, 0, 0, 8,
|
||||
1, 2, 3, 4, 5, 6, 7, 8,
|
||||
}
|
||||
if !bytes.Equal(buf.Bytes(), want) {
|
||||
t.Errorf("unexpected output\nwrote: % 02x\n want: % 02x", buf.Bytes(), want)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func BenchmarkSendRecv(b *testing.B) {
|
||||
for _, size := range []int{10, 100, 1000, 10000} {
|
||||
b.Run(fmt.Sprintf("msgsize=%d", size), func(b *testing.B) { benchmarkSendRecvSize(b, size) })
|
||||
|
||||
@@ -63,6 +63,7 @@ type Client struct {
|
||||
|
||||
mu sync.Mutex
|
||||
preferred bool
|
||||
canAckPings bool
|
||||
closed bool
|
||||
netConn io.Closer
|
||||
client *derp.Client
|
||||
@@ -333,7 +334,11 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
return nil, 0, fmt.Errorf("GET failed: %v: %s", err, b)
|
||||
}
|
||||
}
|
||||
derpClient, err = derp.NewClient(c.privateKey, httpConn, brw, c.logf, derp.MeshKey(c.MeshKey), derp.ServerPublicKey(serverPub))
|
||||
derpClient, err = derp.NewClient(c.privateKey, httpConn, brw, c.logf,
|
||||
derp.MeshKey(c.MeshKey),
|
||||
derp.ServerPublicKey(serverPub),
|
||||
derp.CanAckPings(c.canAckPings),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -358,7 +363,7 @@ func (c *Client) dialURL(ctx context.Context) (net.Conn, error) {
|
||||
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()
|
||||
}
|
||||
@@ -642,6 +647,38 @@ func (c *Client) ForwardPacket(from, to key.Public, b []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// SendPong sends a reply to a ping, with the ping's provided
|
||||
// challenge/identifier data.
|
||||
//
|
||||
// Unlike other send methods, SendPong makes no attempt to connect or
|
||||
// reconnect to the peer. It's best effort. If there's a connection
|
||||
// problem, the server will choose to hang up on us if we're not
|
||||
// replying.
|
||||
func (c *Client) SendPong(data [8]byte) error {
|
||||
c.mu.Lock()
|
||||
if c.closed {
|
||||
c.mu.Unlock()
|
||||
return ErrClientClosed
|
||||
}
|
||||
if c.client == nil {
|
||||
c.mu.Unlock()
|
||||
return errors.New("not connected")
|
||||
}
|
||||
dc := c.client
|
||||
c.mu.Unlock()
|
||||
|
||||
return dc.SendPong(data)
|
||||
}
|
||||
|
||||
// SetCanAckPings sets whether this client will reply to ping requests from the server.
|
||||
//
|
||||
// This only affects future connections.
|
||||
func (c *Client) SetCanAckPings(v bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.canAckPings = v
|
||||
}
|
||||
|
||||
// NotePreferred notes whether this Client is the caller's preferred
|
||||
// (home) DERP node. It's only used for stats.
|
||||
func (c *Client) NotePreferred(v bool) {
|
||||
@@ -709,10 +746,19 @@ func (c *Client) RecvDetail() (m derp.ReceivedMessage, connGen int, err error) {
|
||||
m, err = client.Recv()
|
||||
if err != nil {
|
||||
c.closeForReconnect(client)
|
||||
if c.isClosed() {
|
||||
err = ErrClientClosed
|
||||
}
|
||||
}
|
||||
return m, connGen, err
|
||||
}
|
||||
|
||||
func (c *Client) isClosed() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.closed
|
||||
}
|
||||
|
||||
// Close closes the client. It will not automatically reconnect after
|
||||
// being closed.
|
||||
func (c *Client) Close() error {
|
||||
|
||||
@@ -5,20 +5,32 @@
|
||||
package derphttp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// RunWatchConnectionLoop loops forever, sending WatchConnectionChanges and subscribing to
|
||||
// RunWatchConnectionLoop loops until ctx is done, sending WatchConnectionChanges and subscribing to
|
||||
// connection changes.
|
||||
//
|
||||
// If the server's public key is ignoreServerKey, RunWatchConnectionLoop returns.
|
||||
//
|
||||
// Otherwise, the add and remove funcs are called as clients come & go.
|
||||
func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove func(key.Public)) {
|
||||
//
|
||||
// infoLogf, if non-nil, is the logger to write periodic status
|
||||
// updates about how many peers are on the server. Error log output is
|
||||
// set to the c's logger, regardless of infoLogf's value.
|
||||
//
|
||||
// To force RunWatchConnectionLoop to return quickly, its ctx needs to
|
||||
// be closed, and c itself needs to be closed.
|
||||
func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key.Public, infoLogf logger.Logf, add, remove func(key.Public)) {
|
||||
if infoLogf == nil {
|
||||
infoLogf = logger.Discard
|
||||
}
|
||||
logf := c.logf
|
||||
const retryInterval = 5 * time.Second
|
||||
const statusInterval = 10 * time.Second
|
||||
@@ -45,7 +57,7 @@ func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove
|
||||
if loggedConnected {
|
||||
return
|
||||
}
|
||||
logf("connected; %d peers", len(present))
|
||||
infoLogf("connected; %d peers", len(present))
|
||||
loggedConnected = true
|
||||
}
|
||||
|
||||
@@ -79,12 +91,21 @@ func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
sleep := func(d time.Duration) {
|
||||
t := time.NewTimer(d)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Stop()
|
||||
case <-t.C:
|
||||
}
|
||||
}
|
||||
|
||||
for ctx.Err() == nil {
|
||||
err := c.WatchConnectionChanges()
|
||||
if err != nil {
|
||||
clear()
|
||||
logf("WatchConnectionChanges: %v", err)
|
||||
time.Sleep(retryInterval)
|
||||
sleep(retryInterval)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -97,7 +118,7 @@ func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove
|
||||
if err != nil {
|
||||
clear()
|
||||
logf("Recv: %v", err)
|
||||
time.Sleep(retryInterval)
|
||||
sleep(retryInterval)
|
||||
break
|
||||
}
|
||||
if connGen != lastConnGen {
|
||||
@@ -114,9 +135,8 @@ func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove
|
||||
}
|
||||
if now := time.Now(); now.Sub(lastStatus) > statusInterval {
|
||||
lastStatus = now
|
||||
logf("%d peers", len(present))
|
||||
infoLogf("%d peers", len(present))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func Parse(p []byte) (Message, error) {
|
||||
case TypePong:
|
||||
return parsePong(ver, p)
|
||||
case TypeCallMeMaybe:
|
||||
return CallMeMaybe{}, nil
|
||||
return parseCallMeMaybe(ver, p)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown message type 0x%02x", byte(t))
|
||||
}
|
||||
@@ -122,13 +122,57 @@ func parsePing(ver uint8, p []byte) (m *Ping, err error) {
|
||||
//
|
||||
// The recipient may choose to not open a path back, if it's already
|
||||
// happy with its path. But usually it will.
|
||||
type CallMeMaybe struct{}
|
||||
type CallMeMaybe struct {
|
||||
// MyNumber is what the peer believes its endpoints are.
|
||||
//
|
||||
// Prior to Tailscale 1.4, the endpoints were exchanged purely
|
||||
// between nodes and the control server.
|
||||
//
|
||||
// Starting with Tailscale 1.4, clients advertise their endpoints.
|
||||
// Older clients won't use this, but newer clients should
|
||||
// use any endpoints in here that aren't included from control.
|
||||
//
|
||||
// Control might have sent stale endpoints if the client was idle
|
||||
// before contacting us. In that case, the client likely did a STUN
|
||||
// request immediately before sending the CallMeMaybe to recreate
|
||||
// their NAT port mapping, and that new good endpoint is included
|
||||
// in this field, but might not yet be in control's endpoints.
|
||||
// (And in the future, control will stop distributing endpoints
|
||||
// when clients are suitably new.)
|
||||
MyNumber []netaddr.IPPort
|
||||
}
|
||||
|
||||
func (CallMeMaybe) AppendMarshal(b []byte) []byte {
|
||||
ret, _ := appendMsgHeader(b, TypeCallMeMaybe, v0, 0)
|
||||
const epLength = 16 + 2 // 16 byte IP address + 2 byte port
|
||||
|
||||
func (m *CallMeMaybe) AppendMarshal(b []byte) []byte {
|
||||
ret, p := appendMsgHeader(b, TypeCallMeMaybe, v0, epLength*len(m.MyNumber))
|
||||
for _, ipp := range m.MyNumber {
|
||||
a := ipp.IP.As16()
|
||||
copy(p[:], a[:])
|
||||
binary.BigEndian.PutUint16(p[16:], ipp.Port)
|
||||
p = p[epLength:]
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func parseCallMeMaybe(ver uint8, p []byte) (m *CallMeMaybe, err error) {
|
||||
m = new(CallMeMaybe)
|
||||
if len(p)%epLength != 0 || ver != 0 || len(p) == 0 {
|
||||
return m, nil
|
||||
}
|
||||
m.MyNumber = make([]netaddr.IPPort, 0, len(p)/epLength)
|
||||
for len(p) > 0 {
|
||||
var a [16]byte
|
||||
copy(a[:], p)
|
||||
m.MyNumber = append(m.MyNumber, netaddr.IPPort{
|
||||
IP: netaddr.IPFrom16(a),
|
||||
Port: binary.BigEndian.Uint16(p[16:18]),
|
||||
})
|
||||
p = p[epLength:]
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// Pong is a response a Ping.
|
||||
//
|
||||
// It includes the sender's source IP + port, so it's effectively a
|
||||
@@ -171,7 +215,7 @@ func MessageSummary(m Message) string {
|
||||
return fmt.Sprintf("ping tx=%x", m.TxID[:6])
|
||||
case *Pong:
|
||||
return fmt.Sprintf("pong tx=%x", m.TxID[:6])
|
||||
case CallMeMaybe:
|
||||
case *CallMeMaybe:
|
||||
return "call-me-maybe"
|
||||
default:
|
||||
return fmt.Sprintf("%#v", m)
|
||||
|
||||
@@ -44,9 +44,19 @@ func TestMarshalAndParse(t *testing.T) {
|
||||
},
|
||||
{
|
||||
name: "call_me_maybe",
|
||||
m: CallMeMaybe{},
|
||||
m: &CallMeMaybe{},
|
||||
want: "03 00",
|
||||
},
|
||||
{
|
||||
name: "call_me_maybe_endpoints",
|
||||
m: &CallMeMaybe{
|
||||
MyNumber: []netaddr.IPPort{
|
||||
netaddr.MustParseIPPort("1.2.3.4:567"),
|
||||
netaddr.MustParseIPPort("[2001::3456]:789"),
|
||||
},
|
||||
},
|
||||
want: "03 00 00 00 00 00 00 00 00 00 00 00 ff ff 01 02 03 04 02 37 20 01 00 00 00 00 00 00 00 00 00 00 00 00 34 56 03 15",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
24
go.mod
24
go.mod
@@ -1,6 +1,6 @@
|
||||
module tailscale.com
|
||||
|
||||
go 1.15
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5
|
||||
@@ -12,34 +12,34 @@ require (
|
||||
github.com/go-multierror/multierror v1.0.2
|
||||
github.com/go-ole/go-ole v1.2.4
|
||||
github.com/godbus/dbus/v5 v5.0.3
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e
|
||||
github.com/golang/protobuf v1.4.2 // indirect
|
||||
github.com/google/go-cmp v0.5.4
|
||||
github.com/goreleaser/nfpm v1.1.10
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b
|
||||
github.com/klauspost/compress v1.10.10
|
||||
github.com/kr/pty v1.1.1
|
||||
github.com/mdlayher/netlink v1.2.0
|
||||
github.com/kr/pty v1.1.8
|
||||
github.com/mdlayher/netlink v1.3.2
|
||||
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a
|
||||
github.com/miekg/dns v1.1.30
|
||||
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3
|
||||
github.com/peterbourgon/ff/v2 v2.0.0
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201228234719-da0d2727455d
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210210202228-3cc76ed5f222
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
go4.org/intern v0.0.0-20201223061701-969c7e87e7cb // indirect
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 // indirect
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
|
||||
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
|
||||
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742
|
||||
golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b
|
||||
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8
|
||||
gvisor.dev/gvisor v0.0.0-20210111185822-3ff3110fcdd6
|
||||
honnef.co/go/tools v0.1.0
|
||||
inet.af/netaddr v0.0.0-20201231012616-c5dc91d2a016
|
||||
inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44
|
||||
inet.af/peercred v0.0.0-20210302202138-56e694897155
|
||||
rsc.io/goversion v1.2.0
|
||||
)
|
||||
|
||||
439
go.sum
439
go.sum
@@ -1,8 +1,34 @@
|
||||
bazil.org/fuse v0.0.0-20160811212531-371fbbdaa898/go.mod h1:Xbm+BRKSBEpa4q4hTSxohYNQpsxXPbPry4JJWOB3LB8=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
|
||||
cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
|
||||
cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
|
||||
cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
|
||||
cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
|
||||
cloud.google.com/go v0.52.1-0.20200122224058-0482b626c726/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
|
||||
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
|
||||
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
|
||||
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
|
||||
cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
|
||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||
github.com/Azure/go-autorest/autorest v0.9.0/go.mod h1:xyHB1BMZT0cuDHU7I0+g046+BFDTQ8rEZB0s4Yfa6bI=
|
||||
github.com/Azure/go-autorest/autorest/adal v0.5.0/go.mod h1:8Z9fGy2MpX0PvDjB1pEgQTmVqjGhiHBW7RJJEciWzS0=
|
||||
github.com/Azure/go-autorest/autorest/date v0.1.0/go.mod h1:plvfp3oPSKwf2DNjlBjWF/7vwR+cUD/ELuzDCXwHUVA=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.1.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
||||
github.com/Azure/go-autorest/autorest/mocks v0.2.0/go.mod h1:OTyCOPRA2IgIlWxVYxBee2F5Gr4kF2zd2J5cFRaIDN0=
|
||||
github.com/Azure/go-autorest/logger v0.1.0/go.mod h1:oExouG+K6PryycPJfVSxi/koC6LSNgds39diKLz7Vrc=
|
||||
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/Masterminds/semver/v3 v3.0.3 h1:znjIyLfpXEDQjOIEWh+ehwpTU14UzUPub3c3sm36u14=
|
||||
github.com/Masterminds/semver/v3 v3.0.3/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/Microsoft/go-winio v0.4.15-0.20200908182639-5b44b70ab3ab/go.mod h1:tTuCMEN+UleMWgg9dVx4Hu52b1bJo+59jBh3ajtinzw=
|
||||
github.com/Microsoft/hcsshim v0.8.6/go.mod h1:Op3hHsoHPAvb6lceZHDtd9OkTew38wNoXnJs8iY7rUg=
|
||||
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
||||
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
@@ -18,185 +44,395 @@ github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e h1:hHg27A0RSSp2Om9lubZpiMgVbvn39bsUmW9U5h0twqc=
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A=
|
||||
github.com/cenkalti/backoff v1.1.1-0.20190506075156-2146c9339422/go.mod h1:b6Nc7NRH5C4aCISLry0tLnTjcuTEvoiqcWDdsU0sOGM=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cilium/ebpf v0.0.0-20200110133405-4032b1d8aae3/go.mod h1:MA5e5Lr8slmEg9bt0VpxxWqJlO4iwu3FBdHUzV7wQVg=
|
||||
github.com/cilium/ebpf v0.2.0/go.mod h1:To2CFviqOWL/M0gIMsvSMlqe7em/l1ALkX1PyjrX2Qs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/containerd/cgroups v0.0.0-20201119153540-4cbc285b3327/go.mod h1:ZJeTFisyysqgcCdecO57Dj79RfL0LNeGiFUqLYQRYLE=
|
||||
github.com/containerd/console v0.0.0-20191206165004-02ecf6a7291e/go.mod h1:8Pf4gM6VEbTNRIT26AyyU7hxdQU3MvAvxVI0sc00XBE=
|
||||
github.com/containerd/containerd v1.3.9/go.mod h1:bC6axHOhabU15QhwfG7w5PipXdVtMXFTttgp+kVtyUA=
|
||||
github.com/containerd/continuity v0.0.0-20200928162600-f2cc35102c2a/go.mod h1:W0qIOTD7mp2He++YVq+kgfXezRYqzP1uDuMVH1bITDY=
|
||||
github.com/containerd/fifo v0.0.0-20191213151349-ff969a566b00/go.mod h1:jPQ2IAeZRCYxpS/Cm1495vGFww6ecHmMk1YJH2Q5ln0=
|
||||
github.com/containerd/go-runc v0.0.0-20200220073739-7016d3ce2328/go.mod h1:PpyHrqVs8FTi9vpyHwPwiNEGaACDxT/N/pLcvMSRA9g=
|
||||
github.com/containerd/ttrpc v0.0.0-20200121165050-0be804eadb15/go.mod h1:UAxOpgT9ziI0gJrmKvgcZivgxOp8iFPSk8httJEt98Y=
|
||||
github.com/containerd/typeurl v0.0.0-20200205145503-b45ef1f1f737/go.mod h1:TB1hUtrpaiO88KEK56ijojHS1+NeF0izUACaJW2mdXg=
|
||||
github.com/coreos/go-iptables v0.4.5 h1:DpHb9vJrZQEFMcVLFKAAGMUVX0XoRC0ptCthinRYm38=
|
||||
github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU=
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
||||
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
|
||||
github.com/docker/docker v1.4.2-0.20191028175130-9e7d5ac5ea55/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.3.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM=
|
||||
github.com/dpjacques/clockwork v0.1.1-0.20200827220843-c1f524b839be/go.mod h1:D8mP2A8vVT2GkXqPorSBmhnshhkFBYgzhA90KmJt25Y=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20201127111758-49e582c6c23d/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
|
||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas=
|
||||
github.com/go-multierror/multierror v1.0.2 h1:AwsKbEXkmf49ajdFJgcFXqSG0aLo0HEyAE9zk9JguJo=
|
||||
github.com/go-multierror/multierror v1.0.2/go.mod h1:U7SZR/D9jHgt2nkSj8XcbCWdmVM2igraCHQ3HC1HiKY=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
|
||||
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
|
||||
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
|
||||
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
|
||||
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e h1:BWhy2j3IXJhjCbC68FptL43tDKIq8FladmaTs3Xs7Z8=
|
||||
github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4=
|
||||
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
|
||||
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY=
|
||||
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
|
||||
github.com/gofrs/flock v0.6.1-0.20180915234121-886344bea079/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
|
||||
github.com/gogo/googleapis v1.4.0/go.mod h1:5YRNX2z1oM5gXdAkurHa942MDgEJyk02w4OecKY87+c=
|
||||
github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
|
||||
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo=
|
||||
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.3-0.20201020212313-ab46b8bd0abd/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-github/v28 v28.1.2-0.20191108005307-e555eab49ce8/go.mod h1:g82e6OHbJ0WYrYeOrid1MMfHAtqjxBz+N74tfAt9KrQ=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0 h1:BW6OvS3kpT5UEPbCZ+KyX/OB4Ks9/MNMhWjqPPkZxsE=
|
||||
github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg=
|
||||
github.com/google/subcommands v1.0.2-0.20190508160503-636abe8753b8/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
github.com/gophercloud/gophercloud v0.1.0/go.mod h1:vxM41WHh5uqHVBMZHzuwNOHh8XEoIEcSTewFxm1c5g8=
|
||||
github.com/goreleaser/nfpm v1.1.10 h1:0nwzKUJTcygNxTzVKq2Dh9wpVP1W2biUH6SNKmoxR3w=
|
||||
github.com/goreleaser/nfpm v1.1.10/go.mod h1:oOcoGRVwvKIODz57NUfiRwFWGfn00NXdgnn6MrYtO5k=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
|
||||
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA=
|
||||
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4 h1:nwOc1YaOrYJ37sEBrtWZrdqzK22hiJs3GpDmP3sR2Yw=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391 h1:Dqu/4JhMV1vpXHDjzQCuDCEsjNi0xfuSmQlMOyqayKA=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b h1:c3NTyLNozICy8B4mlMXemD3z/gXgQzVXZS/HqT+i3do=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=
|
||||
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I=
|
||||
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.4-0.20190131011033-7dc38fb350b1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lxn/walk v0.0.0-20191128110447-55ccb3a9f5c1/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/walk v0.0.0-20201110160827-18ea5e372cdb/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
github.com/lxn/win v0.0.0-20191128105842-2da648fda5b4/go.mod h1:ouWl4wViUNh8tPSIwxTVMuS014WakR1hqvBc2I0bMoA=
|
||||
github.com/lxn/win v0.0.0-20201111105847-2a20daff6a55/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0=
|
||||
github.com/mattn/go-zglob v0.0.1 h1:xsEx/XUoVlI6yXjqBK062zYhRTZltCNmYPx6v+8DNaY=
|
||||
github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
|
||||
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY=
|
||||
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
|
||||
github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0=
|
||||
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
|
||||
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
|
||||
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
|
||||
github.com/mdlayher/netlink v1.1.0 h1:mpdLgm+brq10nI9zM1BpX1kpDbh3NLl3RSnVq6ZSkfg=
|
||||
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
|
||||
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
|
||||
github.com/mdlayher/netlink v1.2.0 h1:zPolhRjfuabdf8ofZsl56eoU+92cvSlAn13lw4veCZ0=
|
||||
github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=
|
||||
github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
|
||||
github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
|
||||
github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys=
|
||||
github.com/mdlayher/netlink v1.3.2 h1:fMZOU2/M7PRMzGM3br5l1N2fu6bPSHtRytmQ338a9iA=
|
||||
github.com/mdlayher/netlink v1.3.2/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
|
||||
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a h1:wMv2mvcHRH4jqIxaVL5t6gSq1hjPiaWH7TOcA0Z+uNo=
|
||||
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a/go.mod h1:HtjVsQfsrBm1GDcDTUFn4ZXhftxTwO/hxrvEiRc61U4=
|
||||
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
|
||||
github.com/miekg/dns v1.1.30/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mohae/deepcopy v0.0.0-20170308212314-bb9b5e7adda9/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
|
||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
|
||||
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||
github.com/opencontainers/runtime-spec v1.0.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/runtime-spec v1.0.2-0.20181111125026-1722abf79c2f/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3 h1:YtFkrqsMEj7YqpIhRteVxJxCeC3jJBieuLr0d4C4rSA=
|
||||
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
||||
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
|
||||
github.com/peterbourgon/ff/v2 v2.0.0 h1:lx0oYI5qr/FU1xnpNhQ+EZM04gKgn46jyYvGEEqBBbY=
|
||||
github.com/peterbourgon/ff/v2 v2.0.0/go.mod h1:xjwr+t+SjWm4L46fcj/D+Ap+6ME7+HqFzaP22pP5Ggk=
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 h1:+/+DxvQaYifJ+grD4klzrS5y+KJXldn/2YTl5JG+vZ8=
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
|
||||
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/procfs v0.0.0-20190522114515-bc1a522cf7b1/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b h1:+gCnWOZV8Z/8jehJ2CdqB47Z3S+SREmQcuXkRFLNsiI=
|
||||
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
|
||||
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/tailscale/depaware v0.0.0-20201003033024-5d95aab075be/go.mod h1:jissDaJNHiyV2tFdr3QyNEfsZrax/i2yQiSO+CljThI=
|
||||
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 h1:lK99QQdH3yBWY6aGilF+IRlQIdmhzLrsEmF6JgN+Ryw=
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201220011020-db78fad0bebf h1:HuBwLWbDNIh/G72KSImSEx+dnd7FPGFI1e60LMJtLjU=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201220011020-db78fad0bebf/go.mod h1:9PbAnF5CAklkURoO0uQhm+YUjDmm9T9oCyTGlCHuTPQ=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201228234719-da0d2727455d h1:ha3qx0YBsEYM1VpLoAxVyLsz74H2a/Kv/id+2Bo/WLU=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20201228234719-da0d2727455d/go.mod h1:FEGDKc5yHNWtTS5ugWnHMNF0d9LlaHv/zQwOrVogo2U=
|
||||
github.com/tailscale/wireguard-go v0.0.20201119-0.20201228205120-066446d1733a h1:RUJeuZlAm1DT6Mhk9UTsaHrDeDZhPrbKfNsaEtKF6+0=
|
||||
github.com/tailscale/wireguard-go v0.0.20201119-0.20201228205120-066446d1733a/go.mod h1:UIAx57STfAZOrNVj8QGP2zG3ovWPMTD4DDubFHqMlYI=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210210202228-3cc76ed5f222 h1:VzTS7LIwCH8jlxwrZguU0TsCLV/MDOunoNIDJdFajyM=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210210202228-3cc76ed5f222/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924=
|
||||
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
|
||||
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
|
||||
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
|
||||
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
|
||||
github.com/ulikunitz/xz v0.5.6 h1:jGHAfXawEGZQ3blwU5wnWKQJvAraT7Ftq9EXjnXYgt8=
|
||||
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
|
||||
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
|
||||
github.com/vishvananda/netlink v1.0.1-0.20190930145447-2ec5bdc52b86/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=
|
||||
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go4.org/intern v0.0.0-20201223054237-ef8cbcb8edd7 h1:yeDrXaQ3VRXbTN7lHj70DxW4LdPow83MVwPPRjpP70U=
|
||||
go4.org/intern v0.0.0-20201223054237-ef8cbcb8edd7/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
|
||||
go4.org/intern v0.0.0-20201223061701-969c7e87e7cb h1:yuqO0E4bHRsTPUocDpRKXfLE40lwWplVxENQ2WOV7Gc=
|
||||
go4.org/intern v0.0.0-20201223061701-969c7e87e7cb/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
|
||||
go4.org/mem v0.0.0-20200706164138-185c595c3ecc/go.mod h1:NEYvpHWemiG/E5UWfaN5QAIGZeT1sa0Z2UNk6oeMb/k=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
|
||||
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
|
||||
go4.org/intern v0.0.0-20210101010959-7cab76ca296a h1:28p852HIWWaOS019DYK/A3yTmpm1HJaUce63pvll4C8=
|
||||
go4.org/intern v0.0.0-20210101010959-7cab76ca296a/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
|
||||
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg=
|
||||
go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174 h1:vSug/WNOi2+4jrKdivxayTN/zd8EA1UrStjpWvvo1jk=
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e h1:ExUmGi0ZsQmiVo9giDQqXkr7vreeXPMkOGIusfsfbzI=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 h1:xYJJ3S178yv++9zXV/hnr29plCAGO9vAFG9dorqaFQc=
|
||||
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
|
||||
golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
|
||||
golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
|
||||
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8=
|
||||
golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY=
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d h1:1aflnvSoWWLI2k/dMUAl5lvU1YO4Mb4hz0gh+1rjcxU=
|
||||
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
|
||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190405154228-4b34438f7a67/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191022100944-742c48ecaeb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200120151820-655fe14d7479/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200812155832-6a926be9bd1d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -204,27 +440,55 @@ golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742 h1:+CBz4km/0KPU3RGTwARGh/noP3bEwtHcq+0YcBQM2JQ=
|
||||
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210105210732-16f7687f5001/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210216224549-f992740a1bac/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 h1:SgQ6LNaYJU0JIuEHv9+s6EbhSCwYeAf5Yvj6lpYlqAE=
|
||||
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b h1:kHlr0tATeLRMEiZJu5CknOw/E8V6h69sXXQFGoPtjcc=
|
||||
golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b h1:a0ErnNnPKmhDyIXQvdZr+Lq8dc8xpMeqkF8y5PgQU4Q=
|
||||
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
|
||||
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
|
||||
golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200609164405-eb789aa7ce50/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20201001230009-b5b87423c93b/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201002184944-ecd9fd270d5d/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201021000207-d49c4edd7d96/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU=
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58 h1:1Bs6RVeBFtLZ8Yi1Hk07DiOqzvwLD/4hln4iahvFlag=
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -232,36 +496,91 @@ 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.20200321-0.20200715051853-507f148e1c42/go.mod h1:GJvYs5O24/ASlwPiRklVnjMx2xQzrOic0DuU6GvYJL4=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20201111175144-60b3766b89b9 h1:qowcZ56hhpeoESmWzI4Exhx4Y78TpCyXUJur4/c0CoE=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20201111175144-60b3766b89b9/go.mod h1:LMeNfjlcPZTrBC1juwgbQyA4Zy2XVcsrdO/fIJxwyuA=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201004085714-dd60d0447f81/go.mod h1:GaK5zcgr5XE98WaRzIDilumDBp5/yP8j2kG/LCDnvAM=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8 h1:nlXPqGA98n+qcq1pwZ28KjM5EsFQvamKS00A+VUeVjs=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8/go.mod h1:psva4yDnAHLuh7lUzOK7J7bLYxNFfo0iKWz+mi9gzkA=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
|
||||
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
|
||||
google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
|
||||
google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpCM=
|
||||
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
|
||||
google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
|
||||
google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/genproto v0.0.0-20200117163144-32f20d992d24/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
|
||||
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.29.0/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.1-0.20201020201750-d3470999428b h1:jEdfCm+8YTWSYgU4L7Nq0jjU+q9RxIhi0cXLTY+Ih3A=
|
||||
google.golang.org/protobuf v1.25.1-0.20201020201750-d3470999428b/go.mod h1:hFxJC2f0epmp1elRCiEGJTKAWbwxZ2nvqZdHl3FQXCY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
gvisor.dev/gvisor v0.0.0-20210111185822-3ff3110fcdd6 h1:H5EvGkFG+pgAAbZMV8Me3Gy+HUYdaDcGXKWWixZ0EE8=
|
||||
gvisor.dev/gvisor v0.0.0-20210111185822-3ff3110fcdd6/go.mod h1:5DEMKRjYDiM24fvDUWPjBpABm9ROMcv/kEcox3fHtm0=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.1.0 h1:AWNL1W1i7f0wNZ8VwOKNJ0sliKvOF/adn0EHenfUh+c=
|
||||
honnef.co/go/tools v0.1.0/go.mod h1:XtegFAyX/PfluP4921rXU5IkjkqBCDnUq4W8VCIoKvM=
|
||||
inet.af/netaddr v0.0.0-20200810144936-56928fe48a98/go.mod h1:qqYzz/2whtrbWJvt+DNWQyvekNN4ePQZcg2xc2/Yjww=
|
||||
inet.af/netaddr v0.0.0-20201218162718-658fec415e52/go.mod h1:qqYzz/2whtrbWJvt+DNWQyvekNN4ePQZcg2xc2/Yjww=
|
||||
inet.af/netaddr v0.0.0-20201223185330-97d366981fac h1:aqMW8vft7VmOIhtQhsTWhAuZzOBGYBv+Otyvwj+VGSU=
|
||||
inet.af/netaddr v0.0.0-20201223185330-97d366981fac/go.mod h1:9NdhtHLglxJliAZB6aC5ws3mfnUArdAzHG/iJq7cB/o=
|
||||
inet.af/netaddr v0.0.0-20201224214825-a55841caa437 h1:Li2QBwaT/hU3wE7GdyoqaX+TzIlI+V1zs/CuWrjX8e4=
|
||||
inet.af/netaddr v0.0.0-20201224214825-a55841caa437/go.mod h1:9NdhtHLglxJliAZB6aC5ws3mfnUArdAzHG/iJq7cB/o=
|
||||
inet.af/netaddr v0.0.0-20201226233944-2d1876c01610 h1:9Nnw3NS9SL4SlFtBWSdv7onMbdY+B8nflRNZvhgxuMY=
|
||||
inet.af/netaddr v0.0.0-20201226233944-2d1876c01610/go.mod h1:9NdhtHLglxJliAZB6aC5ws3mfnUArdAzHG/iJq7cB/o=
|
||||
inet.af/netaddr v0.0.0-20201228234250-33d0a924ebbf h1:0eHZ8v6j5wIiOVyoYPd70ueZ/RPEQtRlzi60uneDbRU=
|
||||
inet.af/netaddr v0.0.0-20201228234250-33d0a924ebbf/go.mod h1:9NdhtHLglxJliAZB6aC5ws3mfnUArdAzHG/iJq7cB/o=
|
||||
inet.af/netaddr v0.0.0-20201231012616-c5dc91d2a016 h1:CEeeAJW60aRKE6gGJC5krs2xC/uM2l8SasvgeDXFN5Q=
|
||||
inet.af/netaddr v0.0.0-20201231012616-c5dc91d2a016/go.mod h1:lbePDLSB5c45kkUmF7ETNE5X9z/yuQvWJIv1hhb5rFI=
|
||||
inet.af/netaddr v0.0.0-20210105212526-648fbc18a69d h1:6f0242aW/6x2enQBOSKgDS8KQNw6Tp7IVR8eG3x0Jc8=
|
||||
inet.af/netaddr v0.0.0-20210105212526-648fbc18a69d/go.mod h1:jPZo7Jy4nke2cCgISa4fKJKa5T7+EO8k5fWwWghzneg=
|
||||
inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44 h1:p7fX77zWzZMuNdJUhniBsmN1OvFOrW9SOtvgnzqUZX4=
|
||||
inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44/go.mod h1:I2i9ONCXRZDnG1+7O8fSuYzjcPxHQXrIfzD/IkR87x4=
|
||||
inet.af/peercred v0.0.0-20210216231719-993aa01eacaa h1:6qseJO2iNDHl+MLL2BkO5oURJR4A9pLmRz11Yf7KdGM=
|
||||
inet.af/peercred v0.0.0-20210216231719-993aa01eacaa/go.mod h1:VZeNdG7cRIUqKl9DWoFX86AHyfYwdb4RextAw1CAEO4=
|
||||
inet.af/peercred v0.0.0-20210302202138-56e694897155 h1:KojYNEYqDkZ2O3LdyTstR1l13L3ePKTIEM2h7ONkfkE=
|
||||
inet.af/peercred v0.0.0-20210302202138-56e694897155/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU=
|
||||
k8s.io/api v0.16.13/go.mod h1:QWu8UWSTiuQZMMeYjwLs6ILu5O74qKSJ0c+4vrchDxs=
|
||||
k8s.io/apimachinery v0.16.13/go.mod h1:4HMHS3mDHtVttspuuhrJ1GGr/0S9B6iWYWZ57KnnZqQ=
|
||||
k8s.io/apimachinery v0.16.14-rc.0/go.mod h1:4HMHS3mDHtVttspuuhrJ1GGr/0S9B6iWYWZ57KnnZqQ=
|
||||
k8s.io/client-go v0.16.13/go.mod h1:UKvVT4cajC2iN7DCjLgT0KVY/cbY6DGdUCyRiIfws5M=
|
||||
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
|
||||
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I=
|
||||
k8s.io/kube-openapi v0.0.0-20200410163147-594e756bea31/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E=
|
||||
k8s.io/utils v0.0.0-20190801114015-581e00157fb1/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w=
|
||||
rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=
|
||||
tailscale.com v1.2.10/go.mod h1:JEJiCce3MHtPCTdX2ahLc4tcnxZ7b5etish1Yt0B6+w=
|
||||
sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
|
||||
71
health/health.go
Normal file
71
health/health.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// 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 health is a registry for other packages to report & check
|
||||
// overall health status of the node.
|
||||
package health
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
m = map[string]error{} // error key => err (or nil for no error)
|
||||
watchers = map[*watchHandle]func(string, error){} // opt func to run if error state changes
|
||||
)
|
||||
|
||||
type watchHandle byte
|
||||
|
||||
// RegisterWatcher adds a function that will be called if an
|
||||
// error changes state either to unhealthy or from unhealthy. It is
|
||||
// not called on transition from unknown to healthy. It must be non-nil
|
||||
// and is run in its own goroutine. The returned func unregisters it.
|
||||
func RegisterWatcher(cb func(errKey string, err error)) (unregister func()) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
handle := new(watchHandle)
|
||||
watchers[handle] = cb
|
||||
return func() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
delete(watchers, handle)
|
||||
}
|
||||
}
|
||||
|
||||
// SetRouter sets the state of the wgengine/router.Router.
|
||||
func SetRouterHealth(err error) { set("router", err) }
|
||||
|
||||
// RouterHealth returns the wgengine/router.Router error state.
|
||||
func RouterHealth() error { return get("router") }
|
||||
|
||||
func get(key string) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return m[key]
|
||||
}
|
||||
|
||||
func set(key string, err error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
old, ok := m[key]
|
||||
if !ok && err == nil {
|
||||
// Initial happy path.
|
||||
m[key] = nil
|
||||
return
|
||||
}
|
||||
if ok && (old == nil) == (err == nil) {
|
||||
// No change in overall error status (nil-vs-not), so
|
||||
// don't run callbacks, but exact error might've
|
||||
// changed, so note it.
|
||||
if err != nil {
|
||||
m[key] = err
|
||||
}
|
||||
return
|
||||
}
|
||||
m[key] = err
|
||||
for _, cb := range watchers {
|
||||
go cb(key, err)
|
||||
}
|
||||
}
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/tailscale/wireguard-go/wgcfg"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/router/dns"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
func TestDeepPrint(t *testing.T) {
|
||||
@@ -41,12 +41,7 @@ func getVal() []interface{} {
|
||||
ListenPort: 5,
|
||||
Peers: []wgcfg.Peer{
|
||||
{
|
||||
Endpoints: []wgcfg.Endpoint{
|
||||
{
|
||||
Host: "foo",
|
||||
Port: 5,
|
||||
},
|
||||
},
|
||||
Endpoints: "foo:5",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -9,12 +9,11 @@ import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/structs"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
type State int
|
||||
@@ -46,10 +45,10 @@ func (s State) String() string {
|
||||
|
||||
// EngineStatus contains WireGuard engine stats.
|
||||
type EngineStatus struct {
|
||||
RBytes, WBytes wgengine.ByteCount
|
||||
RBytes, WBytes int64
|
||||
NumLive int
|
||||
LiveDERPs int // number of active DERP connections
|
||||
LivePeers map[tailcfg.NodeKey]wgengine.PeerStatus
|
||||
LivePeers map[tailcfg.NodeKey]ipnstate.PeerStatusLite
|
||||
}
|
||||
|
||||
// Notify is a communication from a backend (e.g. tailscaled) to a frontend
|
||||
@@ -59,16 +58,16 @@ type EngineStatus struct {
|
||||
// They are JSON-encoded on the wire, despite the lack of struct tags.
|
||||
type Notify struct {
|
||||
_ structs.Incomparable
|
||||
Version string // version number of IPN backend
|
||||
ErrMessage *string // critical error message, if any; for InUseOtherUser, the details
|
||||
LoginFinished *empty.Message // event: non-nil when login process succeeded
|
||||
State *State // current IPN state has changed
|
||||
Prefs *Prefs // preferences were changed
|
||||
NetMap *controlclient.NetworkMap // new netmap received
|
||||
Engine *EngineStatus // wireguard engine stats
|
||||
Status *ipnstate.Status // full status
|
||||
BrowseToURL *string // UI should open a browser right now
|
||||
BackendLogID *string // public logtail id used by backend
|
||||
Version string // version number of IPN backend
|
||||
ErrMessage *string // critical error message, if any; for InUseOtherUser, the details
|
||||
LoginFinished *empty.Message // event: non-nil when login process succeeded
|
||||
State *State // current IPN state has changed
|
||||
Prefs *Prefs // preferences were changed
|
||||
NetMap *netmap.NetworkMap // new netmap received
|
||||
Engine *EngineStatus // wireguard engine stats
|
||||
Status *ipnstate.Status // full status
|
||||
BrowseToURL *string // UI should open a browser right now
|
||||
BackendLogID *string // public logtail id used by backend
|
||||
PingResult *ipnstate.PingResult
|
||||
|
||||
// LocalTCPPort, if non-nil, informs the UI frontend which
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
|
||||
type FakeBackend struct {
|
||||
@@ -54,7 +54,7 @@ func (b *FakeBackend) login() {
|
||||
b.newState(NeedsMachineAuth)
|
||||
b.newState(Stopped)
|
||||
// TODO(apenwarr): Fill in a more interesting netmap here.
|
||||
b.notify(Notify{NetMap: &controlclient.NetworkMap{}})
|
||||
b.notify(Notify{NetMap: &netmap.NetworkMap{}})
|
||||
b.newState(Starting)
|
||||
// TODO(apenwarr): Fill in a more interesting status.
|
||||
b.notify(Notify{Engine: &EngineStatus{}})
|
||||
@@ -92,7 +92,7 @@ func (b *FakeBackend) RequestStatus() {
|
||||
}
|
||||
|
||||
func (b *FakeBackend) FakeExpireAfter(x time.Duration) {
|
||||
b.notify(Notify{NetMap: &controlclient.NetworkMap{}})
|
||||
b.notify(Notify{NetMap: &netmap.NetworkMap{}})
|
||||
}
|
||||
|
||||
func (b *FakeBackend) Ping(ip string) {
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
|
||||
"golang.org/x/oauth2"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
|
||||
type Handle struct {
|
||||
@@ -22,7 +22,7 @@ type Handle struct {
|
||||
|
||||
// Mutex protects everything below
|
||||
mu sync.Mutex
|
||||
netmapCache *controlclient.NetworkMap
|
||||
netmapCache *netmap.NetworkMap
|
||||
engineStatusCache EngineStatus
|
||||
stateCache State
|
||||
prefsCache *Prefs
|
||||
@@ -129,7 +129,7 @@ func (h *Handle) LocalAddrs() []netaddr.IPPrefix {
|
||||
return []netaddr.IPPrefix{}
|
||||
}
|
||||
|
||||
func (h *Handle) NetMap() *controlclient.NetworkMap {
|
||||
func (h *Handle) NetMap() *netmap.NetworkMap {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
267
ipn/ipnlocal/local_test.go
Normal file
267
ipn/ipnlocal/local_test.go
Normal file
@@ -0,0 +1,267 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
func TestNetworkMapCompare(t *testing.T) {
|
||||
prefix1, err := netaddr.ParseIPPrefix("192.168.0.0/24")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
node1 := &tailcfg.Node{Addresses: []netaddr.IPPrefix{prefix1}}
|
||||
|
||||
prefix2, err := netaddr.ParseIPPrefix("10.0.0.0/8")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
node2 := &tailcfg.Node{Addresses: []netaddr.IPPrefix{prefix2}}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
a, b *netmap.NetworkMap
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
"both nil",
|
||||
nil,
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"b nil",
|
||||
&netmap.NetworkMap{},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"a nil",
|
||||
nil,
|
||||
&netmap.NetworkMap{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"both default",
|
||||
&netmap.NetworkMap{},
|
||||
&netmap.NetworkMap{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"names identical",
|
||||
&netmap.NetworkMap{Name: "map1"},
|
||||
&netmap.NetworkMap{Name: "map1"},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"names differ",
|
||||
&netmap.NetworkMap{Name: "map1"},
|
||||
&netmap.NetworkMap{Name: "map2"},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Peers identical",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Peer list length",
|
||||
// length of Peers list differs
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Node names identical",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Node names differ",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "B"}}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Node lists identical",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Node lists differ",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node2}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"Node Users differ",
|
||||
// User field is not checked.
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 0}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 1}}},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := dnsMapsEqual(tt.a, tt.b)
|
||||
if got != tt.want {
|
||||
t.Errorf("%s: Equal = %v; want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShrinkDefaultRoute(t *testing.T) {
|
||||
tests := []struct {
|
||||
route string
|
||||
in []string
|
||||
out []string
|
||||
}{
|
||||
{
|
||||
route: "0.0.0.0/0",
|
||||
in: []string{"1.2.3.4", "25.0.0.1"},
|
||||
out: []string{
|
||||
"10.0.0.1",
|
||||
"10.255.255.255",
|
||||
"192.168.0.1",
|
||||
"192.168.255.255",
|
||||
"172.16.0.1",
|
||||
"172.31.255.255",
|
||||
"100.101.102.103",
|
||||
// Some random IPv6 stuff that shouldn't be in a v4
|
||||
// default route.
|
||||
"fe80::",
|
||||
"2601::1",
|
||||
},
|
||||
},
|
||||
{
|
||||
route: "::/0",
|
||||
in: []string{"::1", "2601::1"},
|
||||
out: []string{
|
||||
"fe80::1",
|
||||
tsaddr.TailscaleULARange().IP.String(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
def := netaddr.MustParseIPPrefix(test.route)
|
||||
got, err := shrinkDefaultRoute(def)
|
||||
if err != nil {
|
||||
t.Fatalf("shrinkDefaultRoute(%q): %v", test.route, err)
|
||||
}
|
||||
for _, ip := range test.in {
|
||||
if !got.Contains(netaddr.MustParseIP(ip)) {
|
||||
t.Errorf("shrink(%q).Contains(%v) = false, want true", test.route, ip)
|
||||
}
|
||||
}
|
||||
for _, ip := range test.out {
|
||||
if got.Contains(netaddr.MustParseIP(ip)) {
|
||||
t.Errorf("shrink(%q).Contains(%v) = true, want false", test.route, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerRoutes(t *testing.T) {
|
||||
pp := netaddr.MustParseIPPrefix
|
||||
tests := []struct {
|
||||
name string
|
||||
peers []wgcfg.Peer
|
||||
want []netaddr.IPPrefix
|
||||
}{
|
||||
{
|
||||
name: "small_v4",
|
||||
peers: []wgcfg.Peer{
|
||||
{
|
||||
AllowedIPs: []netaddr.IPPrefix{
|
||||
pp("100.101.102.103/32"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netaddr.IPPrefix{
|
||||
pp("100.101.102.103/32"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "big_v4",
|
||||
peers: []wgcfg.Peer{
|
||||
{
|
||||
AllowedIPs: []netaddr.IPPrefix{
|
||||
pp("100.101.102.103/32"),
|
||||
pp("100.101.102.104/32"),
|
||||
pp("100.101.102.105/32"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netaddr.IPPrefix{
|
||||
pp("100.64.0.0/10"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "has_1_v6",
|
||||
peers: []wgcfg.Peer{
|
||||
{
|
||||
AllowedIPs: []netaddr.IPPrefix{
|
||||
pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b240/128"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netaddr.IPPrefix{
|
||||
pp("fd7a:115c:a1e0::/48"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "has_2_v6",
|
||||
peers: []wgcfg.Peer{
|
||||
{
|
||||
AllowedIPs: []netaddr.IPPrefix{
|
||||
pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b240/128"),
|
||||
pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b241/128"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netaddr.IPPrefix{
|
||||
pp("fd7a:115c:a1e0::/48"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "big_v4_big_v6",
|
||||
peers: []wgcfg.Peer{
|
||||
{
|
||||
AllowedIPs: []netaddr.IPPrefix{
|
||||
pp("100.101.102.103/32"),
|
||||
pp("100.101.102.104/32"),
|
||||
pp("100.101.102.105/32"),
|
||||
pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b240/128"),
|
||||
pp("fd7a:115c:a1e0:ab12:4843:cd96:6258:b241/128"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netaddr.IPPrefix{
|
||||
pp("fd7a:115c:a1e0::/48"),
|
||||
pp("100.64.0.0/10"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := peerRoutes(tt.peers, 2)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("got = %v; want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,18 +2,20 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipn
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
@@ -26,6 +28,7 @@ func TestLocalLogLines(t *testing.T) {
|
||||
"[v1] peer keys: %s",
|
||||
"[v1] v%v peers: %v",
|
||||
})
|
||||
defer logListen.Close()
|
||||
|
||||
logid := func(hex byte) logtail.PublicID {
|
||||
var ret logtail.PublicID
|
||||
@@ -37,9 +40,7 @@ func TestLocalLogLines(t *testing.T) {
|
||||
idA := logid(0xaa)
|
||||
|
||||
// set up a LocalBackend, super bare bones. No functional data.
|
||||
store := &MemoryStore{
|
||||
cache: make(map[StateKey][]byte),
|
||||
}
|
||||
store := &ipn.MemoryStore{}
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logListen.Logf, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -52,7 +53,7 @@ func TestLocalLogLines(t *testing.T) {
|
||||
defer lb.Shutdown()
|
||||
|
||||
// custom adjustments for required non-nil fields
|
||||
lb.prefs = NewPrefs()
|
||||
lb.prefs = ipn.NewPrefs()
|
||||
lb.hostinfo = &tailcfg.Hostinfo{}
|
||||
// hacky manual override of the usual log-on-change behaviour of keylogf
|
||||
lb.keyLogf = logListen.Logf
|
||||
@@ -66,8 +67,8 @@ func TestLocalLogLines(t *testing.T) {
|
||||
}
|
||||
|
||||
// log prefs line
|
||||
persist := &controlclient.Persist{}
|
||||
prefs := NewPrefs()
|
||||
persist := &persist.Persist{}
|
||||
prefs := ipn.NewPrefs()
|
||||
prefs.Persist = persist
|
||||
lb.SetPrefs(prefs)
|
||||
|
||||
@@ -75,7 +76,7 @@ func TestLocalLogLines(t *testing.T) {
|
||||
|
||||
// log peers, peer keys
|
||||
status := &wgengine.Status{
|
||||
Peers: []wgengine.PeerStatus{wgengine.PeerStatus{
|
||||
Peers: []ipnstate.PeerStatusLite{{
|
||||
TxBytes: 10,
|
||||
RxBytes: 10,
|
||||
LastHandshake: time.Now(),
|
||||
@@ -19,14 +19,20 @@ import (
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"inet.af/peercred"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/localapi"
|
||||
"tailscale.com/log/filelogger"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/netstat"
|
||||
@@ -91,7 +97,7 @@ type Options struct {
|
||||
// server is an IPN backend and its set of 0 or more active connections
|
||||
// talking to an IPN backend.
|
||||
type server struct {
|
||||
b *ipn.LocalBackend
|
||||
b *ipnlocal.LocalBackend
|
||||
logf logger.Logf
|
||||
// resetOnZero is whether to call bs.Reset on transition from
|
||||
// 1->0 connections. That is, this is whether the backend is
|
||||
@@ -111,21 +117,34 @@ type server struct {
|
||||
disconnectSub map[chan<- struct{}]struct{} // keys are subscribers of disconnects
|
||||
}
|
||||
|
||||
// connIdentity represents the owner of a localhost TCP connection.
|
||||
// connIdentity represents the owner of a localhost TCP or unix socket connection.
|
||||
type connIdentity struct {
|
||||
Unknown bool
|
||||
Pid int
|
||||
UserID string
|
||||
User *user.User
|
||||
Conn net.Conn
|
||||
NotWindows bool // runtime.GOOS != "windows"
|
||||
|
||||
// Fields used when NotWindows:
|
||||
IsUnixSock bool // Conn is a *net.UnixConn
|
||||
Creds *peercred.Creds // or nil
|
||||
|
||||
// Used on Windows:
|
||||
// TODO(bradfitz): merge these into the peercreds package and
|
||||
// use that for all.
|
||||
Pid int
|
||||
UserID string
|
||||
User *user.User
|
||||
}
|
||||
|
||||
// getConnIdentity returns the localhost TCP connection's identity information
|
||||
// (pid, userid, user). If it's not Windows (for now), it returns a nil error
|
||||
// and a ConnIdentity with Unknown set true. It's only an error if we expected
|
||||
// and a ConnIdentity with NotWindows set true. It's only an error if we expected
|
||||
// to be able to map it and couldn't.
|
||||
func (s *server) getConnIdentity(c net.Conn) (ci connIdentity, err error) {
|
||||
ci = connIdentity{Conn: c}
|
||||
if runtime.GOOS != "windows" { // for now; TODO: expand to other OSes
|
||||
return connIdentity{Unknown: true}, nil
|
||||
ci.NotWindows = true
|
||||
_, ci.IsUnixSock = c.(*net.UnixConn)
|
||||
ci.Creds, _ = peercred.Get(c)
|
||||
return ci, nil
|
||||
}
|
||||
la, err := netaddr.ParseIPPort(c.LocalAddr().String())
|
||||
if err != nil {
|
||||
@@ -216,13 +235,22 @@ func (s *server) blockWhileInUse(conn io.Reader, ci connIdentity) {
|
||||
}
|
||||
}
|
||||
|
||||
// bufferHasHTTPRequest reports whether br looks like it has an HTTP
|
||||
// request in it, without reading any bytes from it.
|
||||
func bufferHasHTTPRequest(br *bufio.Reader) bool {
|
||||
peek, _ := br.Peek(br.Buffered())
|
||||
return mem.HasPrefix(mem.B(peek), mem.S("GET ")) ||
|
||||
mem.HasPrefix(mem.B(peek), mem.S("POST ")) ||
|
||||
mem.Contains(mem.B(peek), mem.S(" HTTP/"))
|
||||
}
|
||||
|
||||
func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
// First see if it's an HTTP request.
|
||||
br := bufio.NewReader(c)
|
||||
c.SetReadDeadline(time.Now().Add(time.Second))
|
||||
peek, _ := br.Peek(4)
|
||||
br.Peek(4)
|
||||
c.SetReadDeadline(time.Time{})
|
||||
isHTTPReq := string(peek) == "GET "
|
||||
isHTTPReq := bufferHasHTTPRequest(br)
|
||||
|
||||
ci, err := s.addConn(c, isHTTPReq)
|
||||
if err != nil {
|
||||
@@ -249,7 +277,7 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
s.b.SetCurrentUserID(ci.UserID)
|
||||
|
||||
if isHTTPReq {
|
||||
httpServer := http.Server{
|
||||
httpServer := &http.Server{
|
||||
// Localhost connections are cheap; so only do
|
||||
// keep-alives for a short period of time, as these
|
||||
// active connections lock the server into only serving
|
||||
@@ -268,6 +296,10 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
defer s.removeAndCloseConn(c)
|
||||
logf("[v1] incoming control connection")
|
||||
|
||||
if isReadonlyConn(ci, logf) {
|
||||
ctx = ipn.ReadonlyContextOf(ctx)
|
||||
}
|
||||
|
||||
for ctx.Err() == nil {
|
||||
msg, err := ipn.ReadMsg(br)
|
||||
if err != nil {
|
||||
@@ -279,7 +311,7 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
return
|
||||
}
|
||||
s.bsMu.Lock()
|
||||
if err := s.bs.GotCommandMsg(msg); err != nil {
|
||||
if err := s.bs.GotCommandMsg(ctx, msg); err != nil {
|
||||
logf("GotCommandMsg: %v", err)
|
||||
}
|
||||
gotQuit := s.bs.GotQuit
|
||||
@@ -290,6 +322,82 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
}
|
||||
}
|
||||
|
||||
func isReadonlyConn(ci connIdentity, logf logger.Logf) bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows doesn't need/use this mechanism, at least yet. It
|
||||
// has a different last-user-wins auth model.
|
||||
return false
|
||||
}
|
||||
const ro = true
|
||||
const rw = false
|
||||
if !safesocket.PlatformUsesPeerCreds() {
|
||||
return rw
|
||||
}
|
||||
creds := ci.Creds
|
||||
if creds == nil {
|
||||
logf("connection from unknown peer; read-only")
|
||||
return ro
|
||||
}
|
||||
uid, ok := creds.UserID()
|
||||
if !ok {
|
||||
logf("connection from peer with unknown userid; read-only")
|
||||
return ro
|
||||
}
|
||||
if uid == "0" {
|
||||
logf("connection from userid %v; root has access", uid)
|
||||
return rw
|
||||
}
|
||||
if selfUID := os.Getuid(); selfUID != 0 && uid == strconv.Itoa(selfUID) {
|
||||
logf("connection from userid %v; connection from non-root user matching daemon has access", uid)
|
||||
return rw
|
||||
}
|
||||
var adminGroupID string
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
adminGroupID = darwinAdminGroupID()
|
||||
default:
|
||||
logf("connection from userid %v; read-only", uid)
|
||||
return ro
|
||||
}
|
||||
if adminGroupID == "" {
|
||||
logf("connection from userid %v; no system admin group found, read-only", uid)
|
||||
return ro
|
||||
}
|
||||
u, err := user.LookupId(uid)
|
||||
if err != nil {
|
||||
logf("connection from userid %v; failed to look up user; read-only", uid)
|
||||
return ro
|
||||
}
|
||||
gids, err := u.GroupIds()
|
||||
if err != nil {
|
||||
logf("connection from userid %v; failed to look up groups; read-only", uid)
|
||||
return ro
|
||||
}
|
||||
for _, gid := range gids {
|
||||
if gid == adminGroupID {
|
||||
logf("connection from userid %v; is local admin, has access", uid)
|
||||
return rw
|
||||
}
|
||||
}
|
||||
logf("connection from userid %v; read-only", uid)
|
||||
return ro
|
||||
}
|
||||
|
||||
var darwinAdminGroupIDCache atomic.Value // of string
|
||||
|
||||
func darwinAdminGroupID() string {
|
||||
s, _ := darwinAdminGroupIDCache.Load().(string)
|
||||
if s != "" {
|
||||
return s
|
||||
}
|
||||
g, err := user.LookupGroup("admin")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
darwinAdminGroupIDCache.Store(g.Gid)
|
||||
return g.Gid
|
||||
}
|
||||
|
||||
// inUseOtherUserError is the error type for when the server is in use
|
||||
// by a different local user.
|
||||
type inUseOtherUserError struct{ error }
|
||||
@@ -322,6 +430,25 @@ func (s *server) checkConnIdentityLocked(ci connIdentity) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// localAPIPermissions returns the permissions for the given identity accessing
|
||||
// the Tailscale local daemon API.
|
||||
//
|
||||
// s.mu must not be held.
|
||||
func (s *server) localAPIPermissions(ci connIdentity) (read, write bool) {
|
||||
if runtime.GOOS == "windows" {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.checkConnIdentityLocked(ci) == nil {
|
||||
return true, true
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
if ci.IsUnixSock {
|
||||
return true, !isReadonlyConn(ci, logger.Discard)
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
|
||||
// registerDisconnectSub adds ch as a subscribe to connection disconnect
|
||||
// events. If add is false, the subscriber is removed.
|
||||
func (s *server) registerDisconnectSub(ch chan<- struct{}, add bool) {
|
||||
@@ -355,7 +482,7 @@ func (s *server) addConn(c net.Conn, isHTTP bool) (ci connIdentity, err error) {
|
||||
if doReset {
|
||||
s.logf("identity changed; resetting server")
|
||||
s.bsMu.Lock()
|
||||
s.bs.Reset()
|
||||
s.bs.Reset(context.TODO())
|
||||
s.bsMu.Unlock()
|
||||
}
|
||||
}()
|
||||
@@ -407,7 +534,7 @@ func (s *server) removeAndCloseConn(c net.Conn) {
|
||||
} else {
|
||||
s.logf("client disconnected; stopping server")
|
||||
s.bsMu.Lock()
|
||||
s.bs.Reset()
|
||||
s.bs.Reset(context.TODO())
|
||||
s.bsMu.Unlock()
|
||||
}
|
||||
}
|
||||
@@ -438,7 +565,7 @@ func (s *server) setServerModeUserLocked() {
|
||||
s.logf("ipnserver: [unexpected] now in server mode, but no connected client")
|
||||
return
|
||||
}
|
||||
if ci.Unknown {
|
||||
if ci.NotWindows {
|
||||
return
|
||||
}
|
||||
if ci.User != nil {
|
||||
@@ -499,41 +626,6 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
}()
|
||||
logf("Listening on %v", listen.Addr())
|
||||
|
||||
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
|
||||
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
|
||||
|
||||
eng, err := getEngine()
|
||||
if err != nil {
|
||||
logf("ipnserver: initial getEngine call: %v", err)
|
||||
for i := 1; ctx.Err() == nil; i++ {
|
||||
c, err := listen.Accept()
|
||||
if err != nil {
|
||||
logf("%d: Accept: %v", i, err)
|
||||
bo.BackOff(ctx, err)
|
||||
continue
|
||||
}
|
||||
logf("ipnserver: try%d: trying getEngine again...", i)
|
||||
eng, err = getEngine()
|
||||
if err == nil {
|
||||
logf("%d: GetEngine worked; exiting failure loop", i)
|
||||
unservedConn = c
|
||||
break
|
||||
}
|
||||
logf("ipnserver%d: getEngine failed again: %v", i, err)
|
||||
errMsg := err.Error()
|
||||
go func() {
|
||||
defer c.Close()
|
||||
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
|
||||
bs := ipn.NewBackendServer(logf, nil, serverToClient)
|
||||
bs.SendErrorMessage(errMsg)
|
||||
time.Sleep(time.Second)
|
||||
}()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var store ipn.StateStore
|
||||
if opts.StatePath != "" {
|
||||
store, err = ipn.NewFileStore(opts.StatePath)
|
||||
@@ -562,7 +654,83 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
store = &ipn.MemoryStore{}
|
||||
}
|
||||
|
||||
b, err := ipn.NewLocalBackend(logf, logid, store, eng)
|
||||
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
|
||||
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
|
||||
|
||||
eng, err := getEngine()
|
||||
if err != nil {
|
||||
logf("ipnserver: initial getEngine call: %v", err)
|
||||
|
||||
// Issue 1187: on Windows, in unattended mode,
|
||||
// sometimes we try 5 times and fail to create the
|
||||
// engine before the system's ready. Hack until the
|
||||
// bug if fixed properly: if we're running in
|
||||
// unattended mode on Windows, keep trying forever,
|
||||
// waiting for the machine to be ready (networking to
|
||||
// come up?) and then dial our own safesocket TCP
|
||||
// listener to wake up the usual mechanism that lets
|
||||
// us surface getEngine errors to UI clients. (We
|
||||
// don't want to just call getEngine in a loop without
|
||||
// the listener.Accept, as we do want to handle client
|
||||
// connections so we can tell them about errors)
|
||||
|
||||
bootRaceWaitForEngine, bootRaceWaitForEngineCancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
if runtime.GOOS == "windows" && opts.AutostartStateKey != "" {
|
||||
logf("ipnserver: in unattended mode, waiting for engine availability")
|
||||
getEngine = getEngineUntilItWorksWrapper(getEngine)
|
||||
// Wait for it to be ready.
|
||||
go func() {
|
||||
defer bootRaceWaitForEngineCancel()
|
||||
t0 := time.Now()
|
||||
for {
|
||||
time.Sleep(10 * time.Second)
|
||||
if _, err := getEngine(); err != nil {
|
||||
logf("ipnserver: unattended mode engine load: %v", err)
|
||||
continue
|
||||
}
|
||||
c, err := net.Dial("tcp", listen.Addr().String())
|
||||
logf("ipnserver: engine created after %v; waking up Accept: Dial error: %v", time.Since(t0).Round(time.Second), err)
|
||||
if err == nil {
|
||||
c.Close()
|
||||
}
|
||||
break
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
bootRaceWaitForEngineCancel()
|
||||
}
|
||||
|
||||
for i := 1; ctx.Err() == nil; i++ {
|
||||
c, err := listen.Accept()
|
||||
if err != nil {
|
||||
logf("%d: Accept: %v", i, err)
|
||||
bo.BackOff(ctx, err)
|
||||
continue
|
||||
}
|
||||
<-bootRaceWaitForEngine.Done()
|
||||
logf("ipnserver: try%d: trying getEngine again...", i)
|
||||
eng, err = getEngine()
|
||||
if err == nil {
|
||||
logf("%d: GetEngine worked; exiting failure loop", i)
|
||||
unservedConn = c
|
||||
break
|
||||
}
|
||||
logf("ipnserver%d: getEngine failed again: %v", i, err)
|
||||
errMsg := err.Error()
|
||||
go func() {
|
||||
defer c.Close()
|
||||
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
|
||||
bs := ipn.NewBackendServer(logf, nil, serverToClient)
|
||||
bs.SendErrorMessage(errMsg)
|
||||
time.Sleep(time.Second)
|
||||
}()
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
b, err := ipnlocal.NewLocalBackend(logf, logid, store, eng)
|
||||
if err != nil {
|
||||
return fmt.Errorf("NewLocalBackend: %v", err)
|
||||
}
|
||||
@@ -581,7 +749,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
server.bs = ipn.NewBackendServer(logf, b, server.writeToClients)
|
||||
|
||||
if opts.AutostartStateKey != "" {
|
||||
server.bs.GotCommand(&ipn.Command{
|
||||
server.bs.GotCommand(context.TODO(), &ipn.Command{
|
||||
Version: version.Long,
|
||||
Start: &ipn.StartArgs{
|
||||
Opts: ipn.Options{
|
||||
@@ -752,6 +920,27 @@ func FixedEngine(eng wgengine.Engine) func() (wgengine.Engine, error) {
|
||||
return func() (wgengine.Engine, error) { return eng, nil }
|
||||
}
|
||||
|
||||
// getEngineUntilItWorksWrapper returns a getEngine wrapper that does
|
||||
// not call getEngine concurrently and stops calling getEngine once
|
||||
// it's returned a working engine.
|
||||
func getEngineUntilItWorksWrapper(getEngine func() (wgengine.Engine, error)) func() (wgengine.Engine, error) {
|
||||
var mu sync.Mutex
|
||||
var engGood wgengine.Engine
|
||||
return func() (wgengine.Engine, error) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if engGood != nil {
|
||||
return engGood, nil
|
||||
}
|
||||
e, err := getEngine()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
engGood = e
|
||||
return e, nil
|
||||
}
|
||||
}
|
||||
|
||||
type dummyAddr string
|
||||
type oneConnListener struct {
|
||||
conn net.Conn
|
||||
@@ -793,8 +982,15 @@ func (psc *protoSwitchConn) Close() error {
|
||||
}
|
||||
|
||||
func (s *server) localhostHandler(ci connIdentity) http.Handler {
|
||||
lah := localapi.NewHandler(s.b)
|
||||
lah.PermitRead, lah.PermitWrite = s.localAPIPermissions(ci)
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if ci.Unknown {
|
||||
if strings.HasPrefix(r.URL.Path, "/localapi/") {
|
||||
lah.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
if ci.NotWindows {
|
||||
io.WriteString(w, "<html><title>Tailscale</title><body><h1>Tailscale</h1>This is the local Tailscale daemon.")
|
||||
return
|
||||
}
|
||||
@@ -802,7 +998,7 @@ func (s *server) localhostHandler(ci connIdentity) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func serveHTMLStatus(w http.ResponseWriter, b *ipn.LocalBackend) {
|
||||
func serveHTMLStatus(w http.ResponseWriter, b *ipnlocal.LocalBackend) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
st := b.Status()
|
||||
// TODO(bradfitz): add LogID and opts to st?
|
||||
|
||||
@@ -21,14 +21,23 @@ import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
// Status represents the entire state of the IPN network.
|
||||
type Status struct {
|
||||
BackendState string
|
||||
AuthURL string // current URL provided by control to authorize client
|
||||
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
||||
Self *PeerStatus
|
||||
|
||||
// MagicDNSSuffix is the network's MagicDNS suffix for nodes
|
||||
// in the network such as "userfoo.tailscale.net".
|
||||
// There are no surrounding dots.
|
||||
// MagicDNSSuffix should be populated regardless of whether a domain
|
||||
// has MagicDNS enabled.
|
||||
MagicDNSSuffix string
|
||||
|
||||
Peer map[key.Public]*PeerStatus
|
||||
User map[tailcfg.UserID]tailcfg.UserProfile
|
||||
}
|
||||
@@ -42,6 +51,12 @@ func (s *Status) Peers() []key.Public {
|
||||
return kk
|
||||
}
|
||||
|
||||
type PeerStatusLite struct {
|
||||
TxBytes, RxBytes int64
|
||||
LastHandshake time.Time
|
||||
NodeKey tailcfg.NodeKey
|
||||
}
|
||||
|
||||
type PeerStatus struct {
|
||||
PublicKey key.Public
|
||||
HostName string // HostInfo's Hostname (not a DNS name or necessarily unique)
|
||||
@@ -63,6 +78,7 @@ type PeerStatus struct {
|
||||
LastSeen time.Time // last seen to tailcontrol
|
||||
LastHandshake time.Time // with local wireguard
|
||||
KeepAlive bool
|
||||
ExitNode bool // true if this is the currently selected exit node.
|
||||
|
||||
// ShareeNode indicates this node exists in the netmap because
|
||||
// it's owned by a shared-to user and that node might connect
|
||||
@@ -83,14 +99,6 @@ type PeerStatus struct {
|
||||
InEngine bool
|
||||
}
|
||||
|
||||
// SimpleHostName returns a potentially simplified version of ps.HostName for display purposes.
|
||||
func (ps *PeerStatus) SimpleHostName() string {
|
||||
n := ps.HostName
|
||||
n = strings.TrimSuffix(n, ".local")
|
||||
n = strings.TrimSuffix(n, ".localdomain")
|
||||
return n
|
||||
}
|
||||
|
||||
type StatusBuilder struct {
|
||||
mu sync.Mutex
|
||||
locked bool
|
||||
@@ -103,6 +111,18 @@ func (sb *StatusBuilder) SetBackendState(v string) {
|
||||
sb.st.BackendState = v
|
||||
}
|
||||
|
||||
func (sb *StatusBuilder) SetAuthURL(v string) {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
sb.st.AuthURL = v
|
||||
}
|
||||
|
||||
func (sb *StatusBuilder) SetMagicDNSSuffix(v string) {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
sb.st.MagicDNSSuffix = v
|
||||
}
|
||||
|
||||
func (sb *StatusBuilder) Status() *Status {
|
||||
sb.mu.Lock()
|
||||
defer sb.mu.Unlock()
|
||||
@@ -224,6 +244,9 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
|
||||
if st.KeepAlive {
|
||||
e.KeepAlive = true
|
||||
}
|
||||
if st.ExitNode {
|
||||
e.ExitNode = true
|
||||
}
|
||||
if st.ShareeNode {
|
||||
e.ShareeNode = true
|
||||
}
|
||||
@@ -267,13 +290,22 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
|
||||
f("<p>Tailscale IP: %s", strings.Join(ips, ", "))
|
||||
|
||||
f("<table>\n<thead>\n")
|
||||
f("<tr><th>Peer</th><th>Node</th><th>Owner</th><th>Rx</th><th>Tx</th><th>Activity</th><th>Endpoints</th></tr>\n")
|
||||
f("<tr><th>Peer</th><th>OS</th><th>Node</th><th>Owner</th><th>Rx</th><th>Tx</th><th>Activity</th><th>Connection</th></tr>\n")
|
||||
f("</thead>\n<tbody>\n")
|
||||
|
||||
now := time.Now()
|
||||
|
||||
var peers []*PeerStatus
|
||||
for _, peer := range st.Peers() {
|
||||
ps := st.Peer[peer]
|
||||
if ps.ShareeNode {
|
||||
continue
|
||||
}
|
||||
peers = append(peers, ps)
|
||||
}
|
||||
SortPeers(peers)
|
||||
|
||||
for _, ps := range peers {
|
||||
var actAgo string
|
||||
if !ps.LastWrite.IsZero() {
|
||||
ago := now.Sub(ps.LastWrite)
|
||||
@@ -289,40 +321,41 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
|
||||
owner = owner[:i]
|
||||
}
|
||||
}
|
||||
f("<tr><td>%s</td><td>%s %s<br><span class=\"tailaddr\">%s</span></td><td class=\"acenter owner\">%s</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td>",
|
||||
peer.ShortString(),
|
||||
html.EscapeString(ps.SimpleHostName()),
|
||||
|
||||
hostName := dnsname.SanitizeHostname(ps.HostName)
|
||||
dnsName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
|
||||
if strings.EqualFold(dnsName, hostName) || ps.UserID != st.Self.UserID {
|
||||
hostName = ""
|
||||
}
|
||||
var hostNameHTML string
|
||||
if hostName != "" {
|
||||
hostNameHTML = "<br>" + html.EscapeString(hostName)
|
||||
}
|
||||
|
||||
f("<tr><td>%s</td><td class=acenter>%s</td>"+
|
||||
"<td><b>%s</b>%s<div class=\"tailaddr\">%s</div></td><td class=\"acenter owner\">%s</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td>",
|
||||
ps.PublicKey.ShortString(),
|
||||
osEmoji(ps.OS),
|
||||
html.EscapeString(dnsName),
|
||||
hostNameHTML,
|
||||
ps.TailAddr,
|
||||
html.EscapeString(owner),
|
||||
ps.RxBytes,
|
||||
ps.TxBytes,
|
||||
actAgo,
|
||||
)
|
||||
f("<td class=\"aright\">")
|
||||
f("<td>")
|
||||
|
||||
// TODO: let server report this active bool instead
|
||||
active := !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
|
||||
relay := ps.Relay
|
||||
if relay != "" {
|
||||
if active && ps.CurAddr == "" {
|
||||
f("🔗 <b>derp-%v</b><br>", html.EscapeString(relay))
|
||||
} else {
|
||||
f("derp-%v<br>", html.EscapeString(relay))
|
||||
if active {
|
||||
if ps.Relay != "" && ps.CurAddr == "" {
|
||||
f("relay <b>%s</b>", html.EscapeString(ps.Relay))
|
||||
} else if ps.CurAddr != "" {
|
||||
f("direct <b>%s</b>", html.EscapeString(ps.CurAddr))
|
||||
}
|
||||
}
|
||||
|
||||
match := false
|
||||
for _, addr := range ps.Addrs {
|
||||
if addr == ps.CurAddr {
|
||||
match = true
|
||||
f("🔗 <b>%s</b><br>", addr)
|
||||
} else {
|
||||
f("%s<br>", addr)
|
||||
}
|
||||
}
|
||||
if ps.CurAddr != "" && !match {
|
||||
f("<b>%s</b> \xf0\x9f\xa7\xb3<br>", ps.CurAddr)
|
||||
}
|
||||
f("</td>") // end Addrs
|
||||
|
||||
f("</tr>\n")
|
||||
@@ -368,3 +401,17 @@ type PingResult struct {
|
||||
|
||||
// TODO(bradfitz): details like whether port mapping was used on either side? (Once supported)
|
||||
}
|
||||
|
||||
func SortPeers(peers []*PeerStatus) {
|
||||
sort.Slice(peers, func(i, j int) bool { return sortKey(peers[i]) < sortKey(peers[j]) })
|
||||
}
|
||||
|
||||
func sortKey(ps *PeerStatus) string {
|
||||
if ps.DNSName != "" {
|
||||
return ps.DNSName
|
||||
}
|
||||
if ps.HostName != "" {
|
||||
return ps.HostName
|
||||
}
|
||||
return ps.TailAddr
|
||||
}
|
||||
|
||||
111
ipn/localapi/localapi.go
Normal file
111
ipn/localapi/localapi.go
Normal file
@@ -0,0 +1,111 @@
|
||||
// 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 localapi contains the HTTP server handlers for tailscaled's API server.
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func NewHandler(b *ipnlocal.LocalBackend) *Handler {
|
||||
return &Handler{b: b}
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
// RequiredPassword, if non-empty, forces all HTTP
|
||||
// requests to have HTTP basic auth with this password.
|
||||
// It's used by the sandboxed macOS sameuserproof GUI auth mechanism.
|
||||
RequiredPassword string
|
||||
|
||||
// PermitRead is whether read-only HTTP handlers are allowed.
|
||||
PermitRead bool
|
||||
|
||||
// PermitWrite is whether mutating HTTP handlers are allowed.
|
||||
PermitWrite bool
|
||||
|
||||
b *ipnlocal.LocalBackend
|
||||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if h.b == nil {
|
||||
http.Error(w, "server has no local backend", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if h.RequiredPassword != "" {
|
||||
_, pass, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
http.Error(w, "auth required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
if pass != h.RequiredPassword {
|
||||
http.Error(w, "bad password", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/localapi/v0/whois":
|
||||
h.serveWhoIs(w, r)
|
||||
case "/localapi/v0/goroutines":
|
||||
h.serveGoroutines(w, r)
|
||||
default:
|
||||
io.WriteString(w, "tailscaled\n")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitRead {
|
||||
http.Error(w, "whois access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
b := h.b
|
||||
var ip netaddr.IP
|
||||
if v := r.FormValue("ip"); v != "" {
|
||||
var err error
|
||||
ip, err = netaddr.ParseIP(r.FormValue("ip"))
|
||||
if err != nil {
|
||||
http.Error(w, "invalid 'ip' parameter", 400)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
http.Error(w, "missing 'ip' parameter", 400)
|
||||
return
|
||||
}
|
||||
n, u, ok := b.WhoIs(ip)
|
||||
if !ok {
|
||||
http.Error(w, "no match for IP", 404)
|
||||
return
|
||||
}
|
||||
res := &tailcfg.WhoIsResponse{
|
||||
Node: n,
|
||||
UserProfile: &u,
|
||||
}
|
||||
j, err := json.MarshalIndent(res, "", "\t")
|
||||
if err != nil {
|
||||
http.Error(w, "JSON encoding error", 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(j)
|
||||
}
|
||||
|
||||
func (h *Handler) serveGoroutines(w http.ResponseWriter, r *http.Request) {
|
||||
// Require write access out of paranoia that the goroutine dump
|
||||
// (at least its arguments) might contain something sensitive.
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "goroutine dump access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
buf := make([]byte, 2<<20)
|
||||
buf = buf[:runtime.Stack(buf, true)]
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write(buf)
|
||||
}
|
||||
@@ -6,6 +6,7 @@ package ipn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -20,6 +21,24 @@ import (
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
type readOnlyContextKey struct{}
|
||||
|
||||
// IsReadonlyContext reports whether ctx is a read-only context, as currently used
|
||||
// by Unix non-root users running the "tailscale" CLI command. They can run "status",
|
||||
// but not much else.
|
||||
func IsReadonlyContext(ctx context.Context) bool {
|
||||
return ctx.Value(readOnlyContextKey{}) != nil
|
||||
}
|
||||
|
||||
// ReadonlyContextOf returns ctx wrapped with a context value that
|
||||
// will make IsReadonlyContext reports true.
|
||||
func ReadonlyContextOf(ctx context.Context) context.Context {
|
||||
if IsReadonlyContext(ctx) {
|
||||
return ctx
|
||||
}
|
||||
return context.WithValue(ctx, readOnlyContextKey{}, readOnlyContextKey{})
|
||||
}
|
||||
|
||||
var jsonEscapedZero = []byte(`\u0000`)
|
||||
|
||||
type NoArgs struct{}
|
||||
@@ -111,7 +130,7 @@ func (bs *BackendServer) SendInUseOtherUserErrorMessage(msg string) {
|
||||
|
||||
// GotCommandMsg parses the incoming message b as a JSON Command and
|
||||
// calls GotCommand with it.
|
||||
func (bs *BackendServer) GotCommandMsg(b []byte) error {
|
||||
func (bs *BackendServer) GotCommandMsg(ctx context.Context, b []byte) error {
|
||||
cmd := &Command{}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
@@ -119,15 +138,19 @@ func (bs *BackendServer) GotCommandMsg(b []byte) error {
|
||||
if err := json.Unmarshal(b, cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
return bs.GotCommand(cmd)
|
||||
return bs.GotCommand(ctx, cmd)
|
||||
}
|
||||
|
||||
func (bs *BackendServer) GotFakeCommand(cmd *Command) error {
|
||||
func (bs *BackendServer) GotFakeCommand(ctx context.Context, cmd *Command) error {
|
||||
cmd.Version = version.Long
|
||||
return bs.GotCommand(cmd)
|
||||
return bs.GotCommand(ctx, cmd)
|
||||
}
|
||||
|
||||
func (bs *BackendServer) GotCommand(cmd *Command) error {
|
||||
// ErrMsgPermissionDenied is the Notify.ErrMessage value used an
|
||||
// operation was done from a user/context that didn't have permission.
|
||||
const ErrMsgPermissionDenied = "permission denied"
|
||||
|
||||
func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error {
|
||||
if cmd.Version != version.Long && !cmd.AllowVersionSkew {
|
||||
vs := fmt.Sprintf("GotCommand: Version mismatch! frontend=%#v backend=%#v",
|
||||
cmd.Version, version.Long)
|
||||
@@ -141,12 +164,33 @@ func (bs *BackendServer) GotCommand(cmd *Command) error {
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO(bradfitz): finish plumbing context down to all the methods below;
|
||||
// currently we just check for read-only contexts in this method and
|
||||
// then never use contexts again.
|
||||
|
||||
// Actions permitted with a read-only context:
|
||||
if c := cmd.RequestEngineStatus; c != nil {
|
||||
bs.b.RequestEngineStatus()
|
||||
return nil
|
||||
} else if c := cmd.RequestStatus; c != nil {
|
||||
bs.b.RequestStatus()
|
||||
return nil
|
||||
} else if c := cmd.Ping; c != nil {
|
||||
bs.b.Ping(c.IP)
|
||||
return nil
|
||||
}
|
||||
|
||||
if IsReadonlyContext(ctx) {
|
||||
msg := ErrMsgPermissionDenied
|
||||
bs.send(Notify{ErrMessage: &msg})
|
||||
return nil
|
||||
}
|
||||
|
||||
if cmd.Quit != nil {
|
||||
bs.GotQuit = true
|
||||
return errors.New("Quit command received")
|
||||
}
|
||||
|
||||
if c := cmd.Start; c != nil {
|
||||
} else if c := cmd.Start; c != nil {
|
||||
opts := c.Opts
|
||||
opts.Notify = bs.send
|
||||
return bs.b.Start(opts)
|
||||
@@ -165,27 +209,17 @@ func (bs *BackendServer) GotCommand(cmd *Command) error {
|
||||
} else if c := cmd.SetWantRunning; c != nil {
|
||||
bs.b.SetWantRunning(*c)
|
||||
return nil
|
||||
} else if c := cmd.RequestEngineStatus; c != nil {
|
||||
bs.b.RequestEngineStatus()
|
||||
return nil
|
||||
} else if c := cmd.RequestStatus; c != nil {
|
||||
bs.b.RequestStatus()
|
||||
return nil
|
||||
} else if c := cmd.FakeExpireAfter; c != nil {
|
||||
bs.b.FakeExpireAfter(c.Duration)
|
||||
return nil
|
||||
} else if c := cmd.Ping; c != nil {
|
||||
bs.b.Ping(c.IP)
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("BackendServer.Do: no command specified")
|
||||
}
|
||||
return fmt.Errorf("BackendServer.Do: no command specified")
|
||||
}
|
||||
|
||||
func (bs *BackendServer) Reset() error {
|
||||
func (bs *BackendServer) Reset(ctx context.Context) error {
|
||||
// Tell the backend we got a Logout command, which will cause it
|
||||
// to forget all its authentication information.
|
||||
return bs.GotFakeCommand(&Command{Logout: &NoArgs{}})
|
||||
return bs.GotFakeCommand(ctx, &Command{Logout: &NoArgs{}})
|
||||
}
|
||||
|
||||
type BackendClient struct {
|
||||
|
||||
@@ -6,6 +6,7 @@ package ipn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -15,9 +16,7 @@ import (
|
||||
|
||||
func TestReadWrite(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
|
||||
rc := tstest.NewResourceCheck()
|
||||
defer rc.Assert(t)
|
||||
tstest.ResourceCheck(t)
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
err := WriteMsg(&buf, []byte("Test string1"))
|
||||
@@ -63,9 +62,7 @@ func TestReadWrite(t *testing.T) {
|
||||
|
||||
func TestClientServer(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
|
||||
rc := tstest.NewResourceCheck()
|
||||
defer rc.Assert(t)
|
||||
tstest.ResourceCheck(t)
|
||||
|
||||
b := &FakeBackend{}
|
||||
var bs *BackendServer
|
||||
@@ -81,7 +78,7 @@ func TestClientServer(t *testing.T) {
|
||||
serverToClientCh <- append([]byte{}, b...)
|
||||
}
|
||||
clientToServer := func(b []byte) {
|
||||
bs.GotCommandMsg(b)
|
||||
bs.GotCommandMsg(context.TODO(), b)
|
||||
}
|
||||
slogf := func(fmt string, args ...interface{}) {
|
||||
t.Logf("s: "+fmt, args...)
|
||||
|
||||
46
ipn/prefs.go
46
ipn/prefs.go
@@ -17,8 +17,9 @@ import (
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=Prefs -output=prefs_clone.go
|
||||
@@ -28,8 +29,10 @@ type Prefs struct {
|
||||
// ControlURL is the URL of the control server to use.
|
||||
ControlURL string
|
||||
|
||||
// RouteAll specifies whether to accept subnet and default routes
|
||||
// advertised by other nodes on the Tailscale network.
|
||||
// RouteAll specifies whether to accept subnets advertised by
|
||||
// other nodes on the Tailscale network. Note that this does not
|
||||
// include default routes (0.0.0.0/0 and ::/0), those are
|
||||
// controlled by ExitNodeID/IP below.
|
||||
RouteAll bool
|
||||
|
||||
// AllowSingleHosts specifies whether to install routes for each
|
||||
@@ -44,6 +47,24 @@ type Prefs struct {
|
||||
// packets stop flowing. What's up with that?
|
||||
AllowSingleHosts bool
|
||||
|
||||
// ExitNodeID and ExitNodeIP specify the node that should be used
|
||||
// as an exit node for internet traffic. At most one of these
|
||||
// should be non-zero.
|
||||
//
|
||||
// The preferred way to express the chosen node is ExitNodeID, but
|
||||
// in some cases it's not possible to use that ID (e.g. in the
|
||||
// linux CLI, before tailscaled has a netmap). For those
|
||||
// situations, we allow specifying the exit node by IP, and
|
||||
// ipnlocal.LocalBackend will translate the IP into an ID when the
|
||||
// node is found in the netmap.
|
||||
//
|
||||
// If the selected exit node doesn't exist (e.g. it's not part of
|
||||
// the current tailnet), or it doesn't offer exit node services, a
|
||||
// blackhole route will be installed on the local system to
|
||||
// prevent any traffic escaping to the local network.
|
||||
ExitNodeID tailcfg.StableNodeID
|
||||
ExitNodeIP netaddr.IP
|
||||
|
||||
// CorpDNS specifies whether to install the Tailscale network's
|
||||
// DNS configuration, if it exists.
|
||||
CorpDNS bool
|
||||
@@ -116,14 +137,14 @@ type Prefs struct {
|
||||
|
||||
// NetfilterMode specifies how much to manage netfilter rules for
|
||||
// Tailscale, if at all.
|
||||
NetfilterMode router.NetfilterMode
|
||||
NetfilterMode preftype.NetfilterMode
|
||||
|
||||
// The Persist field is named 'Config' in the file for backward
|
||||
// compatibility with earlier versions.
|
||||
// TODO(apenwarr): We should move this out of here, it's not a pref.
|
||||
// We can maybe do that once we're sure which module should persist
|
||||
// it (backend or frontend?)
|
||||
Persist *controlclient.Persist `json:"Config"`
|
||||
Persist *persist.Persist `json:"Config"`
|
||||
}
|
||||
|
||||
// IsEmpty reports whether p is nil or pointing to a Prefs zero value.
|
||||
@@ -147,6 +168,11 @@ func (p *Prefs) pretty(goos string) string {
|
||||
if p.ShieldsUp {
|
||||
sb.WriteString("shields=true ")
|
||||
}
|
||||
if !p.ExitNodeIP.IsZero() {
|
||||
fmt.Fprintf(&sb, "exit=%v ", p.ExitNodeIP)
|
||||
} else if !p.ExitNodeID.IsZero() {
|
||||
fmt.Fprintf(&sb, "exit=%v ", p.ExitNodeID)
|
||||
}
|
||||
if len(p.AdvertiseRoutes) > 0 || goos == "linux" {
|
||||
fmt.Fprintf(&sb, "routes=%v ", p.AdvertiseRoutes)
|
||||
}
|
||||
@@ -191,6 +217,8 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
||||
p.ControlURL == p2.ControlURL &&
|
||||
p.RouteAll == p2.RouteAll &&
|
||||
p.AllowSingleHosts == p2.AllowSingleHosts &&
|
||||
p.ExitNodeID == p2.ExitNodeID &&
|
||||
p.ExitNodeIP == p2.ExitNodeIP &&
|
||||
p.CorpDNS == p2.CorpDNS &&
|
||||
p.WantRunning == p2.WantRunning &&
|
||||
p.NotepadURLs == p2.NotepadURLs &&
|
||||
@@ -240,7 +268,7 @@ func NewPrefs() *Prefs {
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
WantRunning: true,
|
||||
NetfilterMode: router.NetfilterOn,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,7 +280,7 @@ func PrefsFromBytes(b []byte, enforceDefaults bool) (*Prefs, error) {
|
||||
if len(b) == 0 {
|
||||
return p, nil
|
||||
}
|
||||
persist := &controlclient.Persist{}
|
||||
persist := &persist.Persist{}
|
||||
err := json.Unmarshal(b, persist)
|
||||
if err == nil && (persist.Provider != "" || persist.LoginName != "") {
|
||||
// old-style relaynode config; import it
|
||||
@@ -296,7 +324,7 @@ func SavePrefs(filename string, p *Prefs) {
|
||||
log.Printf("Saving prefs %v %v\n", filename, p.Pretty())
|
||||
data := p.ToBytes()
|
||||
os.MkdirAll(filepath.Dir(filename), 0700)
|
||||
if err := atomicfile.WriteFile(filename, data, 0666); err != nil {
|
||||
if err := atomicfile.WriteFile(filename, data, 0600); err != nil {
|
||||
log.Printf("SavePrefs: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,9 @@ package ipn
|
||||
|
||||
import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
)
|
||||
|
||||
// Clone makes a deep copy of Prefs.
|
||||
@@ -23,7 +24,7 @@ func (src *Prefs) Clone() *Prefs {
|
||||
dst.AdvertiseTags = append(src.AdvertiseTags[:0:0], src.AdvertiseTags...)
|
||||
dst.AdvertiseRoutes = append(src.AdvertiseRoutes[:0:0], src.AdvertiseRoutes...)
|
||||
if dst.Persist != nil {
|
||||
dst.Persist = new(controlclient.Persist)
|
||||
dst.Persist = new(persist.Persist)
|
||||
*dst.Persist = *src.Persist
|
||||
}
|
||||
return dst
|
||||
@@ -35,6 +36,8 @@ var _PrefsNeedsRegeneration = Prefs(struct {
|
||||
ControlURL string
|
||||
RouteAll bool
|
||||
AllowSingleHosts bool
|
||||
ExitNodeID tailcfg.StableNodeID
|
||||
ExitNodeIP netaddr.IP
|
||||
CorpDNS bool
|
||||
WantRunning bool
|
||||
ShieldsUp bool
|
||||
@@ -46,6 +49,6 @@ var _PrefsNeedsRegeneration = Prefs(struct {
|
||||
ForceDaemon bool
|
||||
AdvertiseRoutes []netaddr.IPPrefix
|
||||
NoSNAT bool
|
||||
NetfilterMode router.NetfilterMode
|
||||
Persist *controlclient.Persist
|
||||
NetfilterMode preftype.NetfilterMode
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
@@ -14,10 +14,11 @@ import (
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
|
||||
func fieldsOf(t reflect.Type) (fields []string) {
|
||||
@@ -30,7 +31,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
|
||||
func TestPrefsEqual(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
|
||||
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"}
|
||||
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "ExitNodeID", "ExitNodeIP", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"}
|
||||
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
|
||||
t.Errorf("Prefs.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||
have, prefsHandles)
|
||||
@@ -99,6 +100,28 @@ func TestPrefsEqual(t *testing.T) {
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Prefs{ExitNodeID: "n1234"},
|
||||
&Prefs{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Prefs{ExitNodeID: "n1234"},
|
||||
&Prefs{ExitNodeID: "n1234"},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Prefs{ExitNodeIP: netaddr.MustParseIP("1.2.3.4")},
|
||||
&Prefs{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Prefs{ExitNodeIP: netaddr.MustParseIP("1.2.3.4")},
|
||||
&Prefs{ExitNodeIP: netaddr.MustParseIP("1.2.3.4")},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Prefs{CorpDNS: true},
|
||||
&Prefs{CorpDNS: false},
|
||||
@@ -192,24 +215,24 @@ func TestPrefsEqual(t *testing.T) {
|
||||
},
|
||||
|
||||
{
|
||||
&Prefs{NetfilterMode: router.NetfilterOff},
|
||||
&Prefs{NetfilterMode: router.NetfilterOn},
|
||||
&Prefs{NetfilterMode: preftype.NetfilterOff},
|
||||
&Prefs{NetfilterMode: preftype.NetfilterOn},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Prefs{NetfilterMode: router.NetfilterOn},
|
||||
&Prefs{NetfilterMode: router.NetfilterOn},
|
||||
&Prefs{NetfilterMode: preftype.NetfilterOn},
|
||||
&Prefs{NetfilterMode: preftype.NetfilterOn},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Prefs{Persist: &controlclient.Persist{}},
|
||||
&Prefs{Persist: &controlclient.Persist{LoginName: "dave"}},
|
||||
&Prefs{Persist: &persist.Persist{}},
|
||||
&Prefs{Persist: &persist.Persist{LoginName: "dave"}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Prefs{Persist: &controlclient.Persist{LoginName: "dave"}},
|
||||
&Prefs{Persist: &controlclient.Persist{LoginName: "dave"}},
|
||||
&Prefs{Persist: &persist.Persist{LoginName: "dave"}},
|
||||
&Prefs{Persist: &persist.Persist{LoginName: "dave"}},
|
||||
true,
|
||||
},
|
||||
}
|
||||
@@ -274,7 +297,7 @@ func TestBasicPrefs(t *testing.T) {
|
||||
func TestPrefsPersist(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
|
||||
c := controlclient.Persist{
|
||||
c := persist.Persist{
|
||||
LoginName: "test@example.com",
|
||||
}
|
||||
p := Prefs{
|
||||
@@ -340,20 +363,34 @@ func TestPrefsPretty(t *testing.T) {
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
Persist: &controlclient.Persist{},
|
||||
Persist: &persist.Persist{},
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist{lm=, o=, n= u=""}}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
Persist: &controlclient.Persist{
|
||||
Persist: &persist.Persist{
|
||||
PrivateNodeKey: wgkey.Private{1: 1},
|
||||
},
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist{lm=, o=, n=[B1VKl] u=""}}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
ExitNodeIP: netaddr.MustParseIP("1.2.3.4"),
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false exit=1.2.3.4 routes=[] nf=off Persist=nil}`,
|
||||
},
|
||||
{
|
||||
Prefs{
|
||||
ExitNodeID: tailcfg.StableNodeID("myNodeABC"),
|
||||
},
|
||||
"linux",
|
||||
`Prefs{ra=false mesh=false dns=false want=false exit=myNodeABC routes=[] nf=off Persist=nil}`,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
got := tt.p.pretty(tt.os)
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -338,6 +339,18 @@ func New(collection string) *Policy {
|
||||
tryFixLogStateLocation(dir, cmdName)
|
||||
|
||||
cfgPath := filepath.Join(dir, fmt.Sprintf("%s.log.conf", cmdName))
|
||||
|
||||
// The Windows service previously ran as tailscale-ipn.exe, so
|
||||
// let's keep using that log base name if it exists.
|
||||
if runtime.GOOS == "windows" && cmdName == "tailscaled" {
|
||||
const oldCmdName = "tailscale-ipn"
|
||||
oldPath := filepath.Join(dir, oldCmdName+".log.conf")
|
||||
if fi, err := os.Stat(oldPath); err == nil && fi.Mode().IsRegular() {
|
||||
cfgPath = oldPath
|
||||
cmdName = oldCmdName
|
||||
}
|
||||
}
|
||||
|
||||
var oldc *Config
|
||||
data, err := ioutil.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
@@ -387,6 +400,13 @@ func New(collection string) *Policy {
|
||||
HTTPC: &http.Client{Transport: newLogtailTransport(logtail.DefaultHost)},
|
||||
}
|
||||
|
||||
if val, ok := os.LookupEnv("TS_LOG_TARGET"); ok {
|
||||
log.Println("You have enabled a non-default log target. Doing without being told to by Tailscale staff or your network administrator will make getting support difficult.")
|
||||
c.BaseURL = val
|
||||
u, _ := url.Parse(val)
|
||||
c.HTTPC = &http.Client{Transport: newLogtailTransport(u.Host)}
|
||||
}
|
||||
|
||||
filchBuf, filchErr := filch.New(filepath.Join(dir, cmdName), filch.Options{})
|
||||
if filchBuf != nil {
|
||||
c.Buffer = filchBuf
|
||||
|
||||
@@ -131,11 +131,11 @@ func New(filePrefix string, opts Options) (f *Filch, err error) {
|
||||
path1 := filePrefix + ".log1.txt"
|
||||
path2 := filePrefix + ".log2.txt"
|
||||
|
||||
f1, err = os.OpenFile(path1, os.O_CREATE|os.O_RDWR, 0666)
|
||||
f1, err = os.OpenFile(path1, os.O_CREATE|os.O_RDWR, 0600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f2, err = os.OpenFile(path2, os.O_CREATE|os.O_RDWR, 0666)
|
||||
f2, err = os.OpenFile(path2, os.O_CREATE|os.O_RDWR, 0600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -18,7 +18,9 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/interfaces"
|
||||
tslogger "tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
// DefaultHost is the default host name to upload logs to when
|
||||
@@ -106,6 +108,7 @@ type Logger struct {
|
||||
url string
|
||||
lowMem bool
|
||||
skipClientTime bool
|
||||
linkMonitor *monitor.Mon
|
||||
buffer Buffer
|
||||
sent chan struct{} // signal to speed up drain
|
||||
drainLogs <-chan struct{} // if non-nil, external signal to attempt a drain
|
||||
@@ -128,6 +131,14 @@ func (l *Logger) SetVerbosityLevel(level int) {
|
||||
l.stderrLevel = level
|
||||
}
|
||||
|
||||
// SetLinkMonitor sets the optional the link monitor.
|
||||
//
|
||||
// It should not be changed concurrently with log writes and should
|
||||
// only be set once.
|
||||
func (l *Logger) SetLinkMonitor(lm *monitor.Mon) {
|
||||
l.linkMonitor = lm
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the logger while completing any
|
||||
// remaining uploads.
|
||||
//
|
||||
@@ -277,6 +288,11 @@ func (l *Logger) uploading(ctx context.Context) {
|
||||
}
|
||||
uploaded, err := l.upload(ctx, body, origlen)
|
||||
if err != nil {
|
||||
if !l.internetUp() {
|
||||
fmt.Fprintf(l.stderr, "logtail: internet down; waiting\n")
|
||||
l.awaitInternetUp(ctx)
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(l.stderr, "logtail: upload: %v\n", err)
|
||||
}
|
||||
l.bo.BackOff(ctx, err)
|
||||
@@ -293,6 +309,34 @@ func (l *Logger) uploading(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) internetUp() bool {
|
||||
if l.linkMonitor == nil {
|
||||
// No way to tell, so assume it is.
|
||||
return true
|
||||
}
|
||||
return l.linkMonitor.InterfaceState().AnyInterfaceUp()
|
||||
}
|
||||
|
||||
func (l *Logger) awaitInternetUp(ctx context.Context) {
|
||||
upc := make(chan bool, 1)
|
||||
defer l.linkMonitor.RegisterChangeCallback(func(changed bool, st *interfaces.State) {
|
||||
if st.AnyInterfaceUp() {
|
||||
select {
|
||||
case upc <- true:
|
||||
default:
|
||||
}
|
||||
}
|
||||
})()
|
||||
if l.internetUp() {
|
||||
return
|
||||
}
|
||||
select {
|
||||
case <-upc:
|
||||
fmt.Fprintf(l.stderr, "logtail: internet back up\n")
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
|
||||
// upload uploads body to the log server.
|
||||
// origlen indicates the pre-compression body length.
|
||||
// origlen of -1 indicates that the body is not compressed.
|
||||
|
||||
@@ -6,6 +6,12 @@ package logtail
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -14,10 +20,209 @@ func TestFastShutdown(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
testServ := httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {}))
|
||||
defer testServ.Close()
|
||||
|
||||
l := NewLogger(Config{
|
||||
BaseURL: "http://localhost:1234",
|
||||
BaseURL: testServ.URL,
|
||||
}, t.Logf)
|
||||
l.Shutdown(ctx)
|
||||
err := l.Shutdown(ctx)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// maximum number of times a test will call l.Write()
|
||||
const logLines = 3
|
||||
|
||||
type LogtailTestServer struct {
|
||||
srv *httptest.Server // Log server
|
||||
uploaded chan []byte
|
||||
}
|
||||
|
||||
func NewLogtailTestHarness(t *testing.T) (*LogtailTestServer, *Logger) {
|
||||
ts := LogtailTestServer{}
|
||||
|
||||
// max channel backlog = 1 "started" + #logLines x "log line" + 1 "closed"
|
||||
ts.uploaded = make(chan []byte, 2+logLines)
|
||||
|
||||
ts.srv = httptest.NewServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
t.Error("failed to read HTTP request")
|
||||
}
|
||||
ts.uploaded <- body
|
||||
}))
|
||||
|
||||
t.Cleanup(ts.srv.Close)
|
||||
|
||||
l := NewLogger(Config{BaseURL: ts.srv.URL}, t.Logf)
|
||||
|
||||
// There is always an initial "logtail started" message
|
||||
body := <-ts.uploaded
|
||||
if !strings.Contains(string(body), "started") {
|
||||
t.Errorf("unknown start logging statement: %q", string(body))
|
||||
}
|
||||
|
||||
return &ts, l
|
||||
}
|
||||
|
||||
func TestDrainPendingMessages(t *testing.T) {
|
||||
ts, l := NewLogtailTestHarness(t)
|
||||
|
||||
for i := 0; i < logLines; i++ {
|
||||
l.Write([]byte("log line"))
|
||||
}
|
||||
|
||||
// all of the "log line" messages usually arrive at once, but poll if needed.
|
||||
body := ""
|
||||
for i := 0; i <= logLines; i++ {
|
||||
body += string(<-ts.uploaded)
|
||||
count := strings.Count(body, "log line")
|
||||
if count == logLines {
|
||||
break
|
||||
}
|
||||
// if we never find count == logLines, the test will eventually time out.
|
||||
}
|
||||
|
||||
err := l.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeAndUploadMessages(t *testing.T) {
|
||||
ts, l := NewLogtailTestHarness(t)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
log string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
"plain text",
|
||||
"log line",
|
||||
"log line",
|
||||
},
|
||||
{
|
||||
"simple JSON",
|
||||
`{"text": "log line"}`,
|
||||
"log line",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
io.WriteString(l, tt.log)
|
||||
body := <-ts.uploaded
|
||||
|
||||
data := make(map[string]interface{})
|
||||
err := json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
got := data["text"]
|
||||
if got != tt.want {
|
||||
t.Errorf("%s: got %q; want %q", tt.name, got.(string), tt.want)
|
||||
}
|
||||
|
||||
ltail, ok := data["logtail"]
|
||||
if ok {
|
||||
logtailmap := ltail.(map[string]interface{})
|
||||
_, ok = logtailmap["client_time"]
|
||||
if !ok {
|
||||
t.Errorf("%s: no client_time present", tt.name)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("%s: no logtail map present", tt.name)
|
||||
}
|
||||
}
|
||||
|
||||
err := l.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncodeSpecialCases(t *testing.T) {
|
||||
ts, l := NewLogtailTestHarness(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)
|
||||
}
|
||||
errorHasLogtail, ok := data["error_has_logtail"]
|
||||
if ok {
|
||||
if errorHasLogtail != "LOGTAIL" {
|
||||
t.Errorf("error_has_logtail: got:%q; want:%q",
|
||||
errorHasLogtail, "LOGTAIL")
|
||||
}
|
||||
} else {
|
||||
t.Errorf("no error_has_logtail field: %v", data)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// special characters
|
||||
io.WriteString(l, "\b\f\n\r\t"+`"\`)
|
||||
bodytext := string(<-ts.uploaded)
|
||||
// json.Unmarshal would unescape the characters, we have to look at the encoded text
|
||||
escaped := strings.Contains(bodytext, `\b\f\n\r\t\"\`)
|
||||
if !escaped {
|
||||
t.Errorf("special characters got %s", bodytext)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// skipClientTime to omit the logtail metadata
|
||||
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)
|
||||
}
|
||||
_, ok = data["logtail"]
|
||||
if ok {
|
||||
t.Errorf("skipClientTime: unexpected logtail map present: %v", data)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// lowMem + long string
|
||||
l.skipClientTime = false
|
||||
l.lowMem = true
|
||||
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)
|
||||
}
|
||||
text, ok := data["text"]
|
||||
if !ok {
|
||||
t.Errorf("lowMem: no text %v", data)
|
||||
}
|
||||
if n := len(text.(string)); n > 300 {
|
||||
t.Errorf("lowMem: got %d chars; want <300 chars", n)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
err = l.Shutdown(context.Background())
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
var sink []byte
|
||||
@@ -47,3 +252,54 @@ func TestLoggerWriteLength(t *testing.T) {
|
||||
t.Errorf("logger.Write wrote %d bytes, expected %d", n, len(inBuf))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAndRemoveLogLevel(t *testing.T) {
|
||||
tests := []struct {
|
||||
log string
|
||||
wantLevel int
|
||||
wantLog string
|
||||
}{
|
||||
{
|
||||
"no level",
|
||||
0,
|
||||
"no level",
|
||||
},
|
||||
{
|
||||
"[v1] level 1",
|
||||
1,
|
||||
"level 1",
|
||||
},
|
||||
{
|
||||
"level 1 [v1] ",
|
||||
1,
|
||||
"level 1 ",
|
||||
},
|
||||
{
|
||||
"[v2] level 2",
|
||||
2,
|
||||
"level 2",
|
||||
},
|
||||
{
|
||||
"level [v2] 2",
|
||||
2,
|
||||
"level 2",
|
||||
},
|
||||
{
|
||||
"[v3] no level 3",
|
||||
0,
|
||||
"[v3] no level 3",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
gotLevel, gotLog := parseAndRemoveLogLevel([]byte(tt.log))
|
||||
if gotLevel != tt.wantLevel {
|
||||
t.Errorf("parseAndRemoveLogLevel(%q): got:%d; want %d",
|
||||
tt.log, gotLevel, tt.wantLevel)
|
||||
}
|
||||
if string(gotLog) != tt.wantLog {
|
||||
t.Errorf("parseAndRemoveLogLevel(%q): got:%q; want %q",
|
||||
tt.log, gotLog, tt.wantLog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ package dnscache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
@@ -18,6 +20,7 @@ import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/singleflight"
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
var single = &Resolver{
|
||||
@@ -55,6 +58,10 @@ type Resolver struct {
|
||||
// If nil, net.DefaultResolver is used.
|
||||
Forward *net.Resolver
|
||||
|
||||
// LookupIPFallback optionally provides a backup DNS mechanism
|
||||
// to use if Forward returns an error or no results.
|
||||
LookupIPFallback func(ctx context.Context, host string) ([]netaddr.IP, error)
|
||||
|
||||
// TTL is how long to keep entries cached
|
||||
//
|
||||
// If zero, a default (currently 10 minutes) is used.
|
||||
@@ -71,7 +78,8 @@ type Resolver struct {
|
||||
}
|
||||
|
||||
type ipCacheEntry struct {
|
||||
ip net.IP
|
||||
ip net.IP // either v4 or v6
|
||||
ip6 net.IP // nil if no v4 or no v6
|
||||
expires time.Time
|
||||
}
|
||||
|
||||
@@ -91,78 +99,87 @@ func (r *Resolver) ttl() time.Duration {
|
||||
|
||||
var debug, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_DNS_CACHE"))
|
||||
|
||||
// LookupIP returns the first IPv4 address found, otherwise the first IPv6 address.
|
||||
func (r *Resolver) LookupIP(ctx context.Context, host string) (net.IP, error) {
|
||||
// LookupIP returns the host's primary IP address (either IPv4 or
|
||||
// IPv6, but preferring IPv4) and optionally its IPv6 address, if
|
||||
// there is both IPv4 and IPv6.
|
||||
//
|
||||
// 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) {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
return ip4, nil
|
||||
return ip4, nil, nil
|
||||
}
|
||||
if debug {
|
||||
log.Printf("dnscache: %q is an IP", host)
|
||||
}
|
||||
return ip, nil
|
||||
return ip, nil, nil
|
||||
}
|
||||
|
||||
if ip, ok := r.lookupIPCache(host); ok {
|
||||
if ip, ip6, ok := r.lookupIPCache(host); ok {
|
||||
if debug {
|
||||
log.Printf("dnscache: %q = %v (cached)", host, ip)
|
||||
}
|
||||
return ip, nil
|
||||
return ip, ip6, nil
|
||||
}
|
||||
|
||||
type ipPair struct {
|
||||
ip, ip6 net.IP
|
||||
}
|
||||
ch := r.sf.DoChan(host, func() (interface{}, error) {
|
||||
ip, err := r.lookupIP(host)
|
||||
ip, ip6, err := r.lookupIP(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ip, nil
|
||||
return ipPair{ip, ip6}, nil
|
||||
})
|
||||
select {
|
||||
case res := <-ch:
|
||||
if res.Err != nil {
|
||||
if r.UseLastGood {
|
||||
if ip, ok := r.lookupIPCacheExpired(host); ok {
|
||||
if ip, ip6, ok := r.lookupIPCacheExpired(host); ok {
|
||||
if debug {
|
||||
log.Printf("dnscache: %q using %v after error", host, ip)
|
||||
}
|
||||
return ip, nil
|
||||
return ip, ip6, nil
|
||||
}
|
||||
}
|
||||
if debug {
|
||||
log.Printf("dnscache: error resolving %q: %v", host, res.Err)
|
||||
}
|
||||
return nil, res.Err
|
||||
return nil, nil, res.Err
|
||||
}
|
||||
return res.Val.(net.IP), nil
|
||||
pair := res.Val.(ipPair)
|
||||
return pair.ip, pair.ip6, nil
|
||||
case <-ctx.Done():
|
||||
if debug {
|
||||
log.Printf("dnscache: context done while resolving %q: %v", host, ctx.Err())
|
||||
}
|
||||
return nil, ctx.Err()
|
||||
return nil, nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupIPCache(host string) (ip net.IP, ok bool) {
|
||||
func (r *Resolver) lookupIPCache(host string) (ip, ip6 net.IP, ok bool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if ent, ok := r.ipCache[host]; ok && ent.expires.After(time.Now()) {
|
||||
return ent.ip, true
|
||||
return ent.ip, ent.ip6, true
|
||||
}
|
||||
return nil, false
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupIPCacheExpired(host string) (ip net.IP, ok bool) {
|
||||
func (r *Resolver) lookupIPCacheExpired(host string) (ip, ip6 net.IP, ok bool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if ent, ok := r.ipCache[host]; ok {
|
||||
return ent.ip, true
|
||||
return ent.ip, ent.ip6, true
|
||||
}
|
||||
return nil, false
|
||||
return 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
|
||||
@@ -177,40 +194,64 @@ func (r *Resolver) lookupTimeoutForHost(host string) time.Duration {
|
||||
return 10 * time.Second
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupIP(host string) (net.IP, error) {
|
||||
if ip, ok := r.lookupIPCache(host); ok {
|
||||
func (r *Resolver) lookupIP(host string) (ip, ip6 net.IP, err error) {
|
||||
if ip, ip6, ok := r.lookupIPCache(host); ok {
|
||||
if debug {
|
||||
log.Printf("dnscache: %q found in cache as %v", host, ip)
|
||||
}
|
||||
return ip, nil
|
||||
return ip, ip6, nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), r.lookupTimeoutForHost(host))
|
||||
defer cancel()
|
||||
ips, err := r.fwd().LookupIPAddr(ctx, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return nil, fmt.Errorf("no IPs for %q found", host)
|
||||
}
|
||||
|
||||
for _, ipa := range ips {
|
||||
if ip4 := ipa.IP.To4(); ip4 != nil {
|
||||
return r.addIPCache(host, ip4, r.ttl()), nil
|
||||
if (err != nil || len(ips) == 0) && r.LookupIPFallback != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
var fips []netaddr.IP
|
||||
fips, err = r.LookupIPFallback(ctx, host)
|
||||
if err == nil {
|
||||
ips = nil
|
||||
for _, fip := range fips {
|
||||
ips = append(ips, *fip.IPAddr())
|
||||
}
|
||||
}
|
||||
}
|
||||
return r.addIPCache(host, ips[0].IP, r.ttl()), nil
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return nil, nil, fmt.Errorf("no IPs for %q found", host)
|
||||
}
|
||||
|
||||
have4 := false
|
||||
for _, ipa := range ips {
|
||||
if ip4 := ipa.IP.To4(); ip4 != nil {
|
||||
if !have4 {
|
||||
ip6 = ip
|
||||
ip = ip4
|
||||
have4 = true
|
||||
}
|
||||
} else {
|
||||
if have4 {
|
||||
ip6 = ipa.IP
|
||||
} else {
|
||||
ip = ipa.IP
|
||||
}
|
||||
}
|
||||
}
|
||||
r.addIPCache(host, ip, ip6, r.ttl())
|
||||
return ip, ip6, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) addIPCache(host string, ip net.IP, d time.Duration) net.IP {
|
||||
func (r *Resolver) addIPCache(host string, ip, ip6 net.IP, d time.Duration) {
|
||||
if isPrivateIP(ip) {
|
||||
// Don't cache obviously wrong entries from captive portals.
|
||||
// TODO: use DoH or DoT for the forwarding resolver?
|
||||
if debug {
|
||||
log.Printf("dnscache: %q resolved to private IP %v; using but not caching", host, ip)
|
||||
}
|
||||
return ip
|
||||
return
|
||||
}
|
||||
|
||||
if debug {
|
||||
@@ -222,8 +263,7 @@ func (r *Resolver) addIPCache(host string, ip net.IP, d time.Duration) net.IP {
|
||||
if r.ipCache == nil {
|
||||
r.ipCache = make(map[string]ipCacheEntry)
|
||||
}
|
||||
r.ipCache[host] = ipCacheEntry{ip: ip, expires: time.Now().Add(d)}
|
||||
return ip
|
||||
r.ipCache[host] = ipCacheEntry{ip: ip, ip6: ip6, expires: time.Now().Add(d)}
|
||||
}
|
||||
|
||||
func mustCIDR(s string) *net.IPNet {
|
||||
@@ -248,14 +288,34 @@ type DialContextFunc func(ctx context.Context, network, address string) (net.Con
|
||||
|
||||
// Dialer returns a wrapped DialContext func that uses the provided dnsCache.
|
||||
func Dialer(fwd DialContextFunc, dnsCache *Resolver) DialContextFunc {
|
||||
return func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return func(ctx context.Context, network, address string) (retConn net.Conn, ret error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
// Bogus. But just let the real dialer return an error rather than
|
||||
// inventing a similar one.
|
||||
return fwd(ctx, network, address)
|
||||
}
|
||||
ip, err := dnsCache.LookupIP(ctx, host)
|
||||
defer func() {
|
||||
// On any failure, assume our DNS is wrong and try our fallback, if any.
|
||||
if ret == nil || dnsCache.LookupIPFallback == nil {
|
||||
return
|
||||
}
|
||||
ips, err := dnsCache.LookupIPFallback(ctx, host)
|
||||
if err != nil {
|
||||
// Return with original error
|
||||
return
|
||||
}
|
||||
for _, ip := range ips {
|
||||
dst := net.JoinHostPort(ip.String(), port)
|
||||
if c, err := fwd(ctx, network, dst); err == nil {
|
||||
retConn = c
|
||||
ret = nil
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ip, ip6, err := dnsCache.LookupIP(ctx, host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve %q: %w", host, err)
|
||||
}
|
||||
@@ -263,6 +323,78 @@ func Dialer(fwd DialContextFunc, dnsCache *Resolver) DialContextFunc {
|
||||
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.
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
var errTLSHandshakeTimeout = errors.New("timeout doing TLS handshake")
|
||||
|
||||
// TLSDialer is like Dialer but returns a func suitable for using with net/http.Transport.DialTLSContext.
|
||||
// It returns a *tls.Conn type on success.
|
||||
// On TLS cert validation failure, it can invoke a backup DNS resolution strategy.
|
||||
func TLSDialer(fwd DialContextFunc, dnsCache *Resolver, tlsConfigBase *tls.Config) DialContextFunc {
|
||||
tcpDialer := Dialer(fwd, dnsCache)
|
||||
return func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tcpConn, err := tcpDialer(ctx, network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := cloneTLSConfig(tlsConfigBase)
|
||||
if cfg.ServerName == "" {
|
||||
cfg.ServerName = host
|
||||
}
|
||||
tlsConn := tls.Client(tcpConn, cfg)
|
||||
|
||||
errc := make(chan error, 2)
|
||||
handshakeCtx, handshakeTimeoutCancel := context.WithTimeout(ctx, 5*time.Second)
|
||||
defer handshakeTimeoutCancel()
|
||||
done := make(chan bool)
|
||||
defer close(done)
|
||||
go func() {
|
||||
select {
|
||||
case <-done:
|
||||
case <-handshakeCtx.Done():
|
||||
errc <- errTLSHandshakeTimeout
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
err := tlsConn.Handshake()
|
||||
handshakeTimeoutCancel()
|
||||
errc <- err
|
||||
}()
|
||||
if err := <-errc; err != nil {
|
||||
tcpConn.Close()
|
||||
// TODO: if err != errTLSHandshakeTimeout,
|
||||
// assume it might be some captive portal or
|
||||
// otherwise incorrect DNS and try the backup
|
||||
// DNS mechanism.
|
||||
return nil, err
|
||||
}
|
||||
return tlsConn, nil
|
||||
}
|
||||
}
|
||||
|
||||
func cloneTLSConfig(cfg *tls.Config) *tls.Config {
|
||||
if cfg == nil {
|
||||
return &tls.Config{}
|
||||
}
|
||||
return cfg.Clone()
|
||||
}
|
||||
|
||||
117
net/dnsfallback/dnsfallback.go
Normal file
117
net/dnsfallback/dnsfallback.go
Normal file
@@ -0,0 +1,117 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package dnsfallback contains a DNS fallback mechanism
|
||||
// for starting up Tailscale when the system DNS is broken or otherwise unavailable.
|
||||
package dnsfallback
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/derp/derpmap"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
)
|
||||
|
||||
func Lookup(ctx context.Context, host string) ([]netaddr.IP, error) {
|
||||
type nameIP struct {
|
||||
dnsName string
|
||||
ip netaddr.IP
|
||||
}
|
||||
|
||||
dm := derpmap.Prod()
|
||||
var cands4, cands6 []nameIP
|
||||
for _, dr := range dm.Regions {
|
||||
for _, n := range dr.Nodes {
|
||||
if ip, err := netaddr.ParseIP(n.IPv4); err == nil {
|
||||
cands4 = append(cands4, nameIP{n.HostName, ip})
|
||||
}
|
||||
if ip, err := netaddr.ParseIP(n.IPv6); err == nil {
|
||||
cands6 = append(cands6, nameIP{n.HostName, ip})
|
||||
}
|
||||
}
|
||||
}
|
||||
rand.Shuffle(len(cands4), func(i, j int) { cands4[i], cands4[j] = cands4[j], cands4[i] })
|
||||
rand.Shuffle(len(cands6), func(i, j int) { cands6[i], cands6[j] = cands6[j], cands6[i] })
|
||||
|
||||
const maxCands = 6
|
||||
var cands []nameIP // up to maxCands alternating v4/v6 as long as we have both
|
||||
for (len(cands4) > 0 || len(cands6) > 0) && len(cands) < maxCands {
|
||||
if len(cands4) > 0 {
|
||||
cands = append(cands, cands4[0])
|
||||
cands4 = cands4[1:]
|
||||
}
|
||||
if len(cands6) > 0 {
|
||||
cands = append(cands, cands6[0])
|
||||
cands6 = cands6[1:]
|
||||
}
|
||||
}
|
||||
if len(cands) == 0 {
|
||||
return nil, fmt.Errorf("no DNS fallback options for %q", host)
|
||||
}
|
||||
for _, cand := range cands {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("trying bootstrapDNS(%q, %q) for %q ...", cand.dnsName, cand.ip, host)
|
||||
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
dm, err := bootstrapDNSMap(ctx, cand.dnsName, cand.ip, host)
|
||||
if err != nil {
|
||||
log.Printf("bootstrapDNS(%q, %q) for %q error: %v", cand.dnsName, cand.ip, host, err)
|
||||
continue
|
||||
}
|
||||
if ips := dm[host]; len(ips) > 0 {
|
||||
log.Printf("bootstrapDNS(%q, %q) for %q = %v", cand.dnsName, cand.ip, host, ips)
|
||||
return ips, nil
|
||||
}
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf("no DNS fallback candidates remain for %q", host)
|
||||
}
|
||||
|
||||
// serverName and serverIP of are, say, "derpN.tailscale.com".
|
||||
// queryName is the name being sought (e.g. "login.tailscale.com"), passed as hint.
|
||||
func bootstrapDNSMap(ctx context.Context, serverName string, serverIP netaddr.IP, queryName string) (dnsMap, error) {
|
||||
dialer := netns.NewDialer()
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
tr.DialContext = func(ctx context.Context, netw, addr string) (net.Conn, error) {
|
||||
return dialer.DialContext(ctx, "tcp", net.JoinHostPort(serverIP.String(), "443"))
|
||||
}
|
||||
c := &http.Client{Transport: tr}
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://"+serverName+"/bootstrap-dns?q="+url.QueryEscape(queryName), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dm := make(dnsMap)
|
||||
res, err := c.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
return nil, errors.New(res.Status)
|
||||
}
|
||||
if err := json.NewDecoder(res.Body).Decode(&dm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return dm, nil
|
||||
}
|
||||
|
||||
// dnsMap is the JSON type returned by the DERP /bootstrap-dns handler:
|
||||
// https://derp10.tailscale.com/bootstrap-dns
|
||||
type dnsMap map[string][]netaddr.IP
|
||||
104
net/flowtrack/flowtrack.go
Normal file
104
net/flowtrack/flowtrack.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// 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.
|
||||
//
|
||||
// Original implementation (from same author) from which this was derived was:
|
||||
// https://github.com/golang/groupcache/blob/5b532d6fd5efaf7fa130d4e859a2fde0fc3a9e1b/lru/lru.go
|
||||
// ... which was Apache licensed:
|
||||
// https://github.com/golang/groupcache/blob/master/LICENSE
|
||||
|
||||
// Package flowtrack contains types for tracking TCP/UDP flows by 4-tuples.
|
||||
package flowtrack
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
// Tuple is a 4-tuple of source and destination IP and port.
|
||||
type Tuple struct {
|
||||
Src netaddr.IPPort
|
||||
Dst netaddr.IPPort
|
||||
}
|
||||
|
||||
func (t Tuple) String() string {
|
||||
return fmt.Sprintf("(%v => %v)", t.Src, t.Dst)
|
||||
}
|
||||
|
||||
// Cache is an LRU cache keyed by Tuple.
|
||||
//
|
||||
// The zero value is valid to use.
|
||||
//
|
||||
// It is not safe for concurrent access.
|
||||
type Cache struct {
|
||||
// MaxEntries is the maximum number of cache entries before
|
||||
// an item is evicted. Zero means no limit.
|
||||
MaxEntries int
|
||||
|
||||
ll *list.List
|
||||
m map[Tuple]*list.Element // of *entry
|
||||
}
|
||||
|
||||
// entry is the container/list element type.
|
||||
type entry struct {
|
||||
key Tuple
|
||||
value interface{}
|
||||
}
|
||||
|
||||
// Add adds a value to the cache, set or updating its assoicated
|
||||
// value.
|
||||
//
|
||||
// If MaxEntries is non-zero and the length of the cache is greater
|
||||
// after any addition, the least recently used value is evicted.
|
||||
func (c *Cache) Add(key Tuple, value interface{}) {
|
||||
if c.m == nil {
|
||||
c.m = make(map[Tuple]*list.Element)
|
||||
c.ll = list.New()
|
||||
}
|
||||
if ee, ok := c.m[key]; ok {
|
||||
c.ll.MoveToFront(ee)
|
||||
ee.Value.(*entry).value = value
|
||||
return
|
||||
}
|
||||
ele := c.ll.PushFront(&entry{key, value})
|
||||
c.m[key] = ele
|
||||
if c.MaxEntries != 0 && c.Len() > c.MaxEntries {
|
||||
c.RemoveOldest()
|
||||
}
|
||||
}
|
||||
|
||||
// Get looks up a key's value from the cache, also reporting
|
||||
// whether it was present.
|
||||
func (c *Cache) Get(key Tuple) (value interface{}, ok bool) {
|
||||
if ele, hit := c.m[key]; hit {
|
||||
c.ll.MoveToFront(ele)
|
||||
return ele.Value.(*entry).value, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Remove removes the provided key from the cache if it was present.
|
||||
func (c *Cache) Remove(key Tuple) {
|
||||
if ele, hit := c.m[key]; hit {
|
||||
c.removeElement(ele)
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveOldest removes the oldest item from the cache, if any.
|
||||
func (c *Cache) RemoveOldest() {
|
||||
if c.ll != nil {
|
||||
if ele := c.ll.Back(); ele != nil {
|
||||
c.removeElement(ele)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) removeElement(e *list.Element) {
|
||||
c.ll.Remove(e)
|
||||
delete(c.m, e.Value.(*entry).key)
|
||||
}
|
||||
|
||||
// Len returns the number of items in the cache.
|
||||
func (c *Cache) Len() int { return len(c.m) }
|
||||
82
net/flowtrack/flowtrack_test.go
Normal file
82
net/flowtrack/flowtrack_test.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// 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 flowtrack
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
c := &Cache{MaxEntries: 2}
|
||||
|
||||
k1 := Tuple{Src: netaddr.MustParseIPPort("1.1.1.1:1"), Dst: netaddr.MustParseIPPort("1.1.1.1:1")}
|
||||
k2 := Tuple{Src: netaddr.MustParseIPPort("1.1.1.1:1"), Dst: netaddr.MustParseIPPort("2.2.2.2:2")}
|
||||
k3 := Tuple{Src: netaddr.MustParseIPPort("1.1.1.1:1"), Dst: netaddr.MustParseIPPort("3.3.3.3:3")}
|
||||
k4 := Tuple{Src: netaddr.MustParseIPPort("1.1.1.1:1"), Dst: netaddr.MustParseIPPort("4.4.4.4:4")}
|
||||
|
||||
wantLen := func(want int) {
|
||||
t.Helper()
|
||||
if got := c.Len(); got != want {
|
||||
t.Fatalf("Len = %d; want %d", got, want)
|
||||
}
|
||||
}
|
||||
wantVal := func(key Tuple, want interface{}) {
|
||||
t.Helper()
|
||||
got, ok := c.Get(key)
|
||||
if !ok {
|
||||
t.Fatalf("Get(%q) failed; want value %v", key, want)
|
||||
}
|
||||
if got != want {
|
||||
t.Fatalf("Get(%q) = %v; want %v", key, got, want)
|
||||
}
|
||||
}
|
||||
wantMissing := func(key Tuple) {
|
||||
t.Helper()
|
||||
if got, ok := c.Get(key); ok {
|
||||
t.Fatalf("Get(%q) = %v; want absent from cache", key, got)
|
||||
}
|
||||
}
|
||||
|
||||
wantLen(0)
|
||||
c.RemoveOldest() // shouldn't panic
|
||||
c.Remove(k4) // shouldn't panic
|
||||
|
||||
c.Add(k1, 1)
|
||||
wantLen(1)
|
||||
c.Add(k2, 2)
|
||||
wantLen(2)
|
||||
c.Add(k3, 3)
|
||||
wantLen(2) // hit the max
|
||||
|
||||
wantMissing(k1)
|
||||
c.Remove(k1)
|
||||
wantLen(2) // no change; k1 should've been the deleted one per LRU
|
||||
|
||||
wantVal(k3, 3)
|
||||
|
||||
wantVal(k2, 2)
|
||||
c.Remove(k2)
|
||||
wantLen(1)
|
||||
wantMissing(k2)
|
||||
|
||||
c.Add(k3, 30)
|
||||
wantVal(k3, 30)
|
||||
wantLen(1)
|
||||
|
||||
allocs := int(testing.AllocsPerRun(1000, func() {
|
||||
got, ok := c.Get(k3)
|
||||
if !ok {
|
||||
t.Fatal("missing k3")
|
||||
}
|
||||
if got != 30 {
|
||||
t.Fatalf("got = %d; want 30", got)
|
||||
}
|
||||
}))
|
||||
if allocs != 0 {
|
||||
t.Errorf("allocs = %v; want 0", allocs)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
@@ -39,8 +40,11 @@ func Tailscale() (net.IP, *net.Interface, error) {
|
||||
continue
|
||||
}
|
||||
for _, a := range addrs {
|
||||
if ipnet, ok := a.(*net.IPNet); ok && IsTailscaleIP(ipnet.IP) {
|
||||
return ipnet.IP, &iface, nil
|
||||
if ipnet, ok := a.(*net.IPNet); ok {
|
||||
nip, ok := netaddr.FromStdIP(ipnet.IP)
|
||||
if ok && tsaddr.IsTailscaleIP(nip) {
|
||||
return ipnet.IP, &iface, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,19 +61,24 @@ func maybeTailscaleInterfaceName(s string) bool {
|
||||
strings.HasPrefix(s, "utun")
|
||||
}
|
||||
|
||||
// IsTailscaleIP reports whether ip is an IP in a range used by
|
||||
// Tailscale virtual network interfaces.
|
||||
func IsTailscaleIP(ip net.IP) bool {
|
||||
nip, _ := netaddr.FromStdIP(ip) // TODO: push this up to caller, change func signature
|
||||
return tsaddr.IsTailscaleIP(nip)
|
||||
}
|
||||
|
||||
func isUp(nif *net.Interface) bool { return nif.Flags&net.FlagUp != 0 }
|
||||
func isLoopback(nif *net.Interface) bool { return nif.Flags&net.FlagLoopback != 0 }
|
||||
|
||||
func isProblematicInterface(nif *net.Interface) bool {
|
||||
name := nif.Name
|
||||
// Don't try to send disco/etc packets over zerotier; they effectively
|
||||
// DoS each other by doing traffic amplification, both of them
|
||||
// preferring/trying to use each other for transport. See:
|
||||
// https://github.com/tailscale/tailscale/issues/1208
|
||||
if strings.HasPrefix(name, "zt") || (runtime.GOOS == "windows" && strings.Contains(name, "ZeroTier")) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// LocalAddresses returns the machine's IP addresses, separated by
|
||||
// whether they're loopback addresses.
|
||||
func LocalAddresses() (regular, loopback []string, err error) {
|
||||
func LocalAddresses() (regular, loopback []netaddr.IP, err error) {
|
||||
// TODO(crawshaw): don't serve interface addresses that we are routing
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
@@ -77,8 +86,10 @@ func LocalAddresses() (regular, loopback []string, err error) {
|
||||
}
|
||||
for i := range ifaces {
|
||||
iface := &ifaces[i]
|
||||
if !isUp(iface) {
|
||||
// Down interfaces don't count
|
||||
if !isUp(iface) || isProblematicInterface(iface) {
|
||||
// Skip down interfaces and ones that are
|
||||
// problematic that we don't want to try to
|
||||
// send Tailscale traffic over.
|
||||
continue
|
||||
}
|
||||
ifcIsLoopback := isLoopback(iface)
|
||||
@@ -106,16 +117,22 @@ func LocalAddresses() (regular, loopback []string, err error) {
|
||||
continue
|
||||
}
|
||||
if ip.IsLoopback() || ifcIsLoopback {
|
||||
loopback = append(loopback, ip.String())
|
||||
loopback = append(loopback, ip)
|
||||
} else {
|
||||
regular = append(regular, ip.String())
|
||||
regular = append(regular, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sortIPs(regular)
|
||||
sortIPs(loopback)
|
||||
return regular, loopback, nil
|
||||
}
|
||||
|
||||
func sortIPs(s []netaddr.IP) {
|
||||
sort.Slice(s, func(i, j int) bool { return s[i].Less(s[j]) })
|
||||
}
|
||||
|
||||
// Interface is a wrapper around Go's net.Interface with some extra methods.
|
||||
type Interface struct {
|
||||
*net.Interface
|
||||
@@ -124,8 +141,10 @@ type Interface struct {
|
||||
func (i Interface) IsLoopback() bool { return isLoopback(i.Interface) }
|
||||
func (i Interface) IsUp() bool { return isUp(i.Interface) }
|
||||
|
||||
// ForeachInterfaceAddress calls fn for each interface's address on the machine.
|
||||
func ForeachInterfaceAddress(fn func(Interface, netaddr.IP)) error {
|
||||
// ForeachInterfaceAddress calls fn for each interface's address on
|
||||
// the machine. The IPPrefix's IP is the IP address assigned to the
|
||||
// interface, and Bits are the subnet mask.
|
||||
func ForeachInterfaceAddress(fn func(Interface, netaddr.IPPrefix)) error {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -139,8 +158,8 @@ func ForeachInterfaceAddress(fn func(Interface, netaddr.IP)) error {
|
||||
for _, a := range addrs {
|
||||
switch v := a.(type) {
|
||||
case *net.IPNet:
|
||||
if ip, ok := netaddr.FromStdIP(v.IP); ok {
|
||||
fn(Interface{iface}, ip)
|
||||
if pfx, ok := netaddr.FromStdIPNet(v); ok {
|
||||
fn(Interface{iface}, pfx)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -148,11 +167,43 @@ func ForeachInterfaceAddress(fn func(Interface, netaddr.IP)) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForeachInterface calls fn for each interface on the machine, with
|
||||
// all its addresses. The IPPrefix's IP is the IP address assigned to
|
||||
// the interface, and Bits are the subnet mask.
|
||||
func ForeachInterface(fn func(Interface, []netaddr.IPPrefix)) error {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for i := range ifaces {
|
||||
iface := &ifaces[i]
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var pfxs []netaddr.IPPrefix
|
||||
for _, a := range addrs {
|
||||
switch v := a.(type) {
|
||||
case *net.IPNet:
|
||||
if pfx, ok := netaddr.FromStdIPNet(v); ok {
|
||||
pfxs = append(pfxs, pfx)
|
||||
}
|
||||
}
|
||||
}
|
||||
fn(Interface{iface}, pfxs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// State is intended to store the state of the machine's network interfaces,
|
||||
// routing table, and other network configuration.
|
||||
// For now it's pretty basic.
|
||||
type State struct {
|
||||
InterfaceIPs map[string][]netaddr.IP
|
||||
// InterfaceIPs maps from an interface name to the IP addresses
|
||||
// configured on that interface. Each address is represented as an
|
||||
// IPPrefix, where the IP is the interface IP address and Bits is
|
||||
// the subnet mask.
|
||||
InterfaceIPs map[string][]netaddr.IPPrefix
|
||||
InterfaceUp map[string]bool
|
||||
|
||||
// HaveV6Global is whether this machine has an IPv6 global address
|
||||
@@ -186,10 +237,9 @@ func (s *State) String() string {
|
||||
fmt.Fprintf(&sb, "interfaces.State{defaultRoute=%v ifs={", s.DefaultRouteInterface)
|
||||
ifs := make([]string, 0, len(s.InterfaceUp))
|
||||
for k := range s.InterfaceUp {
|
||||
if allLoopbackIPs(s.InterfaceIPs[k]) {
|
||||
continue
|
||||
if anyInterestingIP(s.InterfaceIPs[k]) {
|
||||
ifs = append(ifs, k)
|
||||
}
|
||||
ifs = append(ifs, k)
|
||||
}
|
||||
sort.Slice(ifs, func(i, j int) bool {
|
||||
upi, upj := s.InterfaceUp[ifs[i]], s.InterfaceUp[ifs[j]]
|
||||
@@ -206,14 +256,14 @@ func (s *State) String() string {
|
||||
if s.InterfaceUp[ifName] {
|
||||
fmt.Fprintf(&sb, "%s:[", ifName)
|
||||
needSpace := false
|
||||
for _, ip := range s.InterfaceIPs[ifName] {
|
||||
if ip.IsLinkLocalUnicast() {
|
||||
for _, pfx := range s.InterfaceIPs[ifName] {
|
||||
if !isInterestingIP(pfx.IP) {
|
||||
continue
|
||||
}
|
||||
if needSpace {
|
||||
sb.WriteString(" ")
|
||||
}
|
||||
fmt.Fprintf(&sb, "%s", ip)
|
||||
fmt.Fprintf(&sb, "%s", pfx)
|
||||
needSpace = true
|
||||
}
|
||||
sb.WriteString("]")
|
||||
@@ -247,20 +297,60 @@ func (s *State) AnyInterfaceUp() bool {
|
||||
return s != nil && (s.HaveV4 || s.HaveV6Global)
|
||||
}
|
||||
|
||||
// RemoveUninterestingInterfacesAndAddresses removes uninteresting IPs
|
||||
// from InterfaceIPs, also removing from both the InterfaceIPs and
|
||||
// InterfaceUp map any interfaces that don't have any interesting IPs.
|
||||
func (s *State) RemoveUninterestingInterfacesAndAddresses() {
|
||||
for ifName := range s.InterfaceUp {
|
||||
ips := s.InterfaceIPs[ifName]
|
||||
keep := ips[:0]
|
||||
for _, pfx := range ips {
|
||||
if isInterestingIP(pfx.IP) {
|
||||
keep = append(keep, pfx)
|
||||
}
|
||||
}
|
||||
if len(keep) == 0 {
|
||||
delete(s.InterfaceUp, ifName)
|
||||
delete(s.InterfaceIPs, ifName)
|
||||
continue
|
||||
}
|
||||
if len(keep) < len(ips) {
|
||||
s.InterfaceIPs[ifName] = keep
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveTailscaleInterfaces modifes s to remove any interfaces that
|
||||
// are owned by this process. (TODO: make this true; currently it
|
||||
// makes the Linux-only assumption that the interface is named
|
||||
// /^tailscale/)
|
||||
// uses some heuristics)
|
||||
func (s *State) RemoveTailscaleInterfaces() {
|
||||
for name := range s.InterfaceIPs {
|
||||
if isTailscaleInterfaceName(name) {
|
||||
for name, pfxs := range s.InterfaceIPs {
|
||||
if isTailscaleInterface(name, pfxs) {
|
||||
delete(s.InterfaceIPs, name)
|
||||
delete(s.InterfaceUp, name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isTailscaleInterfaceName(name string) bool {
|
||||
func hasTailscaleIP(pfxs []netaddr.IPPrefix) bool {
|
||||
for _, pfx := range pfxs {
|
||||
if tsaddr.IsTailscaleIP(pfx.IP) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isTailscaleInterface(name string, ips []netaddr.IPPrefix) bool {
|
||||
if runtime.GOOS == "darwin" && strings.HasPrefix(name, "utun") && hasTailscaleIP(ips) {
|
||||
// On macOS in the sandboxed app (at least as of
|
||||
// 2021-02-25), we often see two utun devices
|
||||
// (e.g. utun4 and utun7) with the same IPv4 and IPv6
|
||||
// addresses. Just remove all utun devices with
|
||||
// Tailscale IPs until we know what's happening with
|
||||
// macOS NetworkExtensions and utun devices.
|
||||
return true
|
||||
}
|
||||
return name == "Tailscale" || // as it is on Windows
|
||||
strings.HasPrefix(name, "tailscale") // TODO: use --tun flag value, etc; see TODO in method doc
|
||||
}
|
||||
@@ -273,20 +363,27 @@ var getPAC func() string
|
||||
// It does not set the returned State.IsExpensive. The caller can populate that.
|
||||
func GetState() (*State, error) {
|
||||
s := &State{
|
||||
InterfaceIPs: make(map[string][]netaddr.IP),
|
||||
InterfaceIPs: make(map[string][]netaddr.IPPrefix),
|
||||
InterfaceUp: make(map[string]bool),
|
||||
}
|
||||
if err := ForeachInterfaceAddress(func(ni Interface, ip netaddr.IP) {
|
||||
if err := ForeachInterface(func(ni Interface, pfxs []netaddr.IPPrefix) {
|
||||
ifUp := ni.IsUp()
|
||||
s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], ip)
|
||||
s.InterfaceUp[ni.Name] = ifUp
|
||||
if ifUp && !ip.IsLoopback() && !ip.IsLinkLocalUnicast() && !isTailscaleInterfaceName(ni.Name) {
|
||||
s.HaveV6Global = s.HaveV6Global || isGlobalV6(ip)
|
||||
s.HaveV4 = s.HaveV4 || ip.Is4()
|
||||
s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], pfxs...)
|
||||
if !ifUp || isTailscaleInterface(ni.Name, pfxs) {
|
||||
return
|
||||
}
|
||||
for _, pfx := range pfxs {
|
||||
if pfx.IP.IsLoopback() || pfx.IP.IsLinkLocalUnicast() {
|
||||
continue
|
||||
}
|
||||
s.HaveV6Global = s.HaveV6Global || isGlobalV6(pfx.IP)
|
||||
s.HaveV4 = s.HaveV4 || pfx.IP.Is4()
|
||||
}
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.DefaultRouteInterface, _ = DefaultRouteInterface()
|
||||
|
||||
if s.AnyInterfaceUp() {
|
||||
@@ -316,7 +413,8 @@ func HTTPOfListener(ln net.Listener) string {
|
||||
|
||||
var goodIP string
|
||||
var privateIP string
|
||||
ForeachInterfaceAddress(func(i Interface, ip netaddr.IP) {
|
||||
ForeachInterfaceAddress(func(i Interface, pfx netaddr.IPPrefix) {
|
||||
ip := pfx.IP
|
||||
if isPrivateIP(ip) {
|
||||
if privateIP == "" {
|
||||
privateIP = ip.String()
|
||||
@@ -352,7 +450,8 @@ func LikelyHomeRouterIP() (gateway, myIP netaddr.IP, ok bool) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
ForeachInterfaceAddress(func(i Interface, ip netaddr.IP) {
|
||||
ForeachInterfaceAddress(func(i Interface, pfx netaddr.IPPrefix) {
|
||||
ip := pfx.IP
|
||||
if !i.IsUp() || ip.IsZero() || !myIP.IsZero() {
|
||||
return
|
||||
}
|
||||
@@ -392,14 +491,23 @@ var (
|
||||
v6Global1 = mustCIDR("2000::/3")
|
||||
)
|
||||
|
||||
func allLoopbackIPs(ips []netaddr.IP) bool {
|
||||
if len(ips) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, ip := range ips {
|
||||
if !ip.IsLoopback() {
|
||||
return false
|
||||
// anyInterestingIP reports whether pfxs contains any IP that matches
|
||||
// isInterestingIP.
|
||||
func anyInterestingIP(pfxs []netaddr.IPPrefix) bool {
|
||||
for _, pfx := range pfxs {
|
||||
if isInterestingIP(pfx.IP) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isInterestingIP reports whether ip is an interesting IP that we
|
||||
// should log in interfaces.State logging. We don't need to show
|
||||
// localhost or link-local addresses.
|
||||
func isInterestingIP(ip netaddr.IP) bool {
|
||||
if ip.IsLoopback() || ip.IsLinkLocalUnicast() {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -6,9 +6,13 @@ package interfaces
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"go4.org/mem"
|
||||
"golang.org/x/net/route"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/util/lineread"
|
||||
"tailscale.com/version"
|
||||
@@ -72,3 +76,65 @@ func likelyHomeRouterIPDarwinExec() (ret netaddr.IP, ok bool) {
|
||||
}
|
||||
|
||||
var errStopReadingNetstatTable = errors.New("found private gateway")
|
||||
|
||||
func DefaultRouteInterface() (string, error) {
|
||||
idx, err := DefaultRouteInterfaceIndex()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
iface, err := net.InterfaceByIndex(idx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return iface.Name, nil
|
||||
}
|
||||
|
||||
func DefaultRouteInterfaceIndex() (int, error) {
|
||||
// $ netstat -nr
|
||||
// Routing tables
|
||||
// Internet:
|
||||
// Destination Gateway Flags Netif Expire
|
||||
// default 10.0.0.1 UGSc en0 <-- want this one
|
||||
// default 10.0.0.1 UGScI en1
|
||||
|
||||
// From man netstat:
|
||||
// U RTF_UP Route usable
|
||||
// G RTF_GATEWAY Destination requires forwarding by intermediary
|
||||
// S RTF_STATIC Manually added
|
||||
// c RTF_PRCLONING Protocol-specified generate new routes on use
|
||||
// I RTF_IFSCOPE Route is associated with an interface scope
|
||||
|
||||
rib, err := route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route.FetchRIB: %w", err)
|
||||
}
|
||||
msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route.ParseRIB: %w", err)
|
||||
}
|
||||
indexSeen := map[int]int{} // index => count
|
||||
for _, m := range msgs {
|
||||
rm, ok := m.(*route.RouteMessage)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
const RTF_GATEWAY = 0x2
|
||||
const RTF_IFSCOPE = 0x1000000
|
||||
if rm.Flags&RTF_GATEWAY == 0 {
|
||||
continue
|
||||
}
|
||||
if rm.Flags&RTF_IFSCOPE != 0 {
|
||||
continue
|
||||
}
|
||||
indexSeen[rm.Index]++
|
||||
}
|
||||
if len(indexSeen) == 0 {
|
||||
return 0, errors.New("no gateway index found")
|
||||
}
|
||||
if len(indexSeen) == 1 {
|
||||
for idx := range indexSeen {
|
||||
return idx, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("ambiguous gateway interfaces found: %v", indexSeen)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ package interfaces
|
||||
|
||||
// privateGatewayIPFromRoute returns the private gateway ip address from rtm, if it exists.
|
||||
// Otherwise, it returns 0.
|
||||
int privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
|
||||
uint32_t privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
|
||||
{
|
||||
// sockaddrs are after the message header
|
||||
struct sockaddr* dst_sa = (struct sockaddr *)(rtm + 1);
|
||||
@@ -38,7 +38,7 @@ int privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
|
||||
return 0; // gateway not IPv4
|
||||
|
||||
struct sockaddr_in* gateway_si= (struct sockaddr_in *)gateway_sa;
|
||||
int ip;
|
||||
uint32_t ip;
|
||||
ip = gateway_si->sin_addr.s_addr;
|
||||
|
||||
unsigned char a, b;
|
||||
@@ -62,7 +62,7 @@ int privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
|
||||
// If no private gateway IP address was found, it returns 0.
|
||||
// On an error, it returns an error code in (0, 255].
|
||||
// Any private gateway IP address is > 255.
|
||||
int privateGatewayIP()
|
||||
uint32_t privateGatewayIP()
|
||||
{
|
||||
size_t needed;
|
||||
int mib[6];
|
||||
@@ -90,7 +90,7 @@ int privateGatewayIP()
|
||||
struct rt_msghdr2 *rtm;
|
||||
for (next = buf; next < lim; next += rtm->rtm_msglen) {
|
||||
rtm = (struct rt_msghdr2 *)next;
|
||||
int ip;
|
||||
uint32_t ip;
|
||||
ip = privateGatewayIPFromRoute(rtm);
|
||||
if (ip) {
|
||||
free(buf);
|
||||
@@ -105,6 +105,7 @@ import "C"
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"log"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
@@ -116,6 +117,7 @@ func init() {
|
||||
func likelyHomeRouterIPDarwinSyscall() (ret netaddr.IP, ok bool) {
|
||||
ip := C.privateGatewayIP()
|
||||
if ip < 255 {
|
||||
log.Printf("likelyHomeRouterIPDarwinSyscall: error code %v", ip)
|
||||
return netaddr.IP{}, false
|
||||
}
|
||||
var q [4]byte
|
||||
|
||||
17
net/interfaces/interfaces_default_route_test.go
Normal file
17
net/interfaces/interfaces_default_route_test.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.
|
||||
|
||||
// +build linux darwin,!redo
|
||||
|
||||
package interfaces
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDefaultRouteInterface(t *testing.T) {
|
||||
v, err := DefaultRouteInterface()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("got %q", v)
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !linux,!windows
|
||||
// +build !linux,!windows,!darwin
|
||||
|
||||
package interfaces
|
||||
|
||||
|
||||
@@ -5,30 +5,9 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsTailscaleIP(t *testing.T) {
|
||||
tests := []struct {
|
||||
ip string
|
||||
want bool
|
||||
}{
|
||||
{"100.81.251.94", true},
|
||||
{"8.8.8.8", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
ip := net.ParseIP(tt.ip)
|
||||
if ip == nil {
|
||||
t.Fatalf("failed to parse IP %q", tt.ip)
|
||||
}
|
||||
got := IsTailscaleIP(ip)
|
||||
if got != tt.want {
|
||||
t.Errorf("F(%q) = %v; want %v", tt.ip, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetState(t *testing.T) {
|
||||
st, err := GetState()
|
||||
if err != nil {
|
||||
|
||||
@@ -8,9 +8,7 @@ package netcheck
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -25,11 +23,11 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/tcnksm/go-httpstat"
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -68,12 +66,6 @@ const (
|
||||
// more aggressive than defaultActiveRetransmitTime. A few extra
|
||||
// packets at startup is fine.
|
||||
defaultInitialRetransmitTime = 100 * time.Millisecond
|
||||
// portMapServiceProbeTimeout is the time we wait for port mapping
|
||||
// services (UPnP, NAT-PMP, PCP) to respond before we give up and
|
||||
// decide that they're not there. Since these services are on the
|
||||
// same LAN as this machine and a single L3 hop away, we don't
|
||||
// give them much time to respond.
|
||||
portMapServiceProbeTimeout = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
type Report struct {
|
||||
@@ -160,6 +152,10 @@ type Client struct {
|
||||
// It defaults to ":0".
|
||||
UDPBindAddr string
|
||||
|
||||
// PortMapper, if non-nil, is used for portmap queries.
|
||||
// If nil, portmap discovery is not done.
|
||||
PortMapper *portmapper.Client // lazily initialized on first use
|
||||
|
||||
mu sync.Mutex // guards following
|
||||
nextFull bool // do a full region scan, even if last != nil
|
||||
prev map[time.Time]*Report // some previous reports
|
||||
@@ -219,8 +215,8 @@ func (c *Client) handleHairSTUNLocked(pkt []byte, src netaddr.IPPort) bool {
|
||||
// (non-incremental) probe of all DERP regions.
|
||||
func (c *Client) MakeNextReportFull() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.nextFull = true
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Client) ReceiveSTUNPacket(pkt []byte, src netaddr.IPPort) {
|
||||
@@ -309,6 +305,9 @@ type probePlan map[string][]probe
|
||||
func sortRegions(dm *tailcfg.DERPMap, last *Report) (prev []*tailcfg.DERPRegion) {
|
||||
prev = make([]*tailcfg.DERPRegion, 0, len(dm.Regions))
|
||||
for _, reg := range dm.Regions {
|
||||
if reg.Avoid {
|
||||
continue
|
||||
}
|
||||
prev = append(prev, reg)
|
||||
}
|
||||
sort.Slice(prev, func(i, j int) bool {
|
||||
@@ -363,7 +362,7 @@ func makeProbePlan(dm *tailcfg.DERPMap, ifState *interfaces.State, last *Report)
|
||||
tries = 2
|
||||
} else if hadBoth {
|
||||
// For dual stack machines, make the 3rd & slower nodes alternate
|
||||
// breetween
|
||||
// beetween.
|
||||
if ri%2 == 0 {
|
||||
do4, do6 = true, false
|
||||
} else {
|
||||
@@ -374,6 +373,12 @@ func makeProbePlan(dm *tailcfg.DERPMap, ifState *interfaces.State, last *Report)
|
||||
do6 = false
|
||||
}
|
||||
|
||||
if reg.RegionID == last.PreferredDERP {
|
||||
// But if we already had a DERP home, try extra hard to
|
||||
// make sure it's there so we don't flip flop around.
|
||||
tries = 4
|
||||
}
|
||||
|
||||
for try := 0; try < tries; try++ {
|
||||
if len(reg.Nodes) == 0 {
|
||||
// Shouldn't be possible.
|
||||
@@ -388,6 +393,9 @@ func makeProbePlan(dm *tailcfg.DERPMap, ifState *interfaces.State, last *Report)
|
||||
prevLatency = defaultActiveRetransmitTime
|
||||
}
|
||||
delay := time.Duration(try) * prevLatency
|
||||
if try > 1 {
|
||||
delay += time.Duration(try) * 50 * time.Millisecond
|
||||
}
|
||||
if do4 {
|
||||
p4 = append(p4, probe{delay: delay, node: n.Name, proto: probeIPv4})
|
||||
}
|
||||
@@ -680,104 +688,20 @@ func (rs *reportState) setOptBool(b *opt.Bool, v bool) {
|
||||
|
||||
func (rs *reportState) probePortMapServices() {
|
||||
defer rs.waitPortMap.Done()
|
||||
gw, myIP, ok := interfaces.LikelyHomeRouterIP()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
rs.setOptBool(&rs.report.UPnP, false)
|
||||
rs.setOptBool(&rs.report.PMP, false)
|
||||
rs.setOptBool(&rs.report.PCP, false)
|
||||
|
||||
port1900 := netaddr.IPPort{IP: gw, Port: 1900}.UDPAddr()
|
||||
port5351 := netaddr.IPPort{IP: gw, Port: 5351}.UDPAddr()
|
||||
|
||||
rs.c.logf("[v1] probePortMapServices: me %v -> gw %v", myIP, gw)
|
||||
|
||||
// Create a UDP4 socket used just for querying for UPnP, NAT-PMP, and PCP.
|
||||
uc, err := netns.Listener().ListenPacket(context.Background(), "udp4", ":0")
|
||||
res, err := rs.c.PortMapper.Probe(context.Background())
|
||||
if err != nil {
|
||||
rs.c.logf("probePortMapServices: %v", err)
|
||||
return
|
||||
}
|
||||
defer uc.Close()
|
||||
tempPort := uc.LocalAddr().(*net.UDPAddr).Port
|
||||
uc.SetReadDeadline(time.Now().Add(portMapServiceProbeTimeout))
|
||||
|
||||
// Send request packets for all three protocols.
|
||||
uc.WriteTo(uPnPPacket, port1900)
|
||||
uc.WriteTo(pmpPacket, port5351)
|
||||
uc.WriteTo(pcpPacket(myIP, tempPort, false), port5351)
|
||||
|
||||
res := make([]byte, 1500)
|
||||
sentPCPDelete := false
|
||||
for {
|
||||
n, addr, err := uc.ReadFrom(res)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
switch addr.(*net.UDPAddr).Port {
|
||||
case 1900:
|
||||
if mem.Contains(mem.B(res[:n]), mem.S(":InternetGatewayDevice:")) {
|
||||
rs.setOptBool(&rs.report.UPnP, true)
|
||||
}
|
||||
case 5351:
|
||||
if n == 12 && res[0] == 0x00 { // right length and version 0
|
||||
rs.setOptBool(&rs.report.PMP, true)
|
||||
}
|
||||
if n == 60 && res[0] == 0x02 { // right length and version 2
|
||||
rs.setOptBool(&rs.report.PCP, true)
|
||||
|
||||
if !sentPCPDelete {
|
||||
sentPCPDelete = true
|
||||
// And now delete the mapping.
|
||||
// (PCP is the only protocol of the three that requires
|
||||
// we cause a side effect to detect whether it's present,
|
||||
// so we need to redo that side effect now.)
|
||||
uc.WriteTo(pcpPacket(myIP, tempPort, true), port5351)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var pmpPacket = []byte{0, 0} // version 0, opcode 0 = "Public address request"
|
||||
|
||||
var uPnPPacket = []byte("M-SEARCH * HTTP/1.1\r\n" +
|
||||
"HOST: 239.255.255.250:1900\r\n" +
|
||||
"ST: ssdp:all\r\n" +
|
||||
"MAN: \"ssdp:discover\"\r\n" +
|
||||
"MX: 2\r\n\r\n")
|
||||
|
||||
var v4unspec, _ = netaddr.ParseIP("0.0.0.0")
|
||||
|
||||
// pcpPacket generates a PCP packet with a MAP opcode.
|
||||
func pcpPacket(myIP netaddr.IP, mapToLocalPort int, delete bool) []byte {
|
||||
const udpProtoNumber = 17
|
||||
lifetimeSeconds := uint32(1)
|
||||
if delete {
|
||||
lifetimeSeconds = 0
|
||||
}
|
||||
const opMap = 1
|
||||
|
||||
// 24 byte header + 36 byte map opcode
|
||||
pkt := make([]byte, (32+32+128)/8+(96+8+24+16+16+128)/8)
|
||||
|
||||
// The header (https://tools.ietf.org/html/rfc6887#section-7.1)
|
||||
pkt[0] = 2 // version
|
||||
pkt[1] = opMap
|
||||
binary.BigEndian.PutUint32(pkt[4:8], lifetimeSeconds)
|
||||
myIP16 := myIP.As16()
|
||||
copy(pkt[8:], myIP16[:])
|
||||
|
||||
// The map opcode body (https://tools.ietf.org/html/rfc6887#section-11.1)
|
||||
mapOp := pkt[24:]
|
||||
rand.Read(mapOp[:12]) // 96 bit mappping nonce
|
||||
mapOp[12] = udpProtoNumber
|
||||
binary.BigEndian.PutUint16(mapOp[16:], uint16(mapToLocalPort))
|
||||
v4unspec16 := v4unspec.As16()
|
||||
copy(mapOp[20:], v4unspec16[:])
|
||||
return pkt
|
||||
rs.setOptBool(&rs.report.UPnP, res.UPnP)
|
||||
rs.setOptBool(&rs.report.PMP, res.PMP)
|
||||
rs.setOptBool(&rs.report.PCP, res.PCP)
|
||||
}
|
||||
|
||||
func newReport() *Report {
|
||||
@@ -854,7 +778,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
||||
}
|
||||
defer rs.pc4Hair.Close()
|
||||
|
||||
if !c.SkipExternalNetwork {
|
||||
if !c.SkipExternalNetwork && c.PortMapper != nil {
|
||||
rs.waitPortMap.Add(1)
|
||||
go rs.probePortMapServices()
|
||||
}
|
||||
@@ -927,7 +851,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
|
||||
|
||||
rs.waitHairCheck(ctx)
|
||||
c.vlogf("hairCheck done")
|
||||
if !c.SkipExternalNetwork {
|
||||
if !c.SkipExternalNetwork && c.PortMapper != nil {
|
||||
rs.waitPortMap.Wait()
|
||||
c.vlogf("portMap done")
|
||||
}
|
||||
@@ -1102,6 +1026,10 @@ func (c *Client) addReportHistoryAndSetPreferredDERP(r *Report) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
var prevDERP int
|
||||
if c.last != nil {
|
||||
prevDERP = c.last.PreferredDERP
|
||||
}
|
||||
if c.prev == nil {
|
||||
c.prev = map[time.Time]*Report{}
|
||||
}
|
||||
@@ -1119,9 +1047,9 @@ func (c *Client) addReportHistoryAndSetPreferredDERP(r *Report) {
|
||||
delete(c.prev, t)
|
||||
continue
|
||||
}
|
||||
for hp, d := range pr.RegionLatency {
|
||||
if bd, ok := bestRecent[hp]; !ok || d < bd {
|
||||
bestRecent[hp] = d
|
||||
for regionID, d := range pr.RegionLatency {
|
||||
if bd, ok := bestRecent[regionID]; !ok || d < bd {
|
||||
bestRecent[regionID] = d
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1129,13 +1057,27 @@ func (c *Client) addReportHistoryAndSetPreferredDERP(r *Report) {
|
||||
// Then, pick which currently-alive DERP server from the
|
||||
// current report has the best latency over the past maxAge.
|
||||
var bestAny time.Duration
|
||||
for hp := range r.RegionLatency {
|
||||
best := bestRecent[hp]
|
||||
var oldRegionCurLatency time.Duration
|
||||
for regionID, d := range r.RegionLatency {
|
||||
if regionID == prevDERP {
|
||||
oldRegionCurLatency = d
|
||||
}
|
||||
best := bestRecent[regionID]
|
||||
if r.PreferredDERP == 0 || best < bestAny {
|
||||
bestAny = best
|
||||
r.PreferredDERP = hp
|
||||
r.PreferredDERP = regionID
|
||||
}
|
||||
}
|
||||
|
||||
// If we're changing our preferred DERP but the old one's still
|
||||
// accessible and the new one's not much better, just stick with
|
||||
// where we are.
|
||||
if prevDERP != 0 &&
|
||||
r.PreferredDERP != prevDERP &&
|
||||
oldRegionCurLatency != 0 &&
|
||||
bestAny > oldRegionCurLatency/3*2 {
|
||||
r.PreferredDERP = prevDERP
|
||||
}
|
||||
}
|
||||
|
||||
func updateLatency(m map[int]time.Duration, regionID int, d time.Duration) {
|
||||
|
||||
@@ -195,6 +195,24 @@ func TestAddReportHistoryAndSetPreferredDERP(t *testing.T) {
|
||||
wantPrevLen: 1, // t=[0123]s all gone. (too old, older than 10 min)
|
||||
wantDERP: 3, // only option
|
||||
},
|
||||
{
|
||||
name: "preferred_derp_hysteresis_no_switch",
|
||||
steps: []step{
|
||||
{0 * time.Second, report("d1", 4, "d2", 5)},
|
||||
{1 * time.Second, report("d1", 4, "d2", 3)},
|
||||
},
|
||||
wantPrevLen: 2,
|
||||
wantDERP: 1, // 2 didn't get fast enough
|
||||
},
|
||||
{
|
||||
name: "preferred_derp_hysteresis_do_switch",
|
||||
steps: []step{
|
||||
{0 * time.Second, report("d1", 4, "d2", 5)},
|
||||
{1 * time.Second, report("d1", 4, "d2", 1)},
|
||||
},
|
||||
wantPrevLen: 2,
|
||||
wantDERP: 2, // 2 got fast enough
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -392,6 +410,35 @@ func TestMakeProbePlan(t *testing.T) {
|
||||
"region-5-v6": []probe{p("5a", 6), p("5b", 6, 100*ms), p("5c", 6, 200*ms)},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "try_harder_for_preferred_derp",
|
||||
dm: basicMap,
|
||||
have6if: true,
|
||||
last: &Report{
|
||||
RegionLatency: map[int]time.Duration{
|
||||
1: 10 * time.Millisecond,
|
||||
2: 20 * time.Millisecond,
|
||||
3: 30 * time.Millisecond,
|
||||
4: 40 * time.Millisecond,
|
||||
},
|
||||
RegionV4Latency: map[int]time.Duration{
|
||||
1: 10 * time.Millisecond,
|
||||
2: 20 * time.Millisecond,
|
||||
},
|
||||
RegionV6Latency: map[int]time.Duration{
|
||||
3: 30 * time.Millisecond,
|
||||
4: 40 * time.Millisecond,
|
||||
},
|
||||
PreferredDERP: 1,
|
||||
},
|
||||
want: probePlan{
|
||||
"region-1-v4": []probe{p("1a", 4), p("1a", 4, 12*ms), p("1a", 4, 124*ms), p("1a", 4, 186*ms)},
|
||||
"region-1-v6": []probe{p("1a", 6), p("1a", 6, 12*ms), p("1a", 6, 124*ms), p("1a", 6, 186*ms)},
|
||||
"region-2-v4": []probe{p("2a", 4), p("2b", 4, 24*ms)},
|
||||
"region-2-v6": []probe{p("2a", 6), p("2b", 6, 24*ms)},
|
||||
"region-3-v4": []probe{p("3a", 4)},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -560,3 +607,41 @@ func TestLogConciseReport(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSortRegions(t *testing.T) {
|
||||
unsortedMap := &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{},
|
||||
}
|
||||
for rid := 1; rid <= 5; rid++ {
|
||||
var nodes []*tailcfg.DERPNode
|
||||
nodes = append(nodes, &tailcfg.DERPNode{
|
||||
Name: fmt.Sprintf("%da", rid),
|
||||
RegionID: rid,
|
||||
HostName: fmt.Sprintf("derp%d-1", rid),
|
||||
IPv4: fmt.Sprintf("%d.0.0.1", rid),
|
||||
IPv6: fmt.Sprintf("%d::1", rid),
|
||||
})
|
||||
unsortedMap.Regions[rid] = &tailcfg.DERPRegion{
|
||||
RegionID: rid,
|
||||
Nodes: nodes,
|
||||
}
|
||||
}
|
||||
report := newReport()
|
||||
report.RegionLatency[1] = time.Second * time.Duration(5)
|
||||
report.RegionLatency[2] = time.Second * time.Duration(3)
|
||||
report.RegionLatency[3] = time.Second * time.Duration(6)
|
||||
report.RegionLatency[4] = time.Second * time.Duration(0)
|
||||
report.RegionLatency[5] = time.Second * time.Duration(2)
|
||||
sortedMap := sortRegions(unsortedMap, report)
|
||||
|
||||
// Sorting by latency this should result in rid: 5, 2, 1, 3
|
||||
// rid 4 with latency 0 should be at the end
|
||||
want := []int{5, 2, 1, 3, 4}
|
||||
got := make([]int, len(sortedMap))
|
||||
for i, r := range sortedMap {
|
||||
got[i] = r.RegionID
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("got %v; want %v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
52
net/netns/netns_darwin_tailscaled.go
Normal file
52
net/netns/netns_darwin_tailscaled.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin,!redo
|
||||
|
||||
package netns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/net/interfaces"
|
||||
)
|
||||
|
||||
// control marks c as necessary to dial in a separate network namespace.
|
||||
//
|
||||
// It's intentionally the same signature as net.Dialer.Control
|
||||
// and net.ListenConfig.Control.
|
||||
func control(network, address string, c syscall.RawConn) error {
|
||||
if strings.HasPrefix(address, "127.") || address == "::1" {
|
||||
// Don't bind to an interface for localhost connections.
|
||||
return nil
|
||||
}
|
||||
idx, err := interfaces.DefaultRouteInterfaceIndex()
|
||||
if err != nil {
|
||||
log.Printf("netns: DefaultRouteInterfaceIndex: %v", err)
|
||||
return nil
|
||||
}
|
||||
v6 := strings.Contains(address, "]:") || strings.HasSuffix(network, "6") // hacky test for v6
|
||||
proto := unix.IPPROTO_IP
|
||||
opt := unix.IP_BOUND_IF
|
||||
if v6 {
|
||||
proto = unix.IPPROTO_IPV6
|
||||
opt = unix.IPV6_BOUND_IF
|
||||
}
|
||||
|
||||
var sockErr error
|
||||
err = c.Control(func(fd uintptr) {
|
||||
sockErr = unix.SetsockoptInt(int(fd), proto, opt, idx)
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("RawConn.Control on %T: %w", c, err)
|
||||
}
|
||||
if sockErr != nil {
|
||||
log.Printf("netns: control(%q, %q), v6=%v, index=%v: %v", network, address, v6, idx, sockErr)
|
||||
}
|
||||
return sockErr
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !linux,!windows
|
||||
// +build !linux,!windows,!darwin darwin,redo
|
||||
|
||||
package netns
|
||||
|
||||
|
||||
42
net/netns/netns_test.go
Normal file
42
net/netns/netns_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package netns contains the common code for using the Go net package
|
||||
// in a logical "network namespace" to avoid routing loops where
|
||||
// Tailscale-created packets would otherwise loop back through
|
||||
// Tailscale routes.
|
||||
//
|
||||
// Despite the name netns, the exact mechanism used differs by
|
||||
// operating system, and perhaps even by version of the OS.
|
||||
//
|
||||
// The netns package also handles connecting via SOCKS proxies when
|
||||
// configured by the environment.
|
||||
package netns
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var extNetwork = flag.Bool("use-external-network", false, "use the external network in tests")
|
||||
|
||||
func TestDial(t *testing.T) {
|
||||
if !*extNetwork {
|
||||
t.Skip("skipping test without --use-external-network")
|
||||
}
|
||||
d := NewDialer()
|
||||
c, err := d.Dial("tcp", "google.com:80")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
t.Logf("got addr %v", c.RemoteAddr())
|
||||
|
||||
c, err = d.Dial("tcp4", "google.com:80")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
t.Logf("got addr %v", c.RemoteAddr())
|
||||
}
|
||||
@@ -60,7 +60,7 @@ func (p *Pipe) Read(b []byte) (n int, err error) {
|
||||
for {
|
||||
p.mu.Lock()
|
||||
closed := p.closed
|
||||
timedout := !p.readTimeout.IsZero() && time.Now().After(p.readTimeout)
|
||||
timedout := !p.readTimeout.IsZero() && !time.Now().Before(p.readTimeout)
|
||||
blocked := p.blocked
|
||||
if !closed && !timedout && len(p.buf) > 0 {
|
||||
n2 := copy(b, p.buf)
|
||||
@@ -99,7 +99,7 @@ func (p *Pipe) Write(b []byte) (n int, err error) {
|
||||
for {
|
||||
p.mu.Lock()
|
||||
closed := p.closed
|
||||
timedout := !p.writeTimeout.IsZero() && time.Now().After(p.writeTimeout)
|
||||
timedout := !p.writeTimeout.IsZero() && !time.Now().Before(p.writeTimeout)
|
||||
blocked := p.blocked
|
||||
if !closed && !timedout {
|
||||
n2 := len(b)
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestPipeTimeout(t *testing.T) {
|
||||
p := NewPipe("p1", 1<<16)
|
||||
p.SetWriteDeadline(time.Now().Add(-1 * time.Second))
|
||||
n, err := p.Write([]byte{'h'})
|
||||
if err == nil || !errors.Is(err, ErrWriteTimeout) || !errors.Is(err, ErrTimeout) {
|
||||
if !errors.Is(err, ErrWriteTimeout) || !errors.Is(err, ErrTimeout) {
|
||||
t.Errorf("missing write timeout got err: %v", err)
|
||||
}
|
||||
if n != 0 {
|
||||
@@ -49,7 +49,7 @@ func TestPipeTimeout(t *testing.T) {
|
||||
p.SetReadDeadline(time.Now().Add(-1 * time.Second))
|
||||
b := make([]byte, 1)
|
||||
n, err := p.Read(b)
|
||||
if err == nil || !errors.Is(err, ErrReadTimeout) || !errors.Is(err, ErrTimeout) {
|
||||
if !errors.Is(err, ErrReadTimeout) || !errors.Is(err, ErrTimeout) {
|
||||
t.Errorf("missing read timeout got err: %v", err)
|
||||
}
|
||||
if n != 0 {
|
||||
@@ -65,7 +65,7 @@ func TestPipeTimeout(t *testing.T) {
|
||||
if err := p.Block(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := p.Write([]byte{'h'}); err == nil || !errors.Is(err, ErrWriteTimeout) {
|
||||
if _, err := p.Write([]byte{'h'}); !errors.Is(err, ErrWriteTimeout) {
|
||||
t.Fatalf("want write timeout got: %v", err)
|
||||
}
|
||||
})
|
||||
@@ -80,11 +80,10 @@ func TestPipeTimeout(t *testing.T) {
|
||||
if err := p.Block(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := p.Read(b); err == nil || !errors.Is(err, ErrReadTimeout) {
|
||||
if _, err := p.Read(b); !errors.Is(err, ErrReadTimeout) {
|
||||
t.Fatalf("want read timeout got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func TestLimit(t *testing.T) {
|
||||
@@ -117,4 +116,8 @@ func TestLimit(t *testing.T) {
|
||||
} else if n != 1 {
|
||||
t.Errorf("Read(%q): n=%d want 1", string(b), n)
|
||||
}
|
||||
|
||||
if err := <-errCh; err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,9 +36,6 @@ type Header interface {
|
||||
// purpose of computing length and checksum fields. Marshal
|
||||
// implementations must not allocate memory.
|
||||
Marshal(buf []byte) error
|
||||
// ToResponse transforms the header into one for a response packet.
|
||||
// For instance, this swaps the source and destination IPs.
|
||||
ToResponse()
|
||||
}
|
||||
|
||||
// Generate generates a new packet with the given Header and
|
||||
|
||||
@@ -24,6 +24,17 @@ const (
|
||||
TCP IPProto = 0x06
|
||||
UDP IPProto = 0x11
|
||||
|
||||
// TSMP is the Tailscale Message Protocol (our ICMP-ish
|
||||
// thing), an IP protocol used only between Tailscale nodes
|
||||
// (still encrypted by WireGuard) that communicates why things
|
||||
// failed, etc.
|
||||
//
|
||||
// Proto number 99 is reserved for "any private encryption
|
||||
// scheme". We never accept these from the host OS stack nor
|
||||
// send them to the host network stack. It's only used between
|
||||
// nodes.
|
||||
TSMP IPProto = 99
|
||||
|
||||
// Fragment represents any non-first IP fragment, for which we
|
||||
// don't have the sub-protocol header (and therefore can't
|
||||
// figure out what the sub-protocol is).
|
||||
@@ -47,6 +58,8 @@ func (p IPProto) String() string {
|
||||
return "UDP"
|
||||
case TCP:
|
||||
return "TCP"
|
||||
case TSMP:
|
||||
return "TSMP"
|
||||
default:
|
||||
return "Unknown"
|
||||
}
|
||||
|
||||
@@ -17,10 +17,15 @@ import (
|
||||
// RFC1858: prevent overlapping fragment attacks.
|
||||
const minFrag = 60 + 20 // max IPv4 header + basic TCP header
|
||||
|
||||
type TCPFlag uint8
|
||||
|
||||
const (
|
||||
TCPSyn = 0x02
|
||||
TCPAck = 0x10
|
||||
TCPSynAck = TCPSyn | TCPAck
|
||||
TCPFin TCPFlag = 0x01
|
||||
TCPSyn TCPFlag = 0x02
|
||||
TCPRst TCPFlag = 0x04
|
||||
TCPPsh TCPFlag = 0x08
|
||||
TCPAck TCPFlag = 0x10
|
||||
TCPSynAck TCPFlag = TCPSyn | TCPAck
|
||||
)
|
||||
|
||||
// Parsed is a minimal decoding of a packet suitable for use in filters.
|
||||
@@ -46,7 +51,7 @@ type Parsed struct {
|
||||
// DstIP4 is the destination address. Family matches IPVersion.
|
||||
Dst netaddr.IPPort
|
||||
// TCPFlags is the packet's TCP flag bigs. Valid iff IPProto == TCP.
|
||||
TCPFlags uint8
|
||||
TCPFlags TCPFlag
|
||||
}
|
||||
|
||||
func (p *Parsed) String() string {
|
||||
@@ -111,6 +116,12 @@ func (q *Parsed) Decode(b []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
// StuffForTesting makes Parsed contain a len-bytes buffer. Used in
|
||||
// tests to build up a synthetic parse result with a non-zero buffer.
|
||||
func (q *Parsed) StuffForTesting(len int) {
|
||||
q.b = make([]byte, len)
|
||||
}
|
||||
|
||||
func (q *Parsed) decode4(b []byte) {
|
||||
if len(b) < ip4HeaderLength {
|
||||
q.IPVersion = 0
|
||||
@@ -186,7 +197,7 @@ func (q *Parsed) decode4(b []byte) {
|
||||
}
|
||||
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.TCPFlags = sub[13] & 0x3F
|
||||
q.TCPFlags = TCPFlag(sub[13]) & 0x3F
|
||||
headerLength := (sub[12] & 0xF0) >> 2
|
||||
q.dataofs = q.subofs + int(headerLength)
|
||||
return
|
||||
@@ -199,6 +210,10 @@ func (q *Parsed) decode4(b []byte) {
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.dataofs = q.subofs + udpHeaderLength
|
||||
return
|
||||
case TSMP:
|
||||
// Inter-tailscale messages.
|
||||
q.dataofs = q.subofs
|
||||
return
|
||||
default:
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
@@ -274,7 +289,7 @@ func (q *Parsed) decode6(b []byte) {
|
||||
}
|
||||
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.TCPFlags = sub[13] & 0x3F
|
||||
q.TCPFlags = TCPFlag(sub[13]) & 0x3F
|
||||
headerLength := (sub[12] & 0xF0) >> 2
|
||||
q.dataofs = q.subofs + int(headerLength)
|
||||
return
|
||||
@@ -286,6 +301,10 @@ func (q *Parsed) decode6(b []byte) {
|
||||
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.dataofs = q.subofs + udpHeaderLength
|
||||
case TSMP:
|
||||
// Inter-tailscale messages.
|
||||
q.dataofs = q.subofs
|
||||
return
|
||||
default:
|
||||
q.IPProto = Unknown
|
||||
return
|
||||
|
||||
@@ -274,7 +274,38 @@ var igmpPacketDecode = Parsed{
|
||||
Dst: mustIPPort("224.0.0.251:0"),
|
||||
}
|
||||
|
||||
func TestParsed(t *testing.T) {
|
||||
var ipv4TSMPBuffer = []byte{
|
||||
// IPv4 header:
|
||||
0x45, 0x00,
|
||||
0x00, 0x1b, // 20 + 7 bytes total
|
||||
0x00, 0x00, // ID
|
||||
0x00, 0x00, // Fragment
|
||||
0x40, // TTL
|
||||
byte(TSMP),
|
||||
0x5f, 0xc3, // header checksum (wrong here)
|
||||
// source IP:
|
||||
0x64, 0x5e, 0x0c, 0x0e,
|
||||
// dest IP:
|
||||
0x64, 0x4a, 0x46, 0x03,
|
||||
byte(TSMPTypeRejectedConn),
|
||||
byte(TCP),
|
||||
byte(RejectedDueToACLs),
|
||||
0x00, 123, // src port
|
||||
0x00, 80, // dst port
|
||||
}
|
||||
|
||||
var ipv4TSMPDecode = Parsed{
|
||||
b: ipv4TSMPBuffer,
|
||||
subofs: 20,
|
||||
dataofs: 20,
|
||||
length: 27,
|
||||
IPVersion: 4,
|
||||
IPProto: TSMP,
|
||||
Src: mustIPPort("100.94.12.14:0"),
|
||||
Dst: mustIPPort("100.74.70.3:0"),
|
||||
}
|
||||
|
||||
func TestParsedString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
qdecode Parsed
|
||||
@@ -288,6 +319,7 @@ func TestParsed(t *testing.T) {
|
||||
{"icmp6", icmp6PacketDecode, "ICMPv6{[fe80::fb57:1dea:9c39:8fb7]:0 > [ff02::2]:0}"},
|
||||
{"igmp", igmpPacketDecode, "IGMP{192.168.1.82:0 > 224.0.0.251:0}"},
|
||||
{"unknown", unknownPacketDecode, "Unknown{???}"},
|
||||
{"ipv4_tsmp", ipv4TSMPDecode, "TSMP{100.94.12.14:0 > 100.74.70.3:0}"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -324,6 +356,7 @@ func TestDecode(t *testing.T) {
|
||||
{"igmp", igmpPacketBuffer, igmpPacketDecode},
|
||||
{"unknown", unknownPacketBuffer, unknownPacketDecode},
|
||||
{"invalid4", invalid4RequestBuffer, invalid4RequestDecode},
|
||||
{"ipv4_tsmp", ipv4TSMPBuffer, ipv4TSMPDecode},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -331,7 +364,7 @@ func TestDecode(t *testing.T) {
|
||||
var got Parsed
|
||||
got.Decode(tt.buf)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("mismatch\n got: %#v\nwant: %#v", got, tt.want)
|
||||
t.Errorf("mismatch\n got: %s %#v\nwant: %s %#v", got.String(), got, tt.want.String(), tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -416,9 +449,16 @@ func TestMarshalResponse(t *testing.T) {
|
||||
icmpHeader := icmp4RequestDecode.ICMP4Header()
|
||||
udpHeader := udp4RequestDecode.UDP4Header()
|
||||
|
||||
type HeaderToResponser interface {
|
||||
Header
|
||||
// ToResponse transforms the header into one for a response packet.
|
||||
// For instance, this swaps the source and destination IPs.
|
||||
ToResponse()
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
header Header
|
||||
header HeaderToResponser
|
||||
want []byte
|
||||
}{
|
||||
{"icmp", &icmpHeader, icmp4ReplyBuffer},
|
||||
|
||||
196
net/packet/tsmp.go
Normal file
196
net/packet/tsmp.go
Normal file
@@ -0,0 +1,196 @@
|
||||
// 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.
|
||||
|
||||
// TSMP is our ICMP-like "Tailscale Message Protocol" for signaling
|
||||
// Tailscale-specific messages between nodes. It uses IP protocol 99
|
||||
// (reserved for "any private encryption scheme") within
|
||||
// Wireguard's normal encryption between peers and never hits the host
|
||||
// network stack.
|
||||
|
||||
package packet
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/flowtrack"
|
||||
)
|
||||
|
||||
// TailscaleRejectedHeader is a TSMP message that says that one
|
||||
// Tailscale node has rejected the connection from another. Unlike a
|
||||
// TCP RST, this includes a reason.
|
||||
//
|
||||
// On the wire, after the IP header, it's currently 7 or 8 bytes:
|
||||
// * '!'
|
||||
// * IPProto byte (IANA protocol number: TCP or UDP)
|
||||
// * 'A' or 'S' (RejectedDueToACLs, RejectedDueToShieldsUp)
|
||||
// * srcPort big endian uint16
|
||||
// * dstPort big endian uint16
|
||||
// * [optional] byte of flag bits:
|
||||
// lowest bit (0x1): MaybeBroken
|
||||
//
|
||||
// In the future it might also accept 16 byte IP flow src/dst IPs
|
||||
// after the header, if they're different than the IP-level ones.
|
||||
type TailscaleRejectedHeader struct {
|
||||
IPSrc netaddr.IP // IPv4 or IPv6 header's src IP
|
||||
IPDst netaddr.IP // IPv4 or IPv6 header's dst IP
|
||||
Src netaddr.IPPort // rejected flow's src
|
||||
Dst netaddr.IPPort // rejected flow's dst
|
||||
Proto IPProto // proto that was rejected (TCP or UDP)
|
||||
Reason TailscaleRejectReason // why the connection was rejected
|
||||
|
||||
// MaybeBroken is whether the rejection is non-terminal (the
|
||||
// client should not fail immediately). This is sent by a
|
||||
// target when it's not sure whether it's totally broken, but
|
||||
// it might be. For example, the target tailscaled might think
|
||||
// its host firewall or IP forwarding aren't configured
|
||||
// properly, but tailscaled might be wrong (not having enough
|
||||
// visibility into what the OS is doing). When true, the
|
||||
// message is simply an FYI as a potential reason to use for
|
||||
// later when the pendopen connection tracking timer expires.
|
||||
MaybeBroken bool
|
||||
}
|
||||
|
||||
const rejectFlagBitMaybeBroken = 0x1
|
||||
|
||||
func (rh TailscaleRejectedHeader) Flow() flowtrack.Tuple {
|
||||
return flowtrack.Tuple{Src: rh.Src, Dst: rh.Dst}
|
||||
}
|
||||
|
||||
func (rh TailscaleRejectedHeader) String() string {
|
||||
return fmt.Sprintf("TSMP-reject-flow{%s %s > %s}: %s", rh.Proto, rh.Src, rh.Dst, rh.Reason)
|
||||
}
|
||||
|
||||
type TSMPType uint8
|
||||
|
||||
const (
|
||||
// TSMPTypeRejectedConn is the type byte for a TailscaleRejectedHeader.
|
||||
TSMPTypeRejectedConn TSMPType = '!'
|
||||
)
|
||||
|
||||
type TailscaleRejectReason byte
|
||||
|
||||
// IsZero reports whether r is the zero value, representing no rejection.
|
||||
func (r TailscaleRejectReason) IsZero() bool { return r == TailscaleRejectReasonNone }
|
||||
|
||||
const (
|
||||
// TailscaleRejectReasonNone is the TailscaleRejectReason zero value.
|
||||
TailscaleRejectReasonNone TailscaleRejectReason = 0
|
||||
|
||||
// RejectedDueToACLs means that the host rejected the connection due to ACLs.
|
||||
RejectedDueToACLs TailscaleRejectReason = 'A'
|
||||
|
||||
// RejectedDueToShieldsUp means that the host rejected the connection due to shields being up.
|
||||
RejectedDueToShieldsUp TailscaleRejectReason = 'S'
|
||||
|
||||
// RejectedDueToIPForwarding means that the relay node's IP
|
||||
// forwarding is disabled.
|
||||
RejectedDueToIPForwarding TailscaleRejectReason = 'F'
|
||||
|
||||
// RejectedDueToHostFirewall means that the target host's
|
||||
// firewall is blocking the traffic.
|
||||
RejectedDueToHostFirewall TailscaleRejectReason = 'W'
|
||||
)
|
||||
|
||||
func (r TailscaleRejectReason) String() string {
|
||||
switch r {
|
||||
case RejectedDueToACLs:
|
||||
return "acl"
|
||||
case RejectedDueToShieldsUp:
|
||||
return "shields"
|
||||
case RejectedDueToIPForwarding:
|
||||
return "host-ip-forwarding-unavailable"
|
||||
case RejectedDueToHostFirewall:
|
||||
return "host-firewall"
|
||||
}
|
||||
return fmt.Sprintf("0x%02x", byte(r))
|
||||
}
|
||||
|
||||
func (h TailscaleRejectedHeader) hasFlags() bool {
|
||||
return h.MaybeBroken // the only one currently
|
||||
}
|
||||
|
||||
func (h TailscaleRejectedHeader) Len() int {
|
||||
v := 1 + // TSMPType byte
|
||||
1 + // IPProto byte
|
||||
1 + // TailscaleRejectReason byte
|
||||
2*2 // 2 uint16 ports
|
||||
if h.IPSrc.Is4() {
|
||||
v += ip4HeaderLength
|
||||
} else if h.IPSrc.Is6() {
|
||||
v += ip6HeaderLength
|
||||
}
|
||||
if h.hasFlags() {
|
||||
v++
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (h TailscaleRejectedHeader) Marshal(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
if h.Src.IP.Is4() {
|
||||
iph := IP4Header{
|
||||
IPProto: TSMP,
|
||||
Src: h.IPSrc,
|
||||
Dst: h.IPDst,
|
||||
}
|
||||
iph.Marshal(buf)
|
||||
buf = buf[ip4HeaderLength:]
|
||||
} else if h.Src.IP.Is6() {
|
||||
iph := IP6Header{
|
||||
IPProto: TSMP,
|
||||
Src: h.IPSrc,
|
||||
Dst: h.IPDst,
|
||||
}
|
||||
iph.Marshal(buf)
|
||||
buf = buf[ip6HeaderLength:]
|
||||
} else {
|
||||
return errors.New("bogus src IP")
|
||||
}
|
||||
buf[0] = byte(TSMPTypeRejectedConn)
|
||||
buf[1] = byte(h.Proto)
|
||||
buf[2] = byte(h.Reason)
|
||||
binary.BigEndian.PutUint16(buf[3:5], h.Src.Port)
|
||||
binary.BigEndian.PutUint16(buf[5:7], h.Dst.Port)
|
||||
|
||||
if h.hasFlags() {
|
||||
var flags byte
|
||||
if h.MaybeBroken {
|
||||
flags |= rejectFlagBitMaybeBroken
|
||||
}
|
||||
buf[7] = flags
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AsTailscaleRejectedHeader parses pp as an incoming rejection
|
||||
// connection TSMP message.
|
||||
//
|
||||
// ok reports whether pp was a valid TSMP rejection packet.
|
||||
func (pp *Parsed) AsTailscaleRejectedHeader() (h TailscaleRejectedHeader, ok bool) {
|
||||
p := pp.Payload()
|
||||
if len(p) < 7 || p[0] != byte(TSMPTypeRejectedConn) {
|
||||
return
|
||||
}
|
||||
h = TailscaleRejectedHeader{
|
||||
Proto: IPProto(p[1]),
|
||||
Reason: TailscaleRejectReason(p[2]),
|
||||
IPSrc: pp.Src.IP,
|
||||
IPDst: pp.Dst.IP,
|
||||
Src: netaddr.IPPort{IP: pp.Dst.IP, Port: binary.BigEndian.Uint16(p[3:5])},
|
||||
Dst: netaddr.IPPort{IP: pp.Src.IP, Port: binary.BigEndian.Uint16(p[5:7])},
|
||||
}
|
||||
if len(p) > 7 {
|
||||
flags := p[7]
|
||||
h.MaybeBroken = (flags & rejectFlagBitMaybeBroken) != 0
|
||||
}
|
||||
return h, true
|
||||
}
|
||||
75
net/packet/tsmp_test.go
Normal file
75
net/packet/tsmp_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
// 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 packet
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func TestTailscaleRejectedHeader(t *testing.T) {
|
||||
tests := []struct {
|
||||
h TailscaleRejectedHeader
|
||||
wantStr string
|
||||
}{
|
||||
{
|
||||
h: TailscaleRejectedHeader{
|
||||
IPSrc: netaddr.MustParseIP("5.5.5.5"),
|
||||
IPDst: netaddr.MustParseIP("1.2.3.4"),
|
||||
Src: netaddr.MustParseIPPort("1.2.3.4:567"),
|
||||
Dst: netaddr.MustParseIPPort("5.5.5.5:443"),
|
||||
Proto: TCP,
|
||||
Reason: RejectedDueToACLs,
|
||||
},
|
||||
wantStr: "TSMP-reject-flow{TCP 1.2.3.4:567 > 5.5.5.5:443}: acl",
|
||||
},
|
||||
{
|
||||
h: TailscaleRejectedHeader{
|
||||
IPSrc: netaddr.MustParseIP("2::2"),
|
||||
IPDst: netaddr.MustParseIP("1::1"),
|
||||
Src: netaddr.MustParseIPPort("[1::1]:567"),
|
||||
Dst: netaddr.MustParseIPPort("[2::2]:443"),
|
||||
Proto: UDP,
|
||||
Reason: RejectedDueToShieldsUp,
|
||||
},
|
||||
wantStr: "TSMP-reject-flow{UDP [1::1]:567 > [2::2]:443}: shields",
|
||||
},
|
||||
{
|
||||
h: TailscaleRejectedHeader{
|
||||
IPSrc: netaddr.MustParseIP("2::2"),
|
||||
IPDst: netaddr.MustParseIP("1::1"),
|
||||
Src: netaddr.MustParseIPPort("[1::1]:567"),
|
||||
Dst: netaddr.MustParseIPPort("[2::2]:443"),
|
||||
Proto: UDP,
|
||||
Reason: RejectedDueToIPForwarding,
|
||||
MaybeBroken: true,
|
||||
},
|
||||
wantStr: "TSMP-reject-flow{UDP [1::1]:567 > [2::2]:443}: host-ip-forwarding-unavailable",
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
gotStr := tt.h.String()
|
||||
if gotStr != tt.wantStr {
|
||||
t.Errorf("%v. String = %q; want %q", i, gotStr, tt.wantStr)
|
||||
continue
|
||||
}
|
||||
pkt := make([]byte, tt.h.Len())
|
||||
tt.h.Marshal(pkt)
|
||||
|
||||
var p Parsed
|
||||
p.Decode(pkt)
|
||||
t.Logf("Parsed: %+v", p)
|
||||
t.Logf("Parsed: %s", p.String())
|
||||
back, ok := p.AsTailscaleRejectedHeader()
|
||||
if !ok {
|
||||
t.Errorf("%v. %q (%02x) didn't parse back", i, gotStr, pkt)
|
||||
continue
|
||||
}
|
||||
if back != tt.h {
|
||||
t.Errorf("%v. %q parsed back as %q", i, tt.h, back)
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user