Compare commits
32 Commits
tps/tailsc
...
josh/debug
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee2f988775 | ||
|
|
0554b64452 | ||
|
|
9da4181606 | ||
|
|
f6e833748b | ||
|
|
8a3d52e882 | ||
|
|
c2202cc27c | ||
|
|
142670b8c2 | ||
|
|
881bb8bcdc | ||
|
|
b6179b9e83 | ||
|
|
2d35737a7a | ||
|
|
c179b9b535 | ||
|
|
690ade4ee1 | ||
|
|
f414a9cc01 | ||
|
|
1034b17bc7 | ||
|
|
965dccd4fc | ||
|
|
7b9f02fcb1 | ||
|
|
d8d9036dbb | ||
|
|
1b14e1d6bd | ||
|
|
bf7ad05230 | ||
|
|
68df379a7d | ||
|
|
aaf2df7ab1 | ||
|
|
dde8e28f00 | ||
|
|
c17d743886 | ||
|
|
281d503626 | ||
|
|
dfa5e38fad | ||
|
|
e299300b48 | ||
|
|
7428ecfebd | ||
|
|
5c266bdb73 | ||
|
|
3377089583 | ||
|
|
53a2f63658 | ||
|
|
e94ec448a7 | ||
|
|
064b916b1a |
53
.github/workflows/cross-darwin.yml
vendored
53
.github/workflows/cross-darwin.yml
vendored
@@ -1,53 +0,0 @@
|
||||
name: Darwin-Cross
|
||||
|
||||
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
|
||||
|
||||
- name: macOS build cmd
|
||||
env:
|
||||
GOOS: darwin
|
||||
GOARCH: amd64
|
||||
run: go build ./cmd/...
|
||||
|
||||
- name: macOS build tests
|
||||
env:
|
||||
GOOS: darwin
|
||||
GOARCH: amd64
|
||||
run: for d in $(go list -f '{{if .TestGoFiles}}{{.Dir}}{{end}}' ./... ); do (echo $d; cd $d && go test -c ); done
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"attachments": [{
|
||||
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
|
||||
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
|
||||
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
|
||||
"color": "danger"
|
||||
}]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
if: failure() && github.event_name == 'push'
|
||||
53
.github/workflows/cross-freebsd.yml
vendored
53
.github/workflows/cross-freebsd.yml
vendored
@@ -1,53 +0,0 @@
|
||||
name: FreeBSD-Cross
|
||||
|
||||
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
|
||||
|
||||
- name: FreeBSD build cmd
|
||||
env:
|
||||
GOOS: freebsd
|
||||
GOARCH: amd64
|
||||
run: go build ./cmd/...
|
||||
|
||||
- name: FreeBSD build tests
|
||||
env:
|
||||
GOOS: freebsd
|
||||
GOARCH: amd64
|
||||
run: for d in $(go list -f '{{if .TestGoFiles}}{{.Dir}}{{end}}' ./... ); do (echo $d; cd $d && go test -c ); done
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"attachments": [{
|
||||
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
|
||||
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
|
||||
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
|
||||
"color": "danger"
|
||||
}]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
if: failure() && github.event_name == 'push'
|
||||
53
.github/workflows/cross-openbsd.yml
vendored
53
.github/workflows/cross-openbsd.yml
vendored
@@ -1,53 +0,0 @@
|
||||
name: OpenBSD-Cross
|
||||
|
||||
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
|
||||
|
||||
- name: OpenBSD build cmd
|
||||
env:
|
||||
GOOS: openbsd
|
||||
GOARCH: amd64
|
||||
run: go build ./cmd/...
|
||||
|
||||
- name: OpenBSD build tests
|
||||
env:
|
||||
GOOS: openbsd
|
||||
GOARCH: amd64
|
||||
run: for d in $(go list -f '{{if .TestGoFiles}}{{.Dir}}{{end}}' ./... ); do (echo $d; cd $d && go test -c ); done
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"attachments": [{
|
||||
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
|
||||
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
|
||||
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
|
||||
"color": "danger"
|
||||
}]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
if: failure() && github.event_name == 'push'
|
||||
53
.github/workflows/cross-windows.yml
vendored
53
.github/workflows/cross-windows.yml
vendored
@@ -1,53 +0,0 @@
|
||||
name: Windows-Cross
|
||||
|
||||
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
|
||||
|
||||
- name: Windows build cmd
|
||||
env:
|
||||
GOOS: windows
|
||||
GOARCH: amd64
|
||||
run: go build ./cmd/...
|
||||
|
||||
- name: Windows build tests
|
||||
env:
|
||||
GOOS: windows
|
||||
GOARCH: amd64
|
||||
run: for d in $(go list -f '{{if .TestGoFiles}}{{.Dir}}{{end}}' ./... ); do (echo $d; cd $d && go test -c ); done
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"attachments": [{
|
||||
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
|
||||
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
|
||||
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
|
||||
"color": "danger"
|
||||
}]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
if: failure() && github.event_name == 'push'
|
||||
28
.github/workflows/depaware.yml
vendored
28
.github/workflows/depaware.yml
vendored
@@ -1,28 +0,0 @@
|
||||
name: depaware
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: depaware tailscaled
|
||||
run: go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscaled
|
||||
|
||||
- name: depaware tailscale
|
||||
run: go run github.com/tailscale/depaware --check tailscale.com/cmd/tailscale
|
||||
34
.github/workflows/go_generate.yml
vendored
34
.github/workflows/go_generate.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: go generate
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- "release-branch/*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: check 'go generate' is clean
|
||||
run: |
|
||||
mkdir gentools
|
||||
go build -o gentools/stringer golang.org/x/tools/cmd/stringer
|
||||
PATH="$PATH:$(pwd)/gentools" go generate ./...
|
||||
echo
|
||||
echo
|
||||
git diff --name-only --exit-code || (echo "The files above need updating. Please run 'go generate'."; exit 1)
|
||||
40
.github/workflows/license.yml
vendored
40
.github/workflows/license.yml
vendored
@@ -1,40 +0,0 @@
|
||||
name: license
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Run license checker
|
||||
run: ./scripts/check_license_headers.sh .
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"attachments": [{
|
||||
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
|
||||
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
|
||||
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
|
||||
"color": "danger"
|
||||
}]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
if: failure() && github.event_name == 'push'
|
||||
48
.github/workflows/linux-race.yml
vendored
48
.github/workflows/linux-race.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: Linux race
|
||||
|
||||
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
|
||||
|
||||
- name: Basic build
|
||||
run: go build ./cmd/...
|
||||
|
||||
- name: Run tests and benchmarks with -race flag on linux
|
||||
run: go test -race -bench=. -benchtime=1x ./...
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"attachments": [{
|
||||
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
|
||||
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
|
||||
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
|
||||
"color": "danger"
|
||||
}]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
if: failure() && github.event_name == 'push'
|
||||
|
||||
48
.github/workflows/linux.yml
vendored
48
.github/workflows/linux.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: Linux
|
||||
|
||||
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
|
||||
|
||||
- name: Basic build
|
||||
run: go build ./cmd/...
|
||||
|
||||
- name: Run tests on linux
|
||||
run: go test -bench=. -benchtime=1x ./...
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"attachments": [{
|
||||
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
|
||||
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
|
||||
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
|
||||
"color": "danger"
|
||||
}]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
if: failure() && github.event_name == 'push'
|
||||
|
||||
48
.github/workflows/linux32.yml
vendored
48
.github/workflows/linux32.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: Linux 32-bit
|
||||
|
||||
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
|
||||
|
||||
- name: Basic build
|
||||
run: GOARCH=386 go build ./cmd/...
|
||||
|
||||
- name: Run tests on linux
|
||||
run: GOARCH=386 go test -bench=. -benchtime=1x ./...
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"attachments": [{
|
||||
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
|
||||
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
|
||||
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
|
||||
"color": "danger"
|
||||
}]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
if: failure() && github.event_name == 'push'
|
||||
|
||||
58
.github/workflows/staticcheck.yml
vendored
58
.github/workflows/staticcheck.yml
vendored
@@ -1,58 +0,0 @@
|
||||
name: staticcheck
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16
|
||||
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Run go vet
|
||||
run: go vet ./...
|
||||
|
||||
- name: Install staticcheck
|
||||
run: "GOBIN=~/.local/bin go install honnef.co/go/tools/cmd/staticcheck"
|
||||
|
||||
- name: Print staticcheck version
|
||||
run: "staticcheck -version"
|
||||
|
||||
- name: Run staticcheck (linux/amd64)
|
||||
run: "GOOS=linux GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- name: Run staticcheck (darwin/amd64)
|
||||
run: "GOOS=darwin GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- name: Run staticcheck (windows/amd64)
|
||||
run: "GOOS=windows GOARCH=amd64 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- name: Run staticcheck (windows/386)
|
||||
run: "GOOS=windows GOARCH=386 staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"attachments": [{
|
||||
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
|
||||
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
|
||||
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
|
||||
"color": "danger"
|
||||
}]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
if: failure() && github.event_name == 'push'
|
||||
2
.github/workflows/windows-race.yml
vendored
2
.github/workflows/windows-race.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
# Don't use -bench=. -benchtime=1x.
|
||||
# Somewhere in the layers (powershell?)
|
||||
# the equals signs cause great confusion.
|
||||
run: go test -race -bench . -benchtime 1x ./...
|
||||
run: go test -race -bench . -benchtime 1x -timeout 5m -v ./...
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
|
||||
47
.github/workflows/xe-experimental-vm-test.yml
vendored
47
.github/workflows/xe-experimental-vm-test.yml
vendored
@@ -1,47 +0,0 @@
|
||||
name: "integration-vms"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "tstest/integration/vms/**"
|
||||
push:
|
||||
branches: [ main ]
|
||||
release:
|
||||
types: [ created ]
|
||||
|
||||
jobs:
|
||||
experimental-linux-vm-test:
|
||||
# To set up a new runner, see tstest/integration/vms/runner.nix
|
||||
runs-on: [ self-hosted, linux, vm_integration_test ]
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Download VM Images
|
||||
run: go test ./tstest/integration/vms -run-vm-tests -run=Download -timeout=60m -no-s3
|
||||
env:
|
||||
XDG_CACHE_HOME: "/var/lib/ghrunner/cache"
|
||||
|
||||
- name: Run VM tests
|
||||
run: go test ./tstest/integration/vms -v -run-vm-tests
|
||||
env:
|
||||
TMPDIR: "/tmp"
|
||||
XDG_CACHE_HOME: "/var/lib/ghrunner/cache"
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"attachments": [{
|
||||
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
|
||||
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
|
||||
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
|
||||
"color": "danger"
|
||||
}]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
if: failure() && github.event_name == 'push'
|
||||
@@ -42,8 +42,7 @@ FROM golang:1.16-alpine AS build-env
|
||||
|
||||
WORKDIR /go/src/tailscale
|
||||
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.11.0
|
||||
1.13.0
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/tstime/mono"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
@@ -193,7 +194,7 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
//
|
||||
// TODO: have the server report this bool instead.
|
||||
func peerActive(ps *ipnstate.PeerStatus) bool {
|
||||
return !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
|
||||
return !ps.LastWrite.IsZero() && mono.Since(ps.LastWrite) < 2*time.Minute
|
||||
}
|
||||
|
||||
func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||
|
||||
@@ -70,13 +70,13 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
||||
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
|
||||
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "install host routes to other Tailscale nodes")
|
||||
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale IP of the exit node for internet traffic")
|
||||
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale IP of the exit node for internet traffic, or empty string to not use an exit node")
|
||||
upf.BoolVar(&upArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
|
||||
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
||||
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "comma-separated ACL tags to request; each must start with \"tag:\" (e.g. \"tag:eng,tag:montreal,tag:ssh\")")
|
||||
upf.StringVar(&upArgs.authKey, "authkey", "", "node authorization key")
|
||||
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
|
||||
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. \"10.0.0.0/8,192.168.0.0/24\")")
|
||||
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. \"10.0.0.0/8,192.168.0.0/24\") or empty string to not advertise routes")
|
||||
upf.BoolVar(&upArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
|
||||
if safesocket.GOOSUsesPeerCreds(goos) {
|
||||
upf.StringVar(&upArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
|
||||
|
||||
@@ -49,6 +49,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
💣 tailscale.com/tstime/mono from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
|
||||
tailscale.com/types/empty from tailscale.com/ipn
|
||||
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
||||
tailscale.com/types/key from tailscale.com/derp+
|
||||
|
||||
@@ -130,6 +130,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/tailcfg from tailscale.com/control/controlclient+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/wgengine/magicsock
|
||||
💣 tailscale.com/tstime/mono from tailscale.com/net/tstun+
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
|
||||
tailscale.com/types/empty from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
|
||||
|
||||
@@ -29,8 +29,8 @@ import (
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/gliderlabs/ssh"
|
||||
"github.com/kr/pty"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
|
||||
@@ -220,6 +220,13 @@ func packageType() string {
|
||||
// Using tailscaled or IPNExtension?
|
||||
exe, _ := os.Executable()
|
||||
return filepath.Base(exe)
|
||||
case "linux":
|
||||
// Report whether this is in a snap.
|
||||
// See https://snapcraft.io/docs/environment-variables
|
||||
// We just look at two somewhat arbitrarily.
|
||||
if os.Getenv("SNAP_NAME") != "" && os.Getenv("SNAP") != "" {
|
||||
return "snap"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -370,7 +370,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()
|
||||
}
|
||||
|
||||
12
go.mod
12
go.mod
@@ -7,7 +7,8 @@ require (
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||
github.com/aws/aws-sdk-go v1.38.52
|
||||
github.com/coreos/go-iptables v0.6.0
|
||||
github.com/dave/jennifer v1.4.1 // indirect
|
||||
github.com/creack/pty v1.1.9
|
||||
github.com/dave/jennifer v1.4.1
|
||||
github.com/frankban/quicktest v1.13.0
|
||||
github.com/gliderlabs/ssh v0.3.2
|
||||
github.com/go-multierror/multierror v1.0.2
|
||||
@@ -17,11 +18,10 @@ require (
|
||||
github.com/google/goexpect v0.0.0-20210430020637-ab937bf7fd6f
|
||||
github.com/google/uuid v1.1.2
|
||||
github.com/goreleaser/nfpm v1.10.3
|
||||
github.com/iancoleman/strcase v0.2.0 // indirect
|
||||
github.com/iancoleman/strcase v0.2.0
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/klauspost/compress v1.12.2
|
||||
github.com/kr/pty v1.1.8
|
||||
github.com/mdlayher/netlink v1.4.1
|
||||
github.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697
|
||||
github.com/miekg/dns v1.1.42
|
||||
@@ -31,8 +31,8 @@ require (
|
||||
github.com/pkg/sftp v1.13.0
|
||||
github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210710010003-1cf2d718bbb2 // indirect
|
||||
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2 // indirect
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210710010003-1cf2d718bbb2
|
||||
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
|
||||
@@ -46,7 +46,7 @@ require (
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210624150102-15b24b6179e0
|
||||
golang.zx2c4.com/wireguard/windows v0.3.16
|
||||
honnef.co/go/tools v0.1.4
|
||||
inet.af/netaddr v0.0.0-20210602152128-50f8686885e3
|
||||
inet.af/netaddr v0.0.0-20210721214506-ce7a8ad02cc1
|
||||
inet.af/netstack v0.0.0-20210622165351-29b14ebc044e
|
||||
inet.af/peercred v0.0.0-20210318190834-4259e17bb763
|
||||
inet.af/wf v0.0.0-20210516214145-a5343001b756
|
||||
|
||||
13
go.sum
13
go.sum
@@ -82,7 +82,6 @@ github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3Ee
|
||||
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
|
||||
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/daixiang0/gci v0.2.4/go.mod h1:+AV8KmHTGxxwp/pY84TLQfFKp2vuKXXJVzF3kD/hfR4=
|
||||
@@ -359,8 +358,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
@@ -584,10 +581,6 @@ github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3 h1:fEubocuQkrl
|
||||
github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3/go.mod h1:2P+hpOwd53e7JMX/L4f3VXkv1G+33ES6IWZSrkIeWNs=
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 h1:lK99QQdH3yBWY6aGilF+IRlQIdmhzLrsEmF6JgN+Ryw=
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210629174436-7df6c9efe30c h1:F+pROyGPs+9wdB7jBPHr9IZEF8SKj9YUCFFShnyLNZM=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210629174436-7df6c9efe30c/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210629175715-39c5a55db683 h1:ZXmZQuVebYllEJL/dpttpIDGx723ezC5GJkHIu0YKrM=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210629175715-39c5a55db683/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210710010003-1cf2d718bbb2 h1:AIJ8AF9O7jBmCwilP0ydwJMIzW5dw48Us8f3hLJhYBY=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210710010003-1cf2d718bbb2/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
|
||||
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2 h1:reREUgl2FG+o7YCsrZB8XLjnuKv5hEIWtnOdAbRAXZI=
|
||||
@@ -698,7 +691,6 @@ golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73r
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/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-20181011144130-49bb7cea24b1/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -745,7 +737,6 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
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-20200317015054-43a5402ce75a/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/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
|
||||
@@ -964,8 +955,8 @@ honnef.co/go/tools v0.0.1-2020.1.6/go.mod h1:pyyisuGw24ruLjrr1ddx39WE0y9OooInRzE
|
||||
honnef.co/go/tools v0.1.4 h1:SadWOkti5uVN1FAMgxn165+Mw00fuQKyk4Gyn/inxNQ=
|
||||
honnef.co/go/tools v0.1.4/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
|
||||
inet.af/netaddr v0.0.0-20210515010201-ad03edc7c841/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20210602152128-50f8686885e3 h1:RlarOdsmOUCCvy7Xm1JchJIGuQsuKwD/Lo1bjYmfuQI=
|
||||
inet.af/netaddr v0.0.0-20210602152128-50f8686885e3/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20210721214506-ce7a8ad02cc1 h1:mxmfTV6kjXTlFqqFETnG9FQZzNFc6AKunZVAgQ3b7WA=
|
||||
inet.af/netaddr v0.0.0-20210721214506-ce7a8ad02cc1/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netstack v0.0.0-20210622165351-29b14ebc044e h1:z11NK94NQcI3DA+a3pUC/2dRYTph1kPX6B0FnCaMDzk=
|
||||
inet.af/netstack v0.0.0-20210622165351-29b14ebc044e/go.mod h1:fG3G1dekmK8oDX3iVzt8c0zICLMLSN8SjdxbXVt0WjU=
|
||||
inet.af/peercred v0.0.0-20210318190834-4259e17bb763 h1:gPSJmmVzmdy4kHhlCMx912GdiUz3k/RzJGg0ADqy1dg=
|
||||
|
||||
@@ -984,6 +984,44 @@ var removeFromDefaultRoute = []netaddr.IPPrefix{
|
||||
tsaddr.TailscaleULARange(),
|
||||
}
|
||||
|
||||
// internalAndExternalInterfaces splits interface routes into "internal"
|
||||
// and "external" sets. Internal routes are those of virtual ethernet
|
||||
// network interfaces used by guest VMs and containers, such as WSL and
|
||||
// Docker.
|
||||
//
|
||||
// Given that "internal" routes don't leave the device, we choose to
|
||||
// trust them more, allowing access to them when an Exit Node is enabled.
|
||||
func internalAndExternalInterfaces() (internal, external []netaddr.IPPrefix, err error) {
|
||||
if err := interfaces.ForeachInterfaceAddress(func(iface interfaces.Interface, pfx netaddr.IPPrefix) {
|
||||
if tsaddr.IsTailscaleIP(pfx.IP()) {
|
||||
return
|
||||
}
|
||||
if pfx.IsSingleIP() {
|
||||
return
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows Hyper-V prefixes all MAC addresses with 00:15:5d.
|
||||
// https://docs.microsoft.com/en-us/troubleshoot/windows-server/virtualization/default-limit-256-dynamic-mac-addresses
|
||||
//
|
||||
// This includes WSL2 vEthernet.
|
||||
// Importantly: by default WSL2 /etc/resolv.conf points to
|
||||
// a stub resolver running on the host vEthernet IP.
|
||||
// So enabling exit nodes with the default tailnet
|
||||
// configuration breaks WSL2 DNS without this.
|
||||
mac := iface.Interface.HardwareAddr
|
||||
if len(mac) == 6 && mac[0] == 0x00 && mac[1] == 0x15 && mac[2] == 0x5d {
|
||||
internal = append(internal, pfx)
|
||||
return
|
||||
}
|
||||
}
|
||||
external = append(external, pfx)
|
||||
}); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return internal, external, nil
|
||||
}
|
||||
|
||||
func interfaceRoutes() (ips *netaddr.IPSet, hostIPs []netaddr.IP, err error) {
|
||||
var b netaddr.IPSetBuilder
|
||||
if err := interfaces.ForeachInterfaceAddress(func(_ interfaces.Interface, pfx netaddr.IPPrefix) {
|
||||
@@ -2119,18 +2157,21 @@ func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router
|
||||
if !default6 {
|
||||
rs.Routes = append(rs.Routes, ipv6Default)
|
||||
}
|
||||
internalIPs, externalIPs, err := internalAndExternalInterfaces()
|
||||
if err != nil {
|
||||
b.logf("failed to discover interface ips: %v", err)
|
||||
}
|
||||
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" || runtime.GOOS == "windows" {
|
||||
// Only allow local lan access on linux machines for now.
|
||||
ips, _, err := interfaceRoutes()
|
||||
if err != nil {
|
||||
b.logf("failed to discover interface ips: %v", err)
|
||||
}
|
||||
rs.LocalRoutes = internalIPs // unconditionally allow access to guest VM networks
|
||||
if prefs.ExitNodeAllowLANAccess {
|
||||
rs.LocalRoutes = ips.Prefixes()
|
||||
rs.LocalRoutes = append(rs.LocalRoutes, externalIPs...)
|
||||
if len(externalIPs) != 0 {
|
||||
b.logf("allowing exit node access to internal IPs: %v", internalIPs)
|
||||
}
|
||||
} else {
|
||||
// Explicitly add routes to the local network so that we do not
|
||||
// leak any traffic.
|
||||
rs.Routes = append(rs.Routes, ips.Prefixes()...)
|
||||
rs.Routes = append(rs.Routes, externalIPs...)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2159,6 +2200,18 @@ func applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *ipn.Prefs) {
|
||||
}
|
||||
if v := prefs.OSVersion; v != "" {
|
||||
hi.OSVersion = v
|
||||
|
||||
// The Android app annotates when Google Play Services
|
||||
// aren't available by tacking on a string to the
|
||||
// OSVersion. Promote that to the Hostinfo.Package
|
||||
// field instead, rather than adding a new pref, as
|
||||
// this applyPrefsToHostinfo mechanism is mostly
|
||||
// abused currently. TODO(bradfitz): instead let
|
||||
// frontends update Hostinfo, without using Prefs.
|
||||
if runtime.GOOS == "android" && strings.HasSuffix(v, " [nogoogle]") {
|
||||
hi.Package = "nogoogle"
|
||||
hi.OSVersion = strings.TrimSuffix(v, " [nogoogle]")
|
||||
}
|
||||
}
|
||||
if m := prefs.DeviceModel; m != "" {
|
||||
hi.DeviceModel = m
|
||||
@@ -2731,17 +2784,18 @@ func (b *LocalBackend) CheckIPForwarding() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
const suffix = "\nSubnet routes won't work without IP forwarding.\nSee https://tailscale.com/kb/1104/enable-ip-forwarding/"
|
||||
for _, key := range keys {
|
||||
bs, err := exec.Command("sysctl", "-n", key).Output()
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't check %s (%v).\nSubnet routes won't work without IP forwarding.", key, err)
|
||||
return fmt.Errorf("couldn't check %s (%v)%s", key, err, suffix)
|
||||
}
|
||||
on, err := strconv.ParseBool(string(bytes.TrimSpace(bs)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't parse %s (%v).\nSubnet routes won't work without IP forwarding.", key, err)
|
||||
return fmt.Errorf("couldn't parse %s (%v)%s.", key, err, suffix)
|
||||
}
|
||||
if !on {
|
||||
return fmt.Errorf("%s is disabled. Subnet routes won't work.", key)
|
||||
return fmt.Errorf("%s is disabled.%s", key, suffix)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime/mono"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
@@ -90,7 +91,7 @@ type PeerStatus struct {
|
||||
RxBytes int64
|
||||
TxBytes int64
|
||||
Created time.Time // time registered with tailcontrol
|
||||
LastWrite time.Time // time last packet sent
|
||||
LastWrite mono.Time // time last packet sent
|
||||
LastSeen time.Time // last seen to tailcontrol
|
||||
LastHandshake time.Time // with local wireguard
|
||||
KeepAlive bool
|
||||
@@ -320,7 +321,7 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
|
||||
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()
|
||||
now := mono.Now()
|
||||
|
||||
var peers []*PeerStatus
|
||||
for _, peer := range st.Peers() {
|
||||
@@ -378,7 +379,7 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
|
||||
f("<td>")
|
||||
|
||||
// TODO: let server report this active bool instead
|
||||
active := !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
|
||||
active := !ps.LastWrite.IsZero() && mono.Since(ps.LastWrite) < 2*time.Minute
|
||||
if active {
|
||||
if ps.Relay != "" && ps.CurAddr == "" {
|
||||
f("relay <b>%s</b>", html.EscapeString(ps.Relay))
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -40,6 +41,11 @@ const (
|
||||
// connections open to DNS-over-HTTPs servers. This is pretty
|
||||
// arbitrary.
|
||||
dohTransportTimeout = 30 * time.Second
|
||||
|
||||
// wellKnownHostBackupDelay is how long to artificially delay upstream
|
||||
// DNS queries to the "fallback" DNS server IP for a known provider
|
||||
// (e.g. how long to wait to query Google's 8.8.4.4 after 8.8.8.8).
|
||||
wellKnownHostBackupDelay = 200 * time.Millisecond
|
||||
)
|
||||
|
||||
var errNoUpstreams = errors.New("upstream nameservers not set")
|
||||
@@ -119,6 +125,7 @@ func clampEDNSSize(packet []byte, maxSize uint16) {
|
||||
return
|
||||
}
|
||||
|
||||
// https://datatracker.ietf.org/doc/html/rfc6891#section-6.1.2
|
||||
opt := packet[len(packet)-optFixedBytes:]
|
||||
|
||||
if opt[0] != 0 {
|
||||
@@ -135,8 +142,8 @@ func clampEDNSSize(packet []byte, maxSize uint16) {
|
||||
// Be conservative and don't touch unknown versions.
|
||||
return
|
||||
}
|
||||
// Ignore flags in opt[7:9]
|
||||
if binary.BigEndian.Uint16(opt[10:12]) != 0 {
|
||||
// Ignore flags in opt[6:9]
|
||||
if binary.BigEndian.Uint16(opt[9:11]) != 0 {
|
||||
// RDLEN must be 0 (no variable length data). We're at the end of the
|
||||
// packet so this should be 0 anyway)..
|
||||
return
|
||||
@@ -152,7 +159,20 @@ func clampEDNSSize(packet []byte, maxSize uint16) {
|
||||
|
||||
type route struct {
|
||||
Suffix dnsname.FQDN
|
||||
Resolvers []netaddr.IPPort
|
||||
Resolvers []resolverAndDelay
|
||||
}
|
||||
|
||||
// resolverAndDelay is an upstream DNS resolver and a delay for how
|
||||
// long to wait before querying it.
|
||||
type resolverAndDelay struct {
|
||||
// ipp is the upstream resolver.
|
||||
ipp netaddr.IPPort
|
||||
|
||||
// startDelay is an amount to delay this resolver at
|
||||
// start. It's used when, say, there are four Google or
|
||||
// Cloudflare DNS IPs (two IPv4 + two IPv6) and we don't want
|
||||
// to race all four at once.
|
||||
startDelay time.Duration
|
||||
}
|
||||
|
||||
// forwarder forwards DNS packets to a number of upstream nameservers.
|
||||
@@ -204,7 +224,84 @@ func (f *forwarder) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *forwarder) setRoutes(routes []route) {
|
||||
// resolversWithDelays maps from a set of DNS server ip:ports (currently
|
||||
// the port is always 53) to a slice of a type that included a
|
||||
// startDelay. So if ipps contains e.g. four Google DNS IPs (two IPv4
|
||||
// + twoIPv6), this function partition adds delays to some.
|
||||
func resolversWithDelays(ipps []netaddr.IPPort) []resolverAndDelay {
|
||||
type hostAndFam struct {
|
||||
host string // some arbitrary string representing DNS host (currently the DoH base)
|
||||
bits uint8 // either 32 or 128 for IPv4 vs IPv6s address family
|
||||
}
|
||||
|
||||
// Track how many of each known resolver host are in the list,
|
||||
// per address family.
|
||||
total := map[hostAndFam]int{}
|
||||
|
||||
rr := make([]resolverAndDelay, len(ipps))
|
||||
for _, ipp := range ipps {
|
||||
ip := ipp.IP()
|
||||
if host, ok := knownDoH[ip]; ok {
|
||||
total[hostAndFam{host, ip.BitLen()}]++
|
||||
}
|
||||
}
|
||||
|
||||
done := map[hostAndFam]int{}
|
||||
for i, ipp := range ipps {
|
||||
ip := ipp.IP()
|
||||
var startDelay time.Duration
|
||||
if host, ok := knownDoH[ip]; ok {
|
||||
key4 := hostAndFam{host, 32}
|
||||
key6 := hostAndFam{host, 128}
|
||||
switch {
|
||||
case ip.Is4():
|
||||
if done[key4] > 0 {
|
||||
startDelay += wellKnownHostBackupDelay
|
||||
}
|
||||
case ip.Is6():
|
||||
total4 := total[key4]
|
||||
if total4 >= 2 {
|
||||
// If we have two IPv4 IPs of the same provider
|
||||
// already in the set, delay the IPv6 queries
|
||||
// until halfway through the timeout (so wait
|
||||
// 2.5 seconds). Even the network is IPv6-only,
|
||||
// the DoH dialer will fallback to IPv6
|
||||
// immediately anyway.
|
||||
startDelay = responseTimeout / 2
|
||||
} else if total4 == 1 {
|
||||
startDelay += wellKnownHostBackupDelay
|
||||
}
|
||||
if done[key6] > 0 {
|
||||
startDelay += wellKnownHostBackupDelay
|
||||
}
|
||||
}
|
||||
done[hostAndFam{host, ip.BitLen()}]++
|
||||
}
|
||||
rr[i] = resolverAndDelay{
|
||||
ipp: ipp,
|
||||
startDelay: startDelay,
|
||||
}
|
||||
}
|
||||
return rr
|
||||
}
|
||||
|
||||
// setRoutes sets the routes to use for DNS forwarding. It's called by
|
||||
// Resolver.SetConfig on reconfig.
|
||||
//
|
||||
// The memory referenced by routesBySuffix should not be modified.
|
||||
func (f *forwarder) setRoutes(routesBySuffix map[dnsname.FQDN][]netaddr.IPPort) {
|
||||
routes := make([]route, 0, len(routesBySuffix))
|
||||
for suffix, ipps := range routesBySuffix {
|
||||
routes = append(routes, route{
|
||||
Suffix: suffix,
|
||||
Resolvers: resolversWithDelays(ipps),
|
||||
})
|
||||
}
|
||||
// Sort from longest prefix to shortest.
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
return routes[i].Suffix.NumLabels() > routes[j].Suffix.NumLabels()
|
||||
})
|
||||
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.routes = routes
|
||||
@@ -316,22 +413,19 @@ func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client,
|
||||
//
|
||||
// send expects the reply to have the same txid as txidOut.
|
||||
//
|
||||
// The provided closeOnCtxDone lets send register values to Close if
|
||||
// the caller's ctx expires. This avoids send from allocating its own
|
||||
// waiting goroutine to interrupt the ReadFrom, as memory is tight on
|
||||
// iOS and we want the number of pending DNS lookups to be bursty
|
||||
// without too much associated goroutine/memory cost.
|
||||
func (f *forwarder) send(ctx context.Context, txidOut txid, closeOnCtxDone *closePool, packet []byte, dst netaddr.IPPort) ([]byte, error) {
|
||||
func (f *forwarder) send(ctx context.Context, fq *forwardQuery, dst netaddr.IPPort) ([]byte, error) {
|
||||
ip := dst.IP()
|
||||
|
||||
// Upgrade known DNS IPs to DoH (DNS-over-HTTPs).
|
||||
if urlBase, dc, ok := f.getDoHClient(dst.IP()); ok {
|
||||
res, err := f.sendDoH(ctx, urlBase, dc, packet)
|
||||
if urlBase, dc, ok := f.getDoHClient(ip); ok {
|
||||
res, err := f.sendDoH(ctx, urlBase, dc, fq.packet)
|
||||
if err == nil || ctx.Err() != nil {
|
||||
return res, err
|
||||
}
|
||||
f.logf("DoH error from %v: %v", dst.IP, err)
|
||||
f.logf("DoH error from %v: %v", ip, err)
|
||||
}
|
||||
|
||||
ln, err := f.packetListener(dst.IP())
|
||||
ln, err := f.packetListener(ip)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -342,10 +436,10 @@ func (f *forwarder) send(ctx context.Context, txidOut txid, closeOnCtxDone *clos
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
closeOnCtxDone.Add(conn)
|
||||
defer closeOnCtxDone.Remove(conn)
|
||||
fq.closeOnCtxDone.Add(conn)
|
||||
defer fq.closeOnCtxDone.Remove(conn)
|
||||
|
||||
if _, err := conn.WriteTo(packet, dst.UDPAddr()); err != nil {
|
||||
if _, err := conn.WriteTo(fq.packet, dst.UDPAddr()); err != nil {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -374,7 +468,7 @@ func (f *forwarder) send(ctx context.Context, txidOut txid, closeOnCtxDone *clos
|
||||
}
|
||||
out = out[:n]
|
||||
txid := getTxID(out)
|
||||
if txid != txidOut {
|
||||
if txid != fq.txid {
|
||||
return nil, errors.New("txid doesn't match")
|
||||
}
|
||||
|
||||
@@ -397,7 +491,7 @@ func (f *forwarder) send(ctx context.Context, txidOut txid, closeOnCtxDone *clos
|
||||
}
|
||||
|
||||
// resolvers returns the resolvers to use for domain.
|
||||
func (f *forwarder) resolvers(domain dnsname.FQDN) []netaddr.IPPort {
|
||||
func (f *forwarder) resolvers(domain dnsname.FQDN) []resolverAndDelay {
|
||||
f.mu.Lock()
|
||||
routes := f.routes
|
||||
f.mu.Unlock()
|
||||
@@ -409,6 +503,30 @@ func (f *forwarder) resolvers(domain dnsname.FQDN) []netaddr.IPPort {
|
||||
return nil
|
||||
}
|
||||
|
||||
// forwardQuery is information and state about a forwarded DNS query that's
|
||||
// being sent to 1 or more upstreams.
|
||||
//
|
||||
// In the case of racing against multiple equivalent upstreams
|
||||
// (e.g. Google or CloudFlare's 4 DNS IPs: 2 IPv4 + 2 IPv6), this type
|
||||
// handles racing them more intelligently than just blasting away 4
|
||||
// queries at once.
|
||||
type forwardQuery struct {
|
||||
txid txid
|
||||
packet []byte
|
||||
|
||||
// closeOnCtxDone lets send register values to Close if the
|
||||
// caller's ctx expires. This avoids send from allocating its
|
||||
// own waiting goroutine to interrupt the ReadFrom, as memory
|
||||
// is tight on iOS and we want the number of pending DNS
|
||||
// lookups to be bursty without too much associated
|
||||
// goroutine/memory cost.
|
||||
closeOnCtxDone *closePool
|
||||
|
||||
// TODO(bradfitz): add race delay state:
|
||||
// mu sync.Mutex
|
||||
// ...
|
||||
}
|
||||
|
||||
// forward forwards the query to all upstream nameservers and returns the first response.
|
||||
func (f *forwarder) forward(query packet) error {
|
||||
domain, err := nameFromQuery(query.bs)
|
||||
@@ -416,7 +534,6 @@ func (f *forwarder) forward(query packet) error {
|
||||
return err
|
||||
}
|
||||
|
||||
txid := getTxID(query.bs)
|
||||
clampEDNSSize(query.bs, maxResponseBytes)
|
||||
|
||||
resolvers := f.resolvers(domain)
|
||||
@@ -424,8 +541,12 @@ func (f *forwarder) forward(query packet) error {
|
||||
return errNoUpstreams
|
||||
}
|
||||
|
||||
closeOnCtxDone := new(closePool)
|
||||
defer closeOnCtxDone.Close()
|
||||
fq := &forwardQuery{
|
||||
txid: getTxID(query.bs),
|
||||
packet: query.bs,
|
||||
closeOnCtxDone: new(closePool),
|
||||
}
|
||||
defer fq.closeOnCtxDone.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(f.ctx, responseTimeout)
|
||||
defer cancel()
|
||||
@@ -436,9 +557,18 @@ func (f *forwarder) forward(query packet) error {
|
||||
firstErr error
|
||||
)
|
||||
|
||||
for _, ipp := range resolvers {
|
||||
go func(ipp netaddr.IPPort) {
|
||||
resb, err := f.send(ctx, txid, closeOnCtxDone, query.bs, ipp)
|
||||
for _, rr := range resolvers {
|
||||
go func(rr resolverAndDelay) {
|
||||
if rr.startDelay > 0 {
|
||||
timer := time.NewTimer(rr.startDelay)
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
resb, err := f.send(ctx, fq, rr.ipp)
|
||||
if err != nil {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
@@ -451,7 +581,7 @@ func (f *forwarder) forward(query packet) error {
|
||||
case resc <- resb:
|
||||
default:
|
||||
}
|
||||
}(ipp)
|
||||
}(rr)
|
||||
}
|
||||
|
||||
select {
|
||||
|
||||
@@ -208,7 +208,6 @@ func (r *Resolver) SetConfig(cfg Config) error {
|
||||
r.saveConfigForTests(cfg)
|
||||
}
|
||||
|
||||
routes := make([]route, 0, len(cfg.Routes))
|
||||
reverse := make(map[netaddr.IP]dnsname.FQDN, len(cfg.Hosts))
|
||||
|
||||
for host, ips := range cfg.Hosts {
|
||||
@@ -217,18 +216,7 @@ func (r *Resolver) SetConfig(cfg Config) error {
|
||||
}
|
||||
}
|
||||
|
||||
for suffix, ips := range cfg.Routes {
|
||||
routes = append(routes, route{
|
||||
Suffix: suffix,
|
||||
Resolvers: ips,
|
||||
})
|
||||
}
|
||||
// Sort from longest prefix to shortest.
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
return routes[i].Suffix.NumLabels() > routes[j].Suffix.NumLabels()
|
||||
})
|
||||
|
||||
r.forwarder.setRoutes(routes)
|
||||
r.forwarder.setRoutes(cfg.Routes)
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
@@ -931,8 +931,11 @@ func TestAllocs(t *testing.T) {
|
||||
query []byte
|
||||
want int
|
||||
}{
|
||||
// Name lowercasing and response slice created by dns.NewBuilder.
|
||||
{"forward", dnspacket("test1.ipn.dev.", dns.TypeA, noEdns), 2},
|
||||
// Name lowercasing, response slice created by dns.NewBuilder,
|
||||
// and closure allocation from go call.
|
||||
// (Closure allocation only happens when using new register ABI,
|
||||
// which is amd64 with Go 1.17, and probably more platforms later.)
|
||||
{"forward", dnspacket("test1.ipn.dev.", dns.TypeA, noEdns), 3},
|
||||
// 3 extra allocs in rdnsNameToIPv4 and one in marshalPTRRecord (dns.NewName).
|
||||
{"reverse", dnspacket("4.3.2.1.in-addr.arpa.", dns.TypePTR, noEdns), 5},
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// TODO(bradfitz): update this code to use netaddr more
|
||||
|
||||
// Package dnscache contains a minimal DNS cache that makes a bunch of
|
||||
// assumptions that are only valid for us. Not recommended for general use.
|
||||
package dnscache
|
||||
@@ -78,8 +80,9 @@ type Resolver struct {
|
||||
}
|
||||
|
||||
type ipCacheEntry struct {
|
||||
ip net.IP // either v4 or v6
|
||||
ip6 net.IP // nil if no v4 or no v6
|
||||
ip net.IP // either v4 or v6
|
||||
ip6 net.IP // nil if no v4 or no v6
|
||||
allIPs []net.IPAddr // 1+ v4 and/or v6
|
||||
expires time.Time
|
||||
}
|
||||
|
||||
@@ -105,81 +108,82 @@ var debug, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_DNS_CACHE"))
|
||||
//
|
||||
// If err is nil, ip will be non-nil. The v6 address may be nil even
|
||||
// with a nil error.
|
||||
func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 net.IP, err error) {
|
||||
func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 net.IP, allIPs []net.IPAddr, err error) {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
if ip4 := ip.To4(); ip4 != nil {
|
||||
return ip4, nil, nil
|
||||
return ip4, nil, []net.IPAddr{{IP: ip4}}, nil
|
||||
}
|
||||
if debug {
|
||||
log.Printf("dnscache: %q is an IP", host)
|
||||
}
|
||||
return ip, nil, nil
|
||||
return ip, nil, []net.IPAddr{{IP: ip}}, nil
|
||||
}
|
||||
|
||||
if ip, ip6, ok := r.lookupIPCache(host); ok {
|
||||
if ip, ip6, allIPs, ok := r.lookupIPCache(host); ok {
|
||||
if debug {
|
||||
log.Printf("dnscache: %q = %v (cached)", host, ip)
|
||||
}
|
||||
return ip, ip6, nil
|
||||
return ip, ip6, allIPs, nil
|
||||
}
|
||||
|
||||
type ipPair struct {
|
||||
type ipRes struct {
|
||||
ip, ip6 net.IP
|
||||
allIPs []net.IPAddr
|
||||
}
|
||||
ch := r.sf.DoChan(host, func() (interface{}, error) {
|
||||
ip, ip6, err := r.lookupIP(host)
|
||||
ip, ip6, allIPs, err := r.lookupIP(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ipPair{ip, ip6}, nil
|
||||
return ipRes{ip, ip6, allIPs}, nil
|
||||
})
|
||||
select {
|
||||
case res := <-ch:
|
||||
if res.Err != nil {
|
||||
if r.UseLastGood {
|
||||
if ip, ip6, ok := r.lookupIPCacheExpired(host); ok {
|
||||
if ip, ip6, allIPs, ok := r.lookupIPCacheExpired(host); ok {
|
||||
if debug {
|
||||
log.Printf("dnscache: %q using %v after error", host, ip)
|
||||
}
|
||||
return ip, ip6, nil
|
||||
return ip, ip6, allIPs, nil
|
||||
}
|
||||
}
|
||||
if debug {
|
||||
log.Printf("dnscache: error resolving %q: %v", host, res.Err)
|
||||
}
|
||||
return nil, nil, res.Err
|
||||
return nil, nil, nil, res.Err
|
||||
}
|
||||
pair := res.Val.(ipPair)
|
||||
return pair.ip, pair.ip6, nil
|
||||
r := res.Val.(ipRes)
|
||||
return r.ip, r.ip6, r.allIPs, nil
|
||||
case <-ctx.Done():
|
||||
if debug {
|
||||
log.Printf("dnscache: context done while resolving %q: %v", host, ctx.Err())
|
||||
}
|
||||
return nil, nil, ctx.Err()
|
||||
return nil, nil, nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupIPCache(host string) (ip, ip6 net.IP, ok bool) {
|
||||
func (r *Resolver) lookupIPCache(host string) (ip, ip6 net.IP, allIPs []net.IPAddr, ok bool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if ent, ok := r.ipCache[host]; ok && ent.expires.After(time.Now()) {
|
||||
return ent.ip, ent.ip6, true
|
||||
return ent.ip, ent.ip6, ent.allIPs, true
|
||||
}
|
||||
return nil, nil, false
|
||||
return nil, nil, nil, false
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupIPCacheExpired(host string) (ip, ip6 net.IP, ok bool) {
|
||||
func (r *Resolver) lookupIPCacheExpired(host string) (ip, ip6 net.IP, allIPs []net.IPAddr, ok bool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if ent, ok := r.ipCache[host]; ok {
|
||||
return ent.ip, ent.ip6, true
|
||||
return ent.ip, ent.ip6, ent.allIPs, true
|
||||
}
|
||||
return nil, nil, false
|
||||
return nil, nil, nil, false
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupTimeoutForHost(host string) time.Duration {
|
||||
if r.UseLastGood {
|
||||
if _, _, ok := r.lookupIPCacheExpired(host); ok {
|
||||
if _, _, _, ok := r.lookupIPCacheExpired(host); ok {
|
||||
// If we have some previous good value for this host,
|
||||
// don't give this DNS lookup much time. If we're in a
|
||||
// situation where the user's DNS server is unreachable
|
||||
@@ -194,12 +198,12 @@ func (r *Resolver) lookupTimeoutForHost(host string) time.Duration {
|
||||
return 10 * time.Second
|
||||
}
|
||||
|
||||
func (r *Resolver) lookupIP(host string) (ip, ip6 net.IP, err error) {
|
||||
if ip, ip6, ok := r.lookupIPCache(host); ok {
|
||||
func (r *Resolver) lookupIP(host string) (ip, ip6 net.IP, allIPs []net.IPAddr, err error) {
|
||||
if ip, ip6, allIPs, ok := r.lookupIPCache(host); ok {
|
||||
if debug {
|
||||
log.Printf("dnscache: %q found in cache as %v", host, ip)
|
||||
}
|
||||
return ip, ip6, nil
|
||||
return ip, ip6, allIPs, nil
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), r.lookupTimeoutForHost(host))
|
||||
@@ -218,10 +222,10 @@ func (r *Resolver) lookupIP(host string) (ip, ip6 net.IP, err error) {
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return nil, nil, fmt.Errorf("no IPs for %q found", host)
|
||||
return nil, nil, nil, fmt.Errorf("no IPs for %q found", host)
|
||||
}
|
||||
|
||||
have4 := false
|
||||
@@ -240,12 +244,12 @@ func (r *Resolver) lookupIP(host string) (ip, ip6 net.IP, err error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
r.addIPCache(host, ip, ip6, r.ttl())
|
||||
return ip, ip6, nil
|
||||
r.addIPCache(host, ip, ip6, ips, r.ttl())
|
||||
return ip, ip6, ips, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) addIPCache(host string, ip, ip6 net.IP, d time.Duration) {
|
||||
if isPrivateIP(ip) {
|
||||
func (r *Resolver) addIPCache(host string, ip, ip6 net.IP, allIPs []net.IPAddr, d time.Duration) {
|
||||
if naIP, _ := netaddr.FromStdIP(ip); naIP.IsPrivate() {
|
||||
// Don't cache obviously wrong entries from captive portals.
|
||||
// TODO: use DoH or DoT for the forwarding resolver?
|
||||
if debug {
|
||||
@@ -263,27 +267,14 @@ func (r *Resolver) addIPCache(host string, ip, ip6 net.IP, d time.Duration) {
|
||||
if r.ipCache == nil {
|
||||
r.ipCache = make(map[string]ipCacheEntry)
|
||||
}
|
||||
r.ipCache[host] = ipCacheEntry{ip: ip, ip6: ip6, expires: time.Now().Add(d)}
|
||||
}
|
||||
|
||||
func mustCIDR(s string) *net.IPNet {
|
||||
_, ipNet, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
r.ipCache[host] = ipCacheEntry{
|
||||
ip: ip,
|
||||
ip6: ip6,
|
||||
allIPs: allIPs,
|
||||
expires: time.Now().Add(d),
|
||||
}
|
||||
return ipNet
|
||||
}
|
||||
|
||||
func isPrivateIP(ip net.IP) bool {
|
||||
return private1.Contains(ip) || private2.Contains(ip) || private3.Contains(ip)
|
||||
}
|
||||
|
||||
var (
|
||||
private1 = mustCIDR("10.0.0.0/8")
|
||||
private2 = mustCIDR("172.16.0.0/12")
|
||||
private3 = mustCIDR("192.168.0.0/16")
|
||||
)
|
||||
|
||||
type DialContextFunc func(ctx context.Context, network, address string) (net.Conn, error)
|
||||
|
||||
// Dialer returns a wrapped DialContext func that uses the provided dnsCache.
|
||||
@@ -305,41 +296,131 @@ func Dialer(fwd DialContextFunc, dnsCache *Resolver) DialContextFunc {
|
||||
// Return with original error
|
||||
return
|
||||
}
|
||||
for _, ip := range ips {
|
||||
dst := net.JoinHostPort(ip.String(), port)
|
||||
if c, err := fwd(ctx, network, dst); err == nil {
|
||||
retConn = c
|
||||
ret = nil
|
||||
return
|
||||
}
|
||||
if c, err := raceDial(ctx, fwd, network, ips, port); err == nil {
|
||||
retConn = c
|
||||
ret = nil
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
ip, ip6, err := dnsCache.LookupIP(ctx, host)
|
||||
ip, ip6, allIPs, err := dnsCache.LookupIP(ctx, host)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve %q: %w", host, err)
|
||||
}
|
||||
dst := net.JoinHostPort(ip.String(), port)
|
||||
if debug {
|
||||
log.Printf("dnscache: dialing %s, %s for %s", network, dst, address)
|
||||
i4s := v4addrs(allIPs)
|
||||
if len(i4s) < 2 {
|
||||
dst := net.JoinHostPort(ip.String(), port)
|
||||
if debug {
|
||||
log.Printf("dnscache: dialing %s, %s for %s", network, dst, address)
|
||||
}
|
||||
c, err := fwd(ctx, network, dst)
|
||||
if err == nil || ctx.Err() != nil || ip6 == nil {
|
||||
return c, err
|
||||
}
|
||||
// Fall back to trying IPv6.
|
||||
dst = net.JoinHostPort(ip6.String(), port)
|
||||
return fwd(ctx, network, dst)
|
||||
}
|
||||
c, err := fwd(ctx, network, dst)
|
||||
if err == nil || ctx.Err() != nil || ip6 == nil {
|
||||
return c, err
|
||||
}
|
||||
// Fall back to trying IPv6.
|
||||
// TODO(bradfitz): this is a primarily for IPv6-only
|
||||
// hosts; it's not supposed to be a real Happy
|
||||
// Eyeballs implementation. We should use the net
|
||||
// package's implementation of that by plumbing this
|
||||
// dnscache impl into net.Dialer.Resolver.Dial and
|
||||
// unmarshal/marshal DNS queries/responses to the net
|
||||
// package. This works for v6-only hosts for now.
|
||||
dst = net.JoinHostPort(ip6.String(), port)
|
||||
return fwd(ctx, network, dst)
|
||||
|
||||
// Multiple IPv4 candidates, and 0+ IPv6.
|
||||
ipsToTry := append(i4s, v6addrs(allIPs)...)
|
||||
return raceDial(ctx, fwd, network, ipsToTry, port)
|
||||
}
|
||||
}
|
||||
|
||||
// fallbackDelay is how long to wait between trying subsequent
|
||||
// addresses when multiple options are available.
|
||||
// 300ms is the same as Go's Happy Eyeballs fallbackDelay value.
|
||||
const fallbackDelay = 300 * time.Millisecond
|
||||
|
||||
// raceDial tries to dial port on each ip in ips, starting a new race
|
||||
// dial every fallbackDelay apart, returning whichever completes first.
|
||||
func raceDial(ctx context.Context, fwd DialContextFunc, network string, ips []netaddr.IP, port string) (net.Conn, error) {
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
type res struct {
|
||||
c net.Conn
|
||||
err error
|
||||
}
|
||||
resc := make(chan res) // must be unbuffered
|
||||
failBoost := make(chan struct{}) // best effort send on dial failure
|
||||
|
||||
go func() {
|
||||
for i, ip := range ips {
|
||||
if i != 0 {
|
||||
timer := time.NewTimer(fallbackDelay)
|
||||
select {
|
||||
case <-timer.C:
|
||||
case <-failBoost:
|
||||
timer.Stop()
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
go func(ip netaddr.IP) {
|
||||
c, err := fwd(ctx, network, net.JoinHostPort(ip.String(), port))
|
||||
if err != nil {
|
||||
// Best effort wake-up a pending dial.
|
||||
// e.g. IPv4 dials failing quickly on an IPv6-only system.
|
||||
// In that case we don't want to wait 300ms per IPv4 before
|
||||
// we get to the IPv6 addresses.
|
||||
select {
|
||||
case failBoost <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
select {
|
||||
case resc <- res{c, err}:
|
||||
case <-ctx.Done():
|
||||
if c != nil {
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
}(ip)
|
||||
}
|
||||
}()
|
||||
|
||||
var firstErr error
|
||||
var fails int
|
||||
for {
|
||||
select {
|
||||
case r := <-resc:
|
||||
if r.c != nil {
|
||||
return r.c, nil
|
||||
}
|
||||
fails++
|
||||
if firstErr == nil {
|
||||
firstErr = r.err
|
||||
}
|
||||
if fails == len(ips) {
|
||||
return nil, firstErr
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func v4addrs(aa []net.IPAddr) (ret []netaddr.IP) {
|
||||
for _, a := range aa {
|
||||
if ip, ok := netaddr.FromStdIP(a.IP); ok && ip.Is4() {
|
||||
ret = append(ret, ip)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func v6addrs(aa []net.IPAddr) (ret []netaddr.IP) {
|
||||
for _, a := range aa {
|
||||
if ip, ok := netaddr.FromStdIP(a.IP); ok && ip.Is6() {
|
||||
ret = append(ret, ip)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
var errTLSHandshakeTimeout = errors.New("timeout doing TLS handshake")
|
||||
|
||||
// TLSDialer is like Dialer but returns a func suitable for using with net/http.Transport.DialTLSContext.
|
||||
|
||||
@@ -5,24 +5,29 @@
|
||||
package dnscache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestIsPrivateIP(t *testing.T) {
|
||||
tests := []struct {
|
||||
ip string
|
||||
want bool
|
||||
}{
|
||||
{"10.1.2.3", true},
|
||||
{"172.16.1.100", true},
|
||||
{"192.168.1.1", true},
|
||||
{"1.2.3.4", false},
|
||||
}
|
||||
var dialTest = flag.String("dial-test", "", "if non-empty, addr:port to test dial")
|
||||
|
||||
for _, test := range tests {
|
||||
if got := isPrivateIP(net.ParseIP(test.ip)); got != test.want {
|
||||
t.Errorf("isPrivateIP(%q)=%v, want %v", test.ip, got, test.want)
|
||||
}
|
||||
func TestDialer(t *testing.T) {
|
||||
if *dialTest == "" {
|
||||
t.Skip("skipping; --dial-test is blank")
|
||||
}
|
||||
r := new(Resolver)
|
||||
var std net.Dialer
|
||||
dialer := Dialer(std.DialContext, r)
|
||||
t0 := time.Now()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
c, err := dialer(ctx, "tcp", *dialTest)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("dialed in %v", time.Since(t0))
|
||||
c.Close()
|
||||
}
|
||||
|
||||
@@ -102,6 +102,27 @@
|
||||
"HostName": "derp4b.tailscale.com",
|
||||
"IPv4": "157.230.25.0",
|
||||
"IPv6": "2a03:b0c0:3:e0::58f:3001"
|
||||
},
|
||||
{
|
||||
"Name": "4c",
|
||||
"RegionID": 4,
|
||||
"HostName": "derp4c.tailscale.com",
|
||||
"IPv4": "134.122.77.138",
|
||||
"IPv6": "2a03:b0c0:3:d0::1501:6001"
|
||||
},
|
||||
{
|
||||
"Name": "4d",
|
||||
"RegionID": 4,
|
||||
"HostName": "derp4d.tailscale.com",
|
||||
"IPv4": "134.122.94.167",
|
||||
"IPv6": "2a03:b0c0:3:d0::1501:b001"
|
||||
},
|
||||
{
|
||||
"Name": "4e",
|
||||
"RegionID": 4,
|
||||
"HostName": "derp4e.tailscale.com",
|
||||
"IPv4": "134.122.74.153",
|
||||
"IPv6": "2a03:b0c0:3:d0::29:9001"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -479,7 +479,7 @@ func HTTPOfListener(ln net.Listener) string {
|
||||
var privateIP string
|
||||
ForeachInterfaceAddress(func(i Interface, pfx netaddr.IPPrefix) {
|
||||
ip := pfx.IP()
|
||||
if isPrivateIP(ip) {
|
||||
if ip.IsPrivate() {
|
||||
if privateIP == "" {
|
||||
privateIP = ip.String()
|
||||
}
|
||||
@@ -519,21 +519,15 @@ func LikelyHomeRouterIP() (gateway, myIP netaddr.IP, ok bool) {
|
||||
if !i.IsUp() || ip.IsZero() || !myIP.IsZero() {
|
||||
return
|
||||
}
|
||||
for _, prefix := range privatev4s {
|
||||
if prefix.Contains(gateway) && prefix.Contains(ip) {
|
||||
myIP = ip
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
if gateway.IsPrivate() && ip.IsPrivate() {
|
||||
myIP = ip
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
})
|
||||
return gateway, myIP, !myIP.IsZero()
|
||||
}
|
||||
|
||||
func isPrivateIP(ip netaddr.IP) bool {
|
||||
return private1.Contains(ip) || private2.Contains(ip) || private3.Contains(ip)
|
||||
}
|
||||
|
||||
// isUsableV4 reports whether ip is a usable IPv4 address which could
|
||||
// conceivably be used to get Internet connectivity. Globally routable and
|
||||
// private IPv4 addresses are always Usable, and link local 169.254.x.x
|
||||
@@ -557,20 +551,8 @@ func isUsableV6(ip netaddr.IP) bool {
|
||||
(tsaddr.IsULA(ip) && !tsaddr.TailscaleULARange().Contains(ip))
|
||||
}
|
||||
|
||||
func mustCIDR(s string) netaddr.IPPrefix {
|
||||
prefix, err := netaddr.ParseIPPrefix(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return prefix
|
||||
}
|
||||
|
||||
var (
|
||||
private1 = mustCIDR("10.0.0.0/8")
|
||||
private2 = mustCIDR("172.16.0.0/12")
|
||||
private3 = mustCIDR("192.168.0.0/16")
|
||||
privatev4s = []netaddr.IPPrefix{private1, private2, private3}
|
||||
v6Global1 = mustCIDR("2000::/3")
|
||||
v6Global1 = netaddr.MustParseIPPrefix("2000::/3")
|
||||
)
|
||||
|
||||
// anyInterestingIP reports whether pfxs contains any IP that matches
|
||||
|
||||
@@ -73,7 +73,7 @@ func likelyHomeRouterIPDarwinExec() (ret netaddr.IP, ok bool) {
|
||||
return nil
|
||||
}
|
||||
ip, err := netaddr.ParseIP(string(mem.Append(nil, ipm)))
|
||||
if err == nil && isPrivateIP(ip) {
|
||||
if err == nil && ip.IsPrivate() {
|
||||
ret = ip
|
||||
// We've found what we're looking for.
|
||||
return errStopReadingNetstatTable
|
||||
|
||||
@@ -72,7 +72,7 @@ func likelyHomeRouterIPLinux() (ret netaddr.IP, ok bool) {
|
||||
return nil // ignore error, skip line and keep going
|
||||
}
|
||||
ip := netaddr.IPv4(byte(ipu32), byte(ipu32>>8), byte(ipu32>>16), byte(ipu32>>24))
|
||||
if isPrivateIP(ip) {
|
||||
if ip.IsPrivate() {
|
||||
ret = ip
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -93,7 +93,7 @@ func likelyHomeRouterIPWindows() (ret netaddr.IP, ok bool) {
|
||||
}
|
||||
}
|
||||
|
||||
if !ret.IsZero() && !isPrivateIP(ret) {
|
||||
if !ret.IsZero() && !ret.IsPrivate() {
|
||||
// Default route has a non-private gateway
|
||||
return netaddr.IP{}, false
|
||||
}
|
||||
|
||||
@@ -270,7 +270,7 @@ func IsNoMappingError(err error) bool {
|
||||
|
||||
var (
|
||||
ErrNoPortMappingServices = errors.New("no port mapping services were found")
|
||||
ErrGatewayNotFound = errors.New("failed to look up gateway address")
|
||||
ErrGatewayRange = errors.New("skipping portmap; gateway range likely lacks support")
|
||||
)
|
||||
|
||||
// GetCachedMappingOrStartCreatingOne quickly returns with our current cached portmapping, if any.
|
||||
@@ -331,7 +331,7 @@ func (c *Client) createMapping() {
|
||||
func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPort, err error) {
|
||||
gw, myIP, ok := c.gatewayAndSelfIP()
|
||||
if !ok {
|
||||
return netaddr.IPPort{}, NoMappingError{ErrGatewayNotFound}
|
||||
return netaddr.IPPort{}, NoMappingError{ErrGatewayRange}
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
@@ -540,7 +540,7 @@ type ProbeResult struct {
|
||||
func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
gw, myIP, ok := c.gatewayAndSelfIP()
|
||||
if !ok {
|
||||
return res, ErrGatewayNotFound
|
||||
return res, ErrGatewayRange
|
||||
}
|
||||
defer func() {
|
||||
if err == nil {
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/tstime/mono"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine/filter"
|
||||
@@ -64,7 +65,7 @@ type Wrapper struct {
|
||||
|
||||
closeOnce sync.Once
|
||||
|
||||
lastActivityAtomic int64 // unix seconds of last send or receive
|
||||
lastActivityAtomic mono.Time // time of last send or receive
|
||||
|
||||
destIPActivity atomic.Value // of map[netaddr.IP]func()
|
||||
|
||||
@@ -153,9 +154,10 @@ func Wrap(logf logger.Logf, tdev tun.Device) *Wrapper {
|
||||
// a goroutine should not block when setting it, even with no listeners.
|
||||
bufferConsumed: make(chan struct{}, 1),
|
||||
closed: make(chan struct{}),
|
||||
outbound: make(chan tunReadResult),
|
||||
eventsUpDown: make(chan tun.Event),
|
||||
eventsOther: make(chan tun.Event),
|
||||
// outbound can be unbuffered; the buffer is an optimization.
|
||||
outbound: make(chan tunReadResult, 1),
|
||||
eventsUpDown: make(chan tun.Event),
|
||||
eventsOther: make(chan tun.Event),
|
||||
// TODO(dmytro): (highly rate-limited) hexdumps should happen on unknown packets.
|
||||
filterFlags: filter.LogAccepts | filter.LogDrops,
|
||||
}
|
||||
@@ -164,6 +166,7 @@ func Wrap(logf logger.Logf, tdev tun.Device) *Wrapper {
|
||||
go tun.pumpEvents()
|
||||
// The buffer starts out consumed.
|
||||
tun.bufferConsumed <- struct{}{}
|
||||
tun.noteActivity()
|
||||
|
||||
return tun
|
||||
}
|
||||
@@ -363,16 +366,15 @@ func (t *Wrapper) filterOut(p *packet.Parsed) filter.Response {
|
||||
|
||||
// noteActivity records that there was a read or write at the current time.
|
||||
func (t *Wrapper) noteActivity() {
|
||||
atomic.StoreInt64(&t.lastActivityAtomic, time.Now().Unix())
|
||||
t.lastActivityAtomic.StoreAtomic(mono.Now())
|
||||
}
|
||||
|
||||
// IdleDuration reports how long it's been since the last read or write to this device.
|
||||
//
|
||||
// Its value is only accurate to roughly second granularity.
|
||||
// If there's never been activity, the duration is since 1970.
|
||||
// Its value should only be presumed accurate to roughly 10ms granularity.
|
||||
// If there's never been activity, the duration is since the wrapper was created.
|
||||
func (t *Wrapper) IdleDuration() time.Duration {
|
||||
sec := atomic.LoadInt64(&t.lastActivityAtomic)
|
||||
return time.Since(time.Unix(sec, 0))
|
||||
return mono.Since(t.lastActivityAtomic.LoadAtomic())
|
||||
}
|
||||
|
||||
func (t *Wrapper) Read(buf []byte, offset int) (int, error) {
|
||||
|
||||
@@ -10,13 +10,13 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"golang.zx2c4.com/wireguard/tun/tuntest"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/tstime/mono"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine/filter"
|
||||
@@ -335,9 +335,9 @@ func TestFilter(t *testing.T) {
|
||||
// data was actually filtered.
|
||||
// If it stays zero, nothing made it through
|
||||
// to the wrapped TUN.
|
||||
atomic.StoreInt64(&tun.lastActivityAtomic, 0)
|
||||
tun.lastActivityAtomic.StoreAtomic(0)
|
||||
_, err = tun.Write(tt.data, 0)
|
||||
filtered = atomic.LoadInt64(&tun.lastActivityAtomic) == 0
|
||||
filtered = tun.lastActivityAtomic.LoadAtomic() == 0
|
||||
} else {
|
||||
chtun.Outbound <- tt.data
|
||||
n, err = tun.Read(buf[:], 0)
|
||||
@@ -416,7 +416,7 @@ func TestAtomic64Alignment(t *testing.T) {
|
||||
}
|
||||
|
||||
c := new(Wrapper)
|
||||
atomic.StoreInt64(&c.lastActivityAtomic, 123)
|
||||
c.lastActivityAtomic.StoreAtomic(mono.Now())
|
||||
}
|
||||
|
||||
func TestPeerAPIBypass(t *testing.T) {
|
||||
|
||||
@@ -164,6 +164,12 @@ type Node struct {
|
||||
Hostinfo Hostinfo
|
||||
Created time.Time
|
||||
|
||||
// PrimaryRoutes are the routes from AllowedIPs that this node
|
||||
// is currently the primary subnet router for, as determined
|
||||
// by the control plane. It does not include the self address
|
||||
// values from Addresses that are in AllowedIPs.
|
||||
PrimaryRoutes []netaddr.IPPrefix `json:",omitempty"`
|
||||
|
||||
// LastSeen is when the node was last online. It is not
|
||||
// updated when Online is true. It is nil if the current
|
||||
// node doesn't have permission to know, or the node
|
||||
@@ -1142,6 +1148,7 @@ func (n *Node) Equal(n2 *Node) bool {
|
||||
eqBoolPtr(n.Online, n2.Online) &&
|
||||
eqCIDRs(n.Addresses, n2.Addresses) &&
|
||||
eqCIDRs(n.AllowedIPs, n2.AllowedIPs) &&
|
||||
eqCIDRs(n.PrimaryRoutes, n2.PrimaryRoutes) &&
|
||||
eqStrings(n.Endpoints, n2.Endpoints) &&
|
||||
n.DERP == n2.DERP &&
|
||||
n.Hostinfo.Equal(&n2.Hostinfo) &&
|
||||
|
||||
@@ -49,6 +49,7 @@ func (src *Node) Clone() *Node {
|
||||
dst.AllowedIPs = append(src.AllowedIPs[:0:0], src.AllowedIPs...)
|
||||
dst.Endpoints = append(src.Endpoints[:0:0], src.Endpoints...)
|
||||
dst.Hostinfo = *src.Hostinfo.Clone()
|
||||
dst.PrimaryRoutes = append(src.PrimaryRoutes[:0:0], src.PrimaryRoutes...)
|
||||
if dst.LastSeen != nil {
|
||||
dst.LastSeen = new(time.Time)
|
||||
*dst.LastSeen = *src.LastSeen
|
||||
@@ -79,6 +80,7 @@ var _NodeNeedsRegeneration = Node(struct {
|
||||
DERP string
|
||||
Hostinfo Hostinfo
|
||||
Created time.Time
|
||||
PrimaryRoutes []netaddr.IPPrefix
|
||||
LastSeen *time.Time
|
||||
Online *bool
|
||||
KeepAlive bool
|
||||
|
||||
@@ -194,7 +194,8 @@ func TestNodeEqual(t *testing.T) {
|
||||
"ID", "StableID", "Name", "User", "Sharer",
|
||||
"Key", "KeyExpiry", "Machine", "DiscoKey",
|
||||
"Addresses", "AllowedIPs", "Endpoints", "DERP", "Hostinfo",
|
||||
"Created", "LastSeen", "Online", "KeepAlive", "MachineAuthorized",
|
||||
"Created", "PrimaryRoutes",
|
||||
"LastSeen", "Online", "KeepAlive", "MachineAuthorized",
|
||||
"Capabilities",
|
||||
"ComputedName", "computedHostIfDifferent", "ComputedNameWithHost",
|
||||
}
|
||||
|
||||
121
tstime/mono/mono.go
Normal file
121
tstime/mono/mono.go
Normal file
@@ -0,0 +1,121 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package mono provides fast monotonic time.
|
||||
// On most platforms, mono.Now is about 2x faster than time.Now.
|
||||
// However, time.Now is really fast, and nicer to use.
|
||||
//
|
||||
// For almost all purposes, you should use time.Now.
|
||||
//
|
||||
// Package mono exists because we get the current time multiple
|
||||
// times per network packet, at which point it makes a
|
||||
// measurable difference.
|
||||
package mono
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
_ "unsafe" // for go:linkname
|
||||
)
|
||||
|
||||
// Time is the number of nanoseconds elapsed since an unspecified reference start time.
|
||||
type Time int64
|
||||
|
||||
// Now returns the current monotonic time.
|
||||
func Now() Time {
|
||||
// On a newly started machine, the monotonic clock might be very near zero.
|
||||
// Thus mono.Time(0).Before(mono.Now.Add(-time.Minute)) might yield true.
|
||||
// The corresponding package time expression never does, if the wall clock is correct.
|
||||
// Preserve this correspondence by increasing the "base" monotonic clock by a fair amount.
|
||||
const baseOffset int64 = 1 << 55 // approximately 10,000 hours in nanoseconds
|
||||
return Time(now() + baseOffset)
|
||||
}
|
||||
|
||||
// Since returns the time elapsed since t.
|
||||
func Since(t Time) time.Duration {
|
||||
return time.Duration(Now() - t)
|
||||
}
|
||||
|
||||
// Sub returns t-n, the duration from n to t.
|
||||
func (t Time) Sub(n Time) time.Duration {
|
||||
return time.Duration(t - n)
|
||||
}
|
||||
|
||||
// Add returns t+d.
|
||||
func (t Time) Add(d time.Duration) Time {
|
||||
return t + Time(d)
|
||||
}
|
||||
|
||||
// After reports t > n, whether t is after n.
|
||||
func (t Time) After(n Time) bool {
|
||||
return t > n
|
||||
}
|
||||
|
||||
// After reports t < n, whether t is before n.
|
||||
func (t Time) Before(n Time) bool {
|
||||
return t < n
|
||||
}
|
||||
|
||||
// IsZero reports whether t == 0.
|
||||
func (t Time) IsZero() bool {
|
||||
return t == 0
|
||||
}
|
||||
|
||||
// StoreAtomic does an atomic store *t = new.
|
||||
func (t *Time) StoreAtomic(new Time) {
|
||||
atomic.StoreInt64((*int64)(t), int64(new))
|
||||
}
|
||||
|
||||
// LoadAtomic does an atomic load *t.
|
||||
func (t *Time) LoadAtomic() Time {
|
||||
return Time(atomic.LoadInt64((*int64)(t)))
|
||||
}
|
||||
|
||||
//go:linkname now runtime.nanotime1
|
||||
func now() int64
|
||||
|
||||
// baseWall and baseMono are a pair of almost-identical times used to correlate a Time with a wall time.
|
||||
var (
|
||||
baseWall time.Time
|
||||
baseMono Time
|
||||
)
|
||||
|
||||
func init() {
|
||||
baseWall = time.Now()
|
||||
baseMono = Now()
|
||||
}
|
||||
|
||||
// String prints t, including an estimated equivalent wall clock.
|
||||
// This is best-effort only, for rough debugging purposes only.
|
||||
// Since t is a monotonic time, it can vary from the actual wall clock by arbitrary amounts.
|
||||
// Even in the best of circumstances, it may vary by a few milliseconds.
|
||||
func (t Time) String() string {
|
||||
return fmt.Sprintf("mono.Time(ns=%d, estimated wall=%v)", int64(t), baseWall.Add(t.Sub(baseMono)).Truncate(0))
|
||||
}
|
||||
|
||||
// MarshalJSON formats t for JSON as if it were a time.Time.
|
||||
// We format Time this way for backwards-compatibility.
|
||||
// This is best-effort only. Time does not survive a MarshalJSON/UnmarshalJSON round trip unchanged.
|
||||
// Since t is a monotonic time, it can vary from the actual wall clock by arbitrary amounts.
|
||||
// Even in the best of circumstances, it may vary by a few milliseconds.
|
||||
func (t Time) MarshalJSON() ([]byte, error) {
|
||||
var tt time.Time
|
||||
if !t.IsZero() {
|
||||
tt = baseWall.Add(t.Sub(baseMono)).Truncate(0)
|
||||
}
|
||||
return tt.MarshalJSON()
|
||||
}
|
||||
|
||||
// UnmarshalJSON sets t according to data.
|
||||
// This is best-effort only. Time does not survive a MarshalJSON/UnmarshalJSON round trip unchanged.
|
||||
func (t *Time) UnmarshalJSON(data []byte) error {
|
||||
var tt time.Time
|
||||
err := tt.UnmarshalJSON(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*t = Now().Add(-time.Since(tt))
|
||||
return nil
|
||||
}
|
||||
30
tstime/mono/mono_test.go
Normal file
30
tstime/mono/mono_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// 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 mono
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNow(t *testing.T) {
|
||||
start := Now()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if elapsed := Since(start); elapsed < 100*time.Millisecond {
|
||||
t.Errorf("short sleep: %v elapsed, want min %v", elapsed, 100*time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMonoNow(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
Now()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTimeNow(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
time.Now()
|
||||
}
|
||||
}
|
||||
89
tstime/rate/rate.go
Normal file
89
tstime/rate/rate.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// 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.
|
||||
|
||||
// This is a modified, simplified version of code from golang.org/x/time/rate.
|
||||
|
||||
// Copyright 2015 The Go 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 rate provides a rate limiter.
|
||||
package rate
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tstime/mono"
|
||||
)
|
||||
|
||||
// Limit defines the maximum frequency of some events.
|
||||
// Limit is represented as number of events per second.
|
||||
// A zero Limit is invalid.
|
||||
type Limit float64
|
||||
|
||||
// Every converts a minimum time interval between events to a Limit.
|
||||
func Every(interval time.Duration) Limit {
|
||||
if interval <= 0 {
|
||||
panic("invalid interval")
|
||||
}
|
||||
return 1 / Limit(interval.Seconds())
|
||||
}
|
||||
|
||||
// A Limiter controls how frequently events are allowed to happen.
|
||||
// It implements a "token bucket" of size b, initially full and refilled
|
||||
// at rate r tokens per second.
|
||||
// Informally, in any large enough time interval, the Limiter limits the
|
||||
// rate to r tokens per second, with a maximum burst size of b events.
|
||||
// See https://en.wikipedia.org/wiki/Token_bucket for more about token buckets.
|
||||
// Use NewLimiter to create non-zero Limiters.
|
||||
type Limiter struct {
|
||||
limit Limit
|
||||
burst float64
|
||||
mu sync.Mutex // protects following fields
|
||||
tokens float64 // number of tokens currently in bucket
|
||||
last mono.Time // the last time the limiter's tokens field was updated
|
||||
}
|
||||
|
||||
// NewLimiter returns a new Limiter that allows events up to rate r and permits
|
||||
// bursts of at most b tokens.
|
||||
func NewLimiter(r Limit, b int) *Limiter {
|
||||
if b < 1 {
|
||||
panic("bad burst, must be at least 1")
|
||||
}
|
||||
return &Limiter{limit: r, burst: float64(b)}
|
||||
}
|
||||
|
||||
// AllowN reports whether an event may happen now.
|
||||
func (lim *Limiter) Allow() bool {
|
||||
return lim.allow(mono.Now())
|
||||
}
|
||||
|
||||
func (lim *Limiter) allow(now mono.Time) bool {
|
||||
lim.mu.Lock()
|
||||
defer lim.mu.Unlock()
|
||||
|
||||
// If time has moved backwards, look around awkwardly and pretend nothing happened.
|
||||
if now.Before(lim.last) {
|
||||
lim.last = now
|
||||
}
|
||||
|
||||
// Calculate the new number of tokens available due to the passage of time.
|
||||
elapsed := now.Sub(lim.last)
|
||||
tokens := lim.tokens + float64(lim.limit)*elapsed.Seconds()
|
||||
if tokens > lim.burst {
|
||||
tokens = lim.burst
|
||||
}
|
||||
|
||||
// Consume a token.
|
||||
tokens--
|
||||
|
||||
// Update state.
|
||||
ok := tokens >= 0
|
||||
if ok {
|
||||
lim.last = now
|
||||
lim.tokens = tokens
|
||||
}
|
||||
return ok
|
||||
}
|
||||
246
tstime/rate/rate_test.go
Normal file
246
tstime/rate/rate_test.go
Normal file
@@ -0,0 +1,246 @@
|
||||
// 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.
|
||||
|
||||
// This is a modified, simplified version of code from golang.org/x/time/rate.
|
||||
|
||||
// Copyright 2015 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.7
|
||||
// +build go1.7
|
||||
|
||||
package rate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tstime/mono"
|
||||
)
|
||||
|
||||
func closeEnough(a, b Limit) bool {
|
||||
return (math.Abs(float64(a)/float64(b)) - 1.0) < 1e-9
|
||||
}
|
||||
|
||||
func TestEvery(t *testing.T) {
|
||||
cases := []struct {
|
||||
interval time.Duration
|
||||
lim Limit
|
||||
}{
|
||||
{1 * time.Nanosecond, Limit(1e9)},
|
||||
{1 * time.Microsecond, Limit(1e6)},
|
||||
{1 * time.Millisecond, Limit(1e3)},
|
||||
{10 * time.Millisecond, Limit(100)},
|
||||
{100 * time.Millisecond, Limit(10)},
|
||||
{1 * time.Second, Limit(1)},
|
||||
{2 * time.Second, Limit(0.5)},
|
||||
{time.Duration(2.5 * float64(time.Second)), Limit(0.4)},
|
||||
{4 * time.Second, Limit(0.25)},
|
||||
{10 * time.Second, Limit(0.1)},
|
||||
{time.Duration(math.MaxInt64), Limit(1e9 / float64(math.MaxInt64))},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
lim := Every(tc.interval)
|
||||
if !closeEnough(lim, tc.lim) {
|
||||
t.Errorf("Every(%v) = %v want %v", tc.interval, lim, tc.lim)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
d = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
var (
|
||||
t0 = mono.Now()
|
||||
t1 = t0.Add(time.Duration(1) * d)
|
||||
t2 = t0.Add(time.Duration(2) * d)
|
||||
t3 = t0.Add(time.Duration(3) * d)
|
||||
t4 = t0.Add(time.Duration(4) * d)
|
||||
t5 = t0.Add(time.Duration(5) * d)
|
||||
t9 = t0.Add(time.Duration(9) * d)
|
||||
)
|
||||
|
||||
type allow struct {
|
||||
t mono.Time
|
||||
ok bool
|
||||
}
|
||||
|
||||
func run(t *testing.T, lim *Limiter, allows []allow) {
|
||||
t.Helper()
|
||||
for i, allow := range allows {
|
||||
ok := lim.allow(allow.t)
|
||||
if ok != allow.ok {
|
||||
t.Errorf("step %d: lim.AllowN(%v) = %v want %v",
|
||||
i, allow.t, ok, allow.ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLimiterBurst1(t *testing.T) {
|
||||
run(t, NewLimiter(10, 1), []allow{
|
||||
{t0, true},
|
||||
{t0, false},
|
||||
{t0, false},
|
||||
{t1, true},
|
||||
{t1, false},
|
||||
{t1, false},
|
||||
{t2, true},
|
||||
{t2, false},
|
||||
})
|
||||
}
|
||||
|
||||
func TestLimiterJumpBackwards(t *testing.T) {
|
||||
run(t, NewLimiter(10, 3), []allow{
|
||||
{t1, true}, // start at t1
|
||||
{t0, true}, // jump back to t0, two tokens remain
|
||||
{t0, true},
|
||||
{t0, false},
|
||||
{t0, false},
|
||||
{t1, true}, // got a token
|
||||
{t1, false},
|
||||
{t1, false},
|
||||
{t2, true}, // got another token
|
||||
{t2, false},
|
||||
{t2, false},
|
||||
})
|
||||
}
|
||||
|
||||
// Ensure that tokensFromDuration doesn't produce
|
||||
// rounding errors by truncating nanoseconds.
|
||||
// See golang.org/issues/34861.
|
||||
func TestLimiter_noTruncationErrors(t *testing.T) {
|
||||
if !NewLimiter(0.7692307692307693, 1).Allow() {
|
||||
t.Fatal("expected true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimultaneousRequests(t *testing.T) {
|
||||
const (
|
||||
limit = 1
|
||||
burst = 5
|
||||
numRequests = 15
|
||||
)
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
numOK = uint32(0)
|
||||
)
|
||||
|
||||
// Very slow replenishing bucket.
|
||||
lim := NewLimiter(limit, burst)
|
||||
|
||||
// Tries to take a token, atomically updates the counter and decreases the wait
|
||||
// group counter.
|
||||
f := func() {
|
||||
defer wg.Done()
|
||||
if ok := lim.Allow(); ok {
|
||||
atomic.AddUint32(&numOK, 1)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Add(numRequests)
|
||||
for i := 0; i < numRequests; i++ {
|
||||
go f()
|
||||
}
|
||||
wg.Wait()
|
||||
if numOK != burst {
|
||||
t.Errorf("numOK = %d, want %d", numOK, burst)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLongRunningQPS(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping in short mode")
|
||||
}
|
||||
if runtime.GOOS == "openbsd" {
|
||||
t.Skip("low resolution time.Sleep invalidates test (golang.org/issue/14183)")
|
||||
return
|
||||
}
|
||||
|
||||
// The test runs for a few seconds executing many requests and then checks
|
||||
// that overall number of requests is reasonable.
|
||||
const (
|
||||
limit = 100
|
||||
burst = 100
|
||||
)
|
||||
var numOK = int32(0)
|
||||
|
||||
lim := NewLimiter(limit, burst)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
f := func() {
|
||||
if ok := lim.Allow(); ok {
|
||||
atomic.AddInt32(&numOK, 1)
|
||||
}
|
||||
wg.Done()
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
end := start.Add(5 * time.Second)
|
||||
for time.Now().Before(end) {
|
||||
wg.Add(1)
|
||||
go f()
|
||||
|
||||
// This will still offer ~500 requests per second, but won't consume
|
||||
// outrageous amount of CPU.
|
||||
time.Sleep(2 * time.Millisecond)
|
||||
}
|
||||
wg.Wait()
|
||||
elapsed := time.Since(start)
|
||||
ideal := burst + (limit * float64(elapsed) / float64(time.Second))
|
||||
|
||||
// We should never get more requests than allowed.
|
||||
if want := int32(ideal + 1); numOK > want {
|
||||
t.Errorf("numOK = %d, want %d (ideal %f)", numOK, want, ideal)
|
||||
}
|
||||
// We should get very close to the number of requests allowed.
|
||||
if want := int32(0.999 * ideal); numOK < want {
|
||||
t.Errorf("numOK = %d, want %d (ideal %f)", numOK, want, ideal)
|
||||
}
|
||||
}
|
||||
|
||||
type request struct {
|
||||
t time.Time
|
||||
n int
|
||||
act time.Time
|
||||
ok bool
|
||||
}
|
||||
|
||||
// dFromDuration converts a duration to a multiple of the global constant d
|
||||
func dFromDuration(dur time.Duration) int {
|
||||
// Adding a millisecond to be swallowed by the integer division
|
||||
// because we don't care about small inaccuracies
|
||||
return int((dur + time.Millisecond) / d)
|
||||
}
|
||||
|
||||
// dSince returns multiples of d since t0
|
||||
func dSince(t mono.Time) int {
|
||||
return dFromDuration(t.Sub(t0))
|
||||
}
|
||||
|
||||
type wait struct {
|
||||
name string
|
||||
ctx context.Context
|
||||
n int
|
||||
delay int // in multiples of d
|
||||
nilErr bool
|
||||
}
|
||||
|
||||
func BenchmarkAllowN(b *testing.B) {
|
||||
lim := NewLimiter(Every(1*time.Second), 1)
|
||||
now := mono.Now()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
b.RunParallel(func(pb *testing.PB) {
|
||||
for pb.Next() {
|
||||
lim.allow(now)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -409,7 +409,7 @@ func VarzHandler(w http.ResponseWriter, r *http.Request) {
|
||||
case expvar.Func:
|
||||
val := v()
|
||||
switch val.(type) {
|
||||
case int64, int:
|
||||
case float64, int64, int:
|
||||
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", name, typ, name, val)
|
||||
default:
|
||||
fmt.Fprintf(w, "# skipping expvar func %q returning unknown type %T\n", name, val)
|
||||
|
||||
@@ -8,7 +8,7 @@ package version
|
||||
// Long is a full version number for this build, of the form
|
||||
// "x.y.z-commithash", or "date.yyyymmdd" if no actual version was
|
||||
// provided.
|
||||
var Long = "date.20210603"
|
||||
var Long = "date.20210727"
|
||||
|
||||
// Short is a short version number for this build, of the form
|
||||
// "x.y.z", or "date.yyyymmdd" if no actual version was provided.
|
||||
|
||||
@@ -11,10 +11,10 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/flowtrack"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/tstime/rate"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -13,11 +13,11 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"golang.org/x/time/rate"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime/rate"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"golang.zx2c4.com/wireguard/tai64n"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tstime/mono"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/wgkey"
|
||||
@@ -348,12 +349,12 @@ type addrSet struct {
|
||||
ipPorts []netaddr.IPPort
|
||||
|
||||
// clock, if non-nil, is used in tests instead of time.Now.
|
||||
clock func() time.Time
|
||||
clock func() mono.Time
|
||||
Logf logger.Logf // must not be nil
|
||||
|
||||
mu sync.Mutex // guards following fields
|
||||
|
||||
lastSend time.Time
|
||||
lastSend mono.Time
|
||||
|
||||
// roamAddr is non-nil if/when we receive a correctly signed
|
||||
// WireGuard packet from an unexpected address. If so, we
|
||||
@@ -369,10 +370,10 @@ type addrSet struct {
|
||||
curAddr int
|
||||
|
||||
// stopSpray is the time after which we stop spraying packets.
|
||||
stopSpray time.Time
|
||||
stopSpray mono.Time
|
||||
|
||||
// lastSpray is the last time we sprayed a packet.
|
||||
lastSpray time.Time
|
||||
lastSpray mono.Time
|
||||
|
||||
// loggedLogPriMask is a bit field of that tracks whether
|
||||
// we've already logged about receiving a packet from a low
|
||||
@@ -395,11 +396,11 @@ func (as *addrSet) derpID() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (as *addrSet) timeNow() time.Time {
|
||||
func (as *addrSet) timeNow() mono.Time {
|
||||
if as.clock != nil {
|
||||
return as.clock()
|
||||
}
|
||||
return time.Now()
|
||||
return mono.Now()
|
||||
}
|
||||
|
||||
var noAddr, _ = netaddr.FromStdAddr(net.ParseIP("127.127.127.127"), 127, "")
|
||||
|
||||
@@ -47,6 +47,7 @@ import (
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/tstime/mono"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
@@ -814,20 +815,20 @@ func (c *Conn) SetNetInfoCallback(fn func(*tailcfg.NetInfo)) {
|
||||
}
|
||||
}
|
||||
|
||||
// LastRecvActivityOfDisco returns the time we last got traffic from
|
||||
// LastRecvActivityOfDisco describes the time we last got traffic from
|
||||
// this endpoint (updated every ~10 seconds).
|
||||
func (c *Conn) LastRecvActivityOfDisco(dk tailcfg.DiscoKey) time.Time {
|
||||
func (c *Conn) LastRecvActivityOfDisco(dk tailcfg.DiscoKey) string {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
de, ok := c.endpointOfDisco[dk]
|
||||
if !ok {
|
||||
return time.Time{}
|
||||
return "never"
|
||||
}
|
||||
unix := atomic.LoadInt64(&de.lastRecvUnixAtomic)
|
||||
if unix == 0 {
|
||||
return time.Time{}
|
||||
saw := de.lastRecv.LoadAtomic()
|
||||
if saw == 0 {
|
||||
return "never"
|
||||
}
|
||||
return time.Unix(unix, 0)
|
||||
return mono.Since(saw).Round(time.Second).String()
|
||||
}
|
||||
|
||||
// Ping handles a "tailscale ping" CLI query.
|
||||
@@ -3126,7 +3127,7 @@ func ippDebugString(ua netaddr.IPPort) string {
|
||||
// advertise a DiscoKey and participate in active discovery.
|
||||
type discoEndpoint struct {
|
||||
// atomically accessed; declared first for alignment reasons
|
||||
lastRecvUnixAtomic int64
|
||||
lastRecv mono.Time
|
||||
numStopAndResetAtomic int64
|
||||
|
||||
// These fields are initialized once and never modified.
|
||||
@@ -3145,13 +3146,13 @@ type discoEndpoint struct {
|
||||
mu sync.Mutex // Lock ordering: Conn.mu, then discoEndpoint.mu
|
||||
|
||||
heartBeatTimer *time.Timer // nil when idle
|
||||
lastSend time.Time // last time there was outgoing packets sent to this peer (from wireguard-go)
|
||||
lastFullPing time.Time // last time we pinged all endpoints
|
||||
lastSend mono.Time // last time there was outgoing packets sent to this peer (from wireguard-go)
|
||||
lastFullPing mono.Time // last time we pinged all endpoints
|
||||
derpAddr netaddr.IPPort // fallback/bootstrap path, if non-zero (non-zero for well-behaved clients)
|
||||
|
||||
bestAddr addrLatency // best non-DERP path; zero if none
|
||||
bestAddrAt time.Time // time best address re-confirmed
|
||||
trustBestAddrUntil time.Time // time when bestAddr expires
|
||||
bestAddrAt mono.Time // time best address re-confirmed
|
||||
trustBestAddrUntil mono.Time // time when bestAddr expires
|
||||
sentPing map[stun.TxID]sentPing
|
||||
endpointState map[netaddr.IPPort]*endpointState
|
||||
isCallMeMaybeEP map[netaddr.IPPort]bool
|
||||
@@ -3218,7 +3219,7 @@ type endpointState struct {
|
||||
// all fields guarded by discoEndpoint.mu
|
||||
|
||||
// lastPing is the last (outgoing) ping time.
|
||||
lastPing time.Time
|
||||
lastPing mono.Time
|
||||
|
||||
// lastGotPing, if non-zero, means that this was an endpoint
|
||||
// that we learned about at runtime (from an incoming ping)
|
||||
@@ -3266,14 +3267,14 @@ const pongHistoryCount = 64
|
||||
|
||||
type pongReply struct {
|
||||
latency time.Duration
|
||||
pongAt time.Time // when we received the pong
|
||||
pongAt mono.Time // when we received the pong
|
||||
from netaddr.IPPort // the pong's src (usually same as endpoint map key)
|
||||
pongSrc netaddr.IPPort // what they reported they heard
|
||||
}
|
||||
|
||||
type sentPing struct {
|
||||
to netaddr.IPPort
|
||||
at time.Time
|
||||
at mono.Time
|
||||
timer *time.Timer // timeout timer
|
||||
purpose discoPingPurpose
|
||||
}
|
||||
@@ -3293,10 +3294,10 @@ func (de *discoEndpoint) initFakeUDPAddr() {
|
||||
// endpoint and reports whether it's been at least 10 seconds since the last
|
||||
// receive activity (including having never received from this peer before).
|
||||
func (de *discoEndpoint) isFirstRecvActivityInAwhile() bool {
|
||||
now := time.Now().Unix()
|
||||
old := atomic.LoadInt64(&de.lastRecvUnixAtomic)
|
||||
if old <= now-10 {
|
||||
atomic.StoreInt64(&de.lastRecvUnixAtomic, now)
|
||||
now := mono.Now()
|
||||
elapsed := now.Sub(de.lastRecv.LoadAtomic())
|
||||
if elapsed > 10*time.Second {
|
||||
de.lastRecv.StoreAtomic(now)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -3321,7 +3322,7 @@ func (de *discoEndpoint) DstToBytes() []byte { return packIPPort(de.fakeWGAddr)
|
||||
// addr may be non-zero.
|
||||
//
|
||||
// de.mu must be held.
|
||||
func (de *discoEndpoint) addrForSendLocked(now time.Time) (udpAddr, derpAddr netaddr.IPPort) {
|
||||
func (de *discoEndpoint) addrForSendLocked(now mono.Time) (udpAddr, derpAddr netaddr.IPPort) {
|
||||
udpAddr = de.bestAddr.IPPort
|
||||
if udpAddr.IsZero() || now.After(de.trustBestAddrUntil) {
|
||||
// We had a bestAddr but it expired so send both to it
|
||||
@@ -3344,13 +3345,13 @@ func (de *discoEndpoint) heartbeat() {
|
||||
return
|
||||
}
|
||||
|
||||
if time.Since(de.lastSend) > sessionActiveTimeout {
|
||||
if mono.Since(de.lastSend) > sessionActiveTimeout {
|
||||
// Session's idle. Stop heartbeating.
|
||||
de.c.logf("[v1] magicsock: disco: ending heartbeats for idle session to %v (%v)", de.publicKey.ShortString(), de.discoShort)
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
now := mono.Now()
|
||||
udpAddr, _ := de.addrForSendLocked(now)
|
||||
if !udpAddr.IsZero() {
|
||||
// We have a preferred path. Ping that every 2 seconds.
|
||||
@@ -3368,7 +3369,7 @@ func (de *discoEndpoint) heartbeat() {
|
||||
// a better path.
|
||||
//
|
||||
// de.mu must be held.
|
||||
func (de *discoEndpoint) wantFullPingLocked(now time.Time) bool {
|
||||
func (de *discoEndpoint) wantFullPingLocked(now mono.Time) bool {
|
||||
if de.bestAddr.IsZero() || de.lastFullPing.IsZero() {
|
||||
return true
|
||||
}
|
||||
@@ -3385,7 +3386,7 @@ func (de *discoEndpoint) wantFullPingLocked(now time.Time) bool {
|
||||
}
|
||||
|
||||
func (de *discoEndpoint) noteActiveLocked() {
|
||||
de.lastSend = time.Now()
|
||||
de.lastSend = mono.Now()
|
||||
if de.heartBeatTimer == nil {
|
||||
de.heartBeatTimer = time.AfterFunc(heartbeatInterval, de.heartbeat)
|
||||
}
|
||||
@@ -3399,7 +3400,7 @@ func (de *discoEndpoint) cliPing(res *ipnstate.PingResult, cb func(*ipnstate.Pin
|
||||
|
||||
de.pendingCLIPings = append(de.pendingCLIPings, pendingCLIPing{res, cb})
|
||||
|
||||
now := time.Now()
|
||||
now := mono.Now()
|
||||
udpAddr, derpAddr := de.addrForSendLocked(now)
|
||||
if !derpAddr.IsZero() {
|
||||
de.startPingLocked(derpAddr, now, pingCLI)
|
||||
@@ -3419,7 +3420,7 @@ func (de *discoEndpoint) cliPing(res *ipnstate.PingResult, cb func(*ipnstate.Pin
|
||||
}
|
||||
|
||||
func (de *discoEndpoint) send(b []byte) error {
|
||||
now := time.Now()
|
||||
now := mono.Now()
|
||||
|
||||
de.mu.Lock()
|
||||
udpAddr, derpAddr := de.addrForSendLocked(now)
|
||||
@@ -3452,7 +3453,7 @@ func (de *discoEndpoint) pingTimeout(txid stun.TxID) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if debugDisco || de.bestAddr.IsZero() || time.Now().After(de.trustBestAddrUntil) {
|
||||
if debugDisco || de.bestAddr.IsZero() || mono.Now().After(de.trustBestAddrUntil) {
|
||||
de.c.logf("[v1] magicsock: disco: timeout waiting for pong %x from %v (%v, %v)", txid[:6], sp.to, de.publicKey.ShortString(), de.discoShort)
|
||||
}
|
||||
de.removeSentPingLocked(txid, sp)
|
||||
@@ -3504,7 +3505,7 @@ const (
|
||||
pingCLI
|
||||
)
|
||||
|
||||
func (de *discoEndpoint) startPingLocked(ep netaddr.IPPort, now time.Time, purpose discoPingPurpose) {
|
||||
func (de *discoEndpoint) startPingLocked(ep netaddr.IPPort, now mono.Time, purpose discoPingPurpose) {
|
||||
if purpose != pingCLI {
|
||||
st, ok := de.endpointState[ep]
|
||||
if !ok {
|
||||
@@ -3530,7 +3531,7 @@ func (de *discoEndpoint) startPingLocked(ep netaddr.IPPort, now time.Time, purpo
|
||||
go de.sendDiscoPing(ep, txid, logLevel)
|
||||
}
|
||||
|
||||
func (de *discoEndpoint) sendPingsLocked(now time.Time, sendCallMeMaybe bool) {
|
||||
func (de *discoEndpoint) sendPingsLocked(now mono.Time, sendCallMeMaybe bool) {
|
||||
de.lastFullPing = now
|
||||
var sentAny bool
|
||||
for ep, st := range de.endpointState {
|
||||
@@ -3652,7 +3653,7 @@ func (de *discoEndpoint) noteConnectivityChange() {
|
||||
de.mu.Lock()
|
||||
defer de.mu.Unlock()
|
||||
|
||||
de.trustBestAddrUntil = time.Time{}
|
||||
de.trustBestAddrUntil = 0
|
||||
}
|
||||
|
||||
// handlePongConnLocked handles a Pong message (a reply to an earlier ping).
|
||||
@@ -3670,7 +3671,7 @@ func (de *discoEndpoint) handlePongConnLocked(m *disco.Pong, src netaddr.IPPort)
|
||||
}
|
||||
de.removeSentPingLocked(m.TxID, sp)
|
||||
|
||||
now := time.Now()
|
||||
now := mono.Now()
|
||||
latency := now.Sub(sp.at)
|
||||
|
||||
if !isDerp {
|
||||
@@ -3822,9 +3823,9 @@ func (de *discoEndpoint) handleCallMeMaybe(m *disco.CallMeMaybe) {
|
||||
// Zero out all the lastPing times to force sendPingsLocked to send new ones,
|
||||
// even if it's been less than 5 seconds ago.
|
||||
for _, st := range de.endpointState {
|
||||
st.lastPing = time.Time{}
|
||||
st.lastPing = 0
|
||||
}
|
||||
de.sendPingsLocked(time.Now(), false)
|
||||
de.sendPingsLocked(mono.Now(), false)
|
||||
}
|
||||
|
||||
func (de *discoEndpoint) populatePeerStatus(ps *ipnstate.PeerStatus) {
|
||||
@@ -3837,7 +3838,7 @@ func (de *discoEndpoint) populatePeerStatus(ps *ipnstate.PeerStatus) {
|
||||
|
||||
ps.LastWrite = de.lastSend
|
||||
|
||||
now := time.Now()
|
||||
now := mono.Now()
|
||||
if udpAddr, derpAddr := de.addrForSendLocked(now); !udpAddr.IsZero() && derpAddr.IsZero() {
|
||||
ps.CurAddr = udpAddr.String()
|
||||
}
|
||||
@@ -3855,13 +3856,13 @@ func (de *discoEndpoint) stopAndReset() {
|
||||
|
||||
// Zero these fields so if the user re-starts the network, the discovery
|
||||
// state isn't a mix of before & after two sessions.
|
||||
de.lastSend = time.Time{}
|
||||
de.lastFullPing = time.Time{}
|
||||
de.lastSend = 0
|
||||
de.lastFullPing = 0
|
||||
de.bestAddr = addrLatency{}
|
||||
de.bestAddrAt = time.Time{}
|
||||
de.trustBestAddrUntil = time.Time{}
|
||||
de.bestAddrAt = 0
|
||||
de.trustBestAddrUntil = 0
|
||||
for _, es := range de.endpointState {
|
||||
es.lastPing = time.Time{}
|
||||
es.lastPing = 0
|
||||
}
|
||||
|
||||
for txid, sp := range de.sentPing {
|
||||
|
||||
@@ -38,6 +38,7 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/tstest/natlab"
|
||||
"tailscale.com/tstime/mono"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
@@ -1146,13 +1147,13 @@ func TestAddrSet(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
faket := time.Unix(0, 0)
|
||||
faket := mono.Time(0)
|
||||
var logBuf bytes.Buffer
|
||||
tt.as.Logf = func(format string, args ...interface{}) {
|
||||
fmt.Fprintf(&logBuf, format, args...)
|
||||
t.Logf(format, args...)
|
||||
}
|
||||
tt.as.clock = func() time.Time { return faket }
|
||||
tt.as.clock = func() mono.Time { return faket }
|
||||
for i, st := range tt.steps {
|
||||
faket = faket.Add(st.advance)
|
||||
|
||||
@@ -1229,8 +1230,8 @@ func TestDiscoStringLogRace(t *testing.T) {
|
||||
func Test32bitAlignment(t *testing.T) {
|
||||
var de discoEndpoint
|
||||
|
||||
if off := unsafe.Offsetof(de.lastRecvUnixAtomic); off%8 != 0 {
|
||||
t.Fatalf("discoEndpoint.lastRecvUnixAtomic is not 8-byte aligned")
|
||||
if off := unsafe.Offsetof(de.lastRecv); off%8 != 0 {
|
||||
t.Fatalf("discoEndpoint.lastRecv is not 8-byte aligned")
|
||||
}
|
||||
|
||||
if !de.isFirstRecvActivityInAwhile() { // verify this doesn't panic on 32-bit
|
||||
|
||||
@@ -231,7 +231,7 @@ func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) {
|
||||
e.logf("open-conn-track: timeout opening %v to node %v; online=%v, lastRecv=%v",
|
||||
flow, n.Key.ShortString(),
|
||||
online,
|
||||
durFmt(e.magicConn.LastRecvActivityOfDisco(n.DiscoKey)))
|
||||
e.magicConn.LastRecvActivityOfDisco(n.DiscoKey))
|
||||
}
|
||||
|
||||
func durFmt(t time.Time) string {
|
||||
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime/mono"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -83,7 +84,7 @@ type userspaceEngine struct {
|
||||
wgLogger *wglog.Logger //a wireguard-go logging wrapper
|
||||
reqCh chan struct{}
|
||||
waitCh chan struct{} // chan is closed when first Close call completes; contrast with closing bool
|
||||
timeNow func() time.Time
|
||||
timeNow func() mono.Time
|
||||
tundev *tstun.Wrapper
|
||||
wgdev *device.Device
|
||||
router router.Router
|
||||
@@ -110,12 +111,13 @@ type userspaceEngine struct {
|
||||
lastRouterSig deephash.Sum // of router.Config
|
||||
lastEngineSigFull deephash.Sum // of full wireguard config
|
||||
lastEngineSigTrim deephash.Sum // of trimmed wireguard config
|
||||
recvActivityAt map[tailcfg.DiscoKey]time.Time
|
||||
lastDNSConfig *dns.Config
|
||||
recvActivityAt map[tailcfg.DiscoKey]mono.Time
|
||||
trimmedDisco map[tailcfg.DiscoKey]bool // set of disco keys of peers currently excluded from wireguard config
|
||||
sentActivityAt map[netaddr.IP]*int64 // value is atomic int64 of unixtime
|
||||
sentActivityAt map[netaddr.IP]*mono.Time // value is accessed atomically
|
||||
destIPActivityFuncs map[netaddr.IP]func()
|
||||
statusBufioReader *bufio.Reader // reusable for UAPI
|
||||
lastStatusPollTime time.Time // last time we polled the engine status
|
||||
lastStatusPollTime mono.Time // last time we polled the engine status
|
||||
|
||||
mu sync.Mutex // guards following; see lock order comment below
|
||||
netMap *netmap.NetworkMap // or nil
|
||||
@@ -237,7 +239,7 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
|
||||
closePool.add(tsTUNDev)
|
||||
|
||||
e := &userspaceEngine{
|
||||
timeNow: time.Now,
|
||||
timeNow: mono.Now,
|
||||
logf: logf,
|
||||
reqCh: make(chan struct{}, 1),
|
||||
waitCh: make(chan struct{}),
|
||||
@@ -565,7 +567,7 @@ func (e *userspaceEngine) noteReceiveActivity(dk tailcfg.DiscoKey) {
|
||||
// had a packet sent to or received from it since t.
|
||||
//
|
||||
// e.wgLock must be held.
|
||||
func (e *userspaceEngine) isActiveSince(dk tailcfg.DiscoKey, ip netaddr.IP, t time.Time) bool {
|
||||
func (e *userspaceEngine) isActiveSince(dk tailcfg.DiscoKey, ip netaddr.IP, t mono.Time) bool {
|
||||
if e.recvActivityAt[dk].After(t) {
|
||||
return true
|
||||
}
|
||||
@@ -573,8 +575,7 @@ func (e *userspaceEngine) isActiveSince(dk tailcfg.DiscoKey, ip netaddr.IP, t ti
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
unixTime := atomic.LoadInt64(timePtr)
|
||||
return unixTime >= t.Unix()
|
||||
return timePtr.LoadAtomic().After(t)
|
||||
}
|
||||
|
||||
// discoChanged are the set of peers whose disco keys have changed, implying they've restarted.
|
||||
@@ -686,7 +687,7 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked(discoChanged map[key.Publ
|
||||
func (e *userspaceEngine) updateActivityMapsLocked(trackDisco []tailcfg.DiscoKey, trackIPs []netaddr.IP) {
|
||||
// Generate the new map of which discokeys we want to track
|
||||
// receive times for.
|
||||
mr := map[tailcfg.DiscoKey]time.Time{} // TODO: only recreate this if set of keys changed
|
||||
mr := map[tailcfg.DiscoKey]mono.Time{} // TODO: only recreate this if set of keys changed
|
||||
for _, dk := range trackDisco {
|
||||
// Preserve old times in the new map, but also
|
||||
// populate map entries for new trackDisco values with
|
||||
@@ -698,25 +699,29 @@ func (e *userspaceEngine) updateActivityMapsLocked(trackDisco []tailcfg.DiscoKey
|
||||
e.recvActivityAt = mr
|
||||
|
||||
oldTime := e.sentActivityAt
|
||||
e.sentActivityAt = make(map[netaddr.IP]*int64, len(oldTime))
|
||||
e.sentActivityAt = make(map[netaddr.IP]*mono.Time, len(oldTime))
|
||||
oldFunc := e.destIPActivityFuncs
|
||||
e.destIPActivityFuncs = make(map[netaddr.IP]func(), len(oldFunc))
|
||||
|
||||
updateFn := func(timePtr *int64) func() {
|
||||
updateFn := func(timePtr *mono.Time) func() {
|
||||
return func() {
|
||||
now := e.timeNow().Unix()
|
||||
old := atomic.LoadInt64(timePtr)
|
||||
now := e.timeNow()
|
||||
old := timePtr.LoadAtomic()
|
||||
|
||||
// How long's it been since we last sent a packet?
|
||||
// For our first packet, old is Unix epoch time 0 (1970).
|
||||
elapsedSec := now - old
|
||||
elapsed := now.Sub(old)
|
||||
if old == 0 {
|
||||
// For our first packet, old is 0, which has indeterminate meaning.
|
||||
// Set elapsed to a big number (four score and seven years).
|
||||
elapsed = 762642 * time.Hour
|
||||
}
|
||||
|
||||
if elapsedSec >= int64(packetSendTimeUpdateFrequency/time.Second) {
|
||||
atomic.StoreInt64(timePtr, now)
|
||||
if elapsed >= packetSendTimeUpdateFrequency {
|
||||
timePtr.StoreAtomic(now)
|
||||
}
|
||||
// On a big jump, assume we might no longer be in the wireguard
|
||||
// config and go check.
|
||||
if elapsedSec >= int64(packetSendRecheckWireguardThreshold/time.Second) {
|
||||
if elapsed >= packetSendRecheckWireguardThreshold {
|
||||
e.wgLock.Lock()
|
||||
defer e.wgLock.Unlock()
|
||||
e.maybeReconfigWireguardLocked(nil)
|
||||
@@ -727,7 +732,7 @@ func (e *userspaceEngine) updateActivityMapsLocked(trackDisco []tailcfg.DiscoKey
|
||||
for _, ip := range trackIPs {
|
||||
timePtr := oldTime[ip]
|
||||
if timePtr == nil {
|
||||
timePtr = new(int64)
|
||||
timePtr = new(mono.Time)
|
||||
}
|
||||
e.sentActivityAt[ip] = timePtr
|
||||
|
||||
@@ -752,6 +757,7 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config,
|
||||
|
||||
e.wgLock.Lock()
|
||||
defer e.wgLock.Unlock()
|
||||
e.lastDNSConfig = dnsCfg
|
||||
|
||||
peerSet := make(map[key.Public]struct{}, len(cfg.Peers))
|
||||
e.mu.Lock()
|
||||
@@ -1076,6 +1082,23 @@ func (e *userspaceEngine) linkChange(changed bool, cur *interfaces.State) {
|
||||
health.SetAnyInterfaceUp(up)
|
||||
e.magicConn.SetNetworkUp(up)
|
||||
|
||||
// Hacky workaround for Linux DNS issue 2458: on
|
||||
// suspend/resume or whenever NetworkManager is started, it
|
||||
// nukes all systemd-resolved configs. So reapply our DNS
|
||||
// config on major link change.
|
||||
if runtime.GOOS == "linux" && changed {
|
||||
e.wgLock.Lock()
|
||||
dnsCfg := e.lastDNSConfig
|
||||
e.wgLock.Unlock()
|
||||
if dnsCfg != nil {
|
||||
if err := e.dns.Set(*dnsCfg); err != nil {
|
||||
e.logf("wgengine: error setting DNS config after major link change: %v", err)
|
||||
} else {
|
||||
e.logf("wgengine: set DNS config again after major link change")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
why := "link-change-minor"
|
||||
if changed {
|
||||
why = "link-change-major"
|
||||
|
||||
@@ -9,20 +9,20 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime/mono"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
func TestNoteReceiveActivity(t *testing.T) {
|
||||
now := time.Unix(1, 0)
|
||||
now := mono.Time(123456)
|
||||
var logBuf bytes.Buffer
|
||||
|
||||
confc := make(chan bool, 1)
|
||||
@@ -35,8 +35,8 @@ func TestNoteReceiveActivity(t *testing.T) {
|
||||
}
|
||||
}
|
||||
e := &userspaceEngine{
|
||||
timeNow: func() time.Time { return now },
|
||||
recvActivityAt: map[tailcfg.DiscoKey]time.Time{},
|
||||
timeNow: func() mono.Time { return now },
|
||||
recvActivityAt: map[tailcfg.DiscoKey]mono.Time{},
|
||||
logf: func(format string, a ...interface{}) {
|
||||
fmt.Fprintf(&logBuf, format, a...)
|
||||
},
|
||||
@@ -58,7 +58,7 @@ func TestNoteReceiveActivity(t *testing.T) {
|
||||
}
|
||||
|
||||
// Now track it, but don't mark it trimmed, so shouldn't update.
|
||||
ra[dk] = time.Time{}
|
||||
ra[dk] = 0
|
||||
e.noteReceiveActivity(dk)
|
||||
if len(ra) != 1 {
|
||||
t.Fatalf("unexpected growth in map: now has %d keys; want 1", len(ra))
|
||||
@@ -114,8 +114,8 @@ func TestUserspaceEngineReconfig(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wantRecvAt := map[tailcfg.DiscoKey]time.Time{
|
||||
dkFromHex(discoHex): time.Time{},
|
||||
wantRecvAt := map[tailcfg.DiscoKey]mono.Time{
|
||||
dkFromHex(discoHex): 0,
|
||||
}
|
||||
if got := ue.recvActivityAt; !reflect.DeepEqual(got, wantRecvAt) {
|
||||
t.Errorf("wrong recvActivityAt\n got: %v\nwant: %v\n", got, wantRecvAt)
|
||||
|
||||
Reference in New Issue
Block a user