Compare commits
1 Commits
Xe/test-in
...
bradfitz/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea0ca7e721 |
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -2,7 +2,36 @@
|
||||
name: Bug report
|
||||
about: Create a bug report
|
||||
title: ''
|
||||
labels: 'needs-triage'
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Please note, this template is for definite bugs, not requests for
|
||||
support. If you need help with Tailscale, please email
|
||||
support@tailscale.com. We don't provide support via Github issues. -->
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Version information:**
|
||||
- Device: [e.g. iPhone X, laptop]
|
||||
- OS: [e.g. Windows, MacOS]
|
||||
- OS version: [e.g. Windows 10, Ubuntu 18.04]
|
||||
- Tailscale version: [e.g. 0.95-0]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,6 +2,25 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'needs-triage'
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
|
||||
A clear and concise description of what the problem is. Ex. I'm always
|
||||
frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
|
||||
A clear and concise description of any alternative solutions or
|
||||
features you've considered.
|
||||
|
||||
**Additional context**
|
||||
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
48
.github/workflows/coverage.yml
vendored
Normal file
48
.github/workflows/coverage.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name: Code Coverage
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.16
|
||||
id: go
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v1
|
||||
|
||||
# https://markphelps.me/2019/11/speed-up-your-go-builds-with-actions-cache/
|
||||
- name: Restore Cache
|
||||
uses: actions/cache@preview
|
||||
id: cache
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-${{ hashFiles('**/go.sum') }}
|
||||
|
||||
- name: Basic build
|
||||
run: go build ./cmd/...
|
||||
|
||||
- name: Run tests on linux with coverage data
|
||||
run: go test -race -coverprofile=coverage.txt -bench=. -benchtime=1x ./...
|
||||
|
||||
- name: coveralls.io
|
||||
uses: shogo82148/actions-goveralls@v1
|
||||
env:
|
||||
COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.COVERALLS_BOT_PUBLIC_REPO_TOKEN }}
|
||||
with:
|
||||
path-to-profile: ./coverage.txt
|
||||
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'
|
||||
|
||||
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
run: go build ./cmd/...
|
||||
|
||||
- name: Run tests on linux
|
||||
run: go test -bench=. -benchtime=1x ./...
|
||||
run: go test ./...
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
|
||||
2
.github/workflows/linux32.yml
vendored
2
.github/workflows/linux32.yml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
run: GOARCH=386 go build ./cmd/...
|
||||
|
||||
- name: Run tests on linux
|
||||
run: GOARCH=386 go test -bench=. -benchtime=1x ./...
|
||||
run: GOARCH=386 go test ./...
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
|
||||
18
.github/workflows/staticcheck.yml
vendored
18
.github/workflows/staticcheck.yml
vendored
@@ -24,23 +24,11 @@ jobs:
|
||||
- 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"
|
||||
run: go run honnef.co/go/tools/cmd/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)"
|
||||
- name: Run staticcheck
|
||||
run: "go run honnef.co/go/tools/cmd/staticcheck -- $(go list ./... | grep -v tempfork)"
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
|
||||
55
.github/workflows/windows-race.yml
vendored
55
.github/workflows/windows-race.yml
vendored
@@ -1,55 +0,0 @@
|
||||
name: Windows race
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: windows-latest
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.16.x
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Restore Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Test with -race flag
|
||||
# Don't use -bench=. -benchtime=1x.
|
||||
# Somewhere in the layers (powershell?)
|
||||
# the equals signs cause great confusion.
|
||||
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'
|
||||
|
||||
5
.github/workflows/windows.yml
vendored
5
.github/workflows/windows.yml
vendored
@@ -33,10 +33,7 @@ jobs:
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Test
|
||||
# Don't use -bench=. -benchtime=1x.
|
||||
# Somewhere in the layers (powershell?)
|
||||
# the equals signs cause great confusion.
|
||||
run: go test -bench . -benchtime 1x ./...
|
||||
run: go test ./...
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.9.0
|
||||
1.7.0
|
||||
|
||||
@@ -8,16 +8,13 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"text/tabwriter"
|
||||
@@ -34,45 +31,15 @@ import (
|
||||
// CLI based on os.Args, GOOS, the context the process is running in
|
||||
// (pty, parent PID), etc.
|
||||
func ActLikeCLI() bool {
|
||||
// This function is only used on macOS.
|
||||
if runtime.GOOS != "darwin" {
|
||||
if len(os.Args) < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Escape hatch to let people force running the macOS
|
||||
// GUI Tailscale binary as the CLI.
|
||||
if v, _ := strconv.ParseBool(os.Getenv("TAILSCALE_BE_CLI")); v {
|
||||
switch os.Args[1] {
|
||||
case "up", "down", "status", "netcheck", "ping", "version",
|
||||
"debug",
|
||||
"-V", "--version", "-h", "--help":
|
||||
return true
|
||||
}
|
||||
|
||||
// If our parent is launchd, we're definitely not
|
||||
// being run as a CLI.
|
||||
if os.Getppid() == 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Xcode adds the -NSDocumentRevisionsDebugMode flag on execution.
|
||||
// If present, we are almost certainly being run as a GUI.
|
||||
for _, arg := range os.Args {
|
||||
if arg == "-NSDocumentRevisionsDebugMode" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Looking at the environment of the GUI Tailscale app (ps eww
|
||||
// $PID), empirically none of these environment variables are
|
||||
// present. But all or some of these should be present with
|
||||
// Terminal.all and bash or zsh.
|
||||
for _, e := range []string{
|
||||
"SHLVL",
|
||||
"TERM",
|
||||
"TERM_PROGRAM",
|
||||
"PS1",
|
||||
} {
|
||||
if os.Getenv(e) != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -105,7 +72,7 @@ change in the future.
|
||||
pingCmd,
|
||||
versionCmd,
|
||||
webCmd,
|
||||
fileCmd,
|
||||
pushCmd,
|
||||
bugReportCmd,
|
||||
},
|
||||
FlagSet: rootfs,
|
||||
@@ -179,22 +146,21 @@ func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context
|
||||
}
|
||||
|
||||
// pump receives backend messages on conn and pushes them into bc.
|
||||
func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) error {
|
||||
func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) {
|
||||
defer conn.Close()
|
||||
for ctx.Err() == nil {
|
||||
msg, err := ipn.ReadMsg(conn)
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
return
|
||||
}
|
||||
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
|
||||
return fmt.Errorf("%w (tailscaled stopped running?)", err)
|
||||
if !gotSignal.Get() {
|
||||
log.Printf("ReadMsg: %v\n", err)
|
||||
}
|
||||
return err
|
||||
break
|
||||
}
|
||||
bc.GotNotifyMsg(msg)
|
||||
}
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func strSliceContains(ss []string, s string) bool {
|
||||
|
||||
@@ -5,664 +5,113 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/types/preftype"
|
||||
)
|
||||
|
||||
// geese is a collection of gooses. It need not be complete.
|
||||
// But it should include anything handled specially (e.g. linux, windows)
|
||||
// and at least one thing that's not (darwin, freebsd).
|
||||
var geese = []string{"linux", "darwin", "windows", "freebsd"}
|
||||
|
||||
// Test that checkForAccidentalSettingReverts's updateMaskedPrefsFromUpFlag can handle
|
||||
// all flags. This will panic if a new flag creeps in that's unhandled.
|
||||
//
|
||||
// Also, issue 1880: advertise-exit-node was being ignored. Verify that all flags cause an edit.
|
||||
func TestUpdateMaskedPrefsFromUpFlag(t *testing.T) {
|
||||
for _, goos := range geese {
|
||||
var upArgs upArgsT
|
||||
fs := newUpFlagSet(goos, &upArgs)
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
mp := new(ipn.MaskedPrefs)
|
||||
updateMaskedPrefsFromUpFlag(mp, f.Name)
|
||||
got := mp.Pretty()
|
||||
wantEmpty := preflessFlag(f.Name)
|
||||
isEmpty := got == "MaskedPrefs{}"
|
||||
if isEmpty != wantEmpty {
|
||||
t.Errorf("flag %q created MaskedPrefs %s; want empty=%v", f.Name, got, wantEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
mp := new(ipn.MaskedPrefs)
|
||||
upFlagSet.VisitAll(func(f *flag.Flag) {
|
||||
updateMaskedPrefsFromUpFlag(mp, f.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
f := func(flags ...string) map[string]bool {
|
||||
m := make(map[string]bool)
|
||||
for _, f := range flags {
|
||||
m[f] = true
|
||||
}
|
||||
return m
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
flags []string // argv to be parsed by FlagSet
|
||||
flagSet map[string]bool
|
||||
curPrefs *ipn.Prefs
|
||||
|
||||
curExitNodeIP netaddr.IP
|
||||
curUser string // os.Getenv("USER") on the client side
|
||||
goos string // empty means "linux"
|
||||
|
||||
want string
|
||||
mp *ipn.MaskedPrefs
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "bare_up_means_up",
|
||||
flags: []string{},
|
||||
name: "bare_up_means_up",
|
||||
flagSet: f(),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
Hostname: "foo",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "losing_hostname",
|
||||
flags: []string{"--accept-dns"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
Hostname: "foo",
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AllowSingleHosts: true,
|
||||
},
|
||||
want: accidentalUpPrefix + " --accept-dns --hostname=foo",
|
||||
},
|
||||
{
|
||||
name: "hostname_changing_explicitly",
|
||||
flags: []string{"--hostname=bar"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AllowSingleHosts: true,
|
||||
Hostname: "foo",
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
WantRunning: true,
|
||||
},
|
||||
WantRunningSet: true,
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "hostname_changing_empty_explicitly",
|
||||
flags: []string{"--hostname="},
|
||||
name: "losing_hostname",
|
||||
flagSet: f("accept-dns"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AllowSingleHosts: true,
|
||||
Hostname: "foo",
|
||||
WantRunning: false,
|
||||
Hostname: "foo",
|
||||
CorpDNS: true,
|
||||
},
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
WantRunning: true,
|
||||
CorpDNS: true,
|
||||
},
|
||||
WantRunningSet: true,
|
||||
CorpDNSSet: true,
|
||||
},
|
||||
want: `'tailscale up' without --reset requires all preferences with changing values to be explicitly mentioned; --hostname is not specified but its default value of "" differs from current value "foo"`,
|
||||
},
|
||||
{
|
||||
name: "hostname_changing_explicitly",
|
||||
flagSet: f("hostname"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
WantRunning: false,
|
||||
Hostname: "foo",
|
||||
},
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
WantRunning: true,
|
||||
Hostname: "bar",
|
||||
},
|
||||
WantRunningSet: true,
|
||||
HostnameSet: true,
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
// Issue 1725: "tailscale up --authkey=..." (or other non-empty flags) works from
|
||||
// a fresh server's initial prefs.
|
||||
name: "up_with_default_prefs",
|
||||
flags: []string{"--authkey=foosdlkfjskdljf"},
|
||||
curPrefs: ipn.NewPrefs(),
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "implicit_operator_change",
|
||||
flags: []string{"--hostname=foo"},
|
||||
name: "hostname_changing_empty_explicitly",
|
||||
flagSet: f("hostname"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
OperatorUser: "alice",
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
WantRunning: false,
|
||||
Hostname: "foo",
|
||||
},
|
||||
curUser: "eve",
|
||||
want: accidentalUpPrefix + " --hostname=foo --operator=alice",
|
||||
},
|
||||
{
|
||||
name: "implicit_operator_matches_shell_user",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
OperatorUser: "alice",
|
||||
},
|
||||
curUser: "alice",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "error_advertised_routes_exit_node_removed",
|
||||
flags: []string{"--advertise-routes=10.0.42.0/24"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.42.0/24"),
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
},
|
||||
},
|
||||
want: accidentalUpPrefix + " --advertise-routes=10.0.42.0/24 --advertise-exit-node",
|
||||
},
|
||||
{
|
||||
name: "advertised_routes_exit_node_removed_explicit",
|
||||
flags: []string{"--advertise-routes=10.0.42.0/24", "--advertise-exit-node=false"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.42.0/24"),
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
WantRunning: true,
|
||||
Hostname: "",
|
||||
},
|
||||
WantRunningSet: true,
|
||||
HostnameSet: true,
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "advertised_routes_includes_the_0_routes", // but no --advertise-exit-node
|
||||
flags: []string{"--advertise-routes=11.1.43.0/24,0.0.0.0/0,::/0"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.42.0/24"),
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
},
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "advertise_exit_node", // Issue 1859
|
||||
flags: []string{"--advertise-exit-node"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "advertise_exit_node_over_existing_routes",
|
||||
flags: []string{"--advertise-exit-node"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("1.2.0.0/16"),
|
||||
},
|
||||
},
|
||||
want: accidentalUpPrefix + " --advertise-exit-node --advertise-routes=1.2.0.0/16",
|
||||
},
|
||||
{
|
||||
name: "advertise_exit_node_over_existing_routes_and_exit_node",
|
||||
flags: []string{"--advertise-exit-node"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
netaddr.MustParseIPPrefix("1.2.0.0/16"),
|
||||
},
|
||||
},
|
||||
want: accidentalUpPrefix + " --advertise-exit-node --advertise-routes=1.2.0.0/16",
|
||||
},
|
||||
{
|
||||
name: "exit_node_clearing", // Issue 1777
|
||||
flags: []string{"--exit-node="},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
|
||||
ExitNodeID: "fooID",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "remove_all_implicit",
|
||||
flags: []string{"--force-reauth"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
RouteAll: true,
|
||||
AllowSingleHosts: false,
|
||||
ExitNodeIP: netaddr.MustParseIP("100.64.5.6"),
|
||||
CorpDNS: false,
|
||||
ShieldsUp: true,
|
||||
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
||||
Hostname: "myhostname",
|
||||
ForceDaemon: true,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.0.0/16"),
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
},
|
||||
NetfilterMode: preftype.NetfilterNoDivert,
|
||||
OperatorUser: "alice",
|
||||
},
|
||||
curUser: "eve",
|
||||
want: accidentalUpPrefix + " --force-reauth --accept-dns=false --accept-routes --advertise-exit-node --advertise-routes=10.0.0.0/16 --advertise-tags=tag:foo,tag:bar --exit-node=100.64.5.6 --host-routes=false --hostname=myhostname --netfilter-mode=nodivert --operator=alice --shields-up",
|
||||
},
|
||||
{
|
||||
name: "remove_all_implicit_except_hostname",
|
||||
flags: []string{"--hostname=newhostname"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
RouteAll: true,
|
||||
AllowSingleHosts: false,
|
||||
ExitNodeIP: netaddr.MustParseIP("100.64.5.6"),
|
||||
CorpDNS: false,
|
||||
ShieldsUp: true,
|
||||
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
||||
Hostname: "myhostname",
|
||||
ForceDaemon: true,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.0.0/16"),
|
||||
},
|
||||
NetfilterMode: preftype.NetfilterNoDivert,
|
||||
OperatorUser: "alice",
|
||||
},
|
||||
curUser: "eve",
|
||||
want: accidentalUpPrefix + " --hostname=newhostname --accept-dns=false --accept-routes --advertise-routes=10.0.0.0/16 --advertise-tags=tag:foo,tag:bar --exit-node=100.64.5.6 --host-routes=false --netfilter-mode=nodivert --operator=alice --shields-up",
|
||||
},
|
||||
{
|
||||
name: "loggedout_is_implicit",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
LoggedOut: true,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
want: "", // not an error. LoggedOut is implicit.
|
||||
},
|
||||
{
|
||||
// Test that a pre-1.8 version of Tailscale with bogus NoSNAT pref
|
||||
// values is able to enable exit nodes without warnings.
|
||||
name: "make_windows_exit_node",
|
||||
flags: []string{"--advertise-exit-node"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
|
||||
// And assume this no-op accidental pre-1.8 value:
|
||||
NoSNAT: true,
|
||||
},
|
||||
goos: "windows",
|
||||
want: "", // not an error
|
||||
},
|
||||
{
|
||||
name: "ignore_netfilter_change_non_linux",
|
||||
flags: []string{"--accept-dns"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
|
||||
NetfilterMode: preftype.NetfilterNoDivert, // we never had this bug, but pretend it got set non-zero on Windows somehow
|
||||
},
|
||||
goos: "windows",
|
||||
want: "", // not an error
|
||||
},
|
||||
{
|
||||
name: "operator_losing_routes_step1", // https://twitter.com/EXPbits/status/1390418145047887877
|
||||
flags: []string{"--operator=expbits"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
netaddr.MustParseIPPrefix("1.2.0.0/16"),
|
||||
},
|
||||
},
|
||||
want: accidentalUpPrefix + " --operator=expbits --advertise-exit-node --advertise-routes=1.2.0.0/16",
|
||||
},
|
||||
{
|
||||
name: "operator_losing_routes_step2", // https://twitter.com/EXPbits/status/1390418145047887877
|
||||
flags: []string{"--operator=expbits", "--advertise-routes=1.2.0.0/16"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
netaddr.MustParseIPPrefix("1.2.0.0/16"),
|
||||
},
|
||||
},
|
||||
want: accidentalUpPrefix + " --advertise-routes=1.2.0.0/16 --operator=expbits --advertise-exit-node",
|
||||
},
|
||||
{
|
||||
name: "errors_preserve_explicit_flags",
|
||||
flags: []string{"--reset", "--force-reauth=false", "--authkey=secretrand"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AllowSingleHosts: true,
|
||||
|
||||
Hostname: "foo",
|
||||
},
|
||||
want: accidentalUpPrefix + " --authkey=secretrand --force-reauth=false --reset --hostname=foo",
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_omit_with_ip_pref",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
|
||||
ExitNodeIP: netaddr.MustParseIP("100.64.5.4"),
|
||||
},
|
||||
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.4",
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_omit_with_id_pref",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curExitNodeIP: netaddr.MustParseIP("100.64.5.7"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
|
||||
ExitNodeID: "some_stable_id",
|
||||
},
|
||||
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.7",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
goos := "linux"
|
||||
if tt.goos != "" {
|
||||
goos = tt.goos
|
||||
}
|
||||
var upArgs upArgsT
|
||||
flagSet := newUpFlagSet(goos, &upArgs)
|
||||
flagSet.Parse(tt.flags)
|
||||
newPrefs, err := prefsFromUpArgs(upArgs, t.Logf, new(ipnstate.Status), goos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
applyImplicitPrefs(newPrefs, tt.curPrefs, tt.curUser)
|
||||
var got string
|
||||
if err := checkForAccidentalSettingReverts(flagSet, tt.curPrefs, newPrefs, upCheckEnv{
|
||||
goos: goos,
|
||||
curExitNodeIP: tt.curExitNodeIP,
|
||||
}); err != nil {
|
||||
if err := checkForAccidentalSettingReverts(tt.flagSet, tt.curPrefs, tt.mp); err != nil {
|
||||
got = err.Error()
|
||||
}
|
||||
if strings.TrimSpace(got) != tt.want {
|
||||
if got != tt.want {
|
||||
t.Errorf("unexpected result\n got: %s\nwant: %s\n", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func upArgsFromOSArgs(goos string, flagArgs ...string) (args upArgsT) {
|
||||
fs := newUpFlagSet(goos, &args)
|
||||
fs.Parse(flagArgs) // populates args
|
||||
return
|
||||
}
|
||||
|
||||
func TestPrefsFromUpArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args upArgsT
|
||||
goos string // runtime.GOOS; empty means linux
|
||||
st *ipnstate.Status // or nil
|
||||
want *ipn.Prefs
|
||||
wantErr string
|
||||
wantWarn string
|
||||
}{
|
||||
{
|
||||
name: "default_linux",
|
||||
goos: "linux",
|
||||
args: upArgsFromOSArgs("linux"),
|
||||
want: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
NoSNAT: false,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default_windows",
|
||||
goos: "windows",
|
||||
args: upArgsFromOSArgs("windows"),
|
||||
want: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "advertise_default_route",
|
||||
args: upArgsFromOSArgs("linux", "--advertise-exit-node"),
|
||||
want: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
},
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error_advertise_route_invalid_ip",
|
||||
args: upArgsT{
|
||||
advertiseRoutes: "foo",
|
||||
},
|
||||
wantErr: `"foo" is not a valid IP address or CIDR prefix`,
|
||||
},
|
||||
{
|
||||
name: "error_advertise_route_unmasked_bits",
|
||||
args: upArgsT{
|
||||
advertiseRoutes: "1.2.3.4/16",
|
||||
},
|
||||
wantErr: `1.2.3.4/16 has non-address bits set; expected 1.2.0.0/16`,
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_bad_ip",
|
||||
args: upArgsT{
|
||||
exitNodeIP: "foo",
|
||||
},
|
||||
wantErr: `invalid IP address "foo" for --exit-node: ParseIP("foo"): unable to parse IP`,
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_allow_lan_without_exit_node",
|
||||
args: upArgsT{
|
||||
exitNodeAllowLANAccess: true,
|
||||
},
|
||||
wantErr: `--exit-node-allow-lan-access can only be used with --exit-node`,
|
||||
},
|
||||
{
|
||||
name: "error_tag_prefix",
|
||||
args: upArgsT{
|
||||
advertiseTags: "foo",
|
||||
},
|
||||
wantErr: `tag: "foo": tags must start with 'tag:'`,
|
||||
},
|
||||
{
|
||||
name: "error_long_hostname",
|
||||
args: upArgsT{
|
||||
hostname: strings.Repeat("a", 300),
|
||||
},
|
||||
wantErr: `hostname too long: 300 bytes (max 256)`,
|
||||
},
|
||||
{
|
||||
name: "error_linux_netfilter_empty",
|
||||
args: upArgsT{
|
||||
netfilterMode: "",
|
||||
},
|
||||
wantErr: `invalid value --netfilter-mode=""`,
|
||||
},
|
||||
{
|
||||
name: "error_linux_netfilter_bogus",
|
||||
args: upArgsT{
|
||||
netfilterMode: "bogus",
|
||||
},
|
||||
wantErr: `invalid value --netfilter-mode="bogus"`,
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_ip_is_self_ip",
|
||||
args: upArgsT{
|
||||
exitNodeIP: "100.105.106.107",
|
||||
},
|
||||
st: &ipnstate.Status{
|
||||
TailscaleIPs: []netaddr.IP{netaddr.MustParseIP("100.105.106.107")},
|
||||
},
|
||||
wantErr: `cannot use 100.105.106.107 as the exit node as it is a local IP address to this machine, did you mean --advertise-exit-node?`,
|
||||
},
|
||||
{
|
||||
name: "warn_linux_netfilter_nodivert",
|
||||
goos: "linux",
|
||||
args: upArgsT{
|
||||
netfilterMode: "nodivert",
|
||||
},
|
||||
wantWarn: "netfilter=nodivert; add iptables calls to ts-* chains manually.",
|
||||
want: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
NetfilterMode: preftype.NetfilterNoDivert,
|
||||
NoSNAT: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "warn_linux_netfilter_off",
|
||||
goos: "linux",
|
||||
args: upArgsT{
|
||||
netfilterMode: "off",
|
||||
},
|
||||
wantWarn: "netfilter=off; configure iptables yourself.",
|
||||
want: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
NetfilterMode: preftype.NetfilterOff,
|
||||
NoSNAT: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var warnBuf bytes.Buffer
|
||||
warnf := func(format string, a ...interface{}) {
|
||||
fmt.Fprintf(&warnBuf, format, a...)
|
||||
}
|
||||
goos := tt.goos
|
||||
if goos == "" {
|
||||
goos = "linux"
|
||||
}
|
||||
st := tt.st
|
||||
if st == nil {
|
||||
st = new(ipnstate.Status)
|
||||
}
|
||||
got, err := prefsFromUpArgs(tt.args, warnf, st, goos)
|
||||
gotErr := fmt.Sprint(err)
|
||||
if tt.wantErr != "" {
|
||||
if tt.wantErr != gotErr {
|
||||
t.Errorf("wrong error.\n got error: %v\nwant error: %v\n", gotErr, tt.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tt.want == nil {
|
||||
t.Fatal("tt.want is nil")
|
||||
}
|
||||
if !got.Equals(tt.want) {
|
||||
jgot, _ := json.MarshalIndent(got, "", "\t")
|
||||
jwant, _ := json.MarshalIndent(tt.want, "", "\t")
|
||||
if bytes.Equal(jgot, jwant) {
|
||||
t.Logf("prefs differ only in non-JSON-visible ways (nil/non-nil zero-length arrays)")
|
||||
}
|
||||
t.Errorf("wrong prefs\n got: %s\nwant: %s\n\ngot: %s\nwant: %s\n",
|
||||
got.Pretty(), tt.want.Pretty(),
|
||||
jgot, jwant,
|
||||
)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPrefFlagMapping(t *testing.T) {
|
||||
prefHasFlag := map[string]bool{}
|
||||
for _, pv := range prefsOfFlag {
|
||||
for _, pref := range pv {
|
||||
prefHasFlag[pref] = true
|
||||
}
|
||||
}
|
||||
|
||||
prefType := reflect.TypeOf(ipn.Prefs{})
|
||||
for i := 0; i < prefType.NumField(); i++ {
|
||||
prefName := prefType.Field(i).Name
|
||||
if prefHasFlag[prefName] {
|
||||
continue
|
||||
}
|
||||
switch prefName {
|
||||
case "WantRunning", "Persist", "LoggedOut":
|
||||
// All explicitly handled (ignored) by checkForAccidentalSettingReverts.
|
||||
continue
|
||||
case "OSVersion", "DeviceModel":
|
||||
// Only used by Android, which doesn't have a CLI mode anyway, so
|
||||
// fine to not map.
|
||||
continue
|
||||
case "NotepadURLs":
|
||||
// TODO(bradfitz): https://github.com/tailscale/tailscale/issues/1830
|
||||
continue
|
||||
}
|
||||
t.Errorf("unexpected new ipn.Pref field %q is not handled by up.go (see addPrefFlagMapping and checkForAccidentalSettingReverts)", prefName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlagAppliesToOS(t *testing.T) {
|
||||
for _, goos := range geese {
|
||||
var upArgs upArgsT
|
||||
fs := newUpFlagSet(goos, &upArgs)
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
if !flagAppliesToOS(f.Name, goos) {
|
||||
t.Errorf("flagAppliesToOS(%q, %q) = false but found in %s set", f.Name, goos, goos)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,444 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"golang.org/x/time/rate"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var fileCmd = &ffcli.Command{
|
||||
Name: "file",
|
||||
ShortUsage: "file <cp|get> ...",
|
||||
ShortHelp: "Send or receive files",
|
||||
Subcommands: []*ffcli.Command{
|
||||
fileCpCmd,
|
||||
fileGetCmd,
|
||||
},
|
||||
Exec: func(context.Context, []string) error {
|
||||
// TODO(bradfitz): is there a better ffcli way to
|
||||
// annotate subcommand-required commands that don't
|
||||
// have an exec body of their own?
|
||||
return errors.New("file subcommand required; run 'tailscale file -h' for details")
|
||||
},
|
||||
}
|
||||
|
||||
var fileCpCmd = &ffcli.Command{
|
||||
Name: "cp",
|
||||
ShortUsage: "file cp <files...> <target>:",
|
||||
ShortHelp: "Copy file(s) to a host",
|
||||
Exec: runCp,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("cp", flag.ExitOnError)
|
||||
fs.StringVar(&cpArgs.name, "name", "", "alternate filename to use, especially useful when <file> is \"-\" (stdin)")
|
||||
fs.BoolVar(&cpArgs.verbose, "verbose", false, "verbose output")
|
||||
fs.BoolVar(&cpArgs.targets, "targets", false, "list possible file cp targets")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var cpArgs struct {
|
||||
name string
|
||||
verbose bool
|
||||
targets bool
|
||||
}
|
||||
|
||||
func runCp(ctx context.Context, args []string) error {
|
||||
if cpArgs.targets {
|
||||
return runCpTargets(ctx, args)
|
||||
}
|
||||
if len(args) < 2 {
|
||||
//lint:ignore ST1005 no sorry need that colon at the end
|
||||
return errors.New("usage: tailscale file cp <files...> <target>:")
|
||||
}
|
||||
files, target := args[:len(args)-1], args[len(args)-1]
|
||||
if !strings.HasSuffix(target, ":") {
|
||||
return fmt.Errorf("final argument to 'tailscale file cp' must end in colon")
|
||||
}
|
||||
target = strings.TrimSuffix(target, ":")
|
||||
hadBrackets := false
|
||||
if strings.HasPrefix(target, "[") && strings.HasSuffix(target, "]") {
|
||||
hadBrackets = true
|
||||
target = strings.TrimSuffix(strings.TrimPrefix(target, "["), "]")
|
||||
}
|
||||
if ip, err := netaddr.ParseIP(target); err == nil && ip.Is6() && !hadBrackets {
|
||||
return fmt.Errorf("an IPv6 literal must be written as [%s]", ip)
|
||||
} else if hadBrackets && (err != nil || !ip.Is6()) {
|
||||
return errors.New("unexpected brackets around target")
|
||||
}
|
||||
ip, err := tailscaleIPFromArg(ctx, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peerAPIBase, lastSeen, isOffline, err := discoverPeerAPIBase(ctx, ip)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't send to %s: %v", target, err)
|
||||
}
|
||||
if isOffline {
|
||||
fmt.Fprintf(os.Stderr, "# warning: %s is offline\n", target)
|
||||
} else if !lastSeen.IsZero() && time.Since(lastSeen) > lastSeenOld {
|
||||
fmt.Fprintf(os.Stderr, "# warning: %s last seen %v ago\n", target, time.Since(lastSeen).Round(time.Minute))
|
||||
}
|
||||
|
||||
if len(files) > 1 {
|
||||
if cpArgs.name != "" {
|
||||
return errors.New("can't use --name= with multiple files")
|
||||
}
|
||||
for _, fileArg := range files {
|
||||
if fileArg == "-" {
|
||||
return errors.New("can't use '-' as STDIN file when providing filename arguments")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, fileArg := range files {
|
||||
var fileContents io.Reader
|
||||
var name = cpArgs.name
|
||||
var contentLength int64 = -1
|
||||
if fileArg == "-" {
|
||||
fileContents = os.Stdin
|
||||
if name == "" {
|
||||
name, fileContents, err = pickStdinFilename()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
f, err := os.Open(fileArg)
|
||||
if err != nil {
|
||||
if version.IsSandboxedMacOS() {
|
||||
return errors.New("the GUI version of Tailscale on macOS runs in a macOS sandbox that can't read files")
|
||||
}
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
return errors.New("directories not supported")
|
||||
}
|
||||
contentLength = fi.Size()
|
||||
fileContents = io.LimitReader(f, contentLength)
|
||||
if name == "" {
|
||||
name = filepath.Base(fileArg)
|
||||
}
|
||||
|
||||
if slow, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_SLOW_PUSH")); slow {
|
||||
fileContents = &slowReader{r: fileContents}
|
||||
}
|
||||
}
|
||||
|
||||
dstURL := peerAPIBase + "/v0/put/" + url.PathEscape(name)
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", dstURL, fileContents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.ContentLength = contentLength
|
||||
if cpArgs.verbose {
|
||||
log.Printf("sending to %v ...", dstURL)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.StatusCode == 200 {
|
||||
io.Copy(ioutil.Discard, res.Body)
|
||||
res.Body.Close()
|
||||
continue
|
||||
}
|
||||
io.Copy(os.Stdout, res.Body)
|
||||
res.Body.Close()
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, lastSeen time.Time, isOffline bool, err error) {
|
||||
ip, err := netaddr.ParseIP(ipStr)
|
||||
if err != nil {
|
||||
return "", time.Time{}, false, err
|
||||
}
|
||||
fts, err := tailscale.FileTargets(ctx)
|
||||
if err != nil {
|
||||
return "", time.Time{}, false, err
|
||||
}
|
||||
for _, ft := range fts {
|
||||
n := ft.Node
|
||||
for _, a := range n.Addresses {
|
||||
if a.IP != ip {
|
||||
continue
|
||||
}
|
||||
if n.LastSeen != nil {
|
||||
lastSeen = *n.LastSeen
|
||||
}
|
||||
isOffline = n.Online != nil && !*n.Online
|
||||
return ft.PeerAPIURL, lastSeen, isOffline, nil
|
||||
}
|
||||
}
|
||||
return "", time.Time{}, false, fileTargetErrorDetail(ctx, ip)
|
||||
}
|
||||
|
||||
// fileTargetErrorDetail returns a non-nil error saying why ip is an
|
||||
// invalid file sharing target.
|
||||
func fileTargetErrorDetail(ctx context.Context, ip netaddr.IP) error {
|
||||
found := false
|
||||
if st, err := tailscale.Status(ctx); err == nil && st.Self != nil {
|
||||
for _, peer := range st.Peer {
|
||||
for _, pip := range peer.TailscaleIPs {
|
||||
if pip == ip {
|
||||
found = true
|
||||
if peer.UserID != st.Self.UserID {
|
||||
return errors.New("owned by different user; can only send files to your own devices")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if found {
|
||||
return errors.New("target seems to be running an old Tailscale version")
|
||||
}
|
||||
if !tsaddr.IsTailscaleIP(ip) {
|
||||
return fmt.Errorf("unknown target; %v is not a Tailscale IP address", ip)
|
||||
}
|
||||
return errors.New("unknown target; not in your Tailnet")
|
||||
}
|
||||
|
||||
const maxSniff = 4 << 20
|
||||
|
||||
func ext(b []byte) string {
|
||||
if len(b) < maxSniff && utf8.Valid(b) {
|
||||
return ".txt"
|
||||
}
|
||||
if exts, _ := mime.ExtensionsByType(http.DetectContentType(b)); len(exts) > 0 {
|
||||
return exts[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// pickStdinFilename reads a bit of stdin to return a good filename
|
||||
// for its contents. The returned Reader is the concatenation of the
|
||||
// read and unread bits.
|
||||
func pickStdinFilename() (name string, r io.Reader, err error) {
|
||||
sniff, err := io.ReadAll(io.LimitReader(os.Stdin, maxSniff))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return "stdin" + ext(sniff), io.MultiReader(bytes.NewReader(sniff), os.Stdin), nil
|
||||
}
|
||||
|
||||
type slowReader struct {
|
||||
r io.Reader
|
||||
rl *rate.Limiter
|
||||
}
|
||||
|
||||
func (r *slowReader) Read(p []byte) (n int, err error) {
|
||||
const burst = 4 << 10
|
||||
plen := len(p)
|
||||
if plen > burst {
|
||||
plen = burst
|
||||
}
|
||||
if r.rl == nil {
|
||||
r.rl = rate.NewLimiter(rate.Limit(1<<10), burst)
|
||||
}
|
||||
n, err = r.r.Read(p[:plen])
|
||||
r.rl.WaitN(context.Background(), n)
|
||||
return
|
||||
}
|
||||
|
||||
const lastSeenOld = 20 * time.Minute
|
||||
|
||||
func runCpTargets(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("invalid arguments with --targets")
|
||||
}
|
||||
fts, err := tailscale.FileTargets(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ft := range fts {
|
||||
n := ft.Node
|
||||
var detail string
|
||||
if n.Online != nil {
|
||||
if !*n.Online {
|
||||
detail = "offline"
|
||||
}
|
||||
} else {
|
||||
detail = "unknown-status"
|
||||
}
|
||||
if detail != "" && n.LastSeen != nil {
|
||||
d := time.Since(*n.LastSeen)
|
||||
detail += fmt.Sprintf("; last seen %v ago", d.Round(time.Minute))
|
||||
}
|
||||
if detail != "" {
|
||||
detail = "\t" + detail
|
||||
}
|
||||
fmt.Printf("%s\t%s%s\n", n.Addresses[0].IP, n.ComputedName, detail)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var fileGetCmd = &ffcli.Command{
|
||||
Name: "get",
|
||||
ShortUsage: "file get [--wait] [--verbose] <target-directory>",
|
||||
ShortHelp: "Move files out of the Tailscale file inbox",
|
||||
Exec: runFileGet,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("get", flag.ExitOnError)
|
||||
fs.BoolVar(&getArgs.wait, "wait", false, "wait for a file to arrive if inbox is empty")
|
||||
fs.BoolVar(&getArgs.verbose, "verbose", false, "verbose output")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var getArgs struct {
|
||||
wait bool
|
||||
verbose bool
|
||||
}
|
||||
|
||||
func runFileGet(ctx context.Context, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("usage: file get <target-directory>")
|
||||
}
|
||||
log.SetFlags(0)
|
||||
|
||||
dir := args[0]
|
||||
if dir == "/dev/null" {
|
||||
return wipeInbox(ctx)
|
||||
}
|
||||
|
||||
if fi, err := os.Stat(dir); err != nil || !fi.IsDir() {
|
||||
return fmt.Errorf("%q is not a directory", dir)
|
||||
}
|
||||
|
||||
var wfs []apitype.WaitingFile
|
||||
var err error
|
||||
for {
|
||||
wfs, err = tailscale.WaitingFiles(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting WaitingFiles: %v", err)
|
||||
}
|
||||
if len(wfs) != 0 || !getArgs.wait {
|
||||
break
|
||||
}
|
||||
if getArgs.verbose {
|
||||
log.Printf("waiting for file...")
|
||||
}
|
||||
if err := waitForFile(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
deleted := 0
|
||||
for _, wf := range wfs {
|
||||
rc, size, err := tailscale.GetWaitingFile(ctx, wf.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("opening inbox file %q: %v", wf.Name, err)
|
||||
}
|
||||
targetFile := filepath.Join(dir, wf.Name)
|
||||
of, err := os.OpenFile(targetFile, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0644)
|
||||
if err != nil {
|
||||
if _, err := os.Stat(targetFile); err == nil {
|
||||
return fmt.Errorf("refusing to overwrite %v", targetFile)
|
||||
}
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(of, rc)
|
||||
rc.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write %v: %v", targetFile, err)
|
||||
}
|
||||
if err := of.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if getArgs.verbose {
|
||||
log.Printf("wrote %v (%d bytes)", wf.Name, size)
|
||||
}
|
||||
if err := tailscale.DeleteWaitingFile(ctx, wf.Name); err != nil {
|
||||
return fmt.Errorf("deleting %q from inbox: %v", wf.Name, err)
|
||||
}
|
||||
deleted++
|
||||
}
|
||||
if getArgs.verbose {
|
||||
log.Printf("moved %d files", deleted)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func wipeInbox(ctx context.Context) error {
|
||||
if getArgs.wait {
|
||||
return errors.New("can't use --wait with /dev/null target")
|
||||
}
|
||||
wfs, err := tailscale.WaitingFiles(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting WaitingFiles: %v", err)
|
||||
}
|
||||
deleted := 0
|
||||
for _, wf := range wfs {
|
||||
if getArgs.verbose {
|
||||
log.Printf("deleting %v ...", wf.Name)
|
||||
}
|
||||
if err := tailscale.DeleteWaitingFile(ctx, wf.Name); err != nil {
|
||||
return fmt.Errorf("deleting %q: %v", wf.Name, err)
|
||||
}
|
||||
deleted++
|
||||
}
|
||||
if getArgs.verbose {
|
||||
log.Printf("deleted %d files", deleted)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func waitForFile(ctx context.Context) error {
|
||||
c, bc, pumpCtx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
fileWaiting := make(chan bool, 1)
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.ErrMessage != nil {
|
||||
log.Fatal(*n.ErrMessage)
|
||||
}
|
||||
if n.FilesWaiting != nil {
|
||||
select {
|
||||
case fileWaiting <- true:
|
||||
default:
|
||||
}
|
||||
}
|
||||
})
|
||||
go pump(pumpCtx, bc, c)
|
||||
select {
|
||||
case <-fileWaiting:
|
||||
return nil
|
||||
case <-pumpCtx.Done():
|
||||
return pumpCtx.Err()
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
@@ -80,8 +80,7 @@ func runPing(ctx context.Context, args []string) error {
|
||||
prc <- pr
|
||||
}
|
||||
})
|
||||
pumpErr := make(chan error, 1)
|
||||
go func() { pumpErr <- pump(ctx, bc, c) }()
|
||||
go pump(ctx, bc, c)
|
||||
|
||||
hostOrIP := args[0]
|
||||
ip, err := tailscaleIPFromArg(ctx, hostOrIP)
|
||||
@@ -102,8 +101,6 @@ func runPing(ctx context.Context, args []string) error {
|
||||
select {
|
||||
case <-timer.C:
|
||||
fmt.Printf("timeout waiting for ping reply\n")
|
||||
case err := <-pumpErr:
|
||||
return err
|
||||
case pr := <-prc:
|
||||
timer.Stop()
|
||||
if pr.Err != "" {
|
||||
@@ -139,9 +136,6 @@ func runPing(ctx context.Context, args []string) error {
|
||||
if !anyPong {
|
||||
return errors.New("no reply")
|
||||
}
|
||||
if pingArgs.untilDirect {
|
||||
return errors.New("direct connection not established")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
218
cmd/tailscale/cli/push.go
Normal file
218
cmd/tailscale/cli/push.go
Normal file
@@ -0,0 +1,218 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"golang.org/x/time/rate"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
var pushCmd = &ffcli.Command{
|
||||
Name: "push",
|
||||
ShortUsage: "push [--flags] <hostname-or-IP> <file>",
|
||||
ShortHelp: "Push a file to a host",
|
||||
Exec: runPush,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := flag.NewFlagSet("push", flag.ExitOnError)
|
||||
fs.StringVar(&pushArgs.name, "name", "", "alternate filename to use, especially useful when <file> is \"-\" (stdin)")
|
||||
fs.BoolVar(&pushArgs.verbose, "verbose", false, "verbose output")
|
||||
fs.BoolVar(&pushArgs.targets, "targets", false, "list possible push targets")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var pushArgs struct {
|
||||
name string
|
||||
verbose bool
|
||||
targets bool
|
||||
}
|
||||
|
||||
func runPush(ctx context.Context, args []string) error {
|
||||
if pushArgs.targets {
|
||||
return runPushTargets(ctx, args)
|
||||
}
|
||||
if len(args) != 2 || args[0] == "" {
|
||||
return errors.New("usage: push <hostname-or-IP> <file>\n push --targets")
|
||||
}
|
||||
var ip string
|
||||
|
||||
hostOrIP, fileArg := args[0], args[1]
|
||||
ip, err := tailscaleIPFromArg(ctx, hostOrIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peerAPIBase, lastSeen, err := discoverPeerAPIBase(ctx, ip)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !lastSeen.IsZero() && time.Since(lastSeen) > lastSeenOld {
|
||||
fmt.Fprintf(os.Stderr, "# warning: %s last seen %v ago\n", hostOrIP, time.Since(lastSeen).Round(time.Minute))
|
||||
}
|
||||
|
||||
var fileContents io.Reader
|
||||
var name = pushArgs.name
|
||||
var contentLength int64 = -1
|
||||
if fileArg == "-" {
|
||||
fileContents = os.Stdin
|
||||
if name == "" {
|
||||
name, fileContents, err = pickStdinFilename()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
f, err := os.Open(fileArg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fi.IsDir() {
|
||||
return errors.New("directories not supported")
|
||||
}
|
||||
contentLength = fi.Size()
|
||||
fileContents = io.LimitReader(f, contentLength)
|
||||
if name == "" {
|
||||
name = fileArg
|
||||
}
|
||||
|
||||
if slow, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_SLOW_PUSH")); slow {
|
||||
fileContents = &slowReader{r: fileContents}
|
||||
}
|
||||
}
|
||||
|
||||
dstURL := peerAPIBase + "/v0/put/" + url.PathEscape(name)
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", dstURL, fileContents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.ContentLength = contentLength
|
||||
if pushArgs.verbose {
|
||||
log.Printf("sending to %v ...", dstURL)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode == 200 {
|
||||
return nil
|
||||
}
|
||||
io.Copy(os.Stdout, res.Body)
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
|
||||
func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, lastSeen time.Time, err error) {
|
||||
ip, err := netaddr.ParseIP(ipStr)
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
fts, err := tailscale.FileTargets(ctx)
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
for _, ft := range fts {
|
||||
n := ft.Node
|
||||
for _, a := range n.Addresses {
|
||||
if a.IP != ip {
|
||||
continue
|
||||
}
|
||||
if n.LastSeen != nil {
|
||||
lastSeen = *n.LastSeen
|
||||
}
|
||||
return ft.PeerAPIURL, lastSeen, nil
|
||||
}
|
||||
}
|
||||
return "", time.Time{}, errors.New("target seems to be running an old Tailscale version")
|
||||
}
|
||||
|
||||
const maxSniff = 4 << 20
|
||||
|
||||
func ext(b []byte) string {
|
||||
if len(b) < maxSniff && utf8.Valid(b) {
|
||||
return ".txt"
|
||||
}
|
||||
if exts, _ := mime.ExtensionsByType(http.DetectContentType(b)); len(exts) > 0 {
|
||||
return exts[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// pickStdinFilename reads a bit of stdin to return a good filename
|
||||
// for its contents. The returned Reader is the concatenation of the
|
||||
// read and unread bits.
|
||||
func pickStdinFilename() (name string, r io.Reader, err error) {
|
||||
sniff, err := io.ReadAll(io.LimitReader(os.Stdin, maxSniff))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return "stdin" + ext(sniff), io.MultiReader(bytes.NewReader(sniff), os.Stdin), nil
|
||||
}
|
||||
|
||||
type slowReader struct {
|
||||
r io.Reader
|
||||
rl *rate.Limiter
|
||||
}
|
||||
|
||||
func (r *slowReader) Read(p []byte) (n int, err error) {
|
||||
const burst = 4 << 10
|
||||
plen := len(p)
|
||||
if plen > burst {
|
||||
plen = burst
|
||||
}
|
||||
if r.rl == nil {
|
||||
r.rl = rate.NewLimiter(rate.Limit(1<<10), burst)
|
||||
}
|
||||
n, err = r.r.Read(p[:plen])
|
||||
r.rl.WaitN(context.Background(), n)
|
||||
return
|
||||
}
|
||||
|
||||
const lastSeenOld = 20 * time.Minute
|
||||
|
||||
func runPushTargets(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("invalid arguments with --targets")
|
||||
}
|
||||
fts, err := tailscale.FileTargets(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, ft := range fts {
|
||||
n := ft.Node
|
||||
var ago string
|
||||
if n.LastSeen == nil {
|
||||
ago = "\tnode never seen"
|
||||
} else {
|
||||
if d := time.Since(*n.LastSeen); d > lastSeenOld {
|
||||
ago = fmt.Sprintf("\tlast seen %v ago", d.Round(time.Minute))
|
||||
}
|
||||
}
|
||||
fmt.Printf("%s\t%s%s\n", n.Addresses[0].IP, n.ComputedName, ago)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -13,18 +13,16 @@ import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
shellquote "github.com/kballard/go-shellquote"
|
||||
"github.com/go-multierror/multierror"
|
||||
"github.com/peterbourgon/ff/v2/ffcli"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
@@ -51,15 +49,13 @@ flag is also used.
|
||||
Exec: runUp,
|
||||
}
|
||||
|
||||
var upFlagSet = newUpFlagSet(runtime.GOOS, &upArgs)
|
||||
|
||||
func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
||||
var upFlagSet = (func() *flag.FlagSet {
|
||||
upf := flag.NewFlagSet("up", flag.ExitOnError)
|
||||
|
||||
upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication")
|
||||
upf.BoolVar(&upArgs.reset, "reset", false, "reset unspecified settings to their default values")
|
||||
|
||||
upf.StringVar(&upArgs.server, "login-server", ipn.DefaultControlURL, "base URL of control server")
|
||||
upf.StringVar(&upArgs.server, "login-server", "https://login.tailscale.com", "base URL of control server")
|
||||
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")
|
||||
@@ -71,18 +67,15 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
||||
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.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")
|
||||
}
|
||||
switch goos {
|
||||
case "linux":
|
||||
if runtime.GOOS == "linux" {
|
||||
upf.BoolVar(&upArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
|
||||
upf.StringVar(&upArgs.netfilterMode, "netfilter-mode", defaultNetfilterMode(), "netfilter mode (one of on, nodivert, off)")
|
||||
case "windows":
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
upf.BoolVar(&upArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
|
||||
}
|
||||
return upf
|
||||
}
|
||||
})()
|
||||
|
||||
func defaultNetfilterMode() string {
|
||||
if distro.Get() == distro.Synology {
|
||||
@@ -91,7 +84,7 @@ func defaultNetfilterMode() string {
|
||||
return "on"
|
||||
}
|
||||
|
||||
type upArgsT struct {
|
||||
var upArgs struct {
|
||||
reset bool
|
||||
server string
|
||||
acceptRoutes bool
|
||||
@@ -109,11 +102,8 @@ type upArgsT struct {
|
||||
netfilterMode string
|
||||
authKey string
|
||||
hostname string
|
||||
opUser string
|
||||
}
|
||||
|
||||
var upArgs upArgsT
|
||||
|
||||
func warnf(format string, args ...interface{}) {
|
||||
fmt.Printf("Warning: "+format+"\n", args...)
|
||||
}
|
||||
@@ -123,13 +113,29 @@ var (
|
||||
ipv6default = netaddr.MustParseIPPrefix("::/0")
|
||||
)
|
||||
|
||||
// prefsFromUpArgs returns the ipn.Prefs for the provided args.
|
||||
//
|
||||
// Note that the parameters upArgs and warnf are named intentionally
|
||||
// to shadow the globals to prevent accidental misuse of them. This
|
||||
// function exists for testing and should have no side effects or
|
||||
// outside interactions (e.g. no making Tailscale local API calls).
|
||||
func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goos string) (*ipn.Prefs, error) {
|
||||
func runUp(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
fatalf("can't fetch status from tailscaled: %v", err)
|
||||
}
|
||||
|
||||
if distro.Get() == distro.Synology {
|
||||
notSupported := "not yet supported on Synology; see https://github.com/tailscale/tailscale/issues/451"
|
||||
if upArgs.acceptRoutes {
|
||||
return errors.New("--accept-routes is " + notSupported)
|
||||
}
|
||||
if upArgs.exitNodeIP != "" {
|
||||
return errors.New("--exit-node is " + notSupported)
|
||||
}
|
||||
if upArgs.netfilterMode != "off" {
|
||||
return errors.New("--netfilter-mode values besides \"off\" " + notSupported)
|
||||
}
|
||||
}
|
||||
|
||||
routeMap := map[netaddr.IPPrefix]bool{}
|
||||
var default4, default6 bool
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
@@ -137,10 +143,10 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
for _, s := range advroutes {
|
||||
ipp, err := netaddr.ParseIPPrefix(s)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%q is not a valid IP address or CIDR prefix", s)
|
||||
fatalf("%q is not a valid IP address or CIDR prefix", s)
|
||||
}
|
||||
if ipp != ipp.Masked() {
|
||||
return nil, fmt.Errorf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
|
||||
fatalf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
|
||||
}
|
||||
if ipp == ipv4default {
|
||||
default4 = true
|
||||
@@ -150,15 +156,20 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
routeMap[ipp] = true
|
||||
}
|
||||
if default4 && !default6 {
|
||||
return nil, fmt.Errorf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv4default, ipv6default)
|
||||
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv4default, ipv6default)
|
||||
} else if default6 && !default4 {
|
||||
return nil, fmt.Errorf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv6default, ipv4default)
|
||||
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv6default, ipv4default)
|
||||
}
|
||||
}
|
||||
if upArgs.advertiseDefaultRoute {
|
||||
routeMap[netaddr.MustParseIPPrefix("0.0.0.0/0")] = true
|
||||
routeMap[netaddr.MustParseIPPrefix("::/0")] = true
|
||||
}
|
||||
if len(routeMap) > 0 {
|
||||
if err := tailscale.CheckIPForwarding(context.Background()); err != nil {
|
||||
warnf("%v", err)
|
||||
}
|
||||
}
|
||||
routes := make([]netaddr.IPPrefix, 0, len(routeMap))
|
||||
for r := range routeMap {
|
||||
routes = append(routes, r)
|
||||
@@ -175,16 +186,16 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
var err error
|
||||
exitNodeIP, err = netaddr.ParseIP(upArgs.exitNodeIP)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid IP address %q for --exit-node: %v", upArgs.exitNodeIP, err)
|
||||
fatalf("invalid IP address %q for --exit-node: %v", upArgs.exitNodeIP, err)
|
||||
}
|
||||
} else if upArgs.exitNodeAllowLANAccess {
|
||||
return nil, fmt.Errorf("--exit-node-allow-lan-access can only be used with --exit-node")
|
||||
fatalf("--exit-node-allow-lan-access can only be used with --exit-node")
|
||||
}
|
||||
|
||||
if upArgs.exitNodeIP != "" {
|
||||
if !exitNodeIP.IsZero() {
|
||||
for _, ip := range st.TailscaleIPs {
|
||||
if exitNodeIP == ip {
|
||||
return nil, fmt.Errorf("cannot use %s as the exit node as it is a local IP address to this machine, did you mean --advertise-exit-node?", upArgs.exitNodeIP)
|
||||
fatalf("cannot use %s as the exit node as it is a local IP address to this machine, did you mean --advertise-exit-node?", exitNodeIP)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -195,13 +206,13 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
for _, tag := range tags {
|
||||
err := tailcfg.CheckTag(tag)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tag: %q: %s", tag, err)
|
||||
fatalf("tag: %q: %s", tag, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(upArgs.hostname) > 256 {
|
||||
return nil, fmt.Errorf("hostname too long: %d bytes (max 256)", len(upArgs.hostname))
|
||||
fatalf("hostname too long: %d bytes (max 256)", len(upArgs.hostname))
|
||||
}
|
||||
|
||||
prefs := ipn.NewPrefs()
|
||||
@@ -215,13 +226,11 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
prefs.ShieldsUp = upArgs.shieldsUp
|
||||
prefs.AdvertiseRoutes = routes
|
||||
prefs.AdvertiseTags = tags
|
||||
prefs.NoSNAT = !upArgs.snat
|
||||
prefs.Hostname = upArgs.hostname
|
||||
prefs.ForceDaemon = upArgs.forceDaemon
|
||||
prefs.OperatorUser = upArgs.opUser
|
||||
|
||||
if goos == "linux" {
|
||||
prefs.NoSNAT = !upArgs.snat
|
||||
|
||||
if runtime.GOOS == "linux" {
|
||||
switch upArgs.netfilterMode {
|
||||
case "on":
|
||||
prefs.NetfilterMode = preftype.NetfilterOn
|
||||
@@ -232,60 +241,7 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
prefs.NetfilterMode = preftype.NetfilterOff
|
||||
warnf("netfilter=off; configure iptables yourself.")
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid value --netfilter-mode=%q", upArgs.netfilterMode)
|
||||
}
|
||||
}
|
||||
return prefs, nil
|
||||
}
|
||||
|
||||
func runUp(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
fatalf("can't fetch status from tailscaled: %v", err)
|
||||
}
|
||||
origAuthURL := st.AuthURL
|
||||
|
||||
// printAuthURL reports whether we should print out the
|
||||
// provided auth URL from an IPN notify.
|
||||
printAuthURL := func(url string) bool {
|
||||
if upArgs.authKey != "" {
|
||||
// Issue 1755: when using an authkey, don't
|
||||
// show an authURL that might still be pending
|
||||
// from a previous non-completed interactive
|
||||
// login.
|
||||
return false
|
||||
}
|
||||
if upArgs.forceReauth && url == origAuthURL {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if distro.Get() == distro.Synology {
|
||||
notSupported := "not yet supported on Synology; see https://github.com/tailscale/tailscale/issues/451"
|
||||
if upArgs.acceptRoutes {
|
||||
return errors.New("--accept-routes is " + notSupported)
|
||||
}
|
||||
if upArgs.exitNodeIP != "" {
|
||||
return errors.New("--exit-node is " + notSupported)
|
||||
}
|
||||
if upArgs.netfilterMode != "off" {
|
||||
return errors.New("--netfilter-mode values besides \"off\" " + notSupported)
|
||||
}
|
||||
}
|
||||
|
||||
prefs, err := prefsFromUpArgs(upArgs, warnf, st, runtime.GOOS)
|
||||
if err != nil {
|
||||
fatalf("%s", err)
|
||||
}
|
||||
|
||||
if len(prefs.AdvertiseRoutes) > 0 {
|
||||
if err := tailscale.CheckIPForwarding(context.Background()); err != nil {
|
||||
warnf("%v", err)
|
||||
fatalf("invalid value --netfilter-mode: %q", upArgs.netfilterMode)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,13 +250,17 @@ func runUp(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if !upArgs.reset {
|
||||
applyImplicitPrefs(prefs, curPrefs, os.Getenv("USER"))
|
||||
flagSet := map[string]bool{}
|
||||
mp := new(ipn.MaskedPrefs)
|
||||
mp.WantRunningSet = true
|
||||
mp.Prefs = *prefs
|
||||
upFlagSet.Visit(func(f *flag.Flag) {
|
||||
updateMaskedPrefsFromUpFlag(mp, f.Name)
|
||||
flagSet[f.Name] = true
|
||||
})
|
||||
|
||||
if err := checkForAccidentalSettingReverts(upFlagSet, curPrefs, prefs, upCheckEnv{
|
||||
goos: runtime.GOOS,
|
||||
curExitNodeIP: exitNodeIP(prefs, st),
|
||||
}); err != nil {
|
||||
if !upArgs.reset {
|
||||
if err := checkForAccidentalSettingReverts(flagSet, curPrefs, mp); err != nil {
|
||||
fatalf("%s", err)
|
||||
}
|
||||
}
|
||||
@@ -312,7 +272,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
|
||||
// If we're already running and none of the flags require a
|
||||
// restart, we can just do an EditPrefs call and change the
|
||||
// prefs at runtime (e.g. changing hostname, changing
|
||||
// prefs at runtime (e.g. changing hostname, changinged
|
||||
// advertised tags, routes, etc)
|
||||
justEdit := st.BackendState == ipn.Running.String() &&
|
||||
!upArgs.forceReauth &&
|
||||
@@ -320,13 +280,6 @@ func runUp(ctx context.Context, args []string) error {
|
||||
upArgs.authKey == "" &&
|
||||
!controlURLChanged
|
||||
if justEdit {
|
||||
mp := new(ipn.MaskedPrefs)
|
||||
mp.WantRunningSet = true
|
||||
mp.Prefs = *prefs
|
||||
upFlagSet.Visit(func(f *flag.Flag) {
|
||||
updateMaskedPrefsFromUpFlag(mp, f.Name)
|
||||
})
|
||||
|
||||
_, err := tailscale.EditPrefs(ctx, mp)
|
||||
return err
|
||||
}
|
||||
@@ -334,10 +287,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
// simpleUp is whether we're running a simple "tailscale up"
|
||||
// to transition to running from a previously-logged-in but
|
||||
// down state, without changing any settings.
|
||||
simpleUp := upFlagSet.NFlag() == 0 &&
|
||||
curPrefs.Persist != nil &&
|
||||
curPrefs.Persist.LoginName != "" &&
|
||||
st.BackendState != ipn.NeedsLogin.String()
|
||||
simpleUp := len(flagSet) == 0 && curPrefs.Persist != nil && curPrefs.Persist.LoginName != ""
|
||||
|
||||
// At this point we need to subscribe to the IPN bus to watch
|
||||
// for state transitions and possible need to authenticate.
|
||||
@@ -346,8 +296,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
|
||||
startingOrRunning := make(chan bool, 1) // gets value once starting or running
|
||||
gotEngineUpdate := make(chan bool, 1) // gets value upon an engine update
|
||||
pumpErr := make(chan error, 1)
|
||||
go func() { pumpErr <- pump(pumpCtx, bc, c) }()
|
||||
go pump(pumpCtx, bc, c)
|
||||
|
||||
printed := !simpleUp
|
||||
var loginOnce sync.Once
|
||||
@@ -393,7 +342,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
|
||||
if url := n.BrowseToURL; url != nil {
|
||||
printed = true
|
||||
fmt.Fprintf(os.Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
|
||||
}
|
||||
@@ -407,8 +356,6 @@ func runUp(ctx context.Context, args []string) error {
|
||||
case <-gotEngineUpdate:
|
||||
case <-pumpCtx.Done():
|
||||
return pumpCtx.Err()
|
||||
case err := <-pumpErr:
|
||||
return err
|
||||
}
|
||||
|
||||
// Special case: bare "tailscale up" means to just start
|
||||
@@ -424,10 +371,11 @@ func runUp(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
bc.SetPrefs(prefs)
|
||||
|
||||
opts := ipn.Options{
|
||||
StateKey: ipn.GlobalDaemonStateKey,
|
||||
AuthKey: upArgs.authKey,
|
||||
UpdatePrefs: prefs,
|
||||
StateKey: ipn.GlobalDaemonStateKey,
|
||||
AuthKey: upArgs.authKey,
|
||||
}
|
||||
// On Windows, we still run in mostly the "legacy" way that
|
||||
// predated the server's StateStore. That is, we send an empty
|
||||
@@ -445,9 +393,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
bc.Start(opts)
|
||||
if upArgs.forceReauth {
|
||||
startLoginInteractive()
|
||||
}
|
||||
startLoginInteractive()
|
||||
}
|
||||
|
||||
select {
|
||||
@@ -460,26 +406,18 @@ func runUp(ctx context.Context, args []string) error {
|
||||
default:
|
||||
}
|
||||
return pumpCtx.Err()
|
||||
case err := <-pumpErr:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
prefsOfFlag = map[string][]string{} // "exit-node" => ExitNodeIP, ExitNodeID
|
||||
flagForPref = map[string]string{} // "ExitNodeIP" => "exit-node"
|
||||
prefsOfFlag = map[string][]string{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Both these have the same ipn.Pref:
|
||||
addPrefFlagMapping("advertise-exit-node", "AdvertiseRoutes")
|
||||
addPrefFlagMapping("advertise-routes", "AdvertiseRoutes")
|
||||
|
||||
// And this flag has two ipn.Prefs:
|
||||
addPrefFlagMapping("exit-node", "ExitNodeIP", "ExitNodeID")
|
||||
|
||||
// The rest are 1:1:
|
||||
addPrefFlagMapping("accept-dns", "CorpDNS")
|
||||
addPrefFlagMapping("accept-routes", "RouteAll")
|
||||
addPrefFlagMapping("advertise-routes", "AdvertiseRoutes")
|
||||
addPrefFlagMapping("advertise-tags", "AdvertiseTags")
|
||||
addPrefFlagMapping("host-routes", "AllowSingleHosts")
|
||||
addPrefFlagMapping("hostname", "Hostname")
|
||||
@@ -487,15 +425,17 @@ func init() {
|
||||
addPrefFlagMapping("netfilter-mode", "NetfilterMode")
|
||||
addPrefFlagMapping("shields-up", "ShieldsUp")
|
||||
addPrefFlagMapping("snat-subnet-routes", "NoSNAT")
|
||||
addPrefFlagMapping("exit-node", "ExitNodeIP", "ExitNodeIP")
|
||||
addPrefFlagMapping("exit-node-allow-lan-access", "ExitNodeAllowLANAccess")
|
||||
addPrefFlagMapping("unattended", "ForceDaemon")
|
||||
addPrefFlagMapping("operator", "OperatorUser")
|
||||
}
|
||||
|
||||
func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||
prefsOfFlag[flagName] = prefNames
|
||||
prefType := reflect.TypeOf(ipn.Prefs{})
|
||||
for _, pref := range prefNames {
|
||||
flagForPref[pref] = flagName
|
||||
|
||||
// Crash at runtime if there's a typo in the prefName.
|
||||
if _, ok := prefType.FieldByName(pref); !ok {
|
||||
panic(fmt.Sprintf("invalid ipn.Prefs field %q", pref))
|
||||
@@ -503,45 +443,26 @@ func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
// preflessFlag reports whether flagName is a flag that doesn't
|
||||
// correspond to an ipn.Pref.
|
||||
func preflessFlag(flagName string) bool {
|
||||
switch flagName {
|
||||
case "authkey", "force-reauth", "reset":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func updateMaskedPrefsFromUpFlag(mp *ipn.MaskedPrefs, flagName string) {
|
||||
if preflessFlag(flagName) {
|
||||
return
|
||||
}
|
||||
if prefs, ok := prefsOfFlag[flagName]; ok {
|
||||
for _, pref := range prefs {
|
||||
reflect.ValueOf(mp).Elem().FieldByName(pref + "Set").SetBool(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
panic(fmt.Sprintf("internal error: unhandled flag %q", flagName))
|
||||
switch flagName {
|
||||
case "authkey", "force-reauth", "reset":
|
||||
// Not pref-related flags.
|
||||
case "advertise-exit-node":
|
||||
// This pref is a shorthand for advertise-routes.
|
||||
default:
|
||||
panic("internal error: unhandled flag " + flagName)
|
||||
}
|
||||
}
|
||||
|
||||
const accidentalUpPrefix = "Error: changing settings via 'tailscale up' requires mentioning all\n" +
|
||||
"non-default flags. To proceed, either re-run your command with --reset or\n" +
|
||||
"use the command below to explicitly mention the current value of\n" +
|
||||
"all non-default settings:\n\n" +
|
||||
"\ttailscale up"
|
||||
|
||||
// upCheckEnv are extra parameters describing the environment as
|
||||
// needed by checkForAccidentalSettingReverts and friends.
|
||||
type upCheckEnv struct {
|
||||
goos string
|
||||
curExitNodeIP netaddr.IP
|
||||
}
|
||||
|
||||
// checkForAccidentalSettingReverts (the "up checker") checks for
|
||||
// people running "tailscale up" with a subset of the flags they
|
||||
// originally ran it with.
|
||||
// checkForAccidentalSettingReverts checks for people running
|
||||
// "tailscale up" with a subset of the flags they originally ran it
|
||||
// with.
|
||||
//
|
||||
// For example, in Tailscale 1.6 and prior, a user might've advertised
|
||||
// a tag, but later tried to change just one other setting and forgot
|
||||
@@ -553,223 +474,61 @@ type upCheckEnv struct {
|
||||
//
|
||||
// mp is the mask of settings actually set, where mp.Prefs is the new
|
||||
// preferences to set, including any values set from implicit flags.
|
||||
func checkForAccidentalSettingReverts(flagSet *flag.FlagSet, curPrefs, newPrefs *ipn.Prefs, env upCheckEnv) error {
|
||||
if curPrefs.ControlURL == "" {
|
||||
// Don't validate things on initial "up" before a control URL has been set.
|
||||
return nil
|
||||
}
|
||||
|
||||
flagIsSet := map[string]bool{}
|
||||
flagSet.Visit(func(f *flag.Flag) {
|
||||
flagIsSet[f.Name] = true
|
||||
})
|
||||
|
||||
if len(flagIsSet) == 0 {
|
||||
func checkForAccidentalSettingReverts(flagSet map[string]bool, curPrefs *ipn.Prefs, mp *ipn.MaskedPrefs) error {
|
||||
if len(flagSet) == 0 {
|
||||
// A bare "tailscale up" is a special case to just
|
||||
// mean bringing the network up without any changes.
|
||||
return nil
|
||||
}
|
||||
curWithExplicitEdits := curPrefs.Clone()
|
||||
curWithExplicitEdits.ApplyEdits(mp)
|
||||
|
||||
// flagsCur is what flags we'd need to use to keep the exact
|
||||
// settings as-is.
|
||||
flagsCur := prefsToFlags(env, curPrefs)
|
||||
flagsNew := prefsToFlags(env, newPrefs)
|
||||
prefType := reflect.TypeOf(ipn.Prefs{})
|
||||
|
||||
var missing []string
|
||||
for flagName := range flagsCur {
|
||||
valCur, valNew := flagsCur[flagName], flagsNew[flagName]
|
||||
if flagIsSet[flagName] {
|
||||
// Explicit values (current + explicit edit):
|
||||
ev := reflect.ValueOf(curWithExplicitEdits).Elem()
|
||||
// Implicit values (what we'd get if we replaced everything with flag defaults):
|
||||
iv := reflect.ValueOf(&mp.Prefs).Elem()
|
||||
var errs []error
|
||||
var didExitNodeErr bool
|
||||
for i := 0; i < prefType.NumField(); i++ {
|
||||
prefName := prefType.Field(i).Name
|
||||
if prefName == "Persist" {
|
||||
continue
|
||||
}
|
||||
if reflect.DeepEqual(valCur, valNew) {
|
||||
flagName, hasFlag := flagForPref[prefName]
|
||||
if hasFlag && flagSet[flagName] {
|
||||
continue
|
||||
}
|
||||
missing = append(missing, fmtFlagValueArg(flagName, valCur))
|
||||
}
|
||||
if len(missing) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Strings(missing)
|
||||
|
||||
// Compute the stringification of the explicitly provided args in flagSet
|
||||
// to prepend to the command to run.
|
||||
var explicit []string
|
||||
flagSet.Visit(func(f *flag.Flag) {
|
||||
type isBool interface {
|
||||
IsBoolFlag() bool
|
||||
// Get explicit value and implicit value
|
||||
evi, ivi := ev.Field(i).Interface(), iv.Field(i).Interface()
|
||||
if reflect.DeepEqual(evi, ivi) {
|
||||
continue
|
||||
}
|
||||
if ib, ok := f.Value.(isBool); ok && ib.IsBoolFlag() {
|
||||
if f.Value.String() == "false" {
|
||||
explicit = append(explicit, "--"+f.Name+"=false")
|
||||
} else {
|
||||
explicit = append(explicit, "--"+f.Name)
|
||||
}
|
||||
} else {
|
||||
explicit = append(explicit, fmtFlagValueArg(f.Name, f.Value.String()))
|
||||
}
|
||||
})
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(accidentalUpPrefix)
|
||||
|
||||
for _, a := range append(explicit, missing...) {
|
||||
fmt.Fprintf(&sb, " %s", a)
|
||||
}
|
||||
sb.WriteString("\n\n")
|
||||
return errors.New(sb.String())
|
||||
}
|
||||
|
||||
// applyImplicitPrefs mutates prefs to add implicit preferences. Currently
|
||||
// this is just the operator user, which only needs to be set if it doesn't
|
||||
// match the current user.
|
||||
//
|
||||
// curUser is os.Getenv("USER"). It's pulled out for testability.
|
||||
func applyImplicitPrefs(prefs, oldPrefs *ipn.Prefs, curUser string) {
|
||||
if prefs.OperatorUser == "" && oldPrefs.OperatorUser == curUser {
|
||||
prefs.OperatorUser = oldPrefs.OperatorUser
|
||||
}
|
||||
}
|
||||
|
||||
func flagAppliesToOS(flag, goos string) bool {
|
||||
switch flag {
|
||||
case "netfilter-mode", "snat-subnet-routes":
|
||||
return goos == "linux"
|
||||
case "unattended":
|
||||
return goos == "windows"
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func prefsToFlags(env upCheckEnv, prefs *ipn.Prefs) (flagVal map[string]interface{}) {
|
||||
ret := make(map[string]interface{})
|
||||
|
||||
exitNodeIPStr := func() string {
|
||||
if !prefs.ExitNodeIP.IsZero() {
|
||||
return prefs.ExitNodeIP.String()
|
||||
}
|
||||
if prefs.ExitNodeID.IsZero() || env.curExitNodeIP.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return env.curExitNodeIP.String()
|
||||
}
|
||||
|
||||
fs := newUpFlagSet(env.goos, new(upArgsT) /* dummy */)
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
if preflessFlag(f.Name) {
|
||||
return
|
||||
}
|
||||
set := func(v interface{}) {
|
||||
if flagAppliesToOS(f.Name, env.goos) {
|
||||
ret[f.Name] = v
|
||||
} else {
|
||||
ret[f.Name] = nil
|
||||
}
|
||||
}
|
||||
switch f.Name {
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled flag %q", f.Name))
|
||||
case "login-server":
|
||||
set(prefs.ControlURL)
|
||||
case "accept-routes":
|
||||
set(prefs.RouteAll)
|
||||
case "host-routes":
|
||||
set(prefs.AllowSingleHosts)
|
||||
case "accept-dns":
|
||||
set(prefs.CorpDNS)
|
||||
case "shields-up":
|
||||
set(prefs.ShieldsUp)
|
||||
switch flagName {
|
||||
case "":
|
||||
errs = append(errs, fmt.Errorf("'tailscale up' without --reset requires all preferences with changing values to be explicitly mentioned; this command would change the value of flagless pref %q", prefName))
|
||||
case "exit-node":
|
||||
set(exitNodeIPStr())
|
||||
case "exit-node-allow-lan-access":
|
||||
set(prefs.ExitNodeAllowLANAccess)
|
||||
case "advertise-tags":
|
||||
set(strings.Join(prefs.AdvertiseTags, ","))
|
||||
case "hostname":
|
||||
set(prefs.Hostname)
|
||||
case "operator":
|
||||
set(prefs.OperatorUser)
|
||||
case "advertise-routes":
|
||||
var sb strings.Builder
|
||||
for i, r := range withoutExitNodes(prefs.AdvertiseRoutes) {
|
||||
if i > 0 {
|
||||
sb.WriteByte(',')
|
||||
}
|
||||
sb.WriteString(r.String())
|
||||
}
|
||||
set(sb.String())
|
||||
case "advertise-exit-node":
|
||||
set(hasExitNodeRoutes(prefs.AdvertiseRoutes))
|
||||
case "snat-subnet-routes":
|
||||
set(!prefs.NoSNAT)
|
||||
case "netfilter-mode":
|
||||
set(prefs.NetfilterMode.String())
|
||||
case "unattended":
|
||||
set(prefs.ForceDaemon)
|
||||
}
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
func fmtFlagValueArg(flagName string, val interface{}) string {
|
||||
if val == true {
|
||||
return "--" + flagName
|
||||
}
|
||||
if val == "" {
|
||||
return "--" + flagName + "="
|
||||
}
|
||||
return fmt.Sprintf("--%s=%v", flagName, shellquote.Join(fmt.Sprint(val)))
|
||||
}
|
||||
|
||||
func hasExitNodeRoutes(rr []netaddr.IPPrefix) bool {
|
||||
var v4, v6 bool
|
||||
for _, r := range rr {
|
||||
if r.Bits == 0 {
|
||||
if r.IP.Is4() {
|
||||
v4 = true
|
||||
} else if r.IP.Is6() {
|
||||
v6 = true
|
||||
if !didExitNodeErr {
|
||||
didExitNodeErr = true
|
||||
errs = append(errs, errors.New("'tailscale up' without --reset requires all preferences with changing values to be explicitly mentioned; --exit-node is not specified but an exit node is currently configured"))
|
||||
}
|
||||
default:
|
||||
errs = append(errs, fmt.Errorf("'tailscale up' without --reset requires all preferences with changing values to be explicitly mentioned; --%s is not specified but its default value of %v differs from current value %v",
|
||||
flagName, fmtSettingVal(ivi), fmtSettingVal(evi)))
|
||||
}
|
||||
}
|
||||
return v4 && v6
|
||||
return multierror.New(errs)
|
||||
}
|
||||
|
||||
// withoutExitNodes returns rr unchanged if it has only 1 or 0 /0
|
||||
// routes. If it has both IPv4 and IPv6 /0 routes, then it returns
|
||||
// a copy with all /0 routes removed.
|
||||
func withoutExitNodes(rr []netaddr.IPPrefix) []netaddr.IPPrefix {
|
||||
if !hasExitNodeRoutes(rr) {
|
||||
return rr
|
||||
func fmtSettingVal(v interface{}) string {
|
||||
switch v := v.(type) {
|
||||
case bool:
|
||||
return strconv.FormatBool(v)
|
||||
case string, preftype.NetfilterMode:
|
||||
return fmt.Sprintf("%q", v)
|
||||
case []string:
|
||||
return strings.Join(v, ",")
|
||||
}
|
||||
var out []netaddr.IPPrefix
|
||||
for _, r := range rr {
|
||||
if r.Bits > 0 {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// exitNodeIP returns the exit node IP from p, using st to map
|
||||
// it from its ID form to an IP address if needed.
|
||||
func exitNodeIP(p *ipn.Prefs, st *ipnstate.Status) (ip netaddr.IP) {
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !p.ExitNodeIP.IsZero() {
|
||||
return p.ExitNodeIP
|
||||
}
|
||||
id := p.ExitNodeID
|
||||
if id.IsZero() {
|
||||
return
|
||||
}
|
||||
for _, p := range st.Peer {
|
||||
if p.ID == id {
|
||||
if len(p.TailscaleIPs) > 0 {
|
||||
return p.TailscaleIPs[0]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
return fmt.Sprint(v)
|
||||
}
|
||||
|
||||
@@ -73,11 +73,7 @@ func runWeb(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
if webArgs.cgi {
|
||||
if err := cgi.Serve(http.HandlerFunc(webHandler)); err != nil {
|
||||
log.Printf("tailscale.cgi: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return cgi.Serve(http.HandlerFunc(webHandler))
|
||||
}
|
||||
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(webHandler))
|
||||
}
|
||||
@@ -212,7 +208,7 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
type mi map[string]interface{}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
url, err := tailscaleUpForceReauth(r.Context())
|
||||
url, err := tailscaleUp(r.Context())
|
||||
if err != nil {
|
||||
json.NewEncoder(w).Encode(mi{"error": err})
|
||||
return
|
||||
@@ -248,9 +244,9 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// TODO(crawshaw): some of this is very similar to the code in 'tailscale up', can we share anything?
|
||||
func tailscaleUpForceReauth(ctx context.Context) (authURL string, retErr error) {
|
||||
func tailscaleUp(ctx context.Context) (authURL string, retErr error) {
|
||||
prefs := ipn.NewPrefs()
|
||||
prefs.ControlURL = ipn.DefaultControlURL
|
||||
prefs.ControlURL = "https://login.tailscale.com"
|
||||
prefs.WantRunning = true
|
||||
prefs.CorpDNS = true
|
||||
prefs.AllowSingleHosts = true
|
||||
@@ -260,31 +256,10 @@ func tailscaleUpForceReauth(ctx context.Context) (authURL string, retErr error)
|
||||
prefs.NetfilterMode = preftype.NetfilterOff
|
||||
}
|
||||
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("can't fetch status: %v", err)
|
||||
}
|
||||
origAuthURL := st.AuthURL
|
||||
|
||||
// printAuthURL reports whether we should print out the
|
||||
// provided auth URL from an IPN notify.
|
||||
printAuthURL := func(url string) bool {
|
||||
return url != origAuthURL
|
||||
}
|
||||
|
||||
c, bc, pumpCtx, cancel := connect(ctx)
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
gotEngineUpdate := make(chan bool, 1) // gets value upon an engine update
|
||||
go pump(pumpCtx, bc, c)
|
||||
|
||||
bc.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.Engine != nil {
|
||||
select {
|
||||
case gotEngineUpdate <- true:
|
||||
default:
|
||||
}
|
||||
}
|
||||
if n.ErrMessage != nil {
|
||||
msg := *n.ErrMessage
|
||||
if msg == ipn.ErrMsgPermissionDenied {
|
||||
@@ -297,21 +272,11 @@ func tailscaleUpForceReauth(ctx context.Context) (authURL string, retErr error)
|
||||
}
|
||||
retErr = fmt.Errorf("backend error: %v", msg)
|
||||
cancel()
|
||||
} else if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
|
||||
} else if url := n.BrowseToURL; url != nil {
|
||||
authURL = *url
|
||||
cancel()
|
||||
}
|
||||
})
|
||||
// Wait for backend client to be connected so we know
|
||||
// we're subscribed to updates. Otherwise we can miss
|
||||
// an update upon its transition to running. Do so by causing some traffic
|
||||
// back to the bus that we then wait on.
|
||||
bc.RequestEngineStatus()
|
||||
select {
|
||||
case <-gotEngineUpdate:
|
||||
case <-pumpCtx.Done():
|
||||
return authURL, pumpCtx.Err()
|
||||
}
|
||||
|
||||
bc.SetPrefs(prefs)
|
||||
|
||||
@@ -319,6 +284,7 @@ func tailscaleUpForceReauth(ctx context.Context) (authURL string, retErr error)
|
||||
StateKey: ipn.GlobalDaemonStateKey,
|
||||
})
|
||||
bc.StartLoginInteractive()
|
||||
pump(ctx, bc, c)
|
||||
|
||||
if authURL == "" && retErr == nil {
|
||||
return "", fmt.Errorf("login failed with no backend error message")
|
||||
|
||||
@@ -11,133 +11,140 @@
|
||||
</head>
|
||||
|
||||
<body class="py-14">
|
||||
<main class="container max-w-lg mx-auto py-6 px-8 bg-white rounded-md shadow-2xl" style="width: 95%">
|
||||
<header class="flex justify-between items-center min-width-0 py-2 mb-8">
|
||||
<svg width="26" height="26" viewBox="0 0 23 23" title="Tailscale" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
class="flex-shrink-0 mr-4">
|
||||
<circle opacity="0.2" cx="3.4" cy="3.25" r="2.7" fill="currentColor"></circle>
|
||||
<circle cx="3.4" cy="11.3" r="2.7" fill="currentColor"></circle>
|
||||
<circle opacity="0.2" cx="3.4" cy="19.5" r="2.7" fill="currentColor"></circle>
|
||||
<circle cx="11.5" cy="11.3" r="2.7" fill="currentColor"></circle>
|
||||
<circle cx="11.5" cy="19.5" r="2.7" fill="currentColor"></circle>
|
||||
<circle opacity="0.2" cx="11.5" cy="3.25" r="2.7" fill="currentColor"></circle>
|
||||
<circle opacity="0.2" cx="19.5" cy="3.25" r="2.7" fill="currentColor"></circle>
|
||||
<circle cx="19.5" cy="11.3" r="2.7" fill="currentColor"></circle>
|
||||
<circle opacity="0.2" cx="19.5" cy="19.5" r="2.7" fill="currentColor"></circle>
|
||||
</svg>
|
||||
<div class="flex items-center justify-end space-x-2 w-2/3">
|
||||
{{ with .Profile.LoginName }}
|
||||
<div class="text-right truncate leading-4">
|
||||
<h4 class="truncate">{{.}}</h4>
|
||||
<a href="#" class="text-xs text-gray-500 hover:text-gray-700 js-loginButton">Switch account</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="relative flex-shrink-0 w-8 h-8 rounded-full overflow-hidden">
|
||||
{{ with .Profile.ProfilePicURL }}
|
||||
<div class="w-8 h-8 flex pointer-events-none rounded-full bg-gray-200"
|
||||
style="background-image: url('{{.}}'); background-size: cover;"></div>
|
||||
{{ else }}
|
||||
<div class="w-8 h-8 flex pointer-events-none rounded-full border border-gray-400 border-dashed"></div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{{ if .IP }}
|
||||
<div
|
||||
class="border border-gray-200 bg-gray-0 rounded-lg p-2 pl-3 pr-3 mb-8 width-full flex items-center justify-between">
|
||||
<div class="flex items-center min-width-0">
|
||||
<svg class="flex-shrink-0 text-gray-600 mr-3 ml-1" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
|
||||
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
|
||||
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
||||
<line x1="6" y1="18" x2="6.01" y2="18"></line>
|
||||
<main class="container max-w-lg mx-auto py-6 px-8 bg-white rounded-md shadow-2xl" style="width: 95%">
|
||||
<header class="flex justify-between items-center min-width-0 py-2 mb-8">
|
||||
<svg width="26" height="26" viewBox="0 0 23 23" title="Tailscale" fill="none" xmlns="http://www.w3.org/2000/svg"
|
||||
class="flex-shrink-0 mr-4">
|
||||
<circle opacity="0.2" cx="3.4" cy="3.25" r="2.7" fill="currentColor"></circle>
|
||||
<circle cx="3.4" cy="11.3" r="2.7" fill="currentColor"></circle>
|
||||
<circle opacity="0.2" cx="3.4" cy="19.5" r="2.7" fill="currentColor"></circle>
|
||||
<circle cx="11.5" cy="11.3" r="2.7" fill="currentColor"></circle>
|
||||
<circle cx="11.5" cy="19.5" r="2.7" fill="currentColor"></circle>
|
||||
<circle opacity="0.2" cx="11.5" cy="3.25" r="2.7" fill="currentColor"></circle>
|
||||
<circle opacity="0.2" cx="19.5" cy="3.25" r="2.7" fill="currentColor"></circle>
|
||||
<circle cx="19.5" cy="11.3" r="2.7" fill="currentColor"></circle>
|
||||
<circle opacity="0.2" cx="19.5" cy="19.5" r="2.7" fill="currentColor"></circle>
|
||||
</svg>
|
||||
<h4 class="font-semibold truncate mr-2">{{.DeviceName}}</h4>
|
||||
<div class="flex items-center justify-end space-x-2 w-2/3">
|
||||
{{ with .Profile.LoginName }}
|
||||
<div class="text-right truncate leading-4">
|
||||
<h4 class="truncate">{{.}}</h4>
|
||||
<a href="#" class="text-xs text-gray-500 hover:text-gray-700 js-loginButton">Switch account</a>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div class="relative flex-shrink-0 w-8 h-8 rounded-full overflow-hidden">
|
||||
{{ with .Profile.ProfilePicURL }}
|
||||
<div class="w-8 h-8 flex pointer-events-none rounded-full bg-gray-200"
|
||||
style="background-image: url('{{.}}'); background-size: cover;"></div>
|
||||
{{ else }}
|
||||
<div class="w-8 h-8 flex pointer-events-none rounded-full border border-gray-400 border-dashed"></div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{{ if .IP }}
|
||||
<div
|
||||
class="border border-gray-200 bg-gray-0 rounded-lg p-2 pl-3 pr-3 mb-8 width-full flex items-center justify-between">
|
||||
<div class="flex items-center min-width-0">
|
||||
<svg class="flex-shrink-0 text-gray-600 mr-3 ml-1" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round">
|
||||
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
|
||||
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
|
||||
<line x1="6" y1="6" x2="6.01" y2="6"></line>
|
||||
<line x1="6" y1="18" x2="6.01" y2="18"></line>
|
||||
</svg>
|
||||
<h4 class="font-semibold truncate mr-2">{{.DeviceName}}</h4>
|
||||
</div>
|
||||
<h5>{{.IP}}</h5>
|
||||
</div>
|
||||
<h5>{{.IP}}</h5>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if or (eq .Status "NeedsLogin") (eq .Status "NoState") }}
|
||||
{{ if .IP }}
|
||||
<div class="mb-6">
|
||||
<p class="text-gray-700">Your device's key has expired. Reauthenticate this device by logging in again, or <a
|
||||
href="https://tailscale.com/kb/1028/key-expiry" class="link" target="_blank">learn more</a>.</p>
|
||||
</div>
|
||||
<a href="#" class="mb-4 js-loginButton" target="_blank">
|
||||
<button class="button button-blue w-full">Reauthenticate</button>
|
||||
</a>
|
||||
{{ else }}
|
||||
<div class="mb-6">
|
||||
<h3 class="text-3xl font-semibold mb-3">Log in</h3>
|
||||
<p class="text-gray-700">Get started by logging in to your Tailscale network. Or, learn more at <a
|
||||
href="https://tailscale.com/" class="link" target="_blank">tailscale.com</a>.</p>
|
||||
</div>
|
||||
<a href="#" class="mb-4 js-loginButton" target="_blank">
|
||||
<button class="button button-blue w-full">Log In</button>
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ else if eq .Status "NeedsMachineAuth" }}
|
||||
<div class="mb-4">
|
||||
This device is authorized, but needs approval from a network admin before it can connect to the network.
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="mb-4">
|
||||
<p>You are connected! Access this device over Tailscale using the device name or IP address above.</p>
|
||||
</div>
|
||||
<a href="#" class="mb-4 link font-medium js-loginButton" target="_blank">Reauthenticate</a>
|
||||
{{ end }}
|
||||
</main>
|
||||
<script>(function () {
|
||||
let loginButtons = document.querySelectorAll(".js-loginButton");
|
||||
let fetchingUrl = false;
|
||||
{{ end }}
|
||||
{{ if or (eq .Status "NeedsLogin") (eq .Status "NoState") }}
|
||||
{{ if .IP }}
|
||||
<div class="mb-6">
|
||||
<p class="text-gray-700">Your device's key has expired. Reauthenticate this device by logging in again, or <a
|
||||
href="https://tailscale.com/kb/1028/key-expiry" class="link" target="_blank">learn more</a>.</p>
|
||||
</div>
|
||||
<a href="#" class="mb-4 js-loginButton" target="_blank">
|
||||
<button class="button button-blue w-full">Reauthenticate</button>
|
||||
</a>
|
||||
{{ else }}
|
||||
<div class="mb-6">
|
||||
<h3 class="text-3xl font-semibold mb-3">Log in</h3>
|
||||
<p class="text-gray-700">Get started by logging in to your Tailscale network. Or, learn more at <a
|
||||
href="https://tailscale.com/" class="link" target="_blank">tailscale.com</a>.</p>
|
||||
</div>
|
||||
<a href="#" class="mb-4 js-loginButton" target="_blank">
|
||||
<button class="button button-blue w-full">Log In</button>
|
||||
</a>
|
||||
{{ end }}
|
||||
{{ else if eq .Status "NeedsMachineAuth" }}
|
||||
<div class="mb-4">
|
||||
This device is authorized, but needs approval from a network admin before it can connect to the network.
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="mb-4">
|
||||
<p>You are connected! Access this device over Tailscale using the device name or IP address above.</p>
|
||||
</div>
|
||||
<a href="#" class="mb-4 link font-medium js-loginButton" target="_blank">Reauthenticate</a>
|
||||
{{ end }}
|
||||
</main>
|
||||
<script>
|
||||
(function () {
|
||||
let loginButtons = document.querySelectorAll(".js-loginButton");
|
||||
let fetchingUrl = false;
|
||||
|
||||
function handleClick(e) {
|
||||
e.preventDefault();
|
||||
function handleClick(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (fetchingUrl) {
|
||||
return;
|
||||
}
|
||||
if (fetchingUrl) {
|
||||
return;
|
||||
}
|
||||
|
||||
fetchingUrl = true;
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const token = urlParams.get("SynoToken");
|
||||
const nextParams = new URLSearchParams({ up: true });
|
||||
if (token) {
|
||||
nextParams.set("SynoToken", token)
|
||||
}
|
||||
const nextUrl = new URL(window.location);
|
||||
nextUrl.search = nextParams.toString()
|
||||
const url = nextUrl.toString();
|
||||
fetchingUrl = true;
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const token = urlParams.get("SynoToken");
|
||||
const nextParams = new URLSearchParams({ up: true });
|
||||
if (token) {
|
||||
nextParams.set("SynoToken", token)
|
||||
}
|
||||
const nextUrl = new URL(window.location);
|
||||
nextUrl.search = nextParams.toString()
|
||||
const url = nextUrl.toString();
|
||||
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
}).then(res => res.json()).then(res => {
|
||||
fetchingUrl = false;
|
||||
const err = res["error"];
|
||||
if (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
const url = res["url"];
|
||||
if (url) {
|
||||
document.location.href = url;
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
}).catch(err => {
|
||||
alert("Failed to log in: " + err.message);
|
||||
});
|
||||
}
|
||||
const tab = window.open("/redirect", "_blank");
|
||||
|
||||
Array.from(loginButtons).forEach(el => {
|
||||
el.addEventListener("click", handleClick);
|
||||
})
|
||||
})();</script>
|
||||
fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
}).then(res => res.json()).then(res => {
|
||||
fetchingUrl = false;
|
||||
const err = res["error"];
|
||||
if (err) {
|
||||
throw new Error(err);
|
||||
}
|
||||
const url = res["url"];
|
||||
if (url) {
|
||||
authUrl = url;
|
||||
tab.location = url;
|
||||
tab.focus();
|
||||
} else {
|
||||
location.reload();
|
||||
}
|
||||
}).catch(err => {
|
||||
tab.close();
|
||||
alert("Failed to log in: " + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
Array.from(loginButtons).forEach(el => {
|
||||
el.addEventListener("click", handleClick);
|
||||
})
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@@ -2,7 +2,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
|
||||
github.com/go-multierror/multierror from tailscale.com/cmd/tailscale/cli
|
||||
github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli
|
||||
github.com/peterbourgon/ff/v2/ffcli from tailscale.com/cmd/tailscale/cli
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
@@ -15,7 +15,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
rsc.io/goversion/version from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn
|
||||
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale
|
||||
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
|
||||
@@ -33,7 +33,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
|
||||
tailscale.com/net/tsaddr from tailscale.com/net/interfaces
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
|
||||
@@ -85,7 +85,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
golang.org/x/text/unicode/norm from golang.org/x/net/idna
|
||||
golang.org/x/time/rate from tailscale.com/cmd/tailscale/cli+
|
||||
golang.org/x/time/rate from tailscale.com/types/logger+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
|
||||
@@ -78,7 +78,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/derp/derpmap from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/disco from tailscale.com/derp+
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/internal/deephash from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/internal/deepprint from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/ipn from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/ipn/ipnlocal from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
|
||||
@@ -131,21 +131,18 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/types/strbuilder from tailscale.com/net/packet
|
||||
tailscale.com/types/structs from tailscale.com/control/controlclient+
|
||||
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
|
||||
L tailscale.com/util/cmpver from tailscale.com/net/dns
|
||||
tailscale.com/util/dnsname from tailscale.com/ipn/ipnstate+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/netns+
|
||||
L tailscale.com/util/lineread from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/osshare from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/util/winutil from tailscale.com/logpolicy+
|
||||
tailscale.com/version from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/version/distro from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/monitor from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
|
||||
@@ -181,14 +178,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
|
||||
W golang.org/x/sys/windows from github.com/tailscale/wireguard-go/conn+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
W golang.org/x/sys/windows/svc from tailscale.com/cmd/tailscaled+
|
||||
W golang.org/x/sys/windows/svc/mgr from tailscale.com/cmd/tailscaled
|
||||
W golang.org/x/sys/windows/svc from tailscale.com/cmd/tailscaled
|
||||
golang.org/x/term from tailscale.com/logpolicy
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
golang.org/x/text/unicode/norm from golang.org/x/net/idna
|
||||
golang.org/x/time/rate from inet.af/netstack/tcpip/stack+
|
||||
golang.org/x/time/rate from tailscale.com/types/logger+
|
||||
bufio from compress/flate+
|
||||
bytes from bufio+
|
||||
compress/flate from compress/gzip+
|
||||
@@ -221,7 +217,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
debug/elf from rsc.io/goversion/version
|
||||
debug/macho from rsc.io/goversion/version
|
||||
debug/pe from rsc.io/goversion/version
|
||||
L embed from tailscale.com/net/dns
|
||||
embed from tailscale.com/net/dns
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
encoding/base64 from encoding/json+
|
||||
|
||||
@@ -1,123 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"golang.org/x/sys/windows/svc/mgr"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/osshare"
|
||||
)
|
||||
|
||||
func init() {
|
||||
installSystemDaemon = installSystemDaemonWindows
|
||||
uninstallSystemDaemon = uninstallSystemDaemonWindows
|
||||
}
|
||||
|
||||
func installSystemDaemonWindows(args []string) (err error) {
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to Windows service manager: %v", err)
|
||||
}
|
||||
|
||||
service, err := m.OpenService(serviceName)
|
||||
if err == nil {
|
||||
service.Close()
|
||||
return fmt.Errorf("service %q is already installed", serviceName)
|
||||
}
|
||||
|
||||
// no such service; proceed to install the service.
|
||||
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c := mgr.Config{
|
||||
ServiceType: windows.SERVICE_WIN32_OWN_PROCESS,
|
||||
StartType: mgr.StartAutomatic,
|
||||
ErrorControl: mgr.ErrorNormal,
|
||||
DisplayName: serviceName,
|
||||
Description: "Connects this computer to others on the Tailscale network.",
|
||||
}
|
||||
|
||||
service, err = m.CreateService(serviceName, exe, c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create %q service: %v", serviceName, err)
|
||||
}
|
||||
defer service.Close()
|
||||
|
||||
// Exponential backoff is often too aggressive, so use (mostly)
|
||||
// squares instead.
|
||||
ra := []mgr.RecoveryAction{
|
||||
{mgr.ServiceRestart, 1 * time.Second},
|
||||
{mgr.ServiceRestart, 2 * time.Second},
|
||||
{mgr.ServiceRestart, 4 * time.Second},
|
||||
{mgr.ServiceRestart, 9 * time.Second},
|
||||
{mgr.ServiceRestart, 16 * time.Second},
|
||||
{mgr.ServiceRestart, 25 * time.Second},
|
||||
{mgr.ServiceRestart, 36 * time.Second},
|
||||
{mgr.ServiceRestart, 49 * time.Second},
|
||||
{mgr.ServiceRestart, 64 * time.Second},
|
||||
}
|
||||
const resetPeriodSecs = 60
|
||||
err = service.SetRecoveryActions(ra, resetPeriodSecs)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to set service recovery actions: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func uninstallSystemDaemonWindows(args []string) (ret error) {
|
||||
// Remove file sharing from Windows shell (noop in non-windows)
|
||||
osshare.SetFileSharingEnabled(false, logger.Discard)
|
||||
|
||||
m, err := mgr.Connect()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to Windows service manager: %v", err)
|
||||
}
|
||||
defer m.Disconnect()
|
||||
|
||||
service, err := m.OpenService(serviceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open %q service: %v", serviceName, err)
|
||||
}
|
||||
|
||||
st, err := service.Query()
|
||||
if err != nil {
|
||||
service.Close()
|
||||
return fmt.Errorf("failed to query service state: %v", err)
|
||||
}
|
||||
if st.State != svc.Stopped {
|
||||
service.Control(svc.Stop)
|
||||
}
|
||||
err = service.Delete()
|
||||
service.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete service: %v", err)
|
||||
}
|
||||
|
||||
bo := backoff.NewBackoff("uninstall", logger.Discard, 30*time.Second)
|
||||
end := time.Now().Add(15 * time.Second)
|
||||
for time.Until(end) > 0 {
|
||||
service, err = m.OpenService(serviceName)
|
||||
if err != nil {
|
||||
// service is no longer openable; success!
|
||||
break
|
||||
}
|
||||
service.Close()
|
||||
bo.BackOff(context.Background(), errors.New("service not deleted"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -29,21 +29,19 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-multierror/multierror"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/socks5"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/types/flagtype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/util/osshare"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/wgengine/router"
|
||||
@@ -116,7 +114,7 @@ func main() {
|
||||
flag.StringVar(&args.debug, "debug", "", "listen address ([ip]:port) of optional debug server")
|
||||
flag.StringVar(&args.socksAddr, "socks5-server", "", `optional [ip]:port to run a SOCK5 server (e.g. "localhost:1080")`)
|
||||
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
|
||||
flag.Var(flagtype.PortValue(&args.port, 0), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
||||
flag.Var(flagtype.PortValue(&args.port, magicsock.DefaultPort), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
||||
flag.StringVar(&args.statepath, "state", paths.DefaultTailscaledStateFile(), "path of state file")
|
||||
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
|
||||
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
|
||||
@@ -160,12 +158,7 @@ func main() {
|
||||
log.Fatalf("--socket is required")
|
||||
}
|
||||
|
||||
err := run()
|
||||
|
||||
// Remove file sharing from Windows shell (noop in non-windows)
|
||||
osshare.SetFileSharingEnabled(false, logger.Discard)
|
||||
|
||||
if err != nil {
|
||||
if err := run(); err != nil {
|
||||
// No need to log; the func already did
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -246,31 +239,30 @@ func run() error {
|
||||
srv := &socks5.Server{
|
||||
Logf: logger.WithPrefix(logf, "socks5: "),
|
||||
}
|
||||
var (
|
||||
mu sync.Mutex // guards the following field
|
||||
dns netstack.DNSMap
|
||||
)
|
||||
e.AddNetworkMapCallback(func(nm *netmap.NetworkMap) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
dns = netstack.DNSMapFromNetworkMap(nm)
|
||||
})
|
||||
useNetstackForIP := func(ip netaddr.IP) bool {
|
||||
// TODO(bradfitz): this isn't exactly right.
|
||||
// We should also support subnets when the
|
||||
// prefs are configured as such.
|
||||
return tsaddr.IsTailscaleIP(ip)
|
||||
}
|
||||
srv.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
ipp, err := dns.Resolve(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ns != nil && useNetstackForIP(ipp.IP) {
|
||||
// TODO: also consider wrapNetstack, where dials can go to either Tailscale
|
||||
// or non-Tailscale targets. But that's also basically what
|
||||
// https://github.com/tailscale/tailscale/issues/1617 is about, so do them
|
||||
// both at the same time.
|
||||
if useNetstack {
|
||||
srv.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return ns.DialContextTCP(ctx, addr)
|
||||
}
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, network, ipp.String())
|
||||
} else {
|
||||
var mu sync.Mutex
|
||||
var dns netstack.DNSMap
|
||||
e.AddNetworkMapCallback(func(nm *netmap.NetworkMap) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
dns = netstack.DNSMapFromNetworkMap(nm)
|
||||
})
|
||||
srv.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
ipp, err := dns.Resolve(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, network, ipp.String())
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
log.Fatalf("SOCKS5 server exited: %v", srv.Serve(socksListener))
|
||||
@@ -303,7 +295,7 @@ func run() error {
|
||||
Port: 41112,
|
||||
StatePath: args.statepath,
|
||||
AutostartStateKey: globalStateKey,
|
||||
SurviveDisconnects: runtime.GOOS != "windows",
|
||||
SurviveDisconnects: true,
|
||||
DebugMux: debugMux,
|
||||
}
|
||||
err = ipnserver.Run(ctx, logf, pol.PublicID.String(), ipnserver.FixedEngine(e), opts)
|
||||
|
||||
@@ -159,16 +159,18 @@ func beFirewallKillswitch() bool {
|
||||
|
||||
func startIPNServer(ctx context.Context, logid string) error {
|
||||
var logf logger.Logf = log.Printf
|
||||
var eng wgengine.Engine
|
||||
var err error
|
||||
|
||||
getEngineRaw := func() (wgengine.Engine, error) {
|
||||
getEngine := func() (wgengine.Engine, error) {
|
||||
dev, devName, err := tstun.New(logf, "Tailscale")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("TUN: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
r, err := router.New(logf, dev)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
return nil, fmt.Errorf("router: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
if wrapNetstack {
|
||||
r = netstack.NewSubnetRouterWrapper(r)
|
||||
@@ -177,7 +179,7 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
if err != nil {
|
||||
r.Close()
|
||||
dev.Close()
|
||||
return nil, fmt.Errorf("DNS: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
Tun: dev,
|
||||
@@ -188,7 +190,7 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
if err != nil {
|
||||
r.Close()
|
||||
dev.Close()
|
||||
return nil, fmt.Errorf("engine: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
onlySubnets := true
|
||||
if wrapNetstack {
|
||||
@@ -197,82 +199,60 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
return wgengine.NewWatchdog(eng), nil
|
||||
}
|
||||
|
||||
type engineOrError struct {
|
||||
Engine wgengine.Engine
|
||||
Err error
|
||||
}
|
||||
engErrc := make(chan engineOrError)
|
||||
t0 := time.Now()
|
||||
go func() {
|
||||
const ms = time.Millisecond
|
||||
for try := 1; ; try++ {
|
||||
logf("tailscaled: getting engine... (try %v)", try)
|
||||
t1 := time.Now()
|
||||
eng, err := getEngineRaw()
|
||||
d, dt := time.Since(t1).Round(ms), time.Since(t1).Round(ms)
|
||||
if msg := os.Getenv("TS_DEBUG_WIN_FAIL"); msg != "" {
|
||||
err = fmt.Errorf("pretending to be a service failure: %v", msg)
|
||||
} else {
|
||||
// We have a bunch of bug reports of wgengine.NewUserspaceEngine returning a few different errors,
|
||||
// all intermittently. A few times I (Brad) have also seen sporadic failures that simply
|
||||
// restarting fixed. So try a few times.
|
||||
for try := 1; try <= 5; try++ {
|
||||
if try > 1 {
|
||||
// Only sleep a bit. Don't do some massive backoff because
|
||||
// the frontend GUI has a 30 second timeout on connecting to us,
|
||||
// but even 5 seconds is too long for them to get any results.
|
||||
// 5 tries * 1 second each seems fine.
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
eng, err = getEngine()
|
||||
if err != nil {
|
||||
logf("tailscaled: engine fetch error (try %v) in %v (total %v, sysUptime %v): %v",
|
||||
try, d, dt, windowsUptime().Round(time.Second), err)
|
||||
} else {
|
||||
if try > 1 {
|
||||
logf("tailscaled: got engine on try %v in %v (total %v)", try, d, dt)
|
||||
} else {
|
||||
logf("tailscaled: got engine in %v", d)
|
||||
}
|
||||
logf("wgengine.NewUserspaceEngine: (try %v) %v", try, err)
|
||||
continue
|
||||
}
|
||||
timer := time.NewTimer(5 * time.Second)
|
||||
engErrc <- engineOrError{eng, err}
|
||||
if err == nil {
|
||||
timer.Stop()
|
||||
return
|
||||
if try > 1 {
|
||||
logf("wgengine.NewUserspaceEngine: ended up working on try %v", try)
|
||||
}
|
||||
<-timer.C
|
||||
break
|
||||
}
|
||||
}()
|
||||
|
||||
}
|
||||
if err != nil {
|
||||
// Log the error, but don't fatalf. We want to
|
||||
// propagate the error message to the UI frontend. So
|
||||
// we continue and tell the ipnserver to return that
|
||||
// in a Notify message.
|
||||
logf("wgengine.NewUserspaceEngine: %v", err)
|
||||
}
|
||||
opts := ipnserver.Options{
|
||||
Port: 41112,
|
||||
SurviveDisconnects: false,
|
||||
StatePath: args.statepath,
|
||||
}
|
||||
|
||||
// getEngine is called by ipnserver to get the engine. It's
|
||||
// not called concurrently and is not called again once it
|
||||
// successfully returns an engine.
|
||||
getEngine := func() (wgengine.Engine, error) {
|
||||
if msg := os.Getenv("TS_DEBUG_WIN_FAIL"); msg != "" {
|
||||
return nil, fmt.Errorf("pretending to be a service failure: %v", msg)
|
||||
}
|
||||
for {
|
||||
res := <-engErrc
|
||||
if res.Engine != nil {
|
||||
return res.Engine, nil
|
||||
if err != nil {
|
||||
// Return nicer errors to users, annotated with logids, which helps
|
||||
// when they file bugs.
|
||||
rawGetEngine := getEngine // raw == without verbose logid-containing error
|
||||
getEngine = func() (wgengine.Engine, error) {
|
||||
eng, err := rawGetEngine()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wgengine.NewUserspaceEngine: %v\n\nlogid: %v", err, logid)
|
||||
}
|
||||
if time.Since(t0) < time.Minute || windowsUptime() < 10*time.Minute {
|
||||
// Ignore errors during early boot. Windows 10 auto logs in the GUI
|
||||
// way sooner than the networking stack components start up.
|
||||
// So the network will fail for a bit (and require a few tries) while
|
||||
// the GUI is still fine.
|
||||
continue
|
||||
}
|
||||
// Return nicer errors to users, annotated with logids, which helps
|
||||
// when they file bugs.
|
||||
return nil, fmt.Errorf("%w\n\nlogid: %v", res.Err, logid)
|
||||
return eng, nil
|
||||
}
|
||||
} else {
|
||||
getEngine = ipnserver.FixedEngine(eng)
|
||||
}
|
||||
err := ipnserver.Run(ctx, logf, logid, getEngine, opts)
|
||||
err = ipnserver.Run(ctx, logf, logid, getEngine, opts)
|
||||
if err != nil {
|
||||
logf("ipnserver.Run: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
|
||||
getTickCount64Proc = kernel32.NewProc("GetTickCount64")
|
||||
)
|
||||
|
||||
func windowsUptime() time.Duration {
|
||||
r, _, _ := getTickCount64Proc.Call()
|
||||
return time.Duration(int64(r)) * time.Millisecond
|
||||
}
|
||||
|
||||
@@ -59,11 +59,11 @@ func main() {
|
||||
|
||||
warned := false
|
||||
for {
|
||||
addrs, iface, err := interfaces.Tailscale()
|
||||
addr, iface, err := interfaces.Tailscale()
|
||||
if err != nil {
|
||||
log.Fatalf("listing interfaces: %v", err)
|
||||
}
|
||||
if len(addrs) == 0 {
|
||||
if addr == nil {
|
||||
if !warned {
|
||||
log.Printf("no tailscale interface found; polling until one is available")
|
||||
warned = true
|
||||
@@ -75,13 +75,6 @@ func main() {
|
||||
continue
|
||||
}
|
||||
warned = false
|
||||
var addr netaddr.IP
|
||||
for _, a := range addrs {
|
||||
if a.Is4() {
|
||||
addr = a
|
||||
break
|
||||
}
|
||||
}
|
||||
listen := net.JoinHostPort(addr.String(), fmt.Sprint(*port))
|
||||
log.Printf("tailscale ssh server listening on %v, %v", iface.Name, listen)
|
||||
s := &ssh.Server{
|
||||
|
||||
@@ -2,11 +2,18 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package controlclient implements the client for the Tailscale
|
||||
// control plane.
|
||||
//
|
||||
// It handles authentication, port picking, and collects the local
|
||||
// network configuration.
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -21,6 +28,77 @@ import (
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
// State is the high-level state of the client. It is used only in
|
||||
// unit tests for proper sequencing, don't depend on it anywhere else.
|
||||
// TODO(apenwarr): eliminate 'state', as it's now obsolete.
|
||||
type State int
|
||||
|
||||
const (
|
||||
StateNew = State(iota)
|
||||
StateNotAuthenticated
|
||||
StateAuthenticating
|
||||
StateURLVisitRequired
|
||||
StateAuthenticated
|
||||
StateSynchronized // connected and received map update
|
||||
)
|
||||
|
||||
func (s State) MarshalText() ([]byte, error) {
|
||||
return []byte(s.String()), nil
|
||||
}
|
||||
|
||||
func (s State) String() string {
|
||||
switch s {
|
||||
case StateNew:
|
||||
return "state:new"
|
||||
case StateNotAuthenticated:
|
||||
return "state:not-authenticated"
|
||||
case StateAuthenticating:
|
||||
return "state:authenticating"
|
||||
case StateURLVisitRequired:
|
||||
return "state:url-visit-required"
|
||||
case StateAuthenticated:
|
||||
return "state:authenticated"
|
||||
case StateSynchronized:
|
||||
return "state:synchronized"
|
||||
default:
|
||||
return fmt.Sprintf("state:unknown:%d", int(s))
|
||||
}
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
_ structs.Incomparable
|
||||
LoginFinished *empty.Message
|
||||
Err string
|
||||
URL string
|
||||
Persist *persist.Persist // locally persisted configuration
|
||||
NetMap *netmap.NetworkMap // server-pushed configuration
|
||||
Hostinfo *tailcfg.Hostinfo // current Hostinfo data
|
||||
State State
|
||||
}
|
||||
|
||||
// Equal reports whether s and s2 are equal.
|
||||
func (s *Status) Equal(s2 *Status) bool {
|
||||
if s == nil && s2 == nil {
|
||||
return true
|
||||
}
|
||||
return s != nil && s2 != nil &&
|
||||
(s.LoginFinished == nil) == (s2.LoginFinished == nil) &&
|
||||
s.Err == s2.Err &&
|
||||
s.URL == s2.URL &&
|
||||
reflect.DeepEqual(s.Persist, s2.Persist) &&
|
||||
reflect.DeepEqual(s.NetMap, s2.NetMap) &&
|
||||
reflect.DeepEqual(s.Hostinfo, s2.Hostinfo) &&
|
||||
s.State == s2.State
|
||||
}
|
||||
|
||||
func (s Status) String() string {
|
||||
b, err := json.MarshalIndent(s, "", "\t")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s.State.String() + " " + string(b)
|
||||
}
|
||||
|
||||
type LoginGoal struct {
|
||||
_ structs.Incomparable
|
||||
wantLoggedIn bool // true if we *want* to be logged in
|
||||
@@ -40,9 +118,8 @@ func (g *LoginGoal) sendLogoutError(err error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Auto connects to a tailcontrol server for a node.
|
||||
// It's a concrete implementation of the Client interface.
|
||||
type Auto struct {
|
||||
// Client connects to a tailcontrol server for a node.
|
||||
type Client struct {
|
||||
direct *Direct // our interface to the server APIs
|
||||
timeNow func() time.Time
|
||||
logf logger.Logf
|
||||
@@ -75,8 +152,8 @@ type Auto struct {
|
||||
mapDone chan struct{} // when closed, map goroutine is done
|
||||
}
|
||||
|
||||
// New creates and starts a new Auto.
|
||||
func New(opts Options) (*Auto, error) {
|
||||
// New creates and starts a new Client.
|
||||
func New(opts Options) (*Client, error) {
|
||||
c, err := NewNoStart(opts)
|
||||
if c != nil {
|
||||
c.Start()
|
||||
@@ -84,8 +161,8 @@ func New(opts Options) (*Auto, error) {
|
||||
return c, err
|
||||
}
|
||||
|
||||
// NewNoStart creates a new Auto, but without calling Start on it.
|
||||
func NewNoStart(opts Options) (*Auto, error) {
|
||||
// NewNoStart creates a new Client, but without calling Start on it.
|
||||
func NewNoStart(opts Options) (*Client, error) {
|
||||
direct, err := NewDirect(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -96,7 +173,7 @@ func NewNoStart(opts Options) (*Auto, error) {
|
||||
if opts.TimeNow == nil {
|
||||
opts.TimeNow = time.Now
|
||||
}
|
||||
c := &Auto{
|
||||
c := &Client{
|
||||
direct: direct,
|
||||
timeNow: opts.TimeNow,
|
||||
logf: opts.Logf,
|
||||
@@ -112,7 +189,7 @@ func NewNoStart(opts Options) (*Auto, error) {
|
||||
|
||||
}
|
||||
|
||||
func (c *Auto) onHealthChange(sys health.Subsystem, err error) {
|
||||
func (c *Client) onHealthChange(sys health.Subsystem, err error) {
|
||||
if sys == health.SysOverall {
|
||||
return
|
||||
}
|
||||
@@ -123,13 +200,12 @@ func (c *Auto) onHealthChange(sys health.Subsystem, err error) {
|
||||
// SetPaused controls whether HTTP activity should be paused.
|
||||
//
|
||||
// The client can be paused and unpaused repeatedly, unlike Start and Shutdown, which can only be used once.
|
||||
func (c *Auto) SetPaused(paused bool) {
|
||||
func (c *Client) SetPaused(paused bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if paused == c.paused {
|
||||
return
|
||||
}
|
||||
c.logf("setPaused(%v)", paused)
|
||||
c.paused = paused
|
||||
if paused {
|
||||
// Only cancel the map routine. (The auth routine isn't expensive
|
||||
@@ -146,7 +222,7 @@ func (c *Auto) SetPaused(paused bool) {
|
||||
// Start starts the client's goroutines.
|
||||
//
|
||||
// It should only be called for clients created by NewNoStart.
|
||||
func (c *Auto) Start() {
|
||||
func (c *Client) Start() {
|
||||
go c.authRoutine()
|
||||
go c.mapRoutine()
|
||||
}
|
||||
@@ -156,7 +232,7 @@ func (c *Auto) Start() {
|
||||
// streaming response open), or start a new streaming one if necessary.
|
||||
//
|
||||
// It should be called whenever there's something new to tell the server.
|
||||
func (c *Auto) sendNewMapRequest() {
|
||||
func (c *Client) sendNewMapRequest() {
|
||||
c.mu.Lock()
|
||||
|
||||
// If we're not already streaming a netmap, or if we're already stuck
|
||||
@@ -195,7 +271,7 @@ func (c *Auto) sendNewMapRequest() {
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *Auto) cancelAuth() {
|
||||
func (c *Client) cancelAuth() {
|
||||
c.mu.Lock()
|
||||
if c.authCancel != nil {
|
||||
c.authCancel()
|
||||
@@ -206,7 +282,7 @@ func (c *Auto) cancelAuth() {
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Auto) cancelMapLocked() {
|
||||
func (c *Client) cancelMapLocked() {
|
||||
if c.mapCancel != nil {
|
||||
c.mapCancel()
|
||||
}
|
||||
@@ -215,13 +291,13 @@ func (c *Auto) cancelMapLocked() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Auto) cancelMapUnsafely() {
|
||||
func (c *Client) cancelMapUnsafely() {
|
||||
c.mu.Lock()
|
||||
c.cancelMapLocked()
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Auto) cancelMapSafely() {
|
||||
func (c *Client) cancelMapSafely() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -257,7 +333,7 @@ func (c *Auto) cancelMapSafely() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Auto) authRoutine() {
|
||||
func (c *Client) authRoutine() {
|
||||
defer close(c.authDone)
|
||||
bo := backoff.NewBackoff("authRoutine", c.logf, 30*time.Second)
|
||||
|
||||
@@ -268,7 +344,7 @@ func (c *Auto) authRoutine() {
|
||||
if goal != nil {
|
||||
c.logf("authRoutine: %s; wantLoggedIn=%v", c.state, goal.wantLoggedIn)
|
||||
} else {
|
||||
c.logf("authRoutine: %s; goal=nil paused=%v", c.state, c.paused)
|
||||
c.logf("authRoutine: %s; goal=nil", c.state)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
@@ -376,7 +452,7 @@ func (c *Auto) authRoutine() {
|
||||
|
||||
// Expiry returns the credential expiration time, or the zero time if
|
||||
// the expiration time isn't known. Used in tests only.
|
||||
func (c *Auto) Expiry() *time.Time {
|
||||
func (c *Client) Expiry() *time.Time {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.expiry
|
||||
@@ -384,21 +460,21 @@ func (c *Auto) Expiry() *time.Time {
|
||||
|
||||
// Direct returns the underlying direct client object. Used in tests
|
||||
// only.
|
||||
func (c *Auto) Direct() *Direct {
|
||||
func (c *Client) Direct() *Direct {
|
||||
return c.direct
|
||||
}
|
||||
|
||||
// unpausedChanLocked returns a new channel that is closed when the
|
||||
// current Auto pause is unpaused.
|
||||
// current Client pause is unpaused.
|
||||
//
|
||||
// c.mu must be held
|
||||
func (c *Auto) unpausedChanLocked() <-chan struct{} {
|
||||
func (c *Client) unpausedChanLocked() <-chan struct{} {
|
||||
unpaused := make(chan struct{})
|
||||
c.unpauseWaiters = append(c.unpauseWaiters, unpaused)
|
||||
return unpaused
|
||||
}
|
||||
|
||||
func (c *Auto) mapRoutine() {
|
||||
func (c *Client) mapRoutine() {
|
||||
defer close(c.mapDone)
|
||||
bo := backoff.NewBackoff("mapRoutine", c.logf, 30*time.Second)
|
||||
|
||||
@@ -520,10 +596,7 @@ func (c *Auto) mapRoutine() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Auto) AuthCantContinue() bool {
|
||||
if c == nil {
|
||||
return true
|
||||
}
|
||||
func (c *Client) AuthCantContinue() bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -531,13 +604,13 @@ func (c *Auto) AuthCantContinue() bool {
|
||||
}
|
||||
|
||||
// SetStatusFunc sets fn as the callback to run on any status change.
|
||||
func (c *Auto) SetStatusFunc(fn func(Status)) {
|
||||
func (c *Client) SetStatusFunc(fn func(Status)) {
|
||||
c.mu.Lock()
|
||||
c.statusFunc = fn
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Auto) SetHostinfo(hi *tailcfg.Hostinfo) {
|
||||
func (c *Client) SetHostinfo(hi *tailcfg.Hostinfo) {
|
||||
if hi == nil {
|
||||
panic("nil Hostinfo")
|
||||
}
|
||||
@@ -550,7 +623,7 @@ func (c *Auto) SetHostinfo(hi *tailcfg.Hostinfo) {
|
||||
c.sendNewMapRequest()
|
||||
}
|
||||
|
||||
func (c *Auto) SetNetInfo(ni *tailcfg.NetInfo) {
|
||||
func (c *Client) SetNetInfo(ni *tailcfg.NetInfo) {
|
||||
if ni == nil {
|
||||
panic("nil NetInfo")
|
||||
}
|
||||
@@ -563,7 +636,7 @@ func (c *Auto) SetNetInfo(ni *tailcfg.NetInfo) {
|
||||
c.sendNewMapRequest()
|
||||
}
|
||||
|
||||
func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkMap) {
|
||||
func (c *Client) sendStatus(who string, err error, url string, nm *netmap.NetworkMap) {
|
||||
c.mu.Lock()
|
||||
state := c.state
|
||||
loggedIn := c.loggedIn
|
||||
@@ -608,7 +681,7 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
func (c *Auto) Login(t *tailcfg.Oauth2Token, flags LoginFlags) {
|
||||
func (c *Client) Login(t *tailcfg.Oauth2Token, flags LoginFlags) {
|
||||
c.logf("client.Login(%v, %v)", t != nil, flags)
|
||||
|
||||
c.mu.Lock()
|
||||
@@ -622,7 +695,7 @@ func (c *Auto) Login(t *tailcfg.Oauth2Token, flags LoginFlags) {
|
||||
c.cancelAuth()
|
||||
}
|
||||
|
||||
func (c *Auto) StartLogout() {
|
||||
func (c *Client) StartLogout() {
|
||||
c.logf("client.StartLogout()")
|
||||
|
||||
c.mu.Lock()
|
||||
@@ -633,7 +706,7 @@ func (c *Auto) StartLogout() {
|
||||
c.cancelAuth()
|
||||
}
|
||||
|
||||
func (c *Auto) Logout(ctx context.Context) error {
|
||||
func (c *Client) Logout(ctx context.Context) error {
|
||||
c.logf("client.Logout()")
|
||||
|
||||
errc := make(chan error, 1)
|
||||
@@ -665,14 +738,14 @@ func (c *Auto) Logout(ctx context.Context) error {
|
||||
//
|
||||
// The localPort field is unused except for integration tests in
|
||||
// another repo.
|
||||
func (c *Auto) UpdateEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) {
|
||||
func (c *Client) UpdateEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) {
|
||||
changed := c.direct.SetEndpoints(localPort, endpoints)
|
||||
if changed {
|
||||
c.sendNewMapRequest()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Auto) Shutdown() {
|
||||
func (c *Client) Shutdown() {
|
||||
c.logf("client.Shutdown()")
|
||||
|
||||
c.mu.Lock()
|
||||
@@ -698,17 +771,17 @@ func (c *Auto) Shutdown() {
|
||||
|
||||
// NodePublicKey returns the node public key currently in use. This is
|
||||
// used exclusively in tests.
|
||||
func (c *Auto) TestOnlyNodePublicKey() wgkey.Key {
|
||||
func (c *Client) TestOnlyNodePublicKey() wgkey.Key {
|
||||
priv := c.direct.GetPersist()
|
||||
return priv.PrivateNodeKey.Public()
|
||||
}
|
||||
|
||||
func (c *Auto) TestOnlySetAuthKey(authkey string) {
|
||||
func (c *Client) TestOnlySetAuthKey(authkey string) {
|
||||
c.direct.mu.Lock()
|
||||
defer c.direct.mu.Unlock()
|
||||
c.direct.authKey = authkey
|
||||
}
|
||||
|
||||
func (c *Auto) TestOnlyTimeNow() time.Time {
|
||||
func (c *Client) TestOnlyTimeNow() time.Time {
|
||||
return c.timeNow()
|
||||
}
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package controlclient implements the client for the Tailscale
|
||||
// control plane.
|
||||
//
|
||||
// It handles authentication, port picking, and collects the local
|
||||
// network configuration.
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
type LoginFlags int
|
||||
|
||||
const (
|
||||
LoginDefault = LoginFlags(0)
|
||||
LoginInteractive = LoginFlags(1 << iota) // force user login and key refresh
|
||||
)
|
||||
|
||||
// Client represents a client connection to the control server.
|
||||
// Currently this is done through a pair of polling https requests in
|
||||
// the Auto client, but that might change eventually.
|
||||
type Client interface {
|
||||
// SetStatusFunc provides a callback to call when control sends us
|
||||
// a message.
|
||||
SetStatusFunc(func(Status))
|
||||
// Shutdown closes this session, which should not be used any further
|
||||
// afterwards.
|
||||
Shutdown()
|
||||
// Login begins an interactive or non-interactive login process.
|
||||
// Client will eventually call the Status callback with either a
|
||||
// LoginFinished flag (on success) or an auth URL (if further
|
||||
// interaction is needed).
|
||||
Login(*tailcfg.Oauth2Token, LoginFlags)
|
||||
// StartLogout starts an asynchronous logout process.
|
||||
// When it finishes, the Status callback will be called while
|
||||
// AuthCantContinue()==true.
|
||||
StartLogout()
|
||||
// Logout starts a synchronous logout process. It doesn't return
|
||||
// until the logout operation has been completed.
|
||||
Logout(context.Context) error
|
||||
// SetPaused pauses or unpauses the controlclient activity as much
|
||||
// as possible, without losing its internal state, to minimize
|
||||
// unnecessary network activity.
|
||||
// TODO: It might be better to simply shutdown the controlclient and
|
||||
// make a new one when it's time to unpause.
|
||||
SetPaused(bool)
|
||||
// AuthCantContinue returns whether authentication is blocked. If it
|
||||
// is, you either need to visit the auth URL (previously sent in a
|
||||
// Status callback) or call the Login function appropriately.
|
||||
// TODO: this probably belongs in the Status itself instead.
|
||||
AuthCantContinue() bool
|
||||
// SetHostinfo changes the Hostinfo structure that will be sent in
|
||||
// subsequent node registration requests.
|
||||
// TODO: a server-side change would let us simply upload this
|
||||
// in a separate http request. It has nothing to do with the rest of
|
||||
// the state machine.
|
||||
SetHostinfo(*tailcfg.Hostinfo)
|
||||
// SetNetinfo changes the NetIinfo structure that will be sent in
|
||||
// subsequent node registration requests.
|
||||
// TODO: a server-side change would let us simply upload this
|
||||
// in a separate http request. It has nothing to do with the rest of
|
||||
// the state machine.
|
||||
SetNetInfo(*tailcfg.NetInfo)
|
||||
// UpdateEndpoints changes the Endpoint structure that will be sent
|
||||
// in subsequent node registration requests.
|
||||
// TODO: localPort seems to be obsolete, remove it.
|
||||
// TODO: a server-side change would let us simply upload this
|
||||
// in a separate http request. It has nothing to do with the rest of
|
||||
// the state machine.
|
||||
UpdateEndpoints(localPort uint16, endpoints []tailcfg.Endpoint)
|
||||
}
|
||||
@@ -22,7 +22,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
|
||||
|
||||
func TestStatusEqual(t *testing.T) {
|
||||
// Verify that the Equal method stays in sync with reality
|
||||
equalHandles := []string{"LoginFinished", "Err", "URL", "NetMap", "State", "Persist", "Hostinfo"}
|
||||
equalHandles := []string{"LoginFinished", "Err", "URL", "Persist", "NetMap", "Hostinfo", "State"}
|
||||
if have := fieldsOf(reflect.TypeOf(Status{})); !reflect.DeepEqual(have, equalHandles) {
|
||||
t.Errorf("Status.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||
have, equalHandles)
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -48,6 +49,7 @@ import (
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
@@ -251,6 +253,13 @@ func (c *Direct) GetPersist() persist.Persist {
|
||||
return c.persist
|
||||
}
|
||||
|
||||
type LoginFlags int
|
||||
|
||||
const (
|
||||
LoginDefault = LoginFlags(0)
|
||||
LoginInteractive = LoginFlags(1 << iota) // force user login and key refresh
|
||||
)
|
||||
|
||||
func (c *Direct) TryLogout(ctx context.Context) error {
|
||||
c.logf("direct.TryLogout()")
|
||||
|
||||
@@ -406,7 +415,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
|
||||
// Don't log the common error types. Signatures are not usually enabled,
|
||||
// so these are expected.
|
||||
if !errors.Is(err, errCertificateNotConfigured) && !errors.Is(err, errNoCertStore) {
|
||||
if err != errCertificateNotConfigured && err != errNoCertStore {
|
||||
c.logf("RegisterReq sign error: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -460,10 +469,10 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
request.NodeKey.ShortString())
|
||||
return true, "", nil
|
||||
}
|
||||
if resp.Login.Provider != "" {
|
||||
if persist.Provider == "" {
|
||||
persist.Provider = resp.Login.Provider
|
||||
}
|
||||
if resp.Login.LoginName != "" {
|
||||
if persist.LoginName == "" {
|
||||
persist.LoginName = resp.Login.LoginName
|
||||
}
|
||||
|
||||
@@ -563,11 +572,6 @@ func (c *Direct) SendLiteMapUpdate(ctx context.Context) error {
|
||||
return c.sendMapRequest(ctx, 1, nil)
|
||||
}
|
||||
|
||||
// If we go more than pollTimeout without hearing from the server,
|
||||
// end the long poll. We should be receiving a keep alive ping
|
||||
// every minute.
|
||||
const pollTimeout = 120 * time.Second
|
||||
|
||||
// cb nil means to omit peers.
|
||||
func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netmap.NetworkMap)) error {
|
||||
c.mu.Lock()
|
||||
@@ -692,6 +696,10 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
return nil
|
||||
}
|
||||
|
||||
// If we go more than pollTimeout without hearing from the server,
|
||||
// end the long poll. We should be receiving a keep alive ping
|
||||
// every minute.
|
||||
const pollTimeout = 120 * time.Second
|
||||
timeout := time.NewTimer(pollTimeout)
|
||||
timeoutReset := make(chan struct{})
|
||||
pollDone := make(chan struct{})
|
||||
@@ -721,11 +729,11 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
}
|
||||
}()
|
||||
|
||||
sess := newMapSession(persist.PrivateNodeKey)
|
||||
sess.logf = c.logf
|
||||
sess.vlogf = vlogf
|
||||
sess.machinePubKey = machinePubKey
|
||||
sess.keepSharerAndUserSplit = c.keepSharerAndUserSplit
|
||||
var lastDNSConfig = new(tailcfg.DNSConfig)
|
||||
var lastDERPMap *tailcfg.DERPMap
|
||||
var lastUserProfile = map[tailcfg.UserID]tailcfg.UserProfile{}
|
||||
var lastParsedPacketFilter []filter.Match
|
||||
var collectServices bool
|
||||
|
||||
// If allowStream, then the server will use an HTTP long poll to
|
||||
// return incremental results. There is always one response right
|
||||
@@ -734,6 +742,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
// the same format before just closing the connection.
|
||||
// We can use this same read loop either way.
|
||||
var msg []byte
|
||||
var previousPeers []*tailcfg.Node // for delta-purposes
|
||||
for i := 0; i < maxPolls || maxPolls < 0; i++ {
|
||||
vlogf("netmap: starting size read after %v (poll %v)", time.Since(t0).Round(time.Millisecond), i)
|
||||
var siz [4]byte
|
||||
@@ -780,6 +789,16 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
continue
|
||||
}
|
||||
|
||||
undeltaPeers(&resp, previousPeers)
|
||||
previousPeers = cloneNodes(resp.Peers) // defensive/lazy clone, since this escapes to who knows where
|
||||
for _, up := range resp.UserProfiles {
|
||||
lastUserProfile[up.ID] = up
|
||||
}
|
||||
|
||||
if resp.DERPMap != nil {
|
||||
vlogf("netmap: new map contains DERP map")
|
||||
lastDERPMap = resp.DERPMap
|
||||
}
|
||||
if resp.Debug != nil {
|
||||
if resp.Debug.LogHeapPprof {
|
||||
go logheap.LogHeap(resp.Debug.LogHeapURL)
|
||||
@@ -789,40 +808,18 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
}
|
||||
setControlAtomic(&controlUseDERPRoute, resp.Debug.DERPRoute)
|
||||
setControlAtomic(&controlTrimWGConfig, resp.Debug.TrimWGConfig)
|
||||
if sleep := time.Duration(resp.Debug.SleepSeconds * float64(time.Second)); sleep > 0 {
|
||||
if err := sleepAsRequested(ctx, c.logf, timeoutReset, sleep); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nm := sess.netmapForResponse(&resp)
|
||||
if nm.SelfNode == nil {
|
||||
c.logf("MapResponse lacked node")
|
||||
return errors.New("MapResponse lacked node")
|
||||
}
|
||||
|
||||
// Temporarily (2020-06-29) support removing all but
|
||||
// discovery-supporting nodes during development, for
|
||||
// less noise.
|
||||
if Debug.OnlyDisco {
|
||||
anyOld, numDisco := false, 0
|
||||
for _, p := range nm.Peers {
|
||||
if p.DiscoKey.IsZero() {
|
||||
anyOld = true
|
||||
} else {
|
||||
numDisco++
|
||||
filtered := resp.Peers[:0]
|
||||
for _, p := range resp.Peers {
|
||||
if !p.DiscoKey.IsZero() {
|
||||
filtered = append(filtered, p)
|
||||
}
|
||||
}
|
||||
if anyOld {
|
||||
filtered := make([]*tailcfg.Node, 0, numDisco)
|
||||
for _, p := range nm.Peers {
|
||||
if !p.DiscoKey.IsZero() {
|
||||
filtered = append(filtered, p)
|
||||
}
|
||||
}
|
||||
nm.Peers = filtered
|
||||
}
|
||||
resp.Peers = filtered
|
||||
}
|
||||
if Debug.StripEndpoints {
|
||||
for _, p := range resp.Peers {
|
||||
@@ -833,7 +830,18 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
}
|
||||
}
|
||||
if Debug.StripCaps {
|
||||
nm.SelfNode.Capabilities = nil
|
||||
resp.Node.Capabilities = nil
|
||||
}
|
||||
|
||||
if pf := resp.PacketFilter; pf != nil {
|
||||
lastParsedPacketFilter = c.parsePacketFilter(pf)
|
||||
}
|
||||
if c := resp.DNSConfig; c != nil {
|
||||
lastDNSConfig = c
|
||||
}
|
||||
|
||||
if v, ok := resp.CollectServices.Get(); ok {
|
||||
collectServices = v
|
||||
}
|
||||
|
||||
// Get latest localPort. This might've changed if
|
||||
@@ -841,9 +849,67 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
// the end-to-end test.
|
||||
// TODO(bradfitz): remove the NetworkMap.LocalPort field entirely.
|
||||
c.mu.Lock()
|
||||
nm.LocalPort = c.localPort
|
||||
localPort = c.localPort
|
||||
c.mu.Unlock()
|
||||
|
||||
nm := &netmap.NetworkMap{
|
||||
SelfNode: resp.Node,
|
||||
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
|
||||
PrivateKey: persist.PrivateNodeKey,
|
||||
MachineKey: machinePubKey,
|
||||
Expiry: resp.Node.KeyExpiry,
|
||||
Name: resp.Node.Name,
|
||||
Addresses: resp.Node.Addresses,
|
||||
Peers: resp.Peers,
|
||||
LocalPort: localPort,
|
||||
User: resp.Node.User,
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
|
||||
Domain: resp.Domain,
|
||||
DNS: *lastDNSConfig,
|
||||
Hostinfo: resp.Node.Hostinfo,
|
||||
PacketFilter: lastParsedPacketFilter,
|
||||
CollectServices: collectServices,
|
||||
DERPMap: lastDERPMap,
|
||||
Debug: resp.Debug,
|
||||
}
|
||||
addUserProfile := func(userID tailcfg.UserID) {
|
||||
if _, dup := nm.UserProfiles[userID]; dup {
|
||||
// Already populated it from a previous peer.
|
||||
return
|
||||
}
|
||||
if up, ok := lastUserProfile[userID]; ok {
|
||||
nm.UserProfiles[userID] = up
|
||||
}
|
||||
}
|
||||
addUserProfile(nm.User)
|
||||
magicDNSSuffix := nm.MagicDNSSuffix()
|
||||
nm.SelfNode.InitDisplayNames(magicDNSSuffix)
|
||||
for _, peer := range resp.Peers {
|
||||
peer.InitDisplayNames(magicDNSSuffix)
|
||||
if !peer.Sharer.IsZero() {
|
||||
if c.keepSharerAndUserSplit {
|
||||
addUserProfile(peer.Sharer)
|
||||
} else {
|
||||
peer.User = peer.Sharer
|
||||
}
|
||||
}
|
||||
addUserProfile(peer.User)
|
||||
}
|
||||
if resp.Node.MachineAuthorized {
|
||||
nm.MachineStatus = tailcfg.MachineAuthorized
|
||||
} else {
|
||||
nm.MachineStatus = tailcfg.MachineUnauthorized
|
||||
}
|
||||
if len(resp.DNS) > 0 {
|
||||
nm.DNS.Nameservers = resp.DNS
|
||||
}
|
||||
if len(resp.SearchPaths) > 0 {
|
||||
nm.DNS.Domains = resp.SearchPaths
|
||||
}
|
||||
if Debug.ProxyDNS {
|
||||
nm.DNS.Proxied = true
|
||||
}
|
||||
|
||||
// Printing the netmap can be extremely verbose, but is very
|
||||
// handy for debugging. Let's limit how often we do it.
|
||||
// Code elsewhere prints netmap diffs every time, so this
|
||||
@@ -1038,6 +1104,124 @@ func envBool(k string) bool {
|
||||
|
||||
var clockNow = time.Now
|
||||
|
||||
// undeltaPeers updates mapRes.Peers to be complete based on the
|
||||
// provided previous peer list and the PeersRemoved and PeersChanged
|
||||
// fields in mapRes, as well as the PeerSeenChange and OnlineChange
|
||||
// maps.
|
||||
//
|
||||
// It then also nils out the delta fields.
|
||||
func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
|
||||
if len(mapRes.Peers) > 0 {
|
||||
// Not delta encoded.
|
||||
if !nodesSorted(mapRes.Peers) {
|
||||
log.Printf("netmap: undeltaPeers: MapResponse.Peers not sorted; sorting")
|
||||
sortNodes(mapRes.Peers)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var removed map[tailcfg.NodeID]bool
|
||||
if pr := mapRes.PeersRemoved; len(pr) > 0 {
|
||||
removed = make(map[tailcfg.NodeID]bool, len(pr))
|
||||
for _, id := range pr {
|
||||
removed[id] = true
|
||||
}
|
||||
}
|
||||
changed := mapRes.PeersChanged
|
||||
|
||||
if !nodesSorted(changed) {
|
||||
log.Printf("netmap: undeltaPeers: MapResponse.PeersChanged not sorted; sorting")
|
||||
sortNodes(changed)
|
||||
}
|
||||
if !nodesSorted(prev) {
|
||||
// Internal error (unrelated to the network) if we get here.
|
||||
log.Printf("netmap: undeltaPeers: [unexpected] prev not sorted; sorting")
|
||||
sortNodes(prev)
|
||||
}
|
||||
|
||||
newFull := prev
|
||||
if len(removed) > 0 || len(changed) > 0 {
|
||||
newFull = make([]*tailcfg.Node, 0, len(prev)-len(removed))
|
||||
for len(prev) > 0 && len(changed) > 0 {
|
||||
pID := prev[0].ID
|
||||
cID := changed[0].ID
|
||||
if removed[pID] {
|
||||
prev = prev[1:]
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case pID < cID:
|
||||
newFull = append(newFull, prev[0])
|
||||
prev = prev[1:]
|
||||
case pID == cID:
|
||||
newFull = append(newFull, changed[0])
|
||||
prev, changed = prev[1:], changed[1:]
|
||||
case cID < pID:
|
||||
newFull = append(newFull, changed[0])
|
||||
changed = changed[1:]
|
||||
}
|
||||
}
|
||||
newFull = append(newFull, changed...)
|
||||
for _, n := range prev {
|
||||
if !removed[n.ID] {
|
||||
newFull = append(newFull, n)
|
||||
}
|
||||
}
|
||||
sortNodes(newFull)
|
||||
}
|
||||
|
||||
if len(mapRes.PeerSeenChange) != 0 || len(mapRes.OnlineChange) != 0 {
|
||||
peerByID := make(map[tailcfg.NodeID]*tailcfg.Node, len(newFull))
|
||||
for _, n := range newFull {
|
||||
peerByID[n.ID] = n
|
||||
}
|
||||
now := clockNow()
|
||||
for nodeID, seen := range mapRes.PeerSeenChange {
|
||||
if n, ok := peerByID[nodeID]; ok {
|
||||
if seen {
|
||||
n.LastSeen = &now
|
||||
} else {
|
||||
n.LastSeen = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
for nodeID, online := range mapRes.OnlineChange {
|
||||
if n, ok := peerByID[nodeID]; ok {
|
||||
online := online
|
||||
n.Online = &online
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mapRes.Peers = newFull
|
||||
mapRes.PeersChanged = nil
|
||||
mapRes.PeersRemoved = nil
|
||||
}
|
||||
|
||||
func nodesSorted(v []*tailcfg.Node) bool {
|
||||
for i, n := range v {
|
||||
if i > 0 && n.ID <= v[i-1].ID {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func sortNodes(v []*tailcfg.Node) {
|
||||
sort.Slice(v, func(i, j int) bool { return v[i].ID < v[j].ID })
|
||||
}
|
||||
|
||||
func cloneNodes(v1 []*tailcfg.Node) []*tailcfg.Node {
|
||||
if v1 == nil {
|
||||
return nil
|
||||
}
|
||||
v2 := make([]*tailcfg.Node, len(v1))
|
||||
for i, n := range v1 {
|
||||
v2[i] = n.Clone()
|
||||
}
|
||||
return v2
|
||||
}
|
||||
|
||||
// opt.Bool configs from control.
|
||||
var (
|
||||
controlUseDERPRoute atomic.Value
|
||||
@@ -1180,34 +1364,3 @@ func answerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest) {
|
||||
logf("answerPing complete to %v (after %v)", pr.URL, d)
|
||||
}
|
||||
}
|
||||
|
||||
func sleepAsRequested(ctx context.Context, logf logger.Logf, timeoutReset chan<- struct{}, d time.Duration) error {
|
||||
const maxSleep = 5 * time.Minute
|
||||
if d > maxSleep {
|
||||
logf("sleeping for %v, capped from server-requested %v ...", maxSleep, d)
|
||||
d = maxSleep
|
||||
} else {
|
||||
logf("sleeping for server-requested %v ...", d)
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(pollTimeout / 2)
|
||||
defer ticker.Stop()
|
||||
timer := time.NewTimer(d)
|
||||
defer timer.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-timer.C:
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
select {
|
||||
case timeoutReset <- struct{}{}:
|
||||
case <-timer.C:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,13 +6,169 @@ package controlclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func TestUndeltaPeers(t *testing.T) {
|
||||
defer func(old func() time.Time) { clockNow = old }(clockNow)
|
||||
|
||||
var curTime time.Time
|
||||
clockNow = func() time.Time {
|
||||
return curTime
|
||||
}
|
||||
online := func(v bool) func(*tailcfg.Node) {
|
||||
return func(n *tailcfg.Node) {
|
||||
n.Online = &v
|
||||
}
|
||||
}
|
||||
seenAt := func(t time.Time) func(*tailcfg.Node) {
|
||||
return func(n *tailcfg.Node) {
|
||||
n.LastSeen = &t
|
||||
}
|
||||
}
|
||||
n := func(id tailcfg.NodeID, name string, mod ...func(*tailcfg.Node)) *tailcfg.Node {
|
||||
n := &tailcfg.Node{ID: id, Name: name}
|
||||
for _, f := range mod {
|
||||
f(n)
|
||||
}
|
||||
return n
|
||||
}
|
||||
peers := func(nv ...*tailcfg.Node) []*tailcfg.Node { return nv }
|
||||
tests := []struct {
|
||||
name string
|
||||
mapRes *tailcfg.MapResponse
|
||||
curTime time.Time
|
||||
prev []*tailcfg.Node
|
||||
want []*tailcfg.Node
|
||||
}{
|
||||
{
|
||||
name: "full_peers",
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
Peers: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
want: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
{
|
||||
name: "full_peers_ignores_deltas",
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
Peers: peers(n(1, "foo"), n(2, "bar")),
|
||||
PeersRemoved: []tailcfg.NodeID{2},
|
||||
},
|
||||
want: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
{
|
||||
name: "add_and_update",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChanged: peers(n(0, "zero"), n(2, "bar2"), n(3, "three")),
|
||||
},
|
||||
want: peers(n(0, "zero"), n(1, "foo"), n(2, "bar2"), n(3, "three")),
|
||||
},
|
||||
{
|
||||
name: "remove",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersRemoved: []tailcfg.NodeID{1},
|
||||
},
|
||||
want: peers(n(2, "bar")),
|
||||
},
|
||||
{
|
||||
name: "add_and_remove",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChanged: peers(n(1, "foo2")),
|
||||
PeersRemoved: []tailcfg.NodeID{2},
|
||||
},
|
||||
want: peers(n(1, "foo2")),
|
||||
},
|
||||
{
|
||||
name: "unchanged",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{},
|
||||
want: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
{
|
||||
name: "online_change",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
OnlineChange: map[tailcfg.NodeID]bool{
|
||||
1: true,
|
||||
},
|
||||
},
|
||||
want: peers(
|
||||
n(1, "foo", online(true)),
|
||||
n(2, "bar"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "online_change_offline",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
OnlineChange: map[tailcfg.NodeID]bool{
|
||||
1: false,
|
||||
2: true,
|
||||
},
|
||||
},
|
||||
want: peers(
|
||||
n(1, "foo", online(false)),
|
||||
n(2, "bar", online(true)),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "peer_seen_at",
|
||||
prev: peers(n(1, "foo", seenAt(time.Unix(111, 0))), n(2, "bar")),
|
||||
curTime: time.Unix(123, 0),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeerSeenChange: map[tailcfg.NodeID]bool{
|
||||
1: false,
|
||||
2: true,
|
||||
},
|
||||
},
|
||||
want: peers(
|
||||
n(1, "foo"),
|
||||
n(2, "bar", seenAt(time.Unix(123, 0))),
|
||||
),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if !tt.curTime.IsZero() {
|
||||
curTime = tt.curTime
|
||||
}
|
||||
undeltaPeers(tt.mapRes, tt.prev)
|
||||
if !reflect.DeepEqual(tt.mapRes.Peers, tt.want) {
|
||||
t.Errorf("wrong results\n got: %s\nwant: %s", formatNodes(tt.mapRes.Peers), formatNodes(tt.want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func formatNodes(nodes []*tailcfg.Node) string {
|
||||
var sb strings.Builder
|
||||
for i, n := range nodes {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
var extra string
|
||||
if n.Online != nil {
|
||||
extra += fmt.Sprintf(", online=%v", *n.Online)
|
||||
}
|
||||
if n.LastSeen != nil {
|
||||
extra += fmt.Sprintf(", lastSeen=%v", n.LastSeen.Unix())
|
||||
}
|
||||
fmt.Fprintf(&sb, "(%d, %q%s)", n.ID, n.Name, extra)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func TestNewDirect(t *testing.T) {
|
||||
hi := NewHostinfo()
|
||||
ni := tailcfg.NetInfo{LinkType: "wired"}
|
||||
|
||||
20
control/controlclient/filter.go
Normal file
20
control/controlclient/filter.go
Normal file
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
// Parse a backward-compatible FilterRule used by control's wire
|
||||
// format, producing the most current filter format.
|
||||
func (c *Direct) parsePacketFilter(pf []tailcfg.FilterRule) []filter.Match {
|
||||
mm, err := filter.MatchesFromFilterRules(pf)
|
||||
if err != nil {
|
||||
c.logf("parsePacketFilter: %s\n", err)
|
||||
}
|
||||
return mm
|
||||
}
|
||||
@@ -1,282 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sort"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
// mapSession holds the state over a long-polled "map" request to the
|
||||
// control plane.
|
||||
//
|
||||
// It accepts incremental tailcfg.MapResponse values to
|
||||
// netMapForResponse and returns fully inflated NetworkMaps, filling
|
||||
// in the omitted data implicit from prior MapResponse values from
|
||||
// within the same session (the same long-poll HTTP response to the
|
||||
// one MapRequest).
|
||||
type mapSession struct {
|
||||
// Immutable fields.
|
||||
privateNodeKey wgkey.Private
|
||||
logf logger.Logf
|
||||
vlogf logger.Logf
|
||||
machinePubKey tailcfg.MachineKey
|
||||
keepSharerAndUserSplit bool // see Options.KeepSharerAndUserSplit
|
||||
|
||||
// Fields storing state over the the coards of multiple MapResponses.
|
||||
lastNode *tailcfg.Node
|
||||
lastDNSConfig *tailcfg.DNSConfig
|
||||
lastDERPMap *tailcfg.DERPMap
|
||||
lastUserProfile map[tailcfg.UserID]tailcfg.UserProfile
|
||||
lastParsedPacketFilter []filter.Match
|
||||
collectServices bool
|
||||
previousPeers []*tailcfg.Node // for delta-purposes
|
||||
lastDomain string
|
||||
|
||||
// netMapBuilding is non-nil during a netmapForResponse call,
|
||||
// containing the value to be returned, once fully populated.
|
||||
netMapBuilding *netmap.NetworkMap
|
||||
}
|
||||
|
||||
func newMapSession(privateNodeKey wgkey.Private) *mapSession {
|
||||
ms := &mapSession{
|
||||
privateNodeKey: privateNodeKey,
|
||||
logf: logger.Discard,
|
||||
vlogf: logger.Discard,
|
||||
lastDNSConfig: new(tailcfg.DNSConfig),
|
||||
lastUserProfile: map[tailcfg.UserID]tailcfg.UserProfile{},
|
||||
}
|
||||
return ms
|
||||
}
|
||||
|
||||
func (ms *mapSession) addUserProfile(userID tailcfg.UserID) {
|
||||
nm := ms.netMapBuilding
|
||||
if _, dup := nm.UserProfiles[userID]; dup {
|
||||
// Already populated it from a previous peer.
|
||||
return
|
||||
}
|
||||
if up, ok := ms.lastUserProfile[userID]; ok {
|
||||
nm.UserProfiles[userID] = up
|
||||
}
|
||||
}
|
||||
|
||||
// netmapForResponse returns a fully populated NetworkMap from a full
|
||||
// or incremental MapResponse within the session, filling in omitted
|
||||
// information from prior MapResponse values.
|
||||
func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.NetworkMap {
|
||||
undeltaPeers(resp, ms.previousPeers)
|
||||
|
||||
ms.previousPeers = cloneNodes(resp.Peers) // defensive/lazy clone, since this escapes to who knows where
|
||||
for _, up := range resp.UserProfiles {
|
||||
ms.lastUserProfile[up.ID] = up
|
||||
}
|
||||
|
||||
if resp.DERPMap != nil {
|
||||
ms.vlogf("netmap: new map contains DERP map")
|
||||
ms.lastDERPMap = resp.DERPMap
|
||||
}
|
||||
|
||||
if pf := resp.PacketFilter; pf != nil {
|
||||
var err error
|
||||
ms.lastParsedPacketFilter, err = filter.MatchesFromFilterRules(pf)
|
||||
if err != nil {
|
||||
ms.logf("parsePacketFilter: %v", err)
|
||||
}
|
||||
}
|
||||
if c := resp.DNSConfig; c != nil {
|
||||
ms.lastDNSConfig = c
|
||||
}
|
||||
|
||||
if v, ok := resp.CollectServices.Get(); ok {
|
||||
ms.collectServices = v
|
||||
}
|
||||
if resp.Domain != "" {
|
||||
ms.lastDomain = resp.Domain
|
||||
}
|
||||
|
||||
nm := &netmap.NetworkMap{
|
||||
NodeKey: tailcfg.NodeKey(ms.privateNodeKey.Public()),
|
||||
PrivateKey: ms.privateNodeKey,
|
||||
MachineKey: ms.machinePubKey,
|
||||
Peers: resp.Peers,
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
|
||||
Domain: ms.lastDomain,
|
||||
DNS: *ms.lastDNSConfig,
|
||||
PacketFilter: ms.lastParsedPacketFilter,
|
||||
CollectServices: ms.collectServices,
|
||||
DERPMap: ms.lastDERPMap,
|
||||
Debug: resp.Debug,
|
||||
}
|
||||
ms.netMapBuilding = nm
|
||||
|
||||
if resp.Node != nil {
|
||||
ms.lastNode = resp.Node
|
||||
}
|
||||
if node := ms.lastNode.Clone(); node != nil {
|
||||
nm.SelfNode = node
|
||||
nm.Expiry = node.KeyExpiry
|
||||
nm.Name = node.Name
|
||||
nm.Addresses = node.Addresses
|
||||
nm.User = node.User
|
||||
nm.Hostinfo = node.Hostinfo
|
||||
if node.MachineAuthorized {
|
||||
nm.MachineStatus = tailcfg.MachineAuthorized
|
||||
} else {
|
||||
nm.MachineStatus = tailcfg.MachineUnauthorized
|
||||
}
|
||||
}
|
||||
|
||||
ms.addUserProfile(nm.User)
|
||||
magicDNSSuffix := nm.MagicDNSSuffix()
|
||||
if nm.SelfNode != nil {
|
||||
nm.SelfNode.InitDisplayNames(magicDNSSuffix)
|
||||
}
|
||||
for _, peer := range resp.Peers {
|
||||
peer.InitDisplayNames(magicDNSSuffix)
|
||||
if !peer.Sharer.IsZero() {
|
||||
if ms.keepSharerAndUserSplit {
|
||||
ms.addUserProfile(peer.Sharer)
|
||||
} else {
|
||||
peer.User = peer.Sharer
|
||||
}
|
||||
}
|
||||
ms.addUserProfile(peer.User)
|
||||
}
|
||||
if len(resp.DNS) > 0 {
|
||||
nm.DNS.Nameservers = resp.DNS
|
||||
}
|
||||
if len(resp.SearchPaths) > 0 {
|
||||
nm.DNS.Domains = resp.SearchPaths
|
||||
}
|
||||
if Debug.ProxyDNS {
|
||||
nm.DNS.Proxied = true
|
||||
}
|
||||
ms.netMapBuilding = nil
|
||||
return nm
|
||||
}
|
||||
|
||||
// undeltaPeers updates mapRes.Peers to be complete based on the
|
||||
// provided previous peer list and the PeersRemoved and PeersChanged
|
||||
// fields in mapRes, as well as the PeerSeenChange and OnlineChange
|
||||
// maps.
|
||||
//
|
||||
// It then also nils out the delta fields.
|
||||
func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
|
||||
if len(mapRes.Peers) > 0 {
|
||||
// Not delta encoded.
|
||||
if !nodesSorted(mapRes.Peers) {
|
||||
log.Printf("netmap: undeltaPeers: MapResponse.Peers not sorted; sorting")
|
||||
sortNodes(mapRes.Peers)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var removed map[tailcfg.NodeID]bool
|
||||
if pr := mapRes.PeersRemoved; len(pr) > 0 {
|
||||
removed = make(map[tailcfg.NodeID]bool, len(pr))
|
||||
for _, id := range pr {
|
||||
removed[id] = true
|
||||
}
|
||||
}
|
||||
changed := mapRes.PeersChanged
|
||||
|
||||
if !nodesSorted(changed) {
|
||||
log.Printf("netmap: undeltaPeers: MapResponse.PeersChanged not sorted; sorting")
|
||||
sortNodes(changed)
|
||||
}
|
||||
if !nodesSorted(prev) {
|
||||
// Internal error (unrelated to the network) if we get here.
|
||||
log.Printf("netmap: undeltaPeers: [unexpected] prev not sorted; sorting")
|
||||
sortNodes(prev)
|
||||
}
|
||||
|
||||
newFull := prev
|
||||
if len(removed) > 0 || len(changed) > 0 {
|
||||
newFull = make([]*tailcfg.Node, 0, len(prev)-len(removed))
|
||||
for len(prev) > 0 && len(changed) > 0 {
|
||||
pID := prev[0].ID
|
||||
cID := changed[0].ID
|
||||
if removed[pID] {
|
||||
prev = prev[1:]
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case pID < cID:
|
||||
newFull = append(newFull, prev[0])
|
||||
prev = prev[1:]
|
||||
case pID == cID:
|
||||
newFull = append(newFull, changed[0])
|
||||
prev, changed = prev[1:], changed[1:]
|
||||
case cID < pID:
|
||||
newFull = append(newFull, changed[0])
|
||||
changed = changed[1:]
|
||||
}
|
||||
}
|
||||
newFull = append(newFull, changed...)
|
||||
for _, n := range prev {
|
||||
if !removed[n.ID] {
|
||||
newFull = append(newFull, n)
|
||||
}
|
||||
}
|
||||
sortNodes(newFull)
|
||||
}
|
||||
|
||||
if len(mapRes.PeerSeenChange) != 0 || len(mapRes.OnlineChange) != 0 {
|
||||
peerByID := make(map[tailcfg.NodeID]*tailcfg.Node, len(newFull))
|
||||
for _, n := range newFull {
|
||||
peerByID[n.ID] = n
|
||||
}
|
||||
now := clockNow()
|
||||
for nodeID, seen := range mapRes.PeerSeenChange {
|
||||
if n, ok := peerByID[nodeID]; ok {
|
||||
if seen {
|
||||
n.LastSeen = &now
|
||||
} else {
|
||||
n.LastSeen = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
for nodeID, online := range mapRes.OnlineChange {
|
||||
if n, ok := peerByID[nodeID]; ok {
|
||||
online := online
|
||||
n.Online = &online
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mapRes.Peers = newFull
|
||||
mapRes.PeersChanged = nil
|
||||
mapRes.PeersRemoved = nil
|
||||
}
|
||||
|
||||
func nodesSorted(v []*tailcfg.Node) bool {
|
||||
for i, n := range v {
|
||||
if i > 0 && n.ID <= v[i-1].ID {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func sortNodes(v []*tailcfg.Node) {
|
||||
sort.Slice(v, func(i, j int) bool { return v[i].ID < v[j].ID })
|
||||
}
|
||||
|
||||
func cloneNodes(v1 []*tailcfg.Node) []*tailcfg.Node {
|
||||
if v1 == nil {
|
||||
return nil
|
||||
}
|
||||
v2 := make([]*tailcfg.Node, len(v1))
|
||||
for i, n := range v1 {
|
||||
v2[i] = n.Clone()
|
||||
}
|
||||
return v2
|
||||
}
|
||||
@@ -1,311 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func TestUndeltaPeers(t *testing.T) {
|
||||
defer func(old func() time.Time) { clockNow = old }(clockNow)
|
||||
|
||||
var curTime time.Time
|
||||
clockNow = func() time.Time {
|
||||
return curTime
|
||||
}
|
||||
online := func(v bool) func(*tailcfg.Node) {
|
||||
return func(n *tailcfg.Node) {
|
||||
n.Online = &v
|
||||
}
|
||||
}
|
||||
seenAt := func(t time.Time) func(*tailcfg.Node) {
|
||||
return func(n *tailcfg.Node) {
|
||||
n.LastSeen = &t
|
||||
}
|
||||
}
|
||||
n := func(id tailcfg.NodeID, name string, mod ...func(*tailcfg.Node)) *tailcfg.Node {
|
||||
n := &tailcfg.Node{ID: id, Name: name}
|
||||
for _, f := range mod {
|
||||
f(n)
|
||||
}
|
||||
return n
|
||||
}
|
||||
peers := func(nv ...*tailcfg.Node) []*tailcfg.Node { return nv }
|
||||
tests := []struct {
|
||||
name string
|
||||
mapRes *tailcfg.MapResponse
|
||||
curTime time.Time
|
||||
prev []*tailcfg.Node
|
||||
want []*tailcfg.Node
|
||||
}{
|
||||
{
|
||||
name: "full_peers",
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
Peers: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
want: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
{
|
||||
name: "full_peers_ignores_deltas",
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
Peers: peers(n(1, "foo"), n(2, "bar")),
|
||||
PeersRemoved: []tailcfg.NodeID{2},
|
||||
},
|
||||
want: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
{
|
||||
name: "add_and_update",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChanged: peers(n(0, "zero"), n(2, "bar2"), n(3, "three")),
|
||||
},
|
||||
want: peers(n(0, "zero"), n(1, "foo"), n(2, "bar2"), n(3, "three")),
|
||||
},
|
||||
{
|
||||
name: "remove",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersRemoved: []tailcfg.NodeID{1},
|
||||
},
|
||||
want: peers(n(2, "bar")),
|
||||
},
|
||||
{
|
||||
name: "add_and_remove",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeersChanged: peers(n(1, "foo2")),
|
||||
PeersRemoved: []tailcfg.NodeID{2},
|
||||
},
|
||||
want: peers(n(1, "foo2")),
|
||||
},
|
||||
{
|
||||
name: "unchanged",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{},
|
||||
want: peers(n(1, "foo"), n(2, "bar")),
|
||||
},
|
||||
{
|
||||
name: "online_change",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
OnlineChange: map[tailcfg.NodeID]bool{
|
||||
1: true,
|
||||
},
|
||||
},
|
||||
want: peers(
|
||||
n(1, "foo", online(true)),
|
||||
n(2, "bar"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "online_change_offline",
|
||||
prev: peers(n(1, "foo"), n(2, "bar")),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
OnlineChange: map[tailcfg.NodeID]bool{
|
||||
1: false,
|
||||
2: true,
|
||||
},
|
||||
},
|
||||
want: peers(
|
||||
n(1, "foo", online(false)),
|
||||
n(2, "bar", online(true)),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "peer_seen_at",
|
||||
prev: peers(n(1, "foo", seenAt(time.Unix(111, 0))), n(2, "bar")),
|
||||
curTime: time.Unix(123, 0),
|
||||
mapRes: &tailcfg.MapResponse{
|
||||
PeerSeenChange: map[tailcfg.NodeID]bool{
|
||||
1: false,
|
||||
2: true,
|
||||
},
|
||||
},
|
||||
want: peers(
|
||||
n(1, "foo"),
|
||||
n(2, "bar", seenAt(time.Unix(123, 0))),
|
||||
),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if !tt.curTime.IsZero() {
|
||||
curTime = tt.curTime
|
||||
}
|
||||
undeltaPeers(tt.mapRes, tt.prev)
|
||||
if !reflect.DeepEqual(tt.mapRes.Peers, tt.want) {
|
||||
t.Errorf("wrong results\n got: %s\nwant: %s", formatNodes(tt.mapRes.Peers), formatNodes(tt.want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func formatNodes(nodes []*tailcfg.Node) string {
|
||||
var sb strings.Builder
|
||||
for i, n := range nodes {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
var extra string
|
||||
if n.Online != nil {
|
||||
extra += fmt.Sprintf(", online=%v", *n.Online)
|
||||
}
|
||||
if n.LastSeen != nil {
|
||||
extra += fmt.Sprintf(", lastSeen=%v", n.LastSeen.Unix())
|
||||
}
|
||||
fmt.Fprintf(&sb, "(%d, %q%s)", n.ID, n.Name, extra)
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func newTestMapSession(t *testing.T) *mapSession {
|
||||
k, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return newMapSession(k)
|
||||
}
|
||||
|
||||
func TestNetmapForResponse(t *testing.T) {
|
||||
t.Run("implicit_packetfilter", func(t *testing.T) {
|
||||
somePacketFilter := []tailcfg.FilterRule{
|
||||
{
|
||||
SrcIPs: []string{"*"},
|
||||
DstPorts: []tailcfg.NetPortRange{
|
||||
{IP: "10.2.3.4", Ports: tailcfg.PortRange{First: 22, Last: 22}},
|
||||
},
|
||||
},
|
||||
}
|
||||
ms := newTestMapSession(t)
|
||||
nm1 := ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: new(tailcfg.Node),
|
||||
PacketFilter: somePacketFilter,
|
||||
})
|
||||
if len(nm1.PacketFilter) == 0 {
|
||||
t.Fatalf("zero length PacketFilter")
|
||||
}
|
||||
nm2 := ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: new(tailcfg.Node),
|
||||
PacketFilter: nil, // testing that the server can omit this.
|
||||
})
|
||||
if len(nm1.PacketFilter) == 0 {
|
||||
t.Fatalf("zero length PacketFilter in 2nd netmap")
|
||||
}
|
||||
if !reflect.DeepEqual(nm1.PacketFilter, nm2.PacketFilter) {
|
||||
t.Error("packet filters differ")
|
||||
}
|
||||
})
|
||||
t.Run("implicit_dnsconfig", func(t *testing.T) {
|
||||
someDNSConfig := &tailcfg.DNSConfig{Domains: []string{"foo", "bar"}}
|
||||
ms := newTestMapSession(t)
|
||||
nm1 := ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: new(tailcfg.Node),
|
||||
DNSConfig: someDNSConfig,
|
||||
})
|
||||
if !reflect.DeepEqual(nm1.DNS, *someDNSConfig) {
|
||||
t.Fatalf("1st DNS wrong")
|
||||
}
|
||||
nm2 := ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: new(tailcfg.Node),
|
||||
DNSConfig: nil, // implict
|
||||
})
|
||||
if !reflect.DeepEqual(nm2.DNS, *someDNSConfig) {
|
||||
t.Fatalf("2nd DNS wrong")
|
||||
}
|
||||
})
|
||||
t.Run("collect_services", func(t *testing.T) {
|
||||
ms := newTestMapSession(t)
|
||||
var nm *netmap.NetworkMap
|
||||
wantCollect := func(v bool) {
|
||||
t.Helper()
|
||||
if nm.CollectServices != v {
|
||||
t.Errorf("netmap.CollectServices = %v; want %v", nm.CollectServices, v)
|
||||
}
|
||||
}
|
||||
|
||||
nm = ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: new(tailcfg.Node),
|
||||
})
|
||||
wantCollect(false)
|
||||
|
||||
nm = ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: new(tailcfg.Node),
|
||||
CollectServices: "false",
|
||||
})
|
||||
wantCollect(false)
|
||||
|
||||
nm = ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: new(tailcfg.Node),
|
||||
CollectServices: "true",
|
||||
})
|
||||
wantCollect(true)
|
||||
|
||||
nm = ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: new(tailcfg.Node),
|
||||
CollectServices: "",
|
||||
})
|
||||
wantCollect(true)
|
||||
})
|
||||
t.Run("implicit_domain", func(t *testing.T) {
|
||||
ms := newTestMapSession(t)
|
||||
var nm *netmap.NetworkMap
|
||||
want := func(v string) {
|
||||
t.Helper()
|
||||
if nm.Domain != v {
|
||||
t.Errorf("netmap.Domain = %q; want %q", nm.Domain, v)
|
||||
}
|
||||
}
|
||||
nm = ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: new(tailcfg.Node),
|
||||
Domain: "foo.com",
|
||||
})
|
||||
want("foo.com")
|
||||
|
||||
nm = ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: new(tailcfg.Node),
|
||||
})
|
||||
want("foo.com")
|
||||
})
|
||||
t.Run("implicit_node", func(t *testing.T) {
|
||||
someNode := &tailcfg.Node{
|
||||
Name: "foo",
|
||||
}
|
||||
wantNode := &tailcfg.Node{
|
||||
Name: "foo",
|
||||
ComputedName: "foo",
|
||||
ComputedNameWithHost: "foo",
|
||||
}
|
||||
ms := newTestMapSession(t)
|
||||
|
||||
nm1 := ms.netmapForResponse(&tailcfg.MapResponse{
|
||||
Node: someNode,
|
||||
})
|
||||
if nm1.SelfNode == nil {
|
||||
t.Fatal("nil Node in 1st netmap")
|
||||
}
|
||||
if !reflect.DeepEqual(nm1.SelfNode, wantNode) {
|
||||
j, _ := json.Marshal(nm1.SelfNode)
|
||||
t.Errorf("Node mismatch in 1st netmap; got: %s", j)
|
||||
}
|
||||
|
||||
nm2 := ms.netmapForResponse(&tailcfg.MapResponse{})
|
||||
if nm2.SelfNode == nil {
|
||||
t.Fatal("nil Node in 1st netmap")
|
||||
}
|
||||
if !reflect.DeepEqual(nm2.SelfNode, wantNode) {
|
||||
j, _ := json.Marshal(nm2.SelfNode)
|
||||
t.Errorf("Node mismatch in 2nd netmap; got: %s", j)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/structs"
|
||||
)
|
||||
|
||||
// State is the high-level state of the client. It is used only in
|
||||
// unit tests for proper sequencing, don't depend on it anywhere else.
|
||||
//
|
||||
// TODO(apenwarr): eliminate the state, as it's now obsolete.
|
||||
//
|
||||
// apenwarr: Historical note: controlclient.Auto was originally
|
||||
// intended to be the state machine for the whole tailscale client, but that
|
||||
// turned out to not be the right abstraction layer, and it moved to
|
||||
// ipn.Backend. Since ipn.Backend now has a state machine, it would be
|
||||
// much better if controlclient could be a simple stateless API. But the
|
||||
// current server-side API (two interlocking polling https calls) makes that
|
||||
// very hard to implement. A server side API change could untangle this and
|
||||
// remove all the statefulness.
|
||||
type State int
|
||||
|
||||
const (
|
||||
StateNew = State(iota)
|
||||
StateNotAuthenticated
|
||||
StateAuthenticating
|
||||
StateURLVisitRequired
|
||||
StateAuthenticated
|
||||
StateSynchronized // connected and received map update
|
||||
)
|
||||
|
||||
func (s State) MarshalText() ([]byte, error) {
|
||||
return []byte(s.String()), nil
|
||||
}
|
||||
|
||||
func (s State) String() string {
|
||||
switch s {
|
||||
case StateNew:
|
||||
return "state:new"
|
||||
case StateNotAuthenticated:
|
||||
return "state:not-authenticated"
|
||||
case StateAuthenticating:
|
||||
return "state:authenticating"
|
||||
case StateURLVisitRequired:
|
||||
return "state:url-visit-required"
|
||||
case StateAuthenticated:
|
||||
return "state:authenticated"
|
||||
case StateSynchronized:
|
||||
return "state:synchronized"
|
||||
default:
|
||||
return fmt.Sprintf("state:unknown:%d", int(s))
|
||||
}
|
||||
}
|
||||
|
||||
type Status struct {
|
||||
_ structs.Incomparable
|
||||
LoginFinished *empty.Message // nonempty when login finishes
|
||||
Err string
|
||||
URL string // interactive URL to visit to finish logging in
|
||||
NetMap *netmap.NetworkMap // server-pushed configuration
|
||||
|
||||
// The internal state should not be exposed outside this
|
||||
// package, but we have some automated tests elsewhere that need to
|
||||
// use them. Please don't use these fields.
|
||||
// TODO(apenwarr): Unexport or remove these.
|
||||
State State
|
||||
Persist *persist.Persist // locally persisted configuration
|
||||
Hostinfo *tailcfg.Hostinfo // current Hostinfo data
|
||||
}
|
||||
|
||||
// Equal reports whether s and s2 are equal.
|
||||
func (s *Status) Equal(s2 *Status) bool {
|
||||
if s == nil && s2 == nil {
|
||||
return true
|
||||
}
|
||||
return s != nil && s2 != nil &&
|
||||
(s.LoginFinished == nil) == (s2.LoginFinished == nil) &&
|
||||
s.Err == s2.Err &&
|
||||
s.URL == s2.URL &&
|
||||
reflect.DeepEqual(s.Persist, s2.Persist) &&
|
||||
reflect.DeepEqual(s.NetMap, s2.NetMap) &&
|
||||
reflect.DeepEqual(s.Hostinfo, s2.Hostinfo) &&
|
||||
s.State == s2.State
|
||||
}
|
||||
|
||||
func (s Status) String() string {
|
||||
b, err := json.MarshalIndent(s, "", "\t")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return s.State.String() + " " + string(b)
|
||||
}
|
||||
@@ -83,9 +83,6 @@ func Prod() *tailcfg.DERPMap {
|
||||
10: derpRegion(10, "sea", "Seattle",
|
||||
derpNode("a", "137.220.36.168", "2001:19f0:8001:2d9:5400:2ff:feef:bbb1"),
|
||||
),
|
||||
11: derpRegion(11, "sao", "São Paulo",
|
||||
derpNode("a", "18.230.97.74", "2600:1f1e:ee4:5611:ec5c:1736:d43b:a454"),
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
15
go.mod
15
go.mod
@@ -7,16 +7,14 @@ require (
|
||||
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 // indirect
|
||||
github.com/coreos/go-iptables v0.4.5
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 // indirect
|
||||
github.com/frankban/quicktest v1.12.1
|
||||
github.com/github/certstore v0.1.0
|
||||
github.com/gliderlabs/ssh v0.2.2
|
||||
github.com/go-multierror/multierror v1.0.2
|
||||
github.com/go-ole/go-ole v1.2.4
|
||||
github.com/godbus/dbus/v5 v5.0.3
|
||||
github.com/google/go-cmp v0.5.5
|
||||
github.com/google/go-cmp v0.5.4
|
||||
github.com/goreleaser/nfpm v1.1.10
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/klauspost/compress v1.10.10
|
||||
github.com/kr/pty v1.1.8
|
||||
github.com/mdlayher/netlink v1.3.2
|
||||
@@ -26,24 +24,23 @@ require (
|
||||
github.com/peterbourgon/ff/v2 v2.0.0
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210510192616-d1aa5623121d
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210403171604-17614717a9b5
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
|
||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
|
||||
golang.org/x/tools v0.1.0
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8
|
||||
gopkg.in/yaml.v2 v2.2.8 // indirect
|
||||
honnef.co/go/tools v0.1.0
|
||||
inet.af/netaddr v0.0.0-20210511181906-37180328850c
|
||||
inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44
|
||||
inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22
|
||||
inet.af/peercred v0.0.0-20210302202138-56e694897155
|
||||
inet.af/wf v0.0.0-20210424212123-eaa011a774a4
|
||||
rsc.io/goversion v1.2.0
|
||||
)
|
||||
|
||||
|
||||
72
go.sum
72
go.sum
@@ -23,11 +23,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20201127111758-49e582c6c23d/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/frankban/quicktest v1.12.1 h1:P6vQcHwZYgVGIpUzKB5DXzkEeYJppJOStPLuh9aB89c=
|
||||
github.com/frankban/quicktest v1.12.1/go.mod h1:qLE0fzW0VuyUAJgPU19zByoIr0HtCHN/r/VLSOOIySU=
|
||||
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
||||
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
|
||||
github.com/gliderlabs/ssh v0.2.2 h1:6zsha5zo/TWhRhwqCD3+EarCAgZ2yN28ipRnGPnwkI0=
|
||||
@@ -45,9 +42,8 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0 h1:BW6OvS3kpT5UEPbCZ+KyX/OB4Ks9/MNMhWjqPPkZxsE=
|
||||
github.com/google/rpmpack v0.0.0-20191226140753-aa36bfddb3a0/go.mod h1:RaTPr0KUf2K7fnZYLNDrr8rxAamWs3iNywJLtQ2AzBg=
|
||||
@@ -65,14 +61,11 @@ github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b h1:c3NTyLNozICy8B4mlMXemD3z/gXgQzVXZS/HqT+i3do=
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I=
|
||||
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
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=
|
||||
@@ -107,7 +100,6 @@ github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3/go.mod h1:85jBQOZwp
|
||||
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
||||
github.com/peterbourgon/ff/v2 v2.0.0 h1:lx0oYI5qr/FU1xnpNhQ+EZM04gKgn46jyYvGEEqBBbY=
|
||||
github.com/peterbourgon/ff/v2 v2.0.0/go.mod h1:xjwr+t+SjWm4L46fcj/D+Ap+6ME7+HqFzaP22pP5Ggk=
|
||||
github.com/peterbourgon/ff/v3 v3.0.0/go.mod h1:UILIFjRH5a/ar8TjXYLTkIvSvekZqPm5Eb/qbGk6CT0=
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 h1:+/+DxvQaYifJ+grD4klzrS5y+KJXldn/2YTl5JG+vZ8=
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -125,12 +117,28 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 h1:lK99QQdH3yBWY6aGilF+IRlQIdmhzLrsEmF6JgN+Ryw=
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210429195722-6cd106ab1339 h1:OjLaZ57xeWJUUBAJN5KmsgjsaUABTZhcvgO/lKtZ8sQ=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210429195722-6cd106ab1339/go.mod h1:ys4yUmhKncXy1jWP34qUHKipRjl322VVhxoh1Rkfo7c=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210510175647-030c638da3df h1:ekBw6cxmDhXf9YxTmMZh7SPwUh9rnRRnaoX7HFiGobc=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210510175647-030c638da3df/go.mod h1:ys4yUmhKncXy1jWP34qUHKipRjl322VVhxoh1Rkfo7c=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210510192616-d1aa5623121d h1:qJSz1zlpuPLmfACtnj+tAH4g3iasJMBW8dpeFm5f4wg=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210510192616-d1aa5623121d/go.mod h1:ys4yUmhKncXy1jWP34qUHKipRjl322VVhxoh1Rkfo7c=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210210202228-3cc76ed5f222 h1:VzTS7LIwCH8jlxwrZguU0TsCLV/MDOunoNIDJdFajyM=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210210202228-3cc76ed5f222/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210324165952-2963b66bc23a h1:tQ7Y0ALSe5109GMFB7TVtfNBsVcAuM422hVSJrXWMTE=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210324165952-2963b66bc23a/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210327173134-f6a42a1646a0 h1:7KFBvUmm3TW/K+bAN22D7M6xSSoY/39s+PajaNBGrLw=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210327173134-f6a42a1646a0/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210330185929-1689f2635004 h1:GNEPNdNHsYe5zhoR/0z2Pl/a9zXbr0IySmHV6PhCrzI=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210330185929-1689f2635004/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210330200845-4914b4a944c4 h1:7Y0H5NzrV3fwHeDrUXDFcTy8QNbAEDwr+qHyOfX4VyE=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210330200845-4914b4a944c4/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210401164443-2d6878b6b30d h1:zbDBqtYvc492gcRL5BB7AO5Aed+aVht2jbYg8SKoMYs=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210401164443-2d6878b6b30d/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210401172819-1aca620a8afb h1:6TGRROCOrjTKbt1ucBTZaDMBeScG6yVEXEjuabOiBzU=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210401172819-1aca620a8afb/go.mod h1:jy12FSeiDLRvS7VQvSoiaqH9WtpapbrC8YSzyZ7fUAk=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210401194826-bb7bc2f24083 h1:e3k65apTVs7NM6mhQ1c94XISLe+2gdizPfRdsImNL8Y=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210401194826-bb7bc2f24083/go.mod h1:jy12FSeiDLRvS7VQvSoiaqH9WtpapbrC8YSzyZ7fUAk=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210402173217-0a47c6e64d15 h1:13GZsTKbCmPGwDBurcSXT+ssYID2IfcX0MfsvhaaagY=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210402173217-0a47c6e64d15/go.mod h1:jy12FSeiDLRvS7VQvSoiaqH9WtpapbrC8YSzyZ7fUAk=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210402193818-fc309421dd43 h1:SRUknVD6AHsxfghv0By9SFjQ8dhn8K8gIFwxf3OEPyU=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210402193818-fc309421dd43/go.mod h1:g3WdWX37upLnDT8STKFWhvA34Gwrt4hIpnWR3HGufpM=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210403171604-17614717a9b5 h1:FegsXWjtyhCxpB8bBSL1kLzagtV+e7BaX07phMM8uQM=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210403171604-17614717a9b5/go.mod h1:ys4yUmhKncXy1jWP34qUHKipRjl322VVhxoh1Rkfo7c=
|
||||
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
|
||||
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
|
||||
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
|
||||
@@ -154,6 +162,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201117144127-c1f2f97bffc9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670 h1:gzMM0EjIYiRmJI3+jBdFuoynZlpxa2JQZsolKu09BXo=
|
||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
|
||||
@@ -177,9 +187,8 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -199,39 +208,40 @@ golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201117222635-ba5294a509c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210105210732-16f7687f5001/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e h1:XNp2Flc/1eWQGk5BLzqTAN7fQIwIbfyVTuVxXxZh73M=
|
||||
golang.org/x/sys v0.0.0-20210317225723-c4fcb01b228e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210402192133-700132347e07 h1:4k6HsQjxj6hVMsI2Vf0yKlzt5lXxZsMW1q0zaq2k8zY=
|
||||
golang.org/x/sys v0.0.0-20210402192133-700132347e07/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200609164405-eb789aa7ce50/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58 h1:1Bs6RVeBFtLZ8Yi1Hk07DiOqzvwLD/4hln4iahvFlag=
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -239,6 +249,7 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1N
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20201111175144-60b3766b89b9 h1:qowcZ56hhpeoESmWzI4Exhx4Y78TpCyXUJur4/c0CoE=
|
||||
golang.zx2c4.com/wireguard v0.0.20200321-0.20201111175144-60b3766b89b9/go.mod h1:LMeNfjlcPZTrBC1juwgbQyA4Zy2XVcsrdO/fIJxwyuA=
|
||||
golang.zx2c4.com/wireguard v0.0.20201118/go.mod h1:Dz+cq5bnrai9EpgYj4GDof/+qaGzbRWbeaAOs1bUYa0=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8 h1:nlXPqGA98n+qcq1pwZ28KjM5EsFQvamKS00A+VUeVjs=
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8/go.mod h1:psva4yDnAHLuh7lUzOK7J7bLYxNFfo0iKWz+mi9gzkA=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -253,16 +264,11 @@ gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.1.0 h1:AWNL1W1i7f0wNZ8VwOKNJ0sliKvOF/adn0EHenfUh+c=
|
||||
honnef.co/go/tools v0.1.0/go.mod h1:XtegFAyX/PfluP4921rXU5IkjkqBCDnUq4W8VCIoKvM=
|
||||
inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44 h1:p7fX77zWzZMuNdJUhniBsmN1OvFOrW9SOtvgnzqUZX4=
|
||||
inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44/go.mod h1:I2i9ONCXRZDnG1+7O8fSuYzjcPxHQXrIfzD/IkR87x4=
|
||||
inet.af/netaddr v0.0.0-20210508014949-da1c2a70a83d h1:9tuJMxDV7THGfXWirKBD/v9rbsBC21bHd2eEYsYuIek=
|
||||
inet.af/netaddr v0.0.0-20210508014949-da1c2a70a83d/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20210511181906-37180328850c h1:rzDy/tC8LjEdN94+i0Bu22tTo/qE9cvhKyfD0HMU0NU=
|
||||
inet.af/netaddr v0.0.0-20210511181906-37180328850c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22 h1:DNtszwGa6w76qlIr+PbPEnlBJdiRV8SaxeigOy0q1gg=
|
||||
inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22/go.mod h1:GVx+5OZtbG4TVOW5ilmyRZAZXr1cNwfqUEkTOtWK0PM=
|
||||
inet.af/peercred v0.0.0-20210302202138-56e694897155 h1:KojYNEYqDkZ2O3LdyTstR1l13L3ePKTIEM2h7ONkfkE=
|
||||
inet.af/peercred v0.0.0-20210302202138-56e694897155/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU=
|
||||
inet.af/wf v0.0.0-20210424212123-eaa011a774a4 h1:g1VVXY1xRKoO17aKY3g9KeJxDW0lGx1n2Y+WPSWkOL8=
|
||||
inet.af/wf v0.0.0-20210424212123-eaa011a774a4/go.mod h1:56/0QVlZ4NmPRh1QuU2OfrKqjSgt5P39R534gD2JMpQ=
|
||||
rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w=
|
||||
rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/go-multierror/multierror"
|
||||
@@ -37,7 +36,6 @@ var (
|
||||
ipnState string
|
||||
ipnWantRunning bool
|
||||
anyInterfaceUp = true // until told otherwise
|
||||
udp4Unbound bool
|
||||
)
|
||||
|
||||
// Subsystem is the name of a subsystem whose health can be monitored.
|
||||
@@ -48,7 +46,7 @@ const (
|
||||
// the system, rather than one particular subsystem.
|
||||
SysOverall = Subsystem("overall")
|
||||
|
||||
// SysRouter is the name of the wgengine/router subsystem.
|
||||
// SysRouter is the name the wgengine/router subsystem.
|
||||
SysRouter = Subsystem("router")
|
||||
|
||||
// SysDNS is the name of the net/dns subsystem.
|
||||
@@ -215,18 +213,9 @@ func SetAnyInterfaceUp(up bool) {
|
||||
selfCheckLocked()
|
||||
}
|
||||
|
||||
// SetUDP4Unbound sets whether the udp4 bind failed completely.
|
||||
func SetUDP4Unbound(unbound bool) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
udp4Unbound = unbound
|
||||
selfCheckLocked()
|
||||
}
|
||||
|
||||
func timerSelfCheck() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
checkReceiveFuncs()
|
||||
selfCheckLocked()
|
||||
if timer != nil {
|
||||
timer.Reset(time.Minute)
|
||||
@@ -266,9 +255,6 @@ func overallErrorLocked() error {
|
||||
if d := now.Sub(derpRegionLastFrame[rid]).Round(time.Second); d > tooIdle {
|
||||
return fmt.Errorf("haven't heard from home DERP region %v in %v", rid, d)
|
||||
}
|
||||
if udp4Unbound {
|
||||
return errors.New("no udp4 bind")
|
||||
}
|
||||
|
||||
// TODO: use
|
||||
_ = inMapPollSince
|
||||
@@ -277,11 +263,6 @@ func overallErrorLocked() error {
|
||||
_ = lastMapRequestHeard
|
||||
|
||||
var errs []error
|
||||
for _, recv := range receiveFuncs {
|
||||
if recv.missing {
|
||||
errs = append(errs, fmt.Errorf("%s is not running", recv.name))
|
||||
}
|
||||
}
|
||||
for sys, err := range sysErr {
|
||||
if err == nil || sys == SysOverall {
|
||||
continue
|
||||
@@ -294,58 +275,3 @@ func overallErrorLocked() error {
|
||||
})
|
||||
return multierror.New(errs)
|
||||
}
|
||||
|
||||
var (
|
||||
ReceiveIPv4 = ReceiveFuncStats{name: "ReceiveIPv4"}
|
||||
ReceiveIPv6 = ReceiveFuncStats{name: "ReceiveIPv6"}
|
||||
ReceiveDERP = ReceiveFuncStats{name: "ReceiveDERP"}
|
||||
|
||||
receiveFuncs = []*ReceiveFuncStats{&ReceiveIPv4, &ReceiveIPv6, &ReceiveDERP}
|
||||
)
|
||||
|
||||
// ReceiveFuncStats tracks the calls made to a wireguard-go receive func.
|
||||
type ReceiveFuncStats struct {
|
||||
// name is the name of the receive func.
|
||||
name string
|
||||
// numCalls is the number of times the receive func has ever been called.
|
||||
// It is required because it is possible for a receive func's wireguard-go goroutine
|
||||
// to be active even though the receive func isn't.
|
||||
// The wireguard-go goroutine alternates between calling the receive func and
|
||||
// processing what the func returned.
|
||||
numCalls uint64 // accessed atomically
|
||||
// prevNumCalls is the value of numCalls last time the health check examined it.
|
||||
prevNumCalls uint64
|
||||
// inCall indicates whether the receive func is currently running.
|
||||
inCall uint32 // bool, accessed atomically
|
||||
// missing indicates whether the receive func is not running.
|
||||
missing bool
|
||||
}
|
||||
|
||||
func (s *ReceiveFuncStats) Enter() {
|
||||
atomic.AddUint64(&s.numCalls, 1)
|
||||
atomic.StoreUint32(&s.inCall, 1)
|
||||
}
|
||||
|
||||
func (s *ReceiveFuncStats) Exit() {
|
||||
atomic.StoreUint32(&s.inCall, 0)
|
||||
}
|
||||
|
||||
func checkReceiveFuncs() {
|
||||
for _, recv := range receiveFuncs {
|
||||
recv.missing = false
|
||||
prev := recv.prevNumCalls
|
||||
numCalls := atomic.LoadUint64(&recv.numCalls)
|
||||
recv.prevNumCalls = numCalls
|
||||
if numCalls > prev {
|
||||
// OK: the function has gotten called since last we checked
|
||||
continue
|
||||
}
|
||||
if atomic.LoadUint32(&recv.inCall) == 1 {
|
||||
// OK: the function is active, probably blocked due to inactivity
|
||||
continue
|
||||
}
|
||||
// Not OK: The function is not active, and not accumulating new calls.
|
||||
// It is probably MIA.
|
||||
recv.missing = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,174 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package deephash hashes a Go value recursively, in a predictable
|
||||
// order, without looping.
|
||||
package deephash
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func Hash(v ...interface{}) string {
|
||||
h := sha256.New()
|
||||
// 64 matches the chunk size in crypto/sha256/sha256.go
|
||||
b := bufio.NewWriterSize(h, 64)
|
||||
Print(b, v)
|
||||
b.Flush()
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// UpdateHash sets last to the hash of v and reports whether its value changed.
|
||||
func UpdateHash(last *string, v ...interface{}) (changed bool) {
|
||||
sig := Hash(v)
|
||||
if *last != sig {
|
||||
*last = sig
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func Print(w *bufio.Writer, v ...interface{}) {
|
||||
print(w, reflect.ValueOf(v), make(map[uintptr]bool))
|
||||
}
|
||||
|
||||
var (
|
||||
netaddrIPType = reflect.TypeOf(netaddr.IP{})
|
||||
netaddrIPPrefix = reflect.TypeOf(netaddr.IPPrefix{})
|
||||
wgkeyKeyType = reflect.TypeOf(wgkey.Key{})
|
||||
wgkeyPrivateType = reflect.TypeOf(wgkey.Private{})
|
||||
tailcfgDiscoKeyType = reflect.TypeOf(tailcfg.DiscoKey{})
|
||||
)
|
||||
|
||||
func print(w *bufio.Writer, v reflect.Value, visited map[uintptr]bool) {
|
||||
if !v.IsValid() {
|
||||
return
|
||||
}
|
||||
|
||||
// Special case some common types.
|
||||
if v.CanInterface() {
|
||||
switch v.Type() {
|
||||
case netaddrIPType:
|
||||
var b []byte
|
||||
var err error
|
||||
if v.CanAddr() {
|
||||
x := v.Addr().Interface().(*netaddr.IP)
|
||||
b, err = x.MarshalText()
|
||||
} else {
|
||||
x := v.Interface().(netaddr.IP)
|
||||
b, err = x.MarshalText()
|
||||
}
|
||||
if err == nil {
|
||||
w.Write(b)
|
||||
return
|
||||
}
|
||||
case netaddrIPPrefix:
|
||||
var b []byte
|
||||
var err error
|
||||
if v.CanAddr() {
|
||||
x := v.Addr().Interface().(*netaddr.IPPrefix)
|
||||
b, err = x.MarshalText()
|
||||
} else {
|
||||
x := v.Interface().(netaddr.IPPrefix)
|
||||
b, err = x.MarshalText()
|
||||
}
|
||||
if err == nil {
|
||||
w.Write(b)
|
||||
return
|
||||
}
|
||||
case wgkeyKeyType:
|
||||
if v.CanAddr() {
|
||||
x := v.Addr().Interface().(*wgkey.Key)
|
||||
w.Write(x[:])
|
||||
} else {
|
||||
x := v.Interface().(wgkey.Key)
|
||||
w.Write(x[:])
|
||||
}
|
||||
return
|
||||
case wgkeyPrivateType:
|
||||
if v.CanAddr() {
|
||||
x := v.Addr().Interface().(*wgkey.Private)
|
||||
w.Write(x[:])
|
||||
} else {
|
||||
x := v.Interface().(wgkey.Private)
|
||||
w.Write(x[:])
|
||||
}
|
||||
return
|
||||
case tailcfgDiscoKeyType:
|
||||
if v.CanAddr() {
|
||||
x := v.Addr().Interface().(*tailcfg.DiscoKey)
|
||||
w.Write(x[:])
|
||||
} else {
|
||||
x := v.Interface().(tailcfg.DiscoKey)
|
||||
w.Write(x[:])
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Generic handling.
|
||||
switch v.Kind() {
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled kind %v for type %v", v.Kind(), v.Type()))
|
||||
case reflect.Ptr:
|
||||
ptr := v.Pointer()
|
||||
if visited[ptr] {
|
||||
return
|
||||
}
|
||||
visited[ptr] = true
|
||||
print(w, v.Elem(), visited)
|
||||
return
|
||||
case reflect.Struct:
|
||||
w.WriteString("struct{\n")
|
||||
for i, n := 0, v.NumField(); i < n; i++ {
|
||||
fmt.Fprintf(w, " [%d]: ", i)
|
||||
print(w, v.Field(i), visited)
|
||||
w.WriteString("\n")
|
||||
}
|
||||
w.WriteString("}\n")
|
||||
case reflect.Slice, reflect.Array:
|
||||
if v.Type().Elem().Kind() == reflect.Uint8 && v.CanInterface() {
|
||||
fmt.Fprintf(w, "%q", v.Interface())
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "[%d]{\n", v.Len())
|
||||
for i, ln := 0, v.Len(); i < ln; i++ {
|
||||
fmt.Fprintf(w, " [%d]: ", i)
|
||||
print(w, v.Index(i), visited)
|
||||
w.WriteString("\n")
|
||||
}
|
||||
w.WriteString("}\n")
|
||||
case reflect.Interface:
|
||||
print(w, v.Elem(), visited)
|
||||
case reflect.Map:
|
||||
sm := newSortedMap(v)
|
||||
fmt.Fprintf(w, "map[%d]{\n", len(sm.Key))
|
||||
for i, k := range sm.Key {
|
||||
print(w, k, visited)
|
||||
w.WriteString(": ")
|
||||
print(w, sm.Value[i], visited)
|
||||
w.WriteString("\n")
|
||||
}
|
||||
w.WriteString("}\n")
|
||||
case reflect.String:
|
||||
w.WriteString(v.String())
|
||||
case reflect.Bool:
|
||||
fmt.Fprintf(w, "%v", v.Bool())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
fmt.Fprintf(w, "%v", v.Int())
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
fmt.Fprintf(w, "%v", v.Uint())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
fmt.Fprintf(w, "%v", v.Float())
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
fmt.Fprintf(w, "%v", v.Complex())
|
||||
}
|
||||
}
|
||||
103
internal/deepprint/deepprint.go
Normal file
103
internal/deepprint/deepprint.go
Normal file
@@ -0,0 +1,103 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package deepprint walks a Go value recursively, in a predictable
|
||||
// order, without looping, and prints each value out to a given
|
||||
// Writer, which is assumed to be a hash.Hash, as this package doesn't
|
||||
// format things nicely.
|
||||
//
|
||||
// This is intended as a lighter version of go-spew, etc. We don't need its
|
||||
// features when our writer is just a hash.
|
||||
package deepprint
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func Hash(v ...interface{}) string {
|
||||
h := sha256.New()
|
||||
Print(h, v)
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// UpdateHash sets last to the hash of v and reports whether its value changed.
|
||||
func UpdateHash(last *string, v ...interface{}) (changed bool) {
|
||||
sig := Hash(v)
|
||||
if *last != sig {
|
||||
*last = sig
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func Print(w io.Writer, v ...interface{}) {
|
||||
print(w, reflect.ValueOf(v), make(map[uintptr]bool))
|
||||
}
|
||||
|
||||
func print(w io.Writer, v reflect.Value, visited map[uintptr]bool) {
|
||||
if !v.IsValid() {
|
||||
return
|
||||
}
|
||||
switch v.Kind() {
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled kind %v for type %v", v.Kind(), v.Type()))
|
||||
case reflect.Ptr:
|
||||
ptr := v.Pointer()
|
||||
if visited[ptr] {
|
||||
return
|
||||
}
|
||||
visited[ptr] = true
|
||||
print(w, v.Elem(), visited)
|
||||
return
|
||||
case reflect.Struct:
|
||||
fmt.Fprintf(w, "struct{\n")
|
||||
t := v.Type()
|
||||
for i, n := 0, v.NumField(); i < n; i++ {
|
||||
sf := t.Field(i)
|
||||
fmt.Fprintf(w, "%s: ", sf.Name)
|
||||
print(w, v.Field(i), visited)
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
if v.Type().Elem().Kind() == reflect.Uint8 && v.CanInterface() {
|
||||
fmt.Fprintf(w, "%q", v.Interface())
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "[%d]{\n", v.Len())
|
||||
for i, ln := 0, v.Len(); i < ln; i++ {
|
||||
fmt.Fprintf(w, " [%d]: ", i)
|
||||
print(w, v.Index(i), visited)
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
fmt.Fprintf(w, "}\n")
|
||||
case reflect.Interface:
|
||||
print(w, v.Elem(), visited)
|
||||
case reflect.Map:
|
||||
sm := newSortedMap(v)
|
||||
fmt.Fprintf(w, "map[%d]{\n", len(sm.Key))
|
||||
for i, k := range sm.Key {
|
||||
print(w, k, visited)
|
||||
fmt.Fprintf(w, ": ")
|
||||
print(w, sm.Value[i], visited)
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
fmt.Fprintf(w, "}\n")
|
||||
|
||||
case reflect.String:
|
||||
fmt.Fprintf(w, "%s", v.String())
|
||||
case reflect.Bool:
|
||||
fmt.Fprintf(w, "%v", v.Bool())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
fmt.Fprintf(w, "%v", v.Int())
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
fmt.Fprintf(w, "%v", v.Uint())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
fmt.Fprintf(w, "%v", v.Float())
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
fmt.Fprintf(w, "%v", v.Complex())
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,13 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package deephash
|
||||
package deepprint
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
@@ -19,6 +18,10 @@ func TestDeepPrint(t *testing.T) {
|
||||
// Mostly we're just testing that we don't panic on handled types.
|
||||
v := getVal()
|
||||
|
||||
var buf bytes.Buffer
|
||||
Print(&buf, v)
|
||||
t.Logf("Got: %s", buf.Bytes())
|
||||
|
||||
hash1 := Hash(v)
|
||||
t.Logf("hash: %v", hash1)
|
||||
for i := 0; i < 20; i++ {
|
||||
@@ -36,9 +39,7 @@ func getVal() []interface{} {
|
||||
Addresses: []netaddr.IPPrefix{{Bits: 5, IP: netaddr.IPFrom16([16]byte{3: 3})}},
|
||||
Peers: []wgcfg.Peer{
|
||||
{
|
||||
Endpoints: wgcfg.Endpoints{
|
||||
IPPorts: wgcfg.NewIPPortSet(netaddr.MustParseIPPort("42.42.42.42:5")),
|
||||
},
|
||||
Endpoints: "foo:5",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -48,25 +49,16 @@ func getVal() []interface{} {
|
||||
netaddr.MustParseIPPrefix("1234::/64"),
|
||||
},
|
||||
},
|
||||
map[dnsname.FQDN][]netaddr.IP{
|
||||
dnsname.FQDN("a."): {netaddr.MustParseIP("1.2.3.4"), netaddr.MustParseIP("4.3.2.1")},
|
||||
dnsname.FQDN("b."): {netaddr.MustParseIP("8.8.8.8"), netaddr.MustParseIP("9.9.9.9")},
|
||||
},
|
||||
map[dnsname.FQDN][]netaddr.IPPort{
|
||||
dnsname.FQDN("a."): {netaddr.MustParseIPPort("1.2.3.4:11"), netaddr.MustParseIPPort("4.3.2.1:22")},
|
||||
dnsname.FQDN("b."): {netaddr.MustParseIPPort("8.8.8.8:11"), netaddr.MustParseIPPort("9.9.9.9:22")},
|
||||
},
|
||||
map[tailcfg.DiscoKey]bool{
|
||||
{1: 1}: true,
|
||||
{1: 2}: false,
|
||||
map[string]string{
|
||||
"key1": "val1",
|
||||
"key2": "val2",
|
||||
"key3": "val3",
|
||||
"key4": "val4",
|
||||
"key5": "val5",
|
||||
"key6": "val6",
|
||||
"key7": "val7",
|
||||
"key8": "val8",
|
||||
"key9": "val9",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkHash(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
v := getVal()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Hash(v)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
// This is a slightly modified fork of Go's src/internal/fmtsort/sort.go
|
||||
|
||||
package deephash
|
||||
package deepprint
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
@@ -5,8 +5,6 @@
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -57,21 +55,17 @@ type EngineStatus struct {
|
||||
// that they have not changed.
|
||||
// They are JSON-encoded on the wire, despite the lack of struct tags.
|
||||
type Notify struct {
|
||||
_ structs.Incomparable
|
||||
Version string // version number of IPN backend
|
||||
|
||||
// ErrMessage, if non-nil, contains a critical error message.
|
||||
// For State InUseOtherUser, ErrMessage is not critical and just contains the details.
|
||||
ErrMessage *string
|
||||
|
||||
LoginFinished *empty.Message // non-nil when/if the login process succeeded
|
||||
State *State // if non-nil, the new or current IPN state
|
||||
Prefs *Prefs // if non-nil, the new or current preferences
|
||||
NetMap *netmap.NetworkMap // if non-nil, the new or current netmap
|
||||
Engine *EngineStatus // if non-nil, the new or urrent wireguard stats
|
||||
BrowseToURL *string // if non-nil, UI should open a browser right now
|
||||
BackendLogID *string // if non-nil, the public logtail ID used by backend
|
||||
PingResult *ipnstate.PingResult // if non-nil, a ping response arrived
|
||||
_ structs.Incomparable
|
||||
Version string // version number of IPN backend
|
||||
ErrMessage *string // critical error message, if any; for InUseOtherUser, the details
|
||||
LoginFinished *empty.Message // event: non-nil when login process succeeded
|
||||
State *State // current IPN state has changed
|
||||
Prefs *Prefs // preferences were changed
|
||||
NetMap *netmap.NetworkMap // new netmap received
|
||||
Engine *EngineStatus // wireguard engine stats
|
||||
BrowseToURL *string // UI should open a browser right now
|
||||
BackendLogID *string // public logtail id used by backend
|
||||
PingResult *ipnstate.PingResult
|
||||
|
||||
// FilesWaiting if non-nil means that files are buffered in
|
||||
// the Tailscale daemon and ready for local transfer to the
|
||||
@@ -94,49 +88,6 @@ type Notify struct {
|
||||
// type is mirrored in xcode/Shared/IPN.swift
|
||||
}
|
||||
|
||||
func (n Notify) String() string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Notify{")
|
||||
if n.ErrMessage != nil {
|
||||
fmt.Fprintf(&sb, "err=%q ", *n.ErrMessage)
|
||||
}
|
||||
if n.LoginFinished != nil {
|
||||
sb.WriteString("LoginFinished ")
|
||||
}
|
||||
if n.State != nil {
|
||||
fmt.Fprintf(&sb, "state=%v ", *n.State)
|
||||
}
|
||||
if n.Prefs != nil {
|
||||
fmt.Fprintf(&sb, "%v ", n.Prefs.Pretty())
|
||||
}
|
||||
if n.NetMap != nil {
|
||||
sb.WriteString("NetMap{...} ")
|
||||
}
|
||||
if n.Engine != nil {
|
||||
fmt.Fprintf(&sb, "wg=%v ", *n.Engine)
|
||||
}
|
||||
if n.BrowseToURL != nil {
|
||||
sb.WriteString("URL=<...> ")
|
||||
}
|
||||
if n.BackendLogID != nil {
|
||||
sb.WriteString("BackendLogID ")
|
||||
}
|
||||
if n.PingResult != nil {
|
||||
fmt.Fprintf(&sb, "ping=%v ", *n.PingResult)
|
||||
}
|
||||
if n.FilesWaiting != nil {
|
||||
sb.WriteString("FilesWaiting ")
|
||||
}
|
||||
if len(n.IncomingFiles) != 0 {
|
||||
sb.WriteString("IncomingFiles ")
|
||||
}
|
||||
if n.LocalTCPPort != nil {
|
||||
fmt.Fprintf(&sb, "tcpport=%v ", n.LocalTCPPort)
|
||||
}
|
||||
s := sb.String()
|
||||
return s[0:len(s)-1] + "}"
|
||||
}
|
||||
|
||||
// PartialFile represents an in-progress file transfer.
|
||||
type PartialFile struct {
|
||||
Name string // e.g. "foo.jpg"
|
||||
@@ -185,30 +136,8 @@ type Options struct {
|
||||
// state and use/update that.
|
||||
// - StateKey!="" && Prefs!=nil: like the previous case, but do
|
||||
// an initial overwrite of backend state with Prefs.
|
||||
//
|
||||
// NOTE(apenwarr): The above means that this Prefs field does not do
|
||||
// what you probably think it does. It will overwrite your encryption
|
||||
// keys. Do not use unless you know what you're doing.
|
||||
StateKey StateKey
|
||||
Prefs *Prefs
|
||||
// UpdatePrefs, if provided, overrides Options.Prefs *and* the Prefs
|
||||
// already stored in the backend state, *except* for the Persist
|
||||
// Persist member. If you just want to provide prefs, this is
|
||||
// probably what you want.
|
||||
//
|
||||
// UpdatePrefs.Persist is always ignored. Prefs.Persist will still
|
||||
// be used even if UpdatePrefs is provided. Other than Persist,
|
||||
// UpdatePrefs takes precedence over Prefs.
|
||||
//
|
||||
// This is intended as a purely temporary workaround for the
|
||||
// currently unexpected behaviour of Options.Prefs.
|
||||
//
|
||||
// TODO(apenwarr): Remove this, or rename Prefs to something else
|
||||
// and rename this to Prefs. Or, move Prefs.Persist elsewhere
|
||||
// entirely (as it always should have been), and then we wouldn't
|
||||
// need two separate fields at all. Or, move the fancy state
|
||||
// migration stuff out of Start().
|
||||
UpdatePrefs *Prefs
|
||||
// AuthKey is an optional node auth key used to authorize a
|
||||
// new node key without user interaction.
|
||||
AuthKey string
|
||||
|
||||
@@ -19,7 +19,7 @@ type FakeBackend struct {
|
||||
}
|
||||
|
||||
func (b *FakeBackend) Start(opts Options) error {
|
||||
b.serverURL = opts.Prefs.ControlURLOrDefault()
|
||||
b.serverURL = opts.Prefs.ControlURL
|
||||
if b.notify == nil {
|
||||
panic("FakeBackend.Start: SetNotifyCallback not called")
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ func (h *Handle) Expiry() time.Time {
|
||||
}
|
||||
|
||||
func (h *Handle) AdminPageURL() string {
|
||||
return h.prefsCache.ControlURLOrDefault() + "/admin/machines"
|
||||
return h.prefsCache.ControlURL + "/admin/machines"
|
||||
}
|
||||
|
||||
func (h *Handle) StartLoginInteractive() {
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
@@ -29,7 +28,7 @@ import (
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/internal/deephash"
|
||||
"tailscale.com/internal/deepprint"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn/policy"
|
||||
@@ -46,7 +45,6 @@ import (
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/osshare"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine"
|
||||
@@ -98,16 +96,14 @@ type LocalBackend struct {
|
||||
// The mutex protects the following elements.
|
||||
mu sync.Mutex
|
||||
httpTestClient *http.Client // for controlclient. nil by default, used by tests.
|
||||
ccGen clientGen // function for producing controlclient; lazily populated
|
||||
notify func(ipn.Notify)
|
||||
cc controlclient.Client
|
||||
cc *controlclient.Client
|
||||
stateKey ipn.StateKey // computed in part from user-provided value
|
||||
userID string // current controlling user ID (for Windows, primarily)
|
||||
prefs *ipn.Prefs
|
||||
inServerMode bool
|
||||
machinePrivKey wgkey.Private
|
||||
state ipn.State
|
||||
capFileSharing bool // whether netMap contains the file sharing capability
|
||||
// hostinfo is mutated in-place while mu is held.
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
// netMap is not mutated in-place once set.
|
||||
@@ -117,8 +113,7 @@ type LocalBackend struct {
|
||||
engineStatus ipn.EngineStatus
|
||||
endpoints []tailcfg.Endpoint
|
||||
blocked bool
|
||||
authURL string // cleared on Notify
|
||||
authURLSticky string // not cleared on Notify
|
||||
authURL string
|
||||
interact bool
|
||||
prevIfState *interfaces.State
|
||||
peerAPIServer *peerAPIServer // or nil
|
||||
@@ -141,10 +136,6 @@ type LocalBackend struct {
|
||||
statusChanged *sync.Cond
|
||||
}
|
||||
|
||||
// clientGen is a func that creates a control plane client.
|
||||
// It's the type used by LocalBackend.SetControlClientGetterForTesting.
|
||||
type clientGen func(controlclient.Options) (controlclient.Client, error)
|
||||
|
||||
// NewLocalBackend returns a new LocalBackend that is ready to run,
|
||||
// but is not actually running.
|
||||
func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wgengine.Engine) (*LocalBackend, error) {
|
||||
@@ -152,8 +143,6 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge
|
||||
panic("ipn.NewLocalBackend: wgengine must not be nil")
|
||||
}
|
||||
|
||||
osshare.SetFileSharingEnabled(false, logf)
|
||||
|
||||
// Default filter blocks everything and logs nothing, until Start() is called.
|
||||
e.SetFilter(filter.NewAllowNone(logf, &netaddr.IPSet{}))
|
||||
|
||||
@@ -223,7 +212,7 @@ func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
|
||||
|
||||
networkUp := ifst.AnyInterfaceUp()
|
||||
if b.cc != nil {
|
||||
go b.cc.SetPaused((b.state == ipn.Stopped && b.netMap != nil) || !networkUp)
|
||||
go b.cc.SetPaused(b.state == ipn.Stopped || !networkUp)
|
||||
}
|
||||
|
||||
// If the PAC-ness of the network changed, reconfig wireguard+route to
|
||||
@@ -242,7 +231,7 @@ func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
|
||||
// need updating to tweak default routes.
|
||||
b.updateFilter(b.netMap, b.prefs)
|
||||
|
||||
if runtime.GOOS == "windows" && b.netMap != nil && b.state == ipn.Running {
|
||||
if runtime.GOOS == "windows" && b.netMap != nil {
|
||||
want := len(b.netMap.Addresses)
|
||||
b.logf("linkChange: peerAPIListeners too low; trying again")
|
||||
if len(b.peerAPIListeners) < want {
|
||||
@@ -320,15 +309,12 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
|
||||
sb.MutateStatus(func(s *ipnstate.Status) {
|
||||
s.Version = version.Long
|
||||
s.BackendState = b.state.String()
|
||||
s.AuthURL = b.authURLSticky
|
||||
s.AuthURL = b.authURL
|
||||
if b.netMap != nil {
|
||||
s.MagicDNSSuffix = b.netMap.MagicDNSSuffix()
|
||||
}
|
||||
})
|
||||
sb.MutateSelfStatus(func(ss *ipnstate.PeerStatus) {
|
||||
if b.netMap != nil && b.netMap.SelfNode != nil {
|
||||
ss.ID = b.netMap.SelfNode.StableID
|
||||
}
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
ss.PeerAPIURL = append(ss.PeerAPIURL, pln.urlStr)
|
||||
}
|
||||
@@ -368,7 +354,6 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
|
||||
}
|
||||
sb.AddPeer(key.Public(p.Key), &ipnstate.PeerStatus{
|
||||
InNetworkMap: true,
|
||||
ID: p.StableID,
|
||||
UserID: p.User,
|
||||
TailAddrDeprecated: tailAddr4,
|
||||
TailscaleIPs: tailscaleIPs,
|
||||
@@ -434,12 +419,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
wasBlocked := b.blocked
|
||||
b.mu.Unlock()
|
||||
|
||||
if st.LoginFinished != nil && wasBlocked {
|
||||
if st.LoginFinished != nil {
|
||||
// Auth completed, unblock the engine
|
||||
b.blockEngineUpdates(false)
|
||||
b.authReconfig()
|
||||
@@ -456,15 +436,6 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
netMap := b.netMap
|
||||
interact := b.interact
|
||||
|
||||
if prefs.ControlURL == "" {
|
||||
// Once we get a message from the control plane, set
|
||||
// our ControlURL pref explicitly. This causes a
|
||||
// future "tailscale up" to start checking for
|
||||
// implicit setting reverts, which it doesn't do when
|
||||
// ControlURL is blank.
|
||||
prefs.ControlURL = prefs.ControlURLOrDefault()
|
||||
prefsChanged = true
|
||||
}
|
||||
if st.Persist != nil {
|
||||
if !b.prefs.Persist.Equals(st.Persist) {
|
||||
prefsChanged = true
|
||||
@@ -479,17 +450,12 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
}
|
||||
if st.URL != "" {
|
||||
b.authURL = st.URL
|
||||
b.authURLSticky = st.URL
|
||||
}
|
||||
if wasBlocked && st.LoginFinished != nil {
|
||||
// Interactive login finished successfully (URL visited).
|
||||
// After an interactive login, the user always wants
|
||||
// WantRunning.
|
||||
if !b.prefs.WantRunning || b.prefs.LoggedOut {
|
||||
if b.state == ipn.NeedsLogin {
|
||||
if !b.prefs.WantRunning {
|
||||
prefsChanged = true
|
||||
}
|
||||
b.prefs.WantRunning = true
|
||||
b.prefs.LoggedOut = false
|
||||
}
|
||||
// Prefs will be written out; this is not safe unless locked or cloned.
|
||||
if prefsChanged {
|
||||
@@ -571,18 +537,10 @@ func (b *LocalBackend) findExitNodeIDLocked(nm *netmap.NetworkMap) (prefsChanged
|
||||
func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) {
|
||||
if err != nil {
|
||||
b.logf("wgengine status error: %v", err)
|
||||
|
||||
b.statusLock.Lock()
|
||||
b.statusChanged.Broadcast()
|
||||
b.statusLock.Unlock()
|
||||
return
|
||||
}
|
||||
if s == nil {
|
||||
b.logf("[unexpected] non-error wgengine update with status=nil: %v", s)
|
||||
|
||||
b.statusLock.Lock()
|
||||
b.statusChanged.Broadcast()
|
||||
b.statusLock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -620,51 +578,6 @@ func (b *LocalBackend) SetHTTPTestClient(c *http.Client) {
|
||||
b.httpTestClient = c
|
||||
}
|
||||
|
||||
// SetControlClientGetterForTesting sets the func that creates a
|
||||
// control plane client. It can be called at most once, before Start.
|
||||
func (b *LocalBackend) SetControlClientGetterForTesting(newControlClient func(controlclient.Options) (controlclient.Client, error)) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.ccGen != nil {
|
||||
panic("invalid use of SetControlClientGetterForTesting after Start")
|
||||
}
|
||||
b.ccGen = newControlClient
|
||||
}
|
||||
|
||||
func (b *LocalBackend) getNewControlClientFunc() clientGen {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.ccGen == nil {
|
||||
// Initialize it rather than just returning the
|
||||
// default to make any future call to
|
||||
// SetControlClientGetterForTesting panic.
|
||||
b.ccGen = func(opts controlclient.Options) (controlclient.Client, error) {
|
||||
return controlclient.New(opts)
|
||||
}
|
||||
}
|
||||
return b.ccGen
|
||||
}
|
||||
|
||||
// startIsNoopLocked reports whether a Start call on this LocalBackend
|
||||
// with the provided Start Options would be a useless no-op.
|
||||
//
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) startIsNoopLocked(opts ipn.Options) bool {
|
||||
// Options has 5 fields; check all of them:
|
||||
// * FrontendLogID
|
||||
// * StateKey
|
||||
// * Prefs
|
||||
// * UpdatePrefs
|
||||
// * AuthKey
|
||||
return b.state == ipn.Running &&
|
||||
b.hostinfo != nil &&
|
||||
b.hostinfo.FrontendLogID == opts.FrontendLogID &&
|
||||
b.stateKey == opts.StateKey &&
|
||||
opts.Prefs == nil &&
|
||||
opts.UpdatePrefs == nil &&
|
||||
opts.AuthKey == ""
|
||||
}
|
||||
|
||||
// Start applies the configuration specified in opts, and starts the
|
||||
// state machine.
|
||||
//
|
||||
@@ -686,30 +599,12 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
b.logf("Start")
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
|
||||
// The iOS client sends a "Start" whenever its UI screen comes
|
||||
// up, just because it wants a netmap. That should be fixed,
|
||||
// but meanwhile we can make Start cheaper here for such a
|
||||
// case and not restart the world (which takes a few seconds).
|
||||
// Instead, just send a notify with the state that iOS needs.
|
||||
if b.startIsNoopLocked(opts) {
|
||||
b.logf("Start: already running; sending notify")
|
||||
nm := b.netMap
|
||||
state := b.state
|
||||
b.mu.Unlock()
|
||||
b.send(ipn.Notify{
|
||||
State: &state,
|
||||
NetMap: nm,
|
||||
LoginFinished: new(empty.Message),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
hostinfo := controlclient.NewHostinfo()
|
||||
hostinfo.BackendLogID = b.backendLogID
|
||||
hostinfo.FrontendLogID = opts.FrontendLogID
|
||||
|
||||
b.mu.Lock()
|
||||
|
||||
if b.cc != nil {
|
||||
// TODO(apenwarr): avoid the need to reinit controlclient.
|
||||
// This will trigger a full relogin/reconfigure cycle every
|
||||
@@ -718,9 +613,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
// into sync with the minimal changes. But that's not how it
|
||||
// is right now, which is a sign that the code is still too
|
||||
// complicated.
|
||||
b.mu.Unlock()
|
||||
b.cc.Shutdown()
|
||||
b.mu.Lock()
|
||||
}
|
||||
httpTestClient := b.httpTestClient
|
||||
|
||||
@@ -736,12 +629,6 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
return fmt.Errorf("loading requested state: %v", err)
|
||||
}
|
||||
|
||||
if opts.UpdatePrefs != nil {
|
||||
newPrefs := opts.UpdatePrefs
|
||||
newPrefs.Persist = b.prefs.Persist
|
||||
b.prefs = newPrefs
|
||||
}
|
||||
|
||||
wantRunning := b.prefs.WantRunning
|
||||
if wantRunning {
|
||||
if err := b.initMachineKeyLocked(); err != nil {
|
||||
@@ -749,10 +636,8 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
}
|
||||
}
|
||||
|
||||
loggedOut := b.prefs.LoggedOut
|
||||
|
||||
b.inServerMode = b.prefs.ForceDaemon
|
||||
b.serverURL = b.prefs.ControlURLOrDefault()
|
||||
b.serverURL = b.prefs.ControlURL
|
||||
hostinfo.RoutableIPs = append(hostinfo.RoutableIPs, b.prefs.AdvertiseRoutes...)
|
||||
hostinfo.RequestTags = append(hostinfo.RequestTags, b.prefs.AdvertiseTags...)
|
||||
if b.inServerMode || runtime.GOOS == "windows" {
|
||||
@@ -804,11 +689,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
debugFlags = append([]string{"netstack"}, debugFlags...)
|
||||
}
|
||||
|
||||
// TODO(apenwarr): The only way to change the ServerURL is to
|
||||
// re-run b.Start(), because this is the only place we create a
|
||||
// new controlclient. SetPrefs() allows you to overwrite ServerURL,
|
||||
// but it won't take effect until the next Start().
|
||||
cc, err := b.getNewControlClientFunc()(controlclient.Options{
|
||||
cc, err := controlclient.New(controlclient.Options{
|
||||
GetMachinePrivateKey: b.createGetMachinePrivateKeyFunc(),
|
||||
Logf: logger.WithPrefix(b.logf, "control: "),
|
||||
Persist: *persistv,
|
||||
@@ -852,13 +733,9 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
b.send(ipn.Notify{BackendLogID: &blid})
|
||||
b.send(ipn.Notify{Prefs: prefs})
|
||||
|
||||
if !loggedOut && b.hasNodeKey() {
|
||||
// Even if !WantRunning, we should verify our key, if there
|
||||
// is one. If you want tailscaled to be completely idle,
|
||||
// use logout instead.
|
||||
if wantRunning {
|
||||
cc.Login(nil, controlclient.LoginDefault)
|
||||
}
|
||||
b.stateMachine()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -916,7 +793,7 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
|
||||
localNets := localNetsB.IPSet()
|
||||
logNets := logNetsB.IPSet()
|
||||
|
||||
changed := deephash.UpdateHash(&b.filterHash, haveNetmap, addrs, packetFilter, localNets.Ranges(), logNets.Ranges(), shieldsUp)
|
||||
changed := deepprint.UpdateHash(&b.filterHash, haveNetmap, addrs, packetFilter, localNets.Ranges(), logNets.Ranges(), shieldsUp)
|
||||
if !changed {
|
||||
return
|
||||
}
|
||||
@@ -1143,7 +1020,7 @@ func (b *LocalBackend) popBrowserAuthNow() {
|
||||
b.mu.Lock()
|
||||
url := b.authURL
|
||||
b.interact = false
|
||||
b.authURL = "" // but NOT clearing authURLSticky
|
||||
b.authURL = ""
|
||||
b.mu.Unlock()
|
||||
|
||||
b.logf("popBrowserAuthNow: url=%v", url != "")
|
||||
@@ -1477,12 +1354,14 @@ func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
|
||||
b.mu.Unlock()
|
||||
return p1, nil
|
||||
}
|
||||
cc := b.cc
|
||||
b.logf("EditPrefs: %v", mp.Pretty())
|
||||
b.setPrefsLockedOnEntry("EditPrefs", p1) // does a b.mu.Unlock
|
||||
|
||||
// Note: don't perform any actions for the new prefs here. Not
|
||||
// every prefs change goes through EditPrefs. Put your actions
|
||||
// in setPrefsLocksOnEntry instead.
|
||||
if !p0.WantRunning && p1.WantRunning {
|
||||
b.logf("EditPrefs: transitioning to running; doing Login...")
|
||||
cc.Login(nil, controlclient.LoginDefault)
|
||||
}
|
||||
return p1, nil
|
||||
}
|
||||
|
||||
@@ -1516,7 +1395,6 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
||||
b.hostinfo = newHi
|
||||
hostInfoChanged := !oldHi.Equal(newHi)
|
||||
userID := b.userID
|
||||
cc := b.cc
|
||||
|
||||
b.mu.Unlock()
|
||||
|
||||
@@ -1556,11 +1434,6 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
||||
b.e.SetDERPMap(netMap.DERPMap)
|
||||
}
|
||||
|
||||
if !oldp.WantRunning && newp.WantRunning {
|
||||
b.logf("transitioning to running; doing Login...")
|
||||
cc.Login(nil, controlclient.LoginDefault)
|
||||
}
|
||||
|
||||
if oldp.WantRunning != newp.WantRunning {
|
||||
b.stateMachine()
|
||||
} else {
|
||||
@@ -1698,18 +1571,14 @@ func (b *LocalBackend) authReconfig() {
|
||||
|
||||
// If CorpDNS is false, dcfg remains the zero value.
|
||||
if uc.CorpDNS {
|
||||
addDefault := func(resolvers []tailcfg.DNSResolver) {
|
||||
for _, resolver := range resolvers {
|
||||
res, err := parseResolver(resolver)
|
||||
if err != nil {
|
||||
b.logf("skipping bad resolver: %v", err.Error())
|
||||
continue
|
||||
}
|
||||
dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, res)
|
||||
for _, resolver := range nm.DNS.Resolvers {
|
||||
res, err := parseResolver(resolver)
|
||||
if err != nil {
|
||||
b.logf(err.Error())
|
||||
continue
|
||||
}
|
||||
dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, res)
|
||||
}
|
||||
|
||||
addDefault(nm.DNS.Resolvers)
|
||||
if len(nm.DNS.Routes) > 0 {
|
||||
dcfg.Routes = map[dnsname.FQDN][]netaddr.IPPort{}
|
||||
}
|
||||
@@ -1734,6 +1603,7 @@ func (b *LocalBackend) authReconfig() {
|
||||
}
|
||||
dcfg.SearchDomains = append(dcfg.SearchDomains, fqdn)
|
||||
}
|
||||
dcfg.AuthoritativeSuffixes = magicDNSRootDomains(nm)
|
||||
set := func(name string, addrs []netaddr.IPPrefix) {
|
||||
if len(addrs) == 0 || name == "" {
|
||||
return
|
||||
@@ -1744,55 +1614,17 @@ func (b *LocalBackend) authReconfig() {
|
||||
}
|
||||
var ips []netaddr.IP
|
||||
for _, addr := range addrs {
|
||||
// Remove IPv6 addresses for now, as we don't
|
||||
// guarantee that the peer node actually can speak
|
||||
// IPv6 correctly.
|
||||
//
|
||||
// https://github.com/tailscale/tailscale/issues/1152
|
||||
// tracks adding the right capability reporting to
|
||||
// enable AAAA in MagicDNS.
|
||||
if addr.IP.Is6() {
|
||||
continue
|
||||
}
|
||||
ips = append(ips, addr.IP)
|
||||
}
|
||||
dcfg.Hosts[fqdn] = ips
|
||||
}
|
||||
if nm.DNS.Proxied { // actually means "enable MagicDNS"
|
||||
dcfg.AuthoritativeSuffixes = magicDNSRootDomains(nm)
|
||||
dcfg.Hosts = map[dnsname.FQDN][]netaddr.IP{}
|
||||
set(nm.Name, nm.Addresses)
|
||||
for _, peer := range nm.Peers {
|
||||
set(peer.Name, peer.Addresses)
|
||||
}
|
||||
}
|
||||
|
||||
// Set FallbackResolvers as the default resolvers in the
|
||||
// scenarios that can't handle a purely split-DNS config. See
|
||||
// https://github.com/tailscale/tailscale/issues/1743 for
|
||||
// details.
|
||||
switch {
|
||||
case len(dcfg.DefaultResolvers) != 0:
|
||||
// Default resolvers already set.
|
||||
case !uc.ExitNodeID.IsZero():
|
||||
// When using exit nodes, it's very likely the LAN
|
||||
// resolvers will become unreachable. So, force use of the
|
||||
// fallback resolvers until we implement DNS forwarding to
|
||||
// exit nodes.
|
||||
//
|
||||
// This is especially important on Apple OSes, where
|
||||
// adding the default route to the tunnel interface makes
|
||||
// it "primary", and we MUST provide VPN-sourced DNS
|
||||
// settings or we break all DNS resolution.
|
||||
//
|
||||
// https://github.com/tailscale/tailscale/issues/1713
|
||||
addDefault(nm.DNS.FallbackResolvers)
|
||||
case len(dcfg.Routes) == 0 && len(dcfg.Hosts) == 0 && len(dcfg.AuthoritativeSuffixes) == 0:
|
||||
// No settings requiring split DNS, no problem.
|
||||
case version.OS() == "android":
|
||||
// We don't support split DNS at all on Android yet.
|
||||
addDefault(nm.DNS.FallbackResolvers)
|
||||
}
|
||||
}
|
||||
|
||||
err = b.e.Reconfig(cfg, rcfg, &dcfg)
|
||||
@@ -1849,20 +1681,6 @@ func (b *LocalBackend) fileRootLocked(uid tailcfg.UserID) string {
|
||||
return dir
|
||||
}
|
||||
|
||||
// closePeerAPIListenersLocked closes any existing peer API listeners
|
||||
// and clears out the peer API server state.
|
||||
//
|
||||
// It does not kick off any Hostinfo update with new services.
|
||||
//
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) closePeerAPIListenersLocked() {
|
||||
b.peerAPIServer = nil
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
pln.Close()
|
||||
}
|
||||
b.peerAPIListeners = nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) initPeerAPIListener() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
@@ -1881,7 +1699,11 @@ func (b *LocalBackend) initPeerAPIListener() {
|
||||
}
|
||||
}
|
||||
|
||||
b.closePeerAPIListenersLocked()
|
||||
b.peerAPIServer = nil
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
pln.Close()
|
||||
}
|
||||
b.peerAPIListeners = nil
|
||||
|
||||
selfNode := b.netMap.SelfNode
|
||||
if len(b.netMap.Addresses) == 0 || selfNode == nil {
|
||||
@@ -1917,12 +1739,6 @@ func (b *LocalBackend) initPeerAPIListener() {
|
||||
if !skipListen {
|
||||
ln, err = ps.listen(a.IP, b.prevIfState)
|
||||
if err != nil {
|
||||
if runtime.GOOS == "windows" {
|
||||
// Expected for now. See Issue 1620.
|
||||
// But we fix it later in linkChange
|
||||
// ("peerAPIListeners too low").
|
||||
continue
|
||||
}
|
||||
b.logf("[unexpected] peerapi listen(%q) error: %v", a.IP, err)
|
||||
continue
|
||||
}
|
||||
@@ -1955,19 +1771,7 @@ func magicDNSRootDomains(nm *netmap.NetworkMap) []dnsname.FQDN {
|
||||
// TODO: propagate error
|
||||
return nil
|
||||
}
|
||||
ret := []dnsname.FQDN{
|
||||
fqdn,
|
||||
dnsname.FQDN("0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."),
|
||||
}
|
||||
for i := 64; i <= 127; i++ {
|
||||
fqdn, err = dnsname.ToFQDN(fmt.Sprintf("%d.100.in-addr.arpa.", i))
|
||||
if err != nil {
|
||||
// TODO: propagate error
|
||||
continue
|
||||
}
|
||||
ret = append(ret, fqdn)
|
||||
}
|
||||
return ret
|
||||
return []dnsname.FQDN{fqdn}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -2047,19 +1851,16 @@ func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router
|
||||
if !default6 {
|
||||
rs.Routes = append(rs.Routes, ipv6Default)
|
||||
}
|
||||
if runtime.GOOS == "linux" {
|
||||
// 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)
|
||||
}
|
||||
if prefs.ExitNodeAllowLANAccess {
|
||||
rs.LocalRoutes = ips.Prefixes()
|
||||
} else {
|
||||
// Explicitly add routes to the local network so that we do not
|
||||
// leak any traffic.
|
||||
rs.Routes = append(rs.Routes, ips.Prefixes()...)
|
||||
}
|
||||
ips, _, err := interfaceRoutes()
|
||||
if err != nil {
|
||||
b.logf("failed to discover interface ips: %v", err)
|
||||
}
|
||||
if prefs.ExitNodeAllowLANAccess {
|
||||
rs.LocalRoutes = ips.Prefixes()
|
||||
} else {
|
||||
// Explicitly add routes to the local network so that we do not
|
||||
// leak any traffic.
|
||||
rs.Routes = append(rs.Routes, ips.Prefixes()...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2106,33 +1907,25 @@ func applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *ipn.Prefs) {
|
||||
// happen".
|
||||
func (b *LocalBackend) enterState(newState ipn.State) {
|
||||
b.mu.Lock()
|
||||
oldState := b.state
|
||||
state := b.state
|
||||
b.state = newState
|
||||
prefs := b.prefs
|
||||
cc := b.cc
|
||||
netMap := b.netMap
|
||||
networkUp := b.prevIfState.AnyInterfaceUp()
|
||||
activeLogin := b.activeLogin
|
||||
authURL := b.authURL
|
||||
if newState == ipn.Running {
|
||||
b.authURL = ""
|
||||
b.authURLSticky = ""
|
||||
} else if oldState == ipn.Running {
|
||||
// Transitioning away from running.
|
||||
b.closePeerAPIListenersLocked()
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
if oldState == newState {
|
||||
if state == newState {
|
||||
return
|
||||
}
|
||||
b.logf("Switching ipn state %v -> %v (WantRunning=%v, nm=%v)",
|
||||
oldState, newState, prefs.WantRunning, netMap != nil)
|
||||
b.logf("Switching ipn state %v -> %v (WantRunning=%v)",
|
||||
state, newState, prefs.WantRunning)
|
||||
health.SetIPNState(newState.String(), prefs.WantRunning)
|
||||
b.send(ipn.Notify{State: &newState})
|
||||
|
||||
if cc != nil {
|
||||
cc.SetPaused((newState == ipn.Stopped && netMap != nil) || !networkUp)
|
||||
cc.SetPaused(newState == ipn.Stopped || !networkUp)
|
||||
}
|
||||
|
||||
switch newState {
|
||||
@@ -2165,15 +1958,6 @@ func (b *LocalBackend) enterState(newState ipn.State) {
|
||||
|
||||
}
|
||||
|
||||
func (b *LocalBackend) hasNodeKey() bool {
|
||||
// we can't use b.Prefs(), because it strips the keys, oops!
|
||||
b.mu.Lock()
|
||||
p := b.prefs
|
||||
b.mu.Unlock()
|
||||
|
||||
return p.Persist != nil && !p.Persist.PrivateNodeKey.IsZero()
|
||||
}
|
||||
|
||||
// nextState returns the state the backend seems to be in, based on
|
||||
// its internal state.
|
||||
func (b *LocalBackend) nextState() ipn.State {
|
||||
@@ -2183,37 +1967,18 @@ func (b *LocalBackend) nextState() ipn.State {
|
||||
cc = b.cc
|
||||
netMap = b.netMap
|
||||
state = b.state
|
||||
blocked = b.blocked
|
||||
wantRunning = b.prefs.WantRunning
|
||||
loggedOut = b.prefs.LoggedOut
|
||||
)
|
||||
b.mu.Unlock()
|
||||
|
||||
switch {
|
||||
case !wantRunning && !loggedOut && !blocked && b.hasNodeKey():
|
||||
return ipn.Stopped
|
||||
case netMap == nil:
|
||||
if cc.AuthCantContinue() || loggedOut {
|
||||
if cc.AuthCantContinue() {
|
||||
// Auth was interrupted or waiting for URL visit,
|
||||
// so it won't proceed without human help.
|
||||
return ipn.NeedsLogin
|
||||
} else if state == ipn.Stopped {
|
||||
// If we were already in the Stopped state, then
|
||||
// we can assume auth is in good shape (or we would
|
||||
// have been in NeedsLogin), so transition to Starting
|
||||
// right away.
|
||||
return ipn.Starting
|
||||
} else if state == ipn.NoState {
|
||||
// Our first time connecting to control, and we
|
||||
// don't know if we'll NeedsLogin or not yet.
|
||||
// UIs should print "Loading..." in this state.
|
||||
return ipn.NoState
|
||||
} else if state == ipn.Starting ||
|
||||
state == ipn.Running ||
|
||||
state == ipn.NeedsLogin {
|
||||
return state
|
||||
} else {
|
||||
b.logf("unexpected no-netmap state transition for %v", state)
|
||||
// Auth or map request needs to finish
|
||||
return state
|
||||
}
|
||||
case !wantRunning:
|
||||
@@ -2279,34 +2044,13 @@ func (b *LocalBackend) requestEngineStatusAndWait() {
|
||||
b.statusLock.Unlock()
|
||||
}
|
||||
|
||||
// ResetForClientDisconnect resets the backend for GUI clients running
|
||||
// in interactive (non-headless) mode. This is currently used only by
|
||||
// Windows. This causes all state to be cleared, lest an unrelated user
|
||||
// connect to tailscaled next. But it does not trigger a logout; we
|
||||
// don't want to the user to have to reauthenticate in the future
|
||||
// when they restart the GUI.
|
||||
func (b *LocalBackend) ResetForClientDisconnect() {
|
||||
defer b.enterState(ipn.Stopped)
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.logf("LocalBackend.ResetForClientDisconnect")
|
||||
|
||||
if b.cc != nil {
|
||||
go b.cc.Shutdown()
|
||||
b.cc = nil
|
||||
}
|
||||
b.stateKey = ""
|
||||
b.userID = ""
|
||||
b.setNetMapLocked(nil)
|
||||
b.prefs = new(ipn.Prefs)
|
||||
b.authURL = ""
|
||||
b.authURLSticky = ""
|
||||
b.activeLogin = ""
|
||||
}
|
||||
|
||||
// Logout tells the controlclient that we want to log out, and
|
||||
// transitions the local engine to the logged-out state without
|
||||
// waiting for controlclient to be in that state.
|
||||
//
|
||||
// NOTE(apenwarr): No easy way to persist logged-out status.
|
||||
// Maybe that's for the better; if someone logs out accidentally,
|
||||
// rebooting will fix it.
|
||||
func (b *LocalBackend) Logout() {
|
||||
b.logout(context.Background(), false)
|
||||
}
|
||||
@@ -2323,8 +2067,7 @@ func (b *LocalBackend) logout(ctx context.Context, sync bool) error {
|
||||
|
||||
b.EditPrefs(&ipn.MaskedPrefs{
|
||||
WantRunningSet: true,
|
||||
LoggedOutSet: true,
|
||||
Prefs: ipn.Prefs{WantRunning: false, LoggedOut: true},
|
||||
Prefs: ipn.Prefs{WantRunning: true},
|
||||
})
|
||||
|
||||
if cc == nil {
|
||||
@@ -2376,17 +2119,6 @@ func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) {
|
||||
cc.SetNetInfo(ni)
|
||||
}
|
||||
|
||||
func hasCapability(nm *netmap.NetworkMap, cap string) bool {
|
||||
if nm != nil && nm.SelfNode != nil {
|
||||
for _, c := range nm.SelfNode.Capabilities {
|
||||
if c == cap {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
var login string
|
||||
if nm != nil {
|
||||
@@ -2401,13 +2133,6 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
b.activeLogin = login
|
||||
}
|
||||
|
||||
// Determine if file sharing is enabled
|
||||
fs := hasCapability(nm, tailcfg.CapabilityFileSharing)
|
||||
if fs != b.capFileSharing {
|
||||
osshare.SetFileSharingEnabled(fs, b.logf)
|
||||
}
|
||||
b.capFileSharing = fs
|
||||
|
||||
if nm == nil {
|
||||
b.nodeByAddr = nil
|
||||
return
|
||||
@@ -2442,27 +2167,6 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
}
|
||||
}
|
||||
|
||||
// OperatorUserID returns the current pref's OperatorUser's ID (in
|
||||
// os/user.User.Uid string form), or the empty string if none.
|
||||
func (b *LocalBackend) OperatorUserID() string {
|
||||
b.mu.Lock()
|
||||
if b.prefs == nil {
|
||||
b.mu.Unlock()
|
||||
return ""
|
||||
}
|
||||
opUserName := b.prefs.OperatorUser
|
||||
b.mu.Unlock()
|
||||
if opUserName == "" {
|
||||
return ""
|
||||
}
|
||||
u, err := user.Lookup(opUserName)
|
||||
if err != nil {
|
||||
b.logf("error looking up operator %q uid: %v", opUserName, err)
|
||||
return ""
|
||||
}
|
||||
return u.Uid
|
||||
}
|
||||
|
||||
// TestOnlyPublicKeys returns the current machine and node public
|
||||
// keys. Used in tests only to facilitate automated node authorization
|
||||
// in the test harness.
|
||||
@@ -2516,7 +2220,20 @@ func (b *LocalBackend) OpenFile(name string) (rc io.ReadCloser, size int64, err
|
||||
func (b *LocalBackend) hasCapFileSharing() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.capFileSharing
|
||||
return b.hasCapFileSharingLocked()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) hasCapFileSharingLocked() bool {
|
||||
nm := b.netMap
|
||||
if nm == nil || nm.SelfNode == nil {
|
||||
return false
|
||||
}
|
||||
for _, c := range nm.SelfNode.Capabilities {
|
||||
if c == tailcfg.CapabilityFileSharing {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// FileTargets lists nodes that the current node can send files to.
|
||||
@@ -2525,13 +2242,13 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if !b.hasCapFileSharingLocked() {
|
||||
return nil, errors.New("file sharing not enabled by Tailscale admin")
|
||||
}
|
||||
nm := b.netMap
|
||||
if b.state != ipn.Running || nm == nil {
|
||||
return nil, errors.New("not connected")
|
||||
}
|
||||
if !b.capFileSharing {
|
||||
return nil, errors.New("file sharing not enabled by Tailscale admin")
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
if p.User != nm.User {
|
||||
continue
|
||||
|
||||
@@ -5,20 +5,14 @@
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
@@ -425,71 +419,3 @@ func TestPeerAPIBase(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type panicOnUseTransport struct{}
|
||||
|
||||
func (panicOnUseTransport) RoundTrip(*http.Request) (*http.Response, error) {
|
||||
panic("unexpected HTTP request")
|
||||
}
|
||||
|
||||
// Issue 1573: don't generate a machine key if we don't want to be running.
|
||||
func TestLazyMachineKeyGeneration(t *testing.T) {
|
||||
defer func(old bool) { panicOnMachineKeyGeneration = old }(panicOnMachineKeyGeneration)
|
||||
panicOnMachineKeyGeneration = true
|
||||
|
||||
var logf logger.Logf = logger.Discard
|
||||
store := new(ipn.MemoryStore)
|
||||
eng, err := wgengine.NewFakeUserspaceEngine(logf, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFakeUserspaceEngine: %v", err)
|
||||
}
|
||||
lb, err := NewLocalBackend(logf, "logid", store, eng)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLocalBackend: %v", err)
|
||||
}
|
||||
|
||||
lb.SetHTTPTestClient(&http.Client{
|
||||
Transport: panicOnUseTransport{}, // validate we don't send HTTP requests
|
||||
})
|
||||
|
||||
if err := lb.Start(ipn.Options{
|
||||
StateKey: ipn.GlobalDaemonStateKey,
|
||||
}); err != nil {
|
||||
t.Fatalf("Start: %v", err)
|
||||
}
|
||||
|
||||
// Give the controlclient package goroutines (if they're
|
||||
// accidentally started) extra time to schedule and run (and thus
|
||||
// hit panicOnUseTransport).
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func TestFileTargets(t *testing.T) {
|
||||
b := new(LocalBackend)
|
||||
_, err := b.FileTargets()
|
||||
if got, want := fmt.Sprint(err), "not connected"; got != want {
|
||||
t.Errorf("before connect: got %q; want %q", got, want)
|
||||
}
|
||||
|
||||
b.netMap = new(netmap.NetworkMap)
|
||||
_, err = b.FileTargets()
|
||||
if got, want := fmt.Sprint(err), "not connected"; got != want {
|
||||
t.Errorf("non-running netmap: got %q; want %q", got, want)
|
||||
}
|
||||
|
||||
b.state = ipn.Running
|
||||
_, err = b.FileTargets()
|
||||
if got, want := fmt.Sprint(err), "file sharing not enabled by Tailscale admin"; got != want {
|
||||
t.Errorf("without cap: got %q; want %q", got, want)
|
||||
}
|
||||
|
||||
b.capFileSharing = true
|
||||
got, err := b.FileTargets()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("unexpected %d peers", len(got))
|
||||
}
|
||||
// (other cases handled by TestPeerAPIBase above)
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
@@ -31,12 +30,6 @@ func TestLocalLogLines(t *testing.T) {
|
||||
})
|
||||
defer logListen.Close()
|
||||
|
||||
// Put a rate-limiter with a burst of 0 between the components below.
|
||||
// This instructs the rate-limiter to eliminate all logging that
|
||||
// isn't explicitly exempt from rate-limiting.
|
||||
// This lets the logListen tracker verify that the rate-limiter allows these key lines.
|
||||
logf := logger.RateLimitedFnWithClock(logListen.Logf, 5*time.Second, 0, 10, time.Now)
|
||||
|
||||
logid := func(hex byte) logtail.PublicID {
|
||||
var ret logtail.PublicID
|
||||
for i := 0; i < len(ret); i++ {
|
||||
@@ -48,12 +41,12 @@ func TestLocalLogLines(t *testing.T) {
|
||||
|
||||
// set up a LocalBackend, super bare bones. No functional data.
|
||||
store := &ipn.MemoryStore{}
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logf, 0)
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logListen.Logf, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
lb, err := NewLocalBackend(logf, idA.String(), store, e)
|
||||
lb, err := NewLocalBackend(logListen.Logf, idA.String(), store, e)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -68,7 +61,6 @@ func TestLocalLogLines(t *testing.T) {
|
||||
testWantRemain := func(wantRemain ...string) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
if remain := logListen.Check(); !reflect.DeepEqual(remain, wantRemain) {
|
||||
t.Helper()
|
||||
t.Errorf("remain %q, want %q", remain, wantRemain)
|
||||
}
|
||||
}
|
||||
@@ -83,30 +75,17 @@ func TestLocalLogLines(t *testing.T) {
|
||||
t.Run("after_prefs", testWantRemain("[v1] peer keys: %s", "[v1] v%v peers: %v"))
|
||||
|
||||
// log peers, peer keys
|
||||
lb.mu.Lock()
|
||||
lb.parseWgStatusLocked(&wgengine.Status{
|
||||
status := &wgengine.Status{
|
||||
Peers: []ipnstate.PeerStatusLite{{
|
||||
TxBytes: 10,
|
||||
RxBytes: 10,
|
||||
LastHandshake: time.Now(),
|
||||
NodeKey: tailcfg.NodeKey(key.NewPrivate()),
|
||||
}},
|
||||
})
|
||||
}
|
||||
lb.mu.Lock()
|
||||
lb.parseWgStatusLocked(status)
|
||||
lb.mu.Unlock()
|
||||
|
||||
t.Run("after_peers", testWantRemain())
|
||||
|
||||
// Log it again with different stats to ensure it's not dup-suppressed.
|
||||
logListen.Reset()
|
||||
lb.mu.Lock()
|
||||
lb.parseWgStatusLocked(&wgengine.Status{
|
||||
Peers: []ipnstate.PeerStatusLite{{
|
||||
TxBytes: 11,
|
||||
RxBytes: 12,
|
||||
LastHandshake: time.Now(),
|
||||
NodeKey: tailcfg.NodeKey(key.NewPrivate()),
|
||||
}},
|
||||
})
|
||||
lb.mu.Unlock()
|
||||
t.Run("after_second_peer_status", testWantRemain("SetPrefs: %v"))
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"hash/crc32"
|
||||
"html"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -19,18 +18,14 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -50,66 +45,20 @@ type peerAPIServer struct {
|
||||
// download directory (as *.partial files), rather than making
|
||||
// the frontend retrieve it over localapi HTTP and write it
|
||||
// somewhere itself. This is used on GUI macOS version.
|
||||
// In directFileMode, the peerapi doesn't do the final rename
|
||||
// from "foo.jpg.partial" to "foo.jpg".
|
||||
directFileMode bool
|
||||
}
|
||||
|
||||
const (
|
||||
// partialSuffix is the suffix appened to files while they're
|
||||
// still in the process of being transferred.
|
||||
partialSuffix = ".partial"
|
||||
|
||||
// deletedSuffix is the suffix for a deleted marker file
|
||||
// that's placed next to a file (without the suffix) that we
|
||||
// tried to delete, but Windows wouldn't let us. These are
|
||||
// only written on Windows (and in tests), but they're not
|
||||
// permitted to be uploaded directly on any platform, like
|
||||
// partial files.
|
||||
deletedSuffix = ".deleted"
|
||||
)
|
||||
|
||||
func validFilenameRune(r rune) bool {
|
||||
switch r {
|
||||
case '/':
|
||||
return false
|
||||
case '\\', ':', '*', '"', '<', '>', '|':
|
||||
// Invalid stuff on Windows, but we reject them everywhere
|
||||
// for now.
|
||||
// TODO(bradfitz): figure out a better plan. We initially just
|
||||
// wrote things to disk URL path-escaped, but that's gross
|
||||
// when debugging, and just moves the problem to callers.
|
||||
// So now we put the UTF-8 filenames on disk directly as
|
||||
// sent.
|
||||
return false
|
||||
}
|
||||
return unicode.IsPrint(r)
|
||||
}
|
||||
const partialSuffix = ".partial"
|
||||
|
||||
func (s *peerAPIServer) diskPath(baseName string) (fullPath string, ok bool) {
|
||||
if !utf8.ValidString(baseName) {
|
||||
return "", false
|
||||
}
|
||||
if strings.TrimSpace(baseName) != baseName {
|
||||
return "", false
|
||||
}
|
||||
if len(baseName) > 255 {
|
||||
return "", false
|
||||
}
|
||||
// TODO: validate unicode normalization form too? Varies by platform.
|
||||
clean := path.Clean(baseName)
|
||||
if clean != baseName ||
|
||||
clean == "." || clean == ".." ||
|
||||
strings.HasSuffix(clean, deletedSuffix) ||
|
||||
clean == "." ||
|
||||
strings.ContainsAny(clean, `/\`) ||
|
||||
strings.HasSuffix(clean, partialSuffix) {
|
||||
return "", false
|
||||
}
|
||||
for _, r := range baseName {
|
||||
if !validFilenameRune(r) {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
return filepath.Join(s.rootDir, baseName), true
|
||||
return filepath.Join(s.rootDir, strings.ReplaceAll(url.PathEscape(baseName), ":", "%3a")), true
|
||||
}
|
||||
|
||||
// hasFilesWaiting reports whether any files are buffered in the
|
||||
@@ -134,28 +83,11 @@ func (s *peerAPIServer) hasFilesWaiting() bool {
|
||||
for {
|
||||
des, err := f.ReadDir(10)
|
||||
for _, de := range des {
|
||||
name := de.Name()
|
||||
if strings.HasSuffix(name, partialSuffix) {
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(name, deletedSuffix) { // for Windows + tests
|
||||
// After we're done looping over files, then try
|
||||
// to delete this file. Don't do it proactively,
|
||||
// as the OS may return "foo.jpg.deleted" before "foo.jpg"
|
||||
// and we don't want to delete the ".deleted" file before
|
||||
// enumerating to the "foo.jpg" file.
|
||||
defer tryDeleteAgain(filepath.Join(s.rootDir, strings.TrimSuffix(name, deletedSuffix)))
|
||||
if strings.HasSuffix(de.Name(), partialSuffix) {
|
||||
continue
|
||||
}
|
||||
if de.Type().IsRegular() {
|
||||
_, err := os.Stat(filepath.Join(s.rootDir, name+deletedSuffix))
|
||||
if os.IsNotExist(err) {
|
||||
return true
|
||||
}
|
||||
if err == nil {
|
||||
tryDeleteAgain(filepath.Join(s.rootDir, name))
|
||||
continue
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
@@ -168,12 +100,6 @@ func (s *peerAPIServer) hasFilesWaiting() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// WaitingFiles returns the list of files that have been sent by a
|
||||
// peer that are waiting in the buffered "pick up" directory owned by
|
||||
// the Tailscale daemon.
|
||||
//
|
||||
// As a side effect, it also does any lazy deletion of files as
|
||||
// required by Windows.
|
||||
func (s *peerAPIServer) WaitingFiles() (ret []apitype.WaitingFile, err error) {
|
||||
if s.rootDir == "" {
|
||||
return nil, errors.New("peerapi disabled; no storage configured")
|
||||
@@ -186,7 +112,6 @@ func (s *peerAPIServer) WaitingFiles() (ret []apitype.WaitingFile, err error) {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
var deleted map[string]bool // "foo.jpg" => true (if "foo.jpg.deleted" exists)
|
||||
for {
|
||||
des, err := f.ReadDir(10)
|
||||
for _, de := range des {
|
||||
@@ -194,13 +119,6 @@ func (s *peerAPIServer) WaitingFiles() (ret []apitype.WaitingFile, err error) {
|
||||
if strings.HasSuffix(name, partialSuffix) {
|
||||
continue
|
||||
}
|
||||
if strings.HasSuffix(name, deletedSuffix) { // for Windows + tests
|
||||
if deleted == nil {
|
||||
deleted = map[string]bool{}
|
||||
}
|
||||
deleted[strings.TrimSuffix(name, deletedSuffix)] = true
|
||||
continue
|
||||
}
|
||||
if de.Type().IsRegular() {
|
||||
fi, err := de.Info()
|
||||
if err != nil {
|
||||
@@ -219,41 +137,9 @@ func (s *peerAPIServer) WaitingFiles() (ret []apitype.WaitingFile, err error) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if len(deleted) > 0 {
|
||||
// Filter out any return values "foo.jpg" where a
|
||||
// "foo.jpg.deleted" marker file exists on disk.
|
||||
all := ret
|
||||
ret = ret[:0]
|
||||
for _, wf := range all {
|
||||
if !deleted[wf.Name] {
|
||||
ret = append(ret, wf)
|
||||
}
|
||||
}
|
||||
// And do some opportunistic deleting while we're here.
|
||||
// Maybe Windows is done virus scanning the file we tried
|
||||
// to delete a long time ago and will let us delete it now.
|
||||
for name := range deleted {
|
||||
tryDeleteAgain(filepath.Join(s.rootDir, name))
|
||||
}
|
||||
}
|
||||
sort.Slice(ret, func(i, j int) bool { return ret[i].Name < ret[j].Name })
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// tryDeleteAgain tries to delete path (and path+deletedSuffix) after
|
||||
// it failed earlier. This happens on Windows when various anti-virus
|
||||
// tools hook into filesystem operations and have the file open still
|
||||
// while we're trying to delete it. In that case we instead mark it as
|
||||
// deleted (writing a "foo.jpg.deleted" marker file), but then we
|
||||
// later try to clean them up.
|
||||
//
|
||||
// fullPath is the full path to the file without the deleted suffix.
|
||||
func tryDeleteAgain(fullPath string) {
|
||||
if err := os.Remove(fullPath); err == nil || os.IsNotExist(err) {
|
||||
os.Remove(fullPath + deletedSuffix)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *peerAPIServer) DeleteFile(baseName string) error {
|
||||
if s.rootDir == "" {
|
||||
return errors.New("peerapi disabled; no storage configured")
|
||||
@@ -265,59 +151,11 @@ func (s *peerAPIServer) DeleteFile(baseName string) error {
|
||||
if !ok {
|
||||
return errors.New("bad filename")
|
||||
}
|
||||
var bo *backoff.Backoff
|
||||
logf := s.b.logf
|
||||
t0 := time.Now()
|
||||
for {
|
||||
err := os.Remove(path)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
err = redactErr(err)
|
||||
// Put a retry loop around deletes on Windows. Windows
|
||||
// file descriptor closes are effectively asynchronous,
|
||||
// as a bunch of hooks run on/after close, and we can't
|
||||
// necessarily delete the file for a while after close,
|
||||
// as we need to wait for everybody to be done with
|
||||
// it. (on Windows, unlike Unix, a file can't be deleted
|
||||
// if it's open anywhere)
|
||||
// So try a few times but ultimately just leave a
|
||||
// "foo.jpg.deleted" marker file to note that it's
|
||||
// deleted and we clean it up later.
|
||||
if runtime.GOOS == "windows" {
|
||||
if bo == nil {
|
||||
bo = backoff.NewBackoff("delete-retry", logf, 1*time.Second)
|
||||
}
|
||||
if time.Since(t0) < 5*time.Second {
|
||||
bo.BackOff(context.Background(), err)
|
||||
continue
|
||||
}
|
||||
if err := touchFile(path + deletedSuffix); err != nil {
|
||||
logf("peerapi: failed to leave deleted marker: %v", err)
|
||||
}
|
||||
}
|
||||
logf("peerapi: failed to DeleteFile: %v", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
err := os.Remove(path)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// redacted is a fake path name we use in errors, to avoid
|
||||
// accidentally logging actual filenames anywhere.
|
||||
const redacted = "redacted"
|
||||
|
||||
func redactErr(err error) error {
|
||||
if pe, ok := err.(*os.PathError); ok {
|
||||
pe.Path = redacted
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func touchFile(path string) error {
|
||||
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
return redactErr(err)
|
||||
}
|
||||
return f.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *peerAPIServer) OpenFile(baseName string) (rc io.ReadCloser, size int64, err error) {
|
||||
@@ -331,18 +169,14 @@ func (s *peerAPIServer) OpenFile(baseName string) (rc io.ReadCloser, size int64,
|
||||
if !ok {
|
||||
return nil, 0, errors.New("bad filename")
|
||||
}
|
||||
if fi, err := os.Stat(path + deletedSuffix); err == nil && fi.Mode().IsRegular() {
|
||||
tryDeleteAgain(path)
|
||||
return nil, 0, &fs.PathError{Op: "open", Path: redacted, Err: fs.ErrNotExist}
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, 0, redactErr(err)
|
||||
return nil, 0, err
|
||||
}
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, 0, redactErr(err)
|
||||
return nil, 0, err
|
||||
}
|
||||
return f, fi.Size(), nil
|
||||
}
|
||||
@@ -456,6 +290,7 @@ func (pln *peerAPIListener) serve() {
|
||||
remoteAddr: ipp,
|
||||
peerNode: peerNode,
|
||||
peerUser: peerUser,
|
||||
lb: pln.lb,
|
||||
}
|
||||
httpServer := &http.Server{
|
||||
Handler: h,
|
||||
@@ -489,6 +324,7 @@ type peerAPIHandler struct {
|
||||
isSelf bool // whether peerNode is owned by same user as this node
|
||||
peerNode *tailcfg.Node // peerNode is who's making the request
|
||||
peerUser tailcfg.UserProfile // profile of peerNode
|
||||
lb *LocalBackend
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) logf(format string, a ...interface{}) {
|
||||
@@ -497,11 +333,7 @@ func (h *peerAPIHandler) logf(format string, a ...interface{}) {
|
||||
|
||||
func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.URL.Path, "/v0/put/") {
|
||||
h.handlePeerPut(w, r)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/v0/goroutines" {
|
||||
h.handleServeGoroutines(w, r)
|
||||
h.put(w, r)
|
||||
return
|
||||
}
|
||||
who := h.peerUser.DisplayName
|
||||
@@ -575,7 +407,7 @@ func (f *incomingFile) PartialFile() ipn.PartialFile {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *peerAPIHandler) put(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.isSelf {
|
||||
http.Error(w, "not owner", http.StatusForbidden)
|
||||
return
|
||||
@@ -585,50 +417,32 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if r.Method != "PUT" {
|
||||
http.Error(w, "expected method PUT", http.StatusMethodNotAllowed)
|
||||
http.Error(w, "not method PUT", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if h.ps.rootDir == "" {
|
||||
http.Error(w, "no rootdir", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rawPath := r.URL.EscapedPath()
|
||||
suffix := strings.TrimPrefix(rawPath, "/v0/put/")
|
||||
if suffix == rawPath {
|
||||
http.Error(w, "misconfigured internals", 500)
|
||||
return
|
||||
}
|
||||
if suffix == "" {
|
||||
http.Error(w, "empty filename", 400)
|
||||
return
|
||||
}
|
||||
if strings.Contains(suffix, "/") {
|
||||
http.Error(w, "directories not supported", 400)
|
||||
return
|
||||
}
|
||||
baseName, err := url.PathUnescape(suffix)
|
||||
if err != nil {
|
||||
http.Error(w, "bad path encoding", 400)
|
||||
return
|
||||
}
|
||||
baseName := path.Base(r.URL.Path)
|
||||
dstFile, ok := h.ps.diskPath(baseName)
|
||||
if !ok {
|
||||
http.Error(w, "bad filename", 400)
|
||||
return
|
||||
}
|
||||
t0 := time.Now()
|
||||
// TODO(bradfitz): prevent same filename being sent by two peers at once
|
||||
partialFile := dstFile + partialSuffix
|
||||
f, err := os.Create(partialFile)
|
||||
if h.ps.directFileMode {
|
||||
dstFile += partialSuffix
|
||||
}
|
||||
f, err := os.Create(dstFile)
|
||||
if err != nil {
|
||||
h.logf("put Create error: %v", redactErr(err))
|
||||
h.logf("put Create error: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var success bool
|
||||
defer func() {
|
||||
if !success {
|
||||
os.Remove(partialFile)
|
||||
os.Remove(dstFile)
|
||||
}
|
||||
}()
|
||||
var finalSize int64
|
||||
@@ -642,13 +456,12 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
ph: h,
|
||||
}
|
||||
if h.ps.directFileMode {
|
||||
inFile.partialPath = partialFile
|
||||
inFile.partialPath = dstFile
|
||||
}
|
||||
h.ps.b.registerIncomingFile(inFile, true)
|
||||
defer h.ps.b.registerIncomingFile(inFile, false)
|
||||
n, err := io.Copy(inFile, r.Body)
|
||||
if err != nil {
|
||||
err = redactErr(err)
|
||||
f.Close()
|
||||
h.logf("put Copy error: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
@@ -656,7 +469,7 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
finalSize = n
|
||||
}
|
||||
if err := redactErr(f.Close()); err != nil {
|
||||
if err := f.Close(); err != nil {
|
||||
h.logf("put Close error: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -665,17 +478,9 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
if inFile != nil { // non-zero length; TODO: notify even for zero length
|
||||
inFile.markAndNotifyDone()
|
||||
}
|
||||
} else {
|
||||
if err := os.Rename(partialFile, dstFile); err != nil {
|
||||
err = redactErr(err)
|
||||
h.logf("put final rename: %v", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
d := time.Since(t0).Round(time.Second / 10)
|
||||
h.logf("got put of %s in %v from %v/%v", approxSize(finalSize), d, h.remoteAddr.IP, h.peerNode.ComputedName)
|
||||
h.logf("put of %s from %v/%v", approxSize(finalSize), h.remoteAddr.IP, h.peerNode.ComputedName)
|
||||
|
||||
// TODO: set modtime
|
||||
// TODO: some real response
|
||||
@@ -692,21 +497,5 @@ func approxSize(n int64) string {
|
||||
if n <= 1<<20 {
|
||||
return "<=1MB"
|
||||
}
|
||||
return fmt.Sprintf("~%dMB", n>>20)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeGoroutines(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.isSelf {
|
||||
http.Error(w, "not owner", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
var buf []byte
|
||||
for size := 4 << 10; size <= 2<<20; size *= 2 {
|
||||
buf = make([]byte, size)
|
||||
buf = buf[:runtime.Stack(buf, true)]
|
||||
if len(buf) < size {
|
||||
break
|
||||
}
|
||||
}
|
||||
w.Write(buf)
|
||||
return fmt.Sprintf("~%dMB", n/1<<20)
|
||||
}
|
||||
|
||||
@@ -1,573 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
type peerAPITestEnv struct {
|
||||
ph *peerAPIHandler
|
||||
rr *httptest.ResponseRecorder
|
||||
logBuf bytes.Buffer
|
||||
}
|
||||
|
||||
func (e *peerAPITestEnv) logf(format string, a ...interface{}) {
|
||||
fmt.Fprintf(&e.logBuf, format, a...)
|
||||
}
|
||||
|
||||
type check func(*testing.T, *peerAPITestEnv)
|
||||
|
||||
func checks(vv ...check) []check { return vv }
|
||||
|
||||
func httpStatus(wantStatus int) check {
|
||||
return func(t *testing.T, e *peerAPITestEnv) {
|
||||
if res := e.rr.Result(); res.StatusCode != wantStatus {
|
||||
t.Errorf("HTTP response code = %v; want %v", res.Status, wantStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func bodyContains(sub string) check {
|
||||
return func(t *testing.T, e *peerAPITestEnv) {
|
||||
if body := e.rr.Body.String(); !strings.Contains(body, sub) {
|
||||
t.Errorf("HTTP response body does not contain %q; got: %s", sub, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func bodyNotContains(sub string) check {
|
||||
return func(t *testing.T, e *peerAPITestEnv) {
|
||||
if body := e.rr.Body.String(); strings.Contains(body, sub) {
|
||||
t.Errorf("HTTP response body unexpectedly contains %q; got: %s", sub, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fileHasSize(name string, size int) check {
|
||||
return func(t *testing.T, e *peerAPITestEnv) {
|
||||
root := e.ph.ps.rootDir
|
||||
if root == "" {
|
||||
t.Errorf("no rootdir; can't check whether %q has size %v", name, size)
|
||||
return
|
||||
}
|
||||
path := filepath.Join(root, name)
|
||||
if fi, err := os.Stat(path); err != nil {
|
||||
t.Errorf("fileHasSize(%q, %v): %v", name, size, err)
|
||||
} else if fi.Size() != int64(size) {
|
||||
t.Errorf("file %q has size %v; want %v", name, fi.Size(), size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fileHasContents(name string, want string) check {
|
||||
return func(t *testing.T, e *peerAPITestEnv) {
|
||||
root := e.ph.ps.rootDir
|
||||
if root == "" {
|
||||
t.Errorf("no rootdir; can't check contents of %q", name)
|
||||
return
|
||||
}
|
||||
path := filepath.Join(root, name)
|
||||
got, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Errorf("fileHasContents: %v", err)
|
||||
return
|
||||
}
|
||||
if string(got) != want {
|
||||
t.Errorf("file contents = %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func hexAll(v string) string {
|
||||
var sb strings.Builder
|
||||
for i := 0; i < len(v); i++ {
|
||||
fmt.Fprintf(&sb, "%%%02x", v[i])
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func TestHandlePeerAPI(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
isSelf bool // the peer sending the request is owned by us
|
||||
capSharing bool // self node has file sharing capabilty
|
||||
omitRoot bool // don't configure
|
||||
req *http.Request
|
||||
checks []check
|
||||
}{
|
||||
{
|
||||
name: "not_peer_api",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("GET", "/", nil),
|
||||
checks: checks(
|
||||
httpStatus(200),
|
||||
bodyContains("This is my Tailscale device."),
|
||||
bodyContains("You are the owner of this node."),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "not_peer_api_not_owner",
|
||||
isSelf: false,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("GET", "/", nil),
|
||||
checks: checks(
|
||||
httpStatus(200),
|
||||
bodyContains("This is my Tailscale device."),
|
||||
bodyNotContains("You are the owner of this node."),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "peer_api_goroutines_deny",
|
||||
isSelf: false,
|
||||
req: httptest.NewRequest("GET", "/v0/goroutines", nil),
|
||||
checks: checks(httpStatus(403)),
|
||||
},
|
||||
{
|
||||
name: "peer_api_goroutines",
|
||||
isSelf: true,
|
||||
req: httptest.NewRequest("GET", "/v0/goroutines", nil),
|
||||
checks: checks(
|
||||
httpStatus(200),
|
||||
bodyContains("ServeHTTP"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "reject_non_owner_put",
|
||||
isSelf: false,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/foo", nil),
|
||||
checks: checks(
|
||||
httpStatus(http.StatusForbidden),
|
||||
bodyContains("not owner"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "owner_without_cap",
|
||||
isSelf: true,
|
||||
capSharing: false,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/foo", nil),
|
||||
checks: checks(
|
||||
httpStatus(http.StatusForbidden),
|
||||
bodyContains("file sharing not enabled by Tailscale admin"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "owner_with_cap_no_rootdir",
|
||||
omitRoot: true,
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/foo", nil),
|
||||
checks: checks(
|
||||
httpStatus(http.StatusInternalServerError),
|
||||
bodyContains("no rootdir"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "bad_method",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("POST", "/v0/put/foo", nil),
|
||||
checks: checks(
|
||||
httpStatus(405),
|
||||
bodyContains("expected method PUT"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "put_zero_length",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/foo", nil),
|
||||
checks: checks(
|
||||
httpStatus(200),
|
||||
bodyContains("{}"),
|
||||
fileHasSize("foo", 0),
|
||||
fileHasContents("foo", ""),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "put_non_zero_length_content_length",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/foo", strings.NewReader("contents")),
|
||||
checks: checks(
|
||||
httpStatus(200),
|
||||
bodyContains("{}"),
|
||||
fileHasSize("foo", len("contents")),
|
||||
fileHasContents("foo", "contents"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "put_non_zero_length_chunked",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/foo", struct{ io.Reader }{strings.NewReader("contents")}),
|
||||
checks: checks(
|
||||
httpStatus(200),
|
||||
bodyContains("{}"),
|
||||
fileHasSize("foo", len("contents")),
|
||||
fileHasContents("foo", "contents"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "bad_filename_partial",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/foo.partial", nil),
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "bad_filename_deleted",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/foo.deleted", nil),
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "bad_filename_dot",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/.", nil),
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "bad_filename_empty",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/", nil),
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("empty filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "bad_filename_slash",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/foo/bar", nil),
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("directories not supported"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "bad_filename_encoded_dot",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/"+hexAll("."), nil),
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "bad_filename_encoded_slash",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/"+hexAll("/"), nil),
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "bad_filename_encoded_backslash",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/"+hexAll("\\"), nil),
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "bad_filename_encoded_dotdot",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/"+hexAll(".."), nil),
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "bad_filename_encoded_dotdot_out",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/"+hexAll("foo/../../../../../etc/passwd"), nil),
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "put_spaces_and_caps",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/"+hexAll("Foo Bar.dat"), strings.NewReader("baz")),
|
||||
checks: checks(
|
||||
httpStatus(200),
|
||||
bodyContains("{}"),
|
||||
fileHasContents("Foo Bar.dat", "baz"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "put_unicode",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/"+hexAll("Томас и его друзья.mp3"), strings.NewReader("главный озорник")),
|
||||
checks: checks(
|
||||
httpStatus(200),
|
||||
bodyContains("{}"),
|
||||
fileHasContents("Томас и его друзья.mp3", "главный озорник"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "put_invalid_utf8",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/"+(hexAll("😜")[:3]), nil),
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "put_invalid_null",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/%00", nil),
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "put_invalid_non_printable",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/%01", nil),
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "put_invalid_colon",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/"+hexAll("nul:"), nil),
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "put_invalid_surrounding_whitespace",
|
||||
isSelf: true,
|
||||
capSharing: true,
|
||||
req: httptest.NewRequest("PUT", "/v0/put/"+hexAll(" foo "), nil),
|
||||
checks: checks(
|
||||
httpStatus(400),
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var e peerAPITestEnv
|
||||
lb := &LocalBackend{
|
||||
logf: e.logf,
|
||||
capFileSharing: tt.capSharing,
|
||||
}
|
||||
e.ph = &peerAPIHandler{
|
||||
isSelf: tt.isSelf,
|
||||
peerNode: &tailcfg.Node{
|
||||
ComputedName: "some-peer-name",
|
||||
},
|
||||
ps: &peerAPIServer{
|
||||
b: lb,
|
||||
},
|
||||
}
|
||||
var rootDir string
|
||||
if !tt.omitRoot {
|
||||
rootDir = t.TempDir()
|
||||
e.ph.ps.rootDir = rootDir
|
||||
}
|
||||
e.rr = httptest.NewRecorder()
|
||||
e.ph.ServeHTTP(e.rr, tt.req)
|
||||
for _, f := range tt.checks {
|
||||
f(t, &e)
|
||||
}
|
||||
if t.Failed() && rootDir != "" {
|
||||
t.Logf("Contents of %s:", rootDir)
|
||||
des, _ := fs.ReadDir(os.DirFS(rootDir), ".")
|
||||
for _, de := range des {
|
||||
fi, err := de.Info()
|
||||
if err != nil {
|
||||
t.Log(err)
|
||||
} else {
|
||||
t.Logf(" %v %5d %s", fi.Mode(), fi.Size(), de.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Windows likes to hold on to file descriptors for some indeterminate
|
||||
// amount of time after you close them and not let you delete them for
|
||||
// a bit. So test that we work around that sufficiently.
|
||||
func TestFileDeleteRace(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ps := &peerAPIServer{
|
||||
b: &LocalBackend{
|
||||
logf: t.Logf,
|
||||
capFileSharing: true,
|
||||
},
|
||||
rootDir: dir,
|
||||
}
|
||||
ph := &peerAPIHandler{
|
||||
isSelf: true,
|
||||
peerNode: &tailcfg.Node{
|
||||
ComputedName: "some-peer-name",
|
||||
},
|
||||
ps: ps,
|
||||
}
|
||||
buf := make([]byte, 2<<20)
|
||||
for i := 0; i < 30; i++ {
|
||||
rr := httptest.NewRecorder()
|
||||
ph.ServeHTTP(rr, httptest.NewRequest("PUT", "/v0/put/foo.txt", bytes.NewReader(buf[:rand.Intn(len(buf))])))
|
||||
if res := rr.Result(); res.StatusCode != 200 {
|
||||
t.Fatal(res.Status)
|
||||
}
|
||||
wfs, err := ps.WaitingFiles()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(wfs) != 1 {
|
||||
t.Fatalf("waiting files = %d; want 1", len(wfs))
|
||||
}
|
||||
|
||||
if err := ps.DeleteFile("foo.txt"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wfs, err = ps.WaitingFiles()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(wfs) != 0 {
|
||||
t.Fatalf("waiting files = %d; want 0", len(wfs))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tests "foo.jpg.deleted" marks (for Windows).
|
||||
func TestDeletedMarkers(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
ps := &peerAPIServer{
|
||||
b: &LocalBackend{
|
||||
logf: t.Logf,
|
||||
capFileSharing: true,
|
||||
},
|
||||
rootDir: dir,
|
||||
}
|
||||
|
||||
nothingWaiting := func() {
|
||||
t.Helper()
|
||||
ps.knownEmpty.Set(false)
|
||||
if ps.hasFilesWaiting() {
|
||||
t.Fatal("unexpected files waiting")
|
||||
}
|
||||
}
|
||||
touch := func(base string) {
|
||||
t.Helper()
|
||||
if err := touchFile(filepath.Join(dir, base)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
wantEmptyTempDir := func() {
|
||||
t.Helper()
|
||||
if fis, err := ioutil.ReadDir(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if len(fis) > 0 && runtime.GOOS != "windows" {
|
||||
for _, fi := range fis {
|
||||
t.Errorf("unexpected file in tempdir: %q", fi.Name())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nothingWaiting()
|
||||
wantEmptyTempDir()
|
||||
|
||||
touch("foo.jpg.deleted")
|
||||
nothingWaiting()
|
||||
wantEmptyTempDir()
|
||||
|
||||
touch("foo.jpg.deleted")
|
||||
touch("foo.jpg")
|
||||
nothingWaiting()
|
||||
wantEmptyTempDir()
|
||||
|
||||
touch("foo.jpg.deleted")
|
||||
touch("foo.jpg")
|
||||
wf, err := ps.WaitingFiles()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(wf) != 0 {
|
||||
t.Fatalf("WaitingFiles = %d; want 0", len(wf))
|
||||
}
|
||||
wantEmptyTempDir()
|
||||
|
||||
touch("foo.jpg.deleted")
|
||||
touch("foo.jpg")
|
||||
if rc, _, err := ps.OpenFile("foo.jpg"); err == nil {
|
||||
rc.Close()
|
||||
t.Fatal("unexpected foo.jpg open")
|
||||
}
|
||||
wantEmptyTempDir()
|
||||
|
||||
// And verify basics still work in non-deleted cases.
|
||||
touch("foo.jpg")
|
||||
touch("bar.jpg.deleted")
|
||||
if wf, err := ps.WaitingFiles(); err != nil {
|
||||
t.Error(err)
|
||||
} else if len(wf) != 1 {
|
||||
t.Errorf("WaitingFiles = %d; want 1", len(wf))
|
||||
} else if wf[0].Name != "foo.jpg" {
|
||||
t.Errorf("unexpected waiting file %+v", wf[0])
|
||||
}
|
||||
if rc, _, err := ps.OpenFile("foo.jpg"); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
rc.Close()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,859 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
// notifyThrottler receives notifications from an ipn.Backend, blocking
|
||||
// (with eventual timeout and t.Fatal) if there are too many and complaining
|
||||
// (also with t.Fatal) if they are too few.
|
||||
type notifyThrottler struct {
|
||||
t *testing.T
|
||||
|
||||
// ch gets replaced frequently. Lock the mutex before getting or
|
||||
// setting it, but not while waiting on it.
|
||||
mu sync.Mutex
|
||||
ch chan ipn.Notify
|
||||
}
|
||||
|
||||
// expect tells the throttler to expect count upcoming notifications.
|
||||
func (nt *notifyThrottler) expect(count int) {
|
||||
nt.mu.Lock()
|
||||
nt.ch = make(chan ipn.Notify, count)
|
||||
nt.mu.Unlock()
|
||||
}
|
||||
|
||||
// put adds one notification into the throttler's queue.
|
||||
func (nt *notifyThrottler) put(n ipn.Notify) {
|
||||
nt.mu.Lock()
|
||||
ch := nt.ch
|
||||
nt.mu.Unlock()
|
||||
|
||||
select {
|
||||
case ch <- n:
|
||||
return
|
||||
default:
|
||||
nt.t.Fatalf("put: channel full: %v", n)
|
||||
}
|
||||
}
|
||||
|
||||
// drain pulls the notifications out of the queue, asserting that there are
|
||||
// exactly count notifications that have been put so far.
|
||||
func (nt *notifyThrottler) drain(count int) []ipn.Notify {
|
||||
nt.mu.Lock()
|
||||
ch := nt.ch
|
||||
nt.mu.Unlock()
|
||||
|
||||
nn := []ipn.Notify{}
|
||||
for i := 0; i < count; i++ {
|
||||
select {
|
||||
case n := <-ch:
|
||||
nn = append(nn, n)
|
||||
case <-time.After(6 * time.Second):
|
||||
nt.t.Fatalf("drain: channel empty after %d/%d", i, count)
|
||||
}
|
||||
}
|
||||
|
||||
// no more notifications expected
|
||||
close(ch)
|
||||
|
||||
return nn
|
||||
}
|
||||
|
||||
// mockControl is a mock implementation of controlclient.Client.
|
||||
// Much of the backend state machine depends on callbacks and state
|
||||
// in the controlclient.Client, so by controlling it, we can check that
|
||||
// the state machine works as expected.
|
||||
type mockControl struct {
|
||||
opts controlclient.Options
|
||||
logf logger.Logf
|
||||
statusFunc func(controlclient.Status)
|
||||
|
||||
mu sync.Mutex
|
||||
calls []string
|
||||
authBlocked bool
|
||||
persist persist.Persist
|
||||
machineKey wgkey.Private
|
||||
}
|
||||
|
||||
func newMockControl() *mockControl {
|
||||
return &mockControl{
|
||||
calls: []string{},
|
||||
authBlocked: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (cc *mockControl) SetStatusFunc(fn func(controlclient.Status)) {
|
||||
cc.statusFunc = fn
|
||||
}
|
||||
|
||||
func (cc *mockControl) populateKeys() (newKeys bool) {
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
|
||||
if cc.machineKey.IsZero() {
|
||||
cc.logf("Copying machineKey.")
|
||||
cc.machineKey, _ = cc.opts.GetMachinePrivateKey()
|
||||
newKeys = true
|
||||
}
|
||||
|
||||
if cc.persist.PrivateNodeKey.IsZero() {
|
||||
cc.logf("Generating a new nodekey.")
|
||||
cc.persist.OldPrivateNodeKey = cc.persist.PrivateNodeKey
|
||||
cc.persist.PrivateNodeKey, _ = wgkey.NewPrivate()
|
||||
newKeys = true
|
||||
}
|
||||
|
||||
return newKeys
|
||||
}
|
||||
|
||||
// send publishes a controlclient.Status notification upstream.
|
||||
// (In our tests here, upstream is the ipnlocal.Local instance.)
|
||||
func (cc *mockControl) send(err error, url string, loginFinished bool, nm *netmap.NetworkMap) {
|
||||
if cc.statusFunc != nil {
|
||||
s := controlclient.Status{
|
||||
URL: url,
|
||||
NetMap: nm,
|
||||
Persist: &cc.persist,
|
||||
}
|
||||
if err != nil {
|
||||
s.Err = err.Error()
|
||||
}
|
||||
if loginFinished {
|
||||
s.LoginFinished = &empty.Message{}
|
||||
}
|
||||
cc.statusFunc(s)
|
||||
}
|
||||
}
|
||||
|
||||
// called records that a particular function name was called.
|
||||
func (cc *mockControl) called(s string) {
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
|
||||
cc.calls = append(cc.calls, s)
|
||||
}
|
||||
|
||||
// getCalls returns the list of functions that have been called since the
|
||||
// last time getCalls was run.
|
||||
func (cc *mockControl) getCalls() []string {
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
|
||||
r := cc.calls
|
||||
cc.calls = []string{}
|
||||
return r
|
||||
}
|
||||
|
||||
// setAuthBlocked changes the return value of AuthCantContinue.
|
||||
// Auth is blocked if you haven't called Login, the control server hasn't
|
||||
// provided an auth URL, or it has provided an auth URL and you haven't
|
||||
// visited it yet.
|
||||
func (cc *mockControl) setAuthBlocked(blocked bool) {
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
|
||||
cc.authBlocked = blocked
|
||||
}
|
||||
|
||||
// Shutdown disconnects the client.
|
||||
//
|
||||
// Note that in a normal controlclient, Shutdown would be the last thing you
|
||||
// do before discarding the object. In this mock, we don't actually discard
|
||||
// the object, but if you see a call to Shutdown, you should always see a
|
||||
// call to New right after it, if the object continues to be used.
|
||||
// (Note that "New" is the ccGen function here; it means ipn.Backend wanted
|
||||
// to create an entirely new controlclient.)
|
||||
func (cc *mockControl) Shutdown() {
|
||||
cc.logf("Shutdown")
|
||||
cc.called("Shutdown")
|
||||
}
|
||||
|
||||
// Login starts a login process.
|
||||
// Note that in this mock, we don't automatically generate notifications
|
||||
// about the progress of the login operation. You have to call setAuthBlocked()
|
||||
// and send() as required by the test.
|
||||
func (cc *mockControl) Login(t *tailcfg.Oauth2Token, flags controlclient.LoginFlags) {
|
||||
cc.logf("Login token=%v flags=%v", t, flags)
|
||||
cc.called("Login")
|
||||
newKeys := cc.populateKeys()
|
||||
|
||||
interact := (flags & controlclient.LoginInteractive) != 0
|
||||
cc.logf("Login: interact=%v newKeys=%v", interact, newKeys)
|
||||
cc.setAuthBlocked(interact || newKeys)
|
||||
}
|
||||
|
||||
func (cc *mockControl) StartLogout() {
|
||||
cc.logf("StartLogout")
|
||||
cc.called("StartLogout")
|
||||
}
|
||||
|
||||
func (cc *mockControl) Logout(ctx context.Context) error {
|
||||
cc.logf("Logout")
|
||||
cc.called("Logout")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cc *mockControl) SetPaused(paused bool) {
|
||||
cc.logf("SetPaused=%v", paused)
|
||||
if paused {
|
||||
cc.called("pause")
|
||||
} else {
|
||||
cc.called("unpause")
|
||||
}
|
||||
}
|
||||
|
||||
func (cc *mockControl) AuthCantContinue() bool {
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
|
||||
return cc.authBlocked
|
||||
}
|
||||
|
||||
func (cc *mockControl) SetHostinfo(hi *tailcfg.Hostinfo) {
|
||||
cc.logf("SetHostinfo: %v", *hi)
|
||||
cc.called("SetHostinfo")
|
||||
}
|
||||
|
||||
func (cc *mockControl) SetNetInfo(ni *tailcfg.NetInfo) {
|
||||
cc.called("SetNetinfo")
|
||||
cc.logf("SetNetInfo: %v", *ni)
|
||||
cc.called("SetNetInfo")
|
||||
}
|
||||
|
||||
func (cc *mockControl) UpdateEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) {
|
||||
// validate endpoint information here?
|
||||
cc.logf("UpdateEndpoints: lp=%v ep=%v", localPort, endpoints)
|
||||
cc.called("UpdateEndpoints")
|
||||
}
|
||||
|
||||
// A very precise test of the sequence of function calls generated by
|
||||
// ipnlocal.Local into its controlclient instance, and the events it
|
||||
// produces upstream into the UI.
|
||||
//
|
||||
// [apenwarr] Normally I'm not a fan of "mock" style tests, but the precise
|
||||
// sequence of this state machine is so important for writing our multiple
|
||||
// frontends, that it's worth validating it all in one place.
|
||||
//
|
||||
// Any changes that affect this test will most likely require carefully
|
||||
// re-testing all our GUIs (and the CLI) to make sure we didn't break
|
||||
// anything.
|
||||
//
|
||||
// Note also that this test doesn't have any timers, goroutines, or duplicate
|
||||
// detection. It expects messages to be produced in exactly the right order,
|
||||
// with no duplicates, without doing network activity (other than through
|
||||
// controlclient, which we fake, so there's no network activity there either).
|
||||
//
|
||||
// TODO: A few messages that depend on magicsock (which actually might have
|
||||
// network delays) are just ignored for now, which makes the test
|
||||
// predictable, but maybe a bit less thorough. This is more of an overall
|
||||
// state machine test than a test of the wgengine+magicsock integration.
|
||||
func TestStateMachine(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
|
||||
logf := t.Logf
|
||||
store := new(ipn.MemoryStore)
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logf, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFakeUserspaceEngine: %v", err)
|
||||
}
|
||||
|
||||
cc := newMockControl()
|
||||
b, err := NewLocalBackend(logf, "logid", store, e)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLocalBackend: %v", err)
|
||||
}
|
||||
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
|
||||
cc.mu.Lock()
|
||||
cc.opts = opts
|
||||
cc.logf = opts.Logf
|
||||
cc.authBlocked = true
|
||||
cc.persist = cc.opts.Persist
|
||||
cc.mu.Unlock()
|
||||
|
||||
cc.logf("ccGen: new mockControl.")
|
||||
cc.called("New")
|
||||
return cc, nil
|
||||
})
|
||||
|
||||
notifies := ¬ifyThrottler{t: t}
|
||||
notifies.expect(0)
|
||||
|
||||
b.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if n.State != nil ||
|
||||
n.Prefs != nil ||
|
||||
n.BrowseToURL != nil ||
|
||||
n.LoginFinished != nil {
|
||||
logf("\n%v\n\n", n)
|
||||
notifies.put(n)
|
||||
} else {
|
||||
logf("\n(ignored) %v\n\n", n)
|
||||
}
|
||||
})
|
||||
|
||||
// Check that it hasn't called us right away.
|
||||
// The state machine should be idle until we call Start().
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
|
||||
// Start the state machine.
|
||||
// Since !WantRunning by default, it'll create a controlclient,
|
||||
// but not ask it to do anything yet.
|
||||
t.Logf("\n\nStart")
|
||||
notifies.expect(2)
|
||||
c.Assert(b.Start(ipn.Options{StateKey: ipn.GlobalDaemonStateKey}), qt.IsNil)
|
||||
{
|
||||
// BUG: strictly, it should pause, not unpause, here, since !WantRunning.
|
||||
c.Assert([]string{"New", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
|
||||
nn := notifies.drain(2)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].State, qt.Not(qt.IsNil))
|
||||
prefs := *nn[0].Prefs
|
||||
// Note: a totally fresh system has Prefs.LoggedOut=false by
|
||||
// default. We are logged out, but not because the user asked
|
||||
// for it, so it doesn't count as Prefs.LoggedOut==true.
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[1].State)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
// Restart the state machine.
|
||||
// It's designed to handle frontends coming and going sporadically.
|
||||
// Make the sure the restart not only works, but generates the same
|
||||
// events as the first time, so UIs always know what to expect.
|
||||
t.Logf("\n\nStart2")
|
||||
notifies.expect(2)
|
||||
c.Assert(b.Start(ipn.Options{StateKey: ipn.GlobalDaemonStateKey}), qt.IsNil)
|
||||
{
|
||||
// BUG: strictly, it should pause, not unpause, here, since !WantRunning.
|
||||
c.Assert([]string{"Shutdown", "New", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
|
||||
nn := notifies.drain(2)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].State, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[1].State)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
// Start non-interactive login with no token.
|
||||
// This will ask controlclient to start its own Login() process,
|
||||
// then wait for us to respond.
|
||||
t.Logf("\n\nLogin (noninteractive)")
|
||||
notifies.expect(0)
|
||||
b.Login(nil)
|
||||
{
|
||||
c.Assert(cc.getCalls(), qt.DeepEquals, []string{"Login"})
|
||||
notifies.drain(0)
|
||||
// Note: WantRunning isn't true yet. It'll switch to true
|
||||
// after a successful login finishes.
|
||||
// (This behaviour is needed so that b.Login() won't
|
||||
// start connecting to an old account right away, if one
|
||||
// exists when you launch another login.)
|
||||
}
|
||||
|
||||
// Attempted non-interactive login with no key; indicate that
|
||||
// the user needs to visit a login URL.
|
||||
t.Logf("\n\nLogin (url response)")
|
||||
notifies.expect(1)
|
||||
url1 := "http://localhost:1/1"
|
||||
cc.send(nil, url1, false, nil)
|
||||
{
|
||||
c.Assert(cc.getCalls(), qt.DeepEquals, []string{})
|
||||
|
||||
// ...but backend eats that notification, because the user
|
||||
// didn't explicitly request interactive login yet, and
|
||||
// we're already in NeedsLogin state.
|
||||
nn := notifies.drain(1)
|
||||
|
||||
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
|
||||
}
|
||||
|
||||
// Now we'll try an interactive login.
|
||||
// Since we provided an interactive URL earlier, this shouldn't
|
||||
// ask control to do anything. Instead backend will emit an event
|
||||
// indicating that the UI should browse to the given URL.
|
||||
t.Logf("\n\nLogin (interactive)")
|
||||
notifies.expect(1)
|
||||
b.StartLoginInteractive()
|
||||
{
|
||||
nn := notifies.drain(1)
|
||||
// BUG: UpdateEndpoints shouldn't be called yet.
|
||||
// We're still not logged in so there's nothing we can do
|
||||
// with it. (And empirically, it's providing an empty list
|
||||
// of endpoints.)
|
||||
c.Assert([]string{"UpdateEndpoints"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].BrowseToURL, qt.Not(qt.IsNil))
|
||||
c.Assert(url1, qt.Equals, *nn[0].BrowseToURL)
|
||||
}
|
||||
|
||||
// Sometimes users press the Login button again, in the middle of
|
||||
// a login sequence. For example, they might have closed their
|
||||
// browser window without logging in, or they waited too long and
|
||||
// the login URL expired. If they start another interactive login,
|
||||
// we must always get a *new* login URL first.
|
||||
t.Logf("\n\nLogin2 (interactive)")
|
||||
notifies.expect(0)
|
||||
b.StartLoginInteractive()
|
||||
{
|
||||
notifies.drain(0)
|
||||
// backend asks control for another login sequence
|
||||
c.Assert([]string{"Login"}, qt.DeepEquals, cc.getCalls())
|
||||
}
|
||||
|
||||
// Provide a new interactive login URL.
|
||||
t.Logf("\n\nLogin2 (url response)")
|
||||
notifies.expect(1)
|
||||
url2 := "http://localhost:1/2"
|
||||
cc.send(nil, url2, false, nil)
|
||||
{
|
||||
// BUG: UpdateEndpoints again, this is getting silly.
|
||||
c.Assert([]string{"UpdateEndpoints"}, qt.DeepEquals, cc.getCalls())
|
||||
|
||||
// This time, backend should emit it to the UI right away,
|
||||
// because the UI is anxiously awaiting a new URL to visit.
|
||||
nn := notifies.drain(1)
|
||||
c.Assert(nn[0].BrowseToURL, qt.Not(qt.IsNil))
|
||||
c.Assert(url2, qt.Equals, *nn[0].BrowseToURL)
|
||||
}
|
||||
|
||||
// Pretend that the interactive login actually happened.
|
||||
// Controlclient always sends the netmap and LoginFinished at the
|
||||
// same time.
|
||||
// The backend should propagate this upward for the UI.
|
||||
t.Logf("\n\nLoginFinished")
|
||||
notifies.expect(3)
|
||||
cc.setAuthBlocked(false)
|
||||
cc.persist.LoginName = "user1"
|
||||
cc.send(nil, "", true, &netmap.NetworkMap{})
|
||||
{
|
||||
nn := notifies.drain(3)
|
||||
// BUG: still too soon for UpdateEndpoints.
|
||||
//
|
||||
// Arguably it makes sense to unpause now, since the machine
|
||||
// authorization status is part of the netmap.
|
||||
//
|
||||
// BUG: backend unblocks wgengine at this point, even though
|
||||
// our machine key is not authorized. It probably should
|
||||
// wait until it gets into Starting.
|
||||
// TODO: (Currently this test doesn't detect that bug, but
|
||||
// it's visible in the logs)
|
||||
c.Assert([]string{"unpause", "UpdateEndpoints"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].LoginFinished, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[2].State, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].Prefs.Persist.LoginName, qt.Equals, "user1")
|
||||
c.Assert(ipn.NeedsMachineAuth, qt.Equals, *nn[2].State)
|
||||
}
|
||||
|
||||
// Pretend that the administrator has authorized our machine.
|
||||
t.Logf("\n\nMachineAuthorized")
|
||||
notifies.expect(1)
|
||||
// BUG: the real controlclient sends LoginFinished with every
|
||||
// notification while it's in StateAuthenticated, but not StateSynced.
|
||||
// We should send it exactly once, or every time we're authenticated,
|
||||
// but the current code is brittle.
|
||||
// (ie. I suspect it would be better to change false->true in send()
|
||||
// below, and do the same in the real controlclient.)
|
||||
cc.send(nil, "", false, &netmap.NetworkMap{
|
||||
MachineStatus: tailcfg.MachineAuthorized,
|
||||
})
|
||||
{
|
||||
nn := notifies.drain(1)
|
||||
c.Assert([]string{"unpause", "UpdateEndpoints"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].State, qt.Not(qt.IsNil))
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[0].State)
|
||||
}
|
||||
|
||||
// TODO: add a fake DERP server to our fake netmap, so we can
|
||||
// transition to the Running state here.
|
||||
|
||||
// TODO: test what happens when the admin forcibly deletes our key.
|
||||
// (ie. unsolicited logout)
|
||||
|
||||
// TODO: test what happens when our key expires, client side.
|
||||
// (and when it gets close to expiring)
|
||||
|
||||
// The user changes their preference to !WantRunning.
|
||||
t.Logf("\n\nWantRunning -> false")
|
||||
notifies.expect(2)
|
||||
b.EditPrefs(&ipn.MaskedPrefs{
|
||||
WantRunningSet: true,
|
||||
Prefs: ipn.Prefs{WantRunning: false},
|
||||
})
|
||||
{
|
||||
nn := notifies.drain(2)
|
||||
c.Assert([]string{"pause"}, qt.DeepEquals, cc.getCalls())
|
||||
// BUG: I would expect Prefs to change first, and state after.
|
||||
c.Assert(nn[0].State, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(ipn.Stopped, qt.Equals, *nn[0].State)
|
||||
}
|
||||
|
||||
// The user changes their preference to WantRunning after all.
|
||||
t.Logf("\n\nWantRunning -> true")
|
||||
notifies.expect(2)
|
||||
b.EditPrefs(&ipn.MaskedPrefs{
|
||||
WantRunningSet: true,
|
||||
Prefs: ipn.Prefs{WantRunning: true},
|
||||
})
|
||||
{
|
||||
nn := notifies.drain(2)
|
||||
// BUG: UpdateEndpoints isn't needed here.
|
||||
// BUG: Login isn't needed here. We never logged out.
|
||||
c.Assert([]string{"Login", "unpause", "UpdateEndpoints"}, qt.DeepEquals, cc.getCalls())
|
||||
// BUG: I would expect Prefs to change first, and state after.
|
||||
c.Assert(nn[0].State, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[0].State)
|
||||
}
|
||||
|
||||
// Test the fast-path frontend reconnection.
|
||||
// This one is very finicky, so we have to force State==Running.
|
||||
// TODO: actually get to State==Running, rather than cheating.
|
||||
// That'll require spinning up a fake DERP server and putting it in
|
||||
// the netmap.
|
||||
t.Logf("\n\nFastpath Start()")
|
||||
notifies.expect(1)
|
||||
b.state = ipn.Running
|
||||
c.Assert(b.Start(ipn.Options{StateKey: ipn.GlobalDaemonStateKey}), qt.IsNil)
|
||||
{
|
||||
nn := notifies.drain(1)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(nn[0].State, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[0].LoginFinished, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[0].NetMap, qt.Not(qt.IsNil))
|
||||
// BUG: Prefs should be sent too, or the UI could end up in
|
||||
// a bad state. (iOS, the only current user of this feature,
|
||||
// probably wouldn't notice because it happens to not display
|
||||
// any prefs. Maybe exit nodes will look weird?)
|
||||
}
|
||||
|
||||
// undo the state hack above.
|
||||
b.state = ipn.Starting
|
||||
|
||||
// User wants to logout.
|
||||
t.Logf("\n\nLogout (async)")
|
||||
notifies.expect(2)
|
||||
b.Logout()
|
||||
{
|
||||
nn := notifies.drain(2)
|
||||
// BUG: now is not the time to unpause.
|
||||
c.Assert([]string{"unpause", "StartLogout"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].State, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[0].State)
|
||||
c.Assert(nn[1].Prefs.LoggedOut, qt.IsTrue)
|
||||
c.Assert(nn[1].Prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
// Let's make the logout succeed.
|
||||
t.Logf("\n\nLogout (async) - succeed")
|
||||
notifies.expect(0)
|
||||
cc.setAuthBlocked(true)
|
||||
cc.send(nil, "", false, nil)
|
||||
{
|
||||
notifies.drain(0)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
// A second logout should do nothing, since the prefs haven't changed.
|
||||
t.Logf("\n\nLogout2 (async)")
|
||||
notifies.expect(0)
|
||||
b.Logout()
|
||||
{
|
||||
notifies.drain(0)
|
||||
// BUG: the backend has already called StartLogout, and we're
|
||||
// still logged out. So it shouldn't call it again.
|
||||
c.Assert([]string{"StartLogout"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
// Let's acknowledge the second logout too.
|
||||
t.Logf("\n\nLogout2 (async) - succeed")
|
||||
notifies.expect(0)
|
||||
cc.setAuthBlocked(true)
|
||||
cc.send(nil, "", false, nil)
|
||||
{
|
||||
notifies.drain(0)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
// Try the synchronous logout feature.
|
||||
t.Logf("\n\nLogout3 (sync)")
|
||||
notifies.expect(0)
|
||||
b.LogoutSync(context.Background())
|
||||
// NOTE: This returns as soon as cc.Logout() returns, which is okay
|
||||
// I guess, since that's supposed to be synchronous.
|
||||
{
|
||||
notifies.drain(0)
|
||||
c.Assert([]string{"Logout"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
// Generate the third logout event.
|
||||
t.Logf("\n\nLogout3 (sync) - succeed")
|
||||
notifies.expect(0)
|
||||
cc.setAuthBlocked(true)
|
||||
cc.send(nil, "", false, nil)
|
||||
{
|
||||
notifies.drain(0)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
// Shut down the backend.
|
||||
t.Logf("\n\nShutdown")
|
||||
notifies.expect(0)
|
||||
b.Shutdown()
|
||||
{
|
||||
notifies.drain(0)
|
||||
// BUG: I expect a transition to ipn.NoState here.
|
||||
c.Assert(cc.getCalls(), qt.DeepEquals, []string{"Shutdown"})
|
||||
}
|
||||
|
||||
// Oh, you thought we were done? Ha! Now we have to test what
|
||||
// happens if the user exits and restarts while logged out.
|
||||
// Note that it's explicitly okay to call b.Start() over and over
|
||||
// again, every time the frontend reconnects.
|
||||
|
||||
// TODO: test user switching between statekeys.
|
||||
|
||||
// The frontend restarts!
|
||||
t.Logf("\n\nStart3")
|
||||
notifies.expect(2)
|
||||
c.Assert(b.Start(ipn.Options{StateKey: ipn.GlobalDaemonStateKey}), qt.IsNil)
|
||||
{
|
||||
// BUG: We already called Shutdown(), no need to do it again.
|
||||
// BUG: Way too soon for UpdateEndpoints.
|
||||
// BUG: don't unpause because we're not logged in.
|
||||
c.Assert([]string{"Shutdown", "New", "UpdateEndpoints", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
|
||||
nn := notifies.drain(2)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].State, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsTrue)
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[1].State)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
// Let's break the rules a little. Our control server accepts
|
||||
// your invalid login attempt, with no need for an interactive login.
|
||||
// (This simulates an admin reviving a key that you previously
|
||||
// disabled.)
|
||||
t.Logf("\n\nLoginFinished3")
|
||||
notifies.expect(3)
|
||||
cc.setAuthBlocked(false)
|
||||
cc.persist.LoginName = "user2"
|
||||
cc.send(nil, "", true, &netmap.NetworkMap{
|
||||
MachineStatus: tailcfg.MachineAuthorized,
|
||||
})
|
||||
{
|
||||
nn := notifies.drain(3)
|
||||
c.Assert([]string{"unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].LoginFinished, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[2].State, qt.Not(qt.IsNil))
|
||||
// Prefs after finishing the login, so LoginName updated.
|
||||
c.Assert(nn[1].Prefs.Persist.LoginName, qt.Equals, "user2")
|
||||
c.Assert(nn[1].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[1].Prefs.WantRunning, qt.IsTrue)
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[2].State)
|
||||
}
|
||||
|
||||
// Now we've logged in successfully. Let's disconnect.
|
||||
t.Logf("\n\nWantRunning -> false")
|
||||
notifies.expect(2)
|
||||
b.EditPrefs(&ipn.MaskedPrefs{
|
||||
WantRunningSet: true,
|
||||
Prefs: ipn.Prefs{WantRunning: false},
|
||||
})
|
||||
{
|
||||
nn := notifies.drain(2)
|
||||
c.Assert([]string{"pause"}, qt.DeepEquals, cc.getCalls())
|
||||
// BUG: I would expect Prefs to change first, and state after.
|
||||
c.Assert(nn[0].State, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(ipn.Stopped, qt.Equals, *nn[0].State)
|
||||
c.Assert(nn[1].Prefs.LoggedOut, qt.IsFalse)
|
||||
}
|
||||
|
||||
// One more restart, this time with a valid key, but WantRunning=false.
|
||||
t.Logf("\n\nStart4")
|
||||
notifies.expect(2)
|
||||
c.Assert(b.Start(ipn.Options{StateKey: ipn.GlobalDaemonStateKey}), qt.IsNil)
|
||||
{
|
||||
// NOTE: cc.Shutdown() is correct here, since we didn't call
|
||||
// b.Shutdown() explicitly ourselves.
|
||||
// BUG: UpdateEndpoints should be called here since we're not WantRunning.
|
||||
// Note: unpause happens because ipn needs to get at least one netmap
|
||||
// on startup, otherwise UIs can't show the node list, login
|
||||
// name, etc when in state ipn.Stopped.
|
||||
// Arguably they shouldn't try. But they currently do.
|
||||
c.Assert([]string{"Shutdown", "New", "UpdateEndpoints", "Login", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
|
||||
nn := notifies.drain(2)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].State, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(ipn.Stopped, qt.Equals, *nn[1].State)
|
||||
}
|
||||
|
||||
// Request connection.
|
||||
// The state machine didn't call Login() earlier, so now it needs to.
|
||||
t.Logf("\n\nWantRunning4 -> true")
|
||||
notifies.expect(2)
|
||||
b.EditPrefs(&ipn.MaskedPrefs{
|
||||
WantRunningSet: true,
|
||||
Prefs: ipn.Prefs{WantRunning: true},
|
||||
})
|
||||
{
|
||||
nn := notifies.drain(2)
|
||||
c.Assert([]string{"Login", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
// BUG: I would expect Prefs to change first, and state after.
|
||||
c.Assert(nn[0].State, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[0].State)
|
||||
}
|
||||
|
||||
// Disconnect.
|
||||
t.Logf("\n\nStop")
|
||||
notifies.expect(2)
|
||||
b.EditPrefs(&ipn.MaskedPrefs{
|
||||
WantRunningSet: true,
|
||||
Prefs: ipn.Prefs{WantRunning: false},
|
||||
})
|
||||
{
|
||||
nn := notifies.drain(2)
|
||||
c.Assert([]string{"unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
// BUG: I would expect Prefs to change first, and state after.
|
||||
c.Assert(nn[0].State, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(ipn.Stopped, qt.Equals, *nn[0].State)
|
||||
}
|
||||
|
||||
// We want to try logging in as a different user, while Stopped.
|
||||
// First, start the login process (without logging out first).
|
||||
t.Logf("\n\nLoginDifferent")
|
||||
notifies.expect(2)
|
||||
b.StartLoginInteractive()
|
||||
url3 := "http://localhost:1/3"
|
||||
cc.send(nil, url3, false, nil)
|
||||
{
|
||||
nn := notifies.drain(2)
|
||||
// It might seem like WantRunning should switch to true here,
|
||||
// but that would be risky since we already have a valid
|
||||
// user account. It might try to reconnect to the old account
|
||||
// before the new one is ready. So no change yet.
|
||||
c.Assert([]string{"Login", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].BrowseToURL, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].State, qt.Not(qt.IsNil))
|
||||
c.Assert(*nn[0].BrowseToURL, qt.Equals, url3)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[1].State)
|
||||
}
|
||||
|
||||
// Now, let's say the interactive login completed, using a different
|
||||
// user account than before.
|
||||
t.Logf("\n\nLoginDifferent URL visited")
|
||||
notifies.expect(3)
|
||||
cc.persist.LoginName = "user3"
|
||||
cc.send(nil, "", true, &netmap.NetworkMap{
|
||||
MachineStatus: tailcfg.MachineAuthorized,
|
||||
})
|
||||
{
|
||||
nn := notifies.drain(3)
|
||||
c.Assert([]string{"unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].LoginFinished, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[2].State, qt.Not(qt.IsNil))
|
||||
// Prefs after finishing the login, so LoginName updated.
|
||||
c.Assert(nn[1].Prefs.Persist.LoginName, qt.Equals, "user3")
|
||||
c.Assert(nn[1].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[1].Prefs.WantRunning, qt.IsTrue)
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[2].State)
|
||||
}
|
||||
|
||||
// The last test case is the most common one: restarting when both
|
||||
// logged in and WantRunning.
|
||||
t.Logf("\n\nStart5")
|
||||
notifies.expect(1)
|
||||
c.Assert(b.Start(ipn.Options{StateKey: ipn.GlobalDaemonStateKey}), qt.IsNil)
|
||||
{
|
||||
// NOTE: cc.Shutdown() is correct here, since we didn't call
|
||||
// b.Shutdown() ourselves.
|
||||
c.Assert([]string{"Shutdown", "New", "UpdateEndpoints", "Login"}, qt.DeepEquals, cc.getCalls())
|
||||
|
||||
nn := notifies.drain(1)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsTrue)
|
||||
c.Assert(ipn.NoState, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
// Control server accepts our valid key from before.
|
||||
t.Logf("\n\nLoginFinished5")
|
||||
notifies.expect(1)
|
||||
cc.setAuthBlocked(false)
|
||||
cc.send(nil, "", true, &netmap.NetworkMap{
|
||||
MachineStatus: tailcfg.MachineAuthorized,
|
||||
})
|
||||
{
|
||||
nn := notifies.drain(1)
|
||||
c.Assert([]string{"unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
// NOTE: No LoginFinished message since no interactive
|
||||
// login was needed.
|
||||
c.Assert(nn[0].State, qt.Not(qt.IsNil))
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[0].State)
|
||||
// NOTE: No prefs change this time. WantRunning stays true.
|
||||
// We were in Starting in the first place, so that doesn't
|
||||
// change either.
|
||||
c.Assert(ipn.Starting, qt.Equals, b.State())
|
||||
}
|
||||
}
|
||||
@@ -6,9 +6,7 @@ package ipnserver
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -253,7 +251,8 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
bs := ipn.NewBackendServer(logf, nil, jsonNotifier(c, s.logf))
|
||||
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
|
||||
bs := ipn.NewBackendServer(logf, nil, serverToClient)
|
||||
_, occupied := err.(inUseOtherUserError)
|
||||
if occupied {
|
||||
bs.SendInUseOtherUserErrorMessage(err.Error())
|
||||
@@ -288,7 +287,7 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
defer s.removeAndCloseConn(c)
|
||||
logf("[v1] incoming control connection")
|
||||
|
||||
if isReadonlyConn(ci, s.b.OperatorUserID(), logf) {
|
||||
if isReadonlyConn(ci, logf) {
|
||||
ctx = ipn.ReadonlyContextOf(ctx)
|
||||
}
|
||||
|
||||
@@ -314,7 +313,7 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
}
|
||||
}
|
||||
|
||||
func isReadonlyConn(ci connIdentity, operatorUID string, logf logger.Logf) bool {
|
||||
func isReadonlyConn(ci connIdentity, logf logger.Logf) bool {
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows doesn't need/use this mechanism, at least yet. It
|
||||
// has a different last-user-wins auth model.
|
||||
@@ -343,10 +342,6 @@ func isReadonlyConn(ci connIdentity, operatorUID string, logf logger.Logf) bool
|
||||
logf("connection from userid %v; connection from non-root user matching daemon has access", uid)
|
||||
return rw
|
||||
}
|
||||
if operatorUID != "" && uid == operatorUID {
|
||||
logf("connection from userid %v; is configured operator", uid)
|
||||
return rw
|
||||
}
|
||||
var adminGroupID string
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
@@ -440,7 +435,7 @@ func (s *server) localAPIPermissions(ci connIdentity) (read, write bool) {
|
||||
return false, false
|
||||
}
|
||||
if ci.IsUnixSock {
|
||||
return true, !isReadonlyConn(ci, s.b.OperatorUserID(), logger.Discard)
|
||||
return true, !isReadonlyConn(ci, logger.Discard)
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
@@ -477,7 +472,9 @@ func (s *server) addConn(c net.Conn, isHTTP bool) (ci connIdentity, err error) {
|
||||
defer func() {
|
||||
if doReset {
|
||||
s.logf("identity changed; resetting server")
|
||||
s.b.ResetForClientDisconnect()
|
||||
s.bsMu.Lock()
|
||||
s.bs.Reset(context.TODO())
|
||||
s.bsMu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -527,7 +524,9 @@ func (s *server) removeAndCloseConn(c net.Conn) {
|
||||
s.logf("client disconnected; staying alive in server mode")
|
||||
} else {
|
||||
s.logf("client disconnected; stopping server")
|
||||
s.b.ResetForClientDisconnect()
|
||||
s.bsMu.Lock()
|
||||
s.bs.Reset(context.TODO())
|
||||
s.bsMu.Unlock()
|
||||
}
|
||||
}
|
||||
c.Close()
|
||||
@@ -568,9 +567,7 @@ func (s *server) setServerModeUserLocked() {
|
||||
}
|
||||
}
|
||||
|
||||
var jsonEscapedZero = []byte(`\u0000`)
|
||||
|
||||
func (s *server) writeToClients(n ipn.Notify) {
|
||||
func (s *server) writeToClients(b []byte) {
|
||||
inServerMode := s.b.InServerMode()
|
||||
|
||||
s.mu.Lock()
|
||||
@@ -587,24 +584,14 @@ func (s *server) writeToClients(n ipn.Notify) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(s.clients) == 0 {
|
||||
// Common case (at least on busy servers): nobody
|
||||
// connected (no GUI, etc), so return before
|
||||
// serializing JSON.
|
||||
return
|
||||
}
|
||||
|
||||
if b, ok := marshalNotify(n, s.logf); ok {
|
||||
for c := range s.clients {
|
||||
ipn.WriteMsg(c, b)
|
||||
}
|
||||
for c := range s.clients {
|
||||
ipn.WriteMsg(c, b)
|
||||
}
|
||||
}
|
||||
|
||||
// Run runs a Tailscale backend service.
|
||||
// The getEngine func is called repeatedly, once per connection, until it returns an engine successfully.
|
||||
func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (wgengine.Engine, error), opts Options) error {
|
||||
getEngine = getEngineUntilItWorksWrapper(getEngine)
|
||||
runDone := make(chan struct{})
|
||||
defer close(runDone)
|
||||
|
||||
@@ -665,6 +652,46 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
eng, err := getEngine()
|
||||
if err != nil {
|
||||
logf("ipnserver: initial getEngine call: %v", err)
|
||||
|
||||
// Issue 1187: on Windows, in unattended mode,
|
||||
// sometimes we try 5 times and fail to create the
|
||||
// engine before the system's ready. Hack until the
|
||||
// bug if fixed properly: if we're running in
|
||||
// unattended mode on Windows, keep trying forever,
|
||||
// waiting for the machine to be ready (networking to
|
||||
// come up?) and then dial our own safesocket TCP
|
||||
// listener to wake up the usual mechanism that lets
|
||||
// us surface getEngine errors to UI clients. (We
|
||||
// don't want to just call getEngine in a loop without
|
||||
// the listener.Accept, as we do want to handle client
|
||||
// connections so we can tell them about errors)
|
||||
|
||||
bootRaceWaitForEngine, bootRaceWaitForEngineCancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
if runtime.GOOS == "windows" && opts.AutostartStateKey != "" {
|
||||
logf("ipnserver: in unattended mode, waiting for engine availability")
|
||||
getEngine = getEngineUntilItWorksWrapper(getEngine)
|
||||
// Wait for it to be ready.
|
||||
go func() {
|
||||
defer bootRaceWaitForEngineCancel()
|
||||
t0 := time.Now()
|
||||
for {
|
||||
time.Sleep(10 * time.Second)
|
||||
if _, err := getEngine(); err != nil {
|
||||
logf("ipnserver: unattended mode engine load: %v", err)
|
||||
continue
|
||||
}
|
||||
c, err := net.Dial("tcp", listen.Addr().String())
|
||||
logf("ipnserver: engine created after %v; waking up Accept: Dial error: %v", time.Since(t0).Round(time.Second), err)
|
||||
if err == nil {
|
||||
c.Close()
|
||||
}
|
||||
break
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
bootRaceWaitForEngineCancel()
|
||||
}
|
||||
|
||||
for i := 1; ctx.Err() == nil; i++ {
|
||||
c, err := listen.Accept()
|
||||
if err != nil {
|
||||
@@ -672,6 +699,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
bo.BackOff(ctx, err)
|
||||
continue
|
||||
}
|
||||
<-bootRaceWaitForEngine.Done()
|
||||
logf("ipnserver: try%d: trying getEngine again...", i)
|
||||
eng, err = getEngine()
|
||||
if err == nil {
|
||||
@@ -683,7 +711,8 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
errMsg := err.Error()
|
||||
go func() {
|
||||
defer c.Close()
|
||||
bs := ipn.NewBackendServer(logf, nil, jsonNotifier(c, logf))
|
||||
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
|
||||
bs := ipn.NewBackendServer(logf, nil, serverToClient)
|
||||
bs.SendErrorMessage(errMsg)
|
||||
time.Sleep(time.Second)
|
||||
}()
|
||||
@@ -973,25 +1002,3 @@ func peerPid(entries []netstat.Entry, la, ra netaddr.IPPort) int {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// jsonNotifier returns a notify-writer func that writes ipn.Notify
|
||||
// messages to w.
|
||||
func jsonNotifier(w io.Writer, logf logger.Logf) func(ipn.Notify) {
|
||||
return func(n ipn.Notify) {
|
||||
if b, ok := marshalNotify(n, logf); ok {
|
||||
ipn.WriteMsg(w, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func marshalNotify(n ipn.Notify, logf logger.Logf) (b []byte, ok bool) {
|
||||
b, err := json.Marshal(n)
|
||||
if err != nil {
|
||||
logf("ipnserver: [unexpected] error serializing JSON: %v", err)
|
||||
return nil, false
|
||||
}
|
||||
if bytes.Contains(b, jsonEscapedZero) {
|
||||
logf("[unexpected] zero byte in BackendServer.send notify message: %q", b)
|
||||
}
|
||||
return b, true
|
||||
}
|
||||
|
||||
@@ -65,7 +65,6 @@ type PeerStatusLite struct {
|
||||
}
|
||||
|
||||
type PeerStatus struct {
|
||||
ID tailcfg.StableNodeID
|
||||
PublicKey key.Public
|
||||
HostName string // HostInfo's Hostname (not a DNS name or necessarily unique)
|
||||
DNSName string
|
||||
@@ -204,9 +203,6 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
|
||||
return
|
||||
}
|
||||
|
||||
if v := st.ID; v != "" {
|
||||
e.ID = v
|
||||
}
|
||||
if v := st.HostName; v != "" {
|
||||
e.HostName = v
|
||||
}
|
||||
|
||||
@@ -88,34 +88,31 @@ type Command struct {
|
||||
|
||||
type BackendServer struct {
|
||||
logf logger.Logf
|
||||
b Backend // the Backend we are serving up
|
||||
sendNotifyMsg func(Notify) // send a notification message
|
||||
GotQuit bool // a Quit command was received
|
||||
b Backend // the Backend we are serving up
|
||||
sendNotifyMsg func(jsonMsg []byte) // send a notification message
|
||||
GotQuit bool // a Quit command was received
|
||||
}
|
||||
|
||||
// NewBackendServer creates a new BackendServer using b.
|
||||
//
|
||||
// If sendNotifyMsg is non-nil, it additionally sets the Backend's
|
||||
// notification callback to call the func with ipn.Notify messages in
|
||||
// JSON form. If nil, it does not change the notification callback.
|
||||
func NewBackendServer(logf logger.Logf, b Backend, sendNotifyMsg func(Notify)) *BackendServer {
|
||||
func NewBackendServer(logf logger.Logf, b Backend, sendNotifyMsg func(b []byte)) *BackendServer {
|
||||
bs := &BackendServer{
|
||||
logf: logf,
|
||||
b: b,
|
||||
sendNotifyMsg: sendNotifyMsg,
|
||||
}
|
||||
if sendNotifyMsg != nil {
|
||||
b.SetNotifyCallback(bs.send)
|
||||
}
|
||||
b.SetNotifyCallback(bs.send)
|
||||
return bs
|
||||
}
|
||||
|
||||
func (bs *BackendServer) send(n Notify) {
|
||||
if bs.sendNotifyMsg == nil {
|
||||
return
|
||||
}
|
||||
n.Version = version.Long
|
||||
bs.sendNotifyMsg(n)
|
||||
b, err := json.Marshal(n)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed json.Marshal(notify): %v\n%#v", err, n)
|
||||
}
|
||||
if bytes.Contains(b, jsonEscapedZero) {
|
||||
log.Printf("[unexpected] zero byte in BackendServer.send notify message: %q", b)
|
||||
}
|
||||
bs.sendNotifyMsg(b)
|
||||
}
|
||||
|
||||
func (bs *BackendServer) SendErrorMessage(msg string) {
|
||||
@@ -146,6 +143,11 @@ func (bs *BackendServer) GotCommandMsg(ctx context.Context, b []byte) error {
|
||||
return bs.GotCommand(ctx, cmd)
|
||||
}
|
||||
|
||||
func (bs *BackendServer) GotFakeCommand(ctx context.Context, cmd *Command) error {
|
||||
cmd.Version = version.Long
|
||||
return bs.GotCommand(ctx, cmd)
|
||||
}
|
||||
|
||||
// ErrMsgPermissionDenied is the Notify.ErrMessage value used an
|
||||
// operation was done from a user/context that didn't have permission.
|
||||
const ErrMsgPermissionDenied = "permission denied"
|
||||
@@ -209,6 +211,12 @@ func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error {
|
||||
return fmt.Errorf("BackendServer.Do: no command specified")
|
||||
}
|
||||
|
||||
func (bs *BackendServer) Reset(ctx context.Context) error {
|
||||
// Tell the backend we got a Logout command, which will cause it
|
||||
// to forget all its authentication information.
|
||||
return bs.GotFakeCommand(ctx, &Command{Logout: &NoArgs{}})
|
||||
}
|
||||
|
||||
type BackendClient struct {
|
||||
logf logger.Logf
|
||||
sendCommandMsg func(jsonb []byte)
|
||||
|
||||
@@ -7,7 +7,6 @@ package ipn
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -75,11 +74,7 @@ func TestClientServer(t *testing.T) {
|
||||
bc.GotNotifyMsg(b)
|
||||
}
|
||||
}()
|
||||
serverToClient := func(n Notify) {
|
||||
b, err := json.Marshal(n)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
serverToClient := func(b []byte) {
|
||||
serverToClientCh <- append([]byte{}, b...)
|
||||
}
|
||||
clientToServer := func(b []byte) {
|
||||
@@ -92,8 +87,6 @@ func TestClientServer(t *testing.T) {
|
||||
t.Logf("c: "+fmt, args...)
|
||||
}
|
||||
bs = NewBackendServer(slogf, b, serverToClient)
|
||||
// Verify that this doesn't break bs's callback:
|
||||
NewBackendServer(slogf, b, nil)
|
||||
bc = NewBackendClient(clogf, clientToServer)
|
||||
|
||||
ch := make(chan Notify, 256)
|
||||
|
||||
66
ipn/prefs.go
66
ipn/prefs.go
@@ -25,27 +25,9 @@ import (
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=Prefs -output=prefs_clone.go
|
||||
|
||||
// DefaultControlURL returns the URL base of the control plane
|
||||
// ("coordination server") for use when no explicit one is configured.
|
||||
// The default control plane is the hosted version run by Tailscale.com.
|
||||
const DefaultControlURL = "https://login.tailscale.com"
|
||||
|
||||
// Prefs are the user modifiable settings of the Tailscale node agent.
|
||||
type Prefs struct {
|
||||
// ControlURL is the URL of the control server to use.
|
||||
//
|
||||
// If empty, the default for new installs, DefaultControlURL
|
||||
// is used. It's set non-empty once the daemon has been started
|
||||
// for the first time.
|
||||
//
|
||||
// TODO(apenwarr): Make it safe to update this with SetPrefs().
|
||||
// Right now, you have to pass it in the initial prefs in Start(),
|
||||
// which is the only code that actually uses the ControlURL value.
|
||||
// It would be more consistent to restart controlclient
|
||||
// automatically whenever this variable changes.
|
||||
//
|
||||
// Meanwhile, you have to provide this as part of Options.Prefs or
|
||||
// Options.UpdatePrefs when calling Backend.Start().
|
||||
ControlURL string
|
||||
|
||||
// RouteAll specifies whether to accept subnets advertised by
|
||||
@@ -96,14 +78,6 @@ type Prefs struct {
|
||||
// this node.
|
||||
WantRunning bool
|
||||
|
||||
// LoggedOut indicates whether the user intends to be logged out.
|
||||
// There are other reasons we may be logged out, including no valid
|
||||
// keys.
|
||||
// We need to remember this state so that, on next startup, we can
|
||||
// generate the "Login" vs "Connect" buttons correctly, without having
|
||||
// to contact the server to confirm our nodekey status first.
|
||||
LoggedOut bool
|
||||
|
||||
// ShieldsUp indicates whether to block all incoming connections,
|
||||
// regardless of the control-provided packet filter. If false, we
|
||||
// use the packet filter as provided. If true, we block incoming
|
||||
@@ -170,10 +144,6 @@ type Prefs struct {
|
||||
// Tailscale, if at all.
|
||||
NetfilterMode preftype.NetfilterMode
|
||||
|
||||
// OperatorUser is the local machine user name who is allowed to
|
||||
// operate tailscaled without being root or using sudo.
|
||||
OperatorUser string `json:",omitempty"`
|
||||
|
||||
// The Persist field is named 'Config' in the file for backward
|
||||
// compatibility with earlier versions.
|
||||
// TODO(apenwarr): We should move this out of here, it's not a pref.
|
||||
@@ -194,7 +164,6 @@ type MaskedPrefs struct {
|
||||
ExitNodeAllowLANAccessSet bool `json:",omitempty"`
|
||||
CorpDNSSet bool `json:",omitempty"`
|
||||
WantRunningSet bool `json:",omitempty"`
|
||||
LoggedOutSet bool `json:",omitempty"`
|
||||
ShieldsUpSet bool `json:",omitempty"`
|
||||
AdvertiseTagsSet bool `json:",omitempty"`
|
||||
HostnameSet bool `json:",omitempty"`
|
||||
@@ -205,7 +174,6 @@ type MaskedPrefs struct {
|
||||
AdvertiseRoutesSet bool `json:",omitempty"`
|
||||
NoSNATSet bool `json:",omitempty"`
|
||||
NetfilterModeSet bool `json:",omitempty"`
|
||||
OperatorUserSet bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
// ApplyEdits mutates p, assigning fields from m.Prefs for each MaskedPrefs
|
||||
@@ -264,9 +232,6 @@ func (p *Prefs) pretty(goos string) string {
|
||||
sb.WriteString("mesh=false ")
|
||||
}
|
||||
fmt.Fprintf(&sb, "dns=%v want=%v ", p.CorpDNS, p.WantRunning)
|
||||
if p.LoggedOut {
|
||||
sb.WriteString("loggedout=true ")
|
||||
}
|
||||
if p.ForceDaemon {
|
||||
sb.WriteString("server=true ")
|
||||
}
|
||||
@@ -293,15 +258,12 @@ func (p *Prefs) pretty(goos string) string {
|
||||
if goos == "linux" {
|
||||
fmt.Fprintf(&sb, "nf=%v ", p.NetfilterMode)
|
||||
}
|
||||
if p.ControlURL != "" && p.ControlURL != DefaultControlURL {
|
||||
if p.ControlURL != "" && p.ControlURL != "https://login.tailscale.com" {
|
||||
fmt.Fprintf(&sb, "url=%q ", p.ControlURL)
|
||||
}
|
||||
if p.Hostname != "" {
|
||||
fmt.Fprintf(&sb, "host=%q ", p.Hostname)
|
||||
}
|
||||
if p.OperatorUser != "" {
|
||||
fmt.Fprintf(&sb, "op=%q ", p.OperatorUser)
|
||||
}
|
||||
if p.Persist != nil {
|
||||
sb.WriteString(p.Persist.Pretty())
|
||||
} else {
|
||||
@@ -336,12 +298,10 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
||||
p.ExitNodeAllowLANAccess == p2.ExitNodeAllowLANAccess &&
|
||||
p.CorpDNS == p2.CorpDNS &&
|
||||
p.WantRunning == p2.WantRunning &&
|
||||
p.LoggedOut == p2.LoggedOut &&
|
||||
p.NotepadURLs == p2.NotepadURLs &&
|
||||
p.ShieldsUp == p2.ShieldsUp &&
|
||||
p.NoSNAT == p2.NoSNAT &&
|
||||
p.NetfilterMode == p2.NetfilterMode &&
|
||||
p.OperatorUser == p2.OperatorUser &&
|
||||
p.Hostname == p2.Hostname &&
|
||||
p.OSVersion == p2.OSVersion &&
|
||||
p.DeviceModel == p2.DeviceModel &&
|
||||
@@ -375,19 +335,12 @@ func compareStrings(a, b []string) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// NewPrefs returns the default preferences to use.
|
||||
func NewPrefs() *Prefs {
|
||||
// Provide default values for options which might be missing
|
||||
// from the json data for any reason. The json can still
|
||||
// override them to false.
|
||||
return &Prefs{
|
||||
// ControlURL is explicitly not set to signal that
|
||||
// it's not yet configured, which relaxes the CLI "up"
|
||||
// safety net features. It will get set to DefaultControlURL
|
||||
// on first up. Or, if not, DefaultControlURL will be used
|
||||
// later anyway.
|
||||
ControlURL: "",
|
||||
|
||||
// Provide default values for options which might be missing
|
||||
// from the json data for any reason. The json can still
|
||||
// override them to false.
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
RouteAll: true,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
@@ -396,15 +349,6 @@ func NewPrefs() *Prefs {
|
||||
}
|
||||
}
|
||||
|
||||
// ControlURLOrDefault returns the coordination server's URL base.
|
||||
// If not configured, DefaultControlURL is returned instead.
|
||||
func (p *Prefs) ControlURLOrDefault() string {
|
||||
if p.ControlURL != "" {
|
||||
return p.ControlURL
|
||||
}
|
||||
return DefaultControlURL
|
||||
}
|
||||
|
||||
// PrefsFromBytes deserializes Prefs from a JSON blob. If
|
||||
// enforceDefaults is true, Prefs.RouteAll and Prefs.AllowSingleHosts
|
||||
// are forced on.
|
||||
|
||||
@@ -41,7 +41,6 @@ var _PrefsNeedsRegeneration = Prefs(struct {
|
||||
ExitNodeAllowLANAccess bool
|
||||
CorpDNS bool
|
||||
WantRunning bool
|
||||
LoggedOut bool
|
||||
ShieldsUp bool
|
||||
AdvertiseTags []string
|
||||
Hostname string
|
||||
@@ -52,6 +51,5 @@ var _PrefsNeedsRegeneration = Prefs(struct {
|
||||
AdvertiseRoutes []netaddr.IPPrefix
|
||||
NoSNAT bool
|
||||
NetfilterMode preftype.NetfilterMode
|
||||
OperatorUser string
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
@@ -33,29 +33,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
|
||||
func TestPrefsEqual(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
|
||||
prefsHandles := []string{
|
||||
"ControlURL",
|
||||
"RouteAll",
|
||||
"AllowSingleHosts",
|
||||
"ExitNodeID",
|
||||
"ExitNodeIP",
|
||||
"ExitNodeAllowLANAccess",
|
||||
"CorpDNS",
|
||||
"WantRunning",
|
||||
"LoggedOut",
|
||||
"ShieldsUp",
|
||||
"AdvertiseTags",
|
||||
"Hostname",
|
||||
"OSVersion",
|
||||
"DeviceModel",
|
||||
"NotepadURLs",
|
||||
"ForceDaemon",
|
||||
"AdvertiseRoutes",
|
||||
"NoSNAT",
|
||||
"NetfilterMode",
|
||||
"OperatorUser",
|
||||
"Persist",
|
||||
}
|
||||
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "ExitNodeID", "ExitNodeIP", "ExitNodeAllowLANAccess", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"}
|
||||
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
|
||||
t.Errorf("Prefs.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||
have, prefsHandles)
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
//lint:ignore U1000 work around false positive: https://github.com/dominikh/go-tools/issues/983
|
||||
var stderrFD = 2 // a variable for testing
|
||||
|
||||
type Options struct {
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux freebsd openbsd
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
@@ -91,15 +89,6 @@ func newDebianResolvconfManager(logf logger.Logf) (*resolvconfManager, error) {
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (m *resolvconfManager) deleteTailscaleConfig() error {
|
||||
cmd := exec.Command("resolvconf", "-d", resolvconfConfigName)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("running %s: %s", cmd, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *resolvconfManager) SetDNS(config OSConfig) error {
|
||||
if !m.scriptInstalled {
|
||||
m.logf("injecting resolvconf workaround script")
|
||||
@@ -112,24 +101,18 @@ func (m *resolvconfManager) SetDNS(config OSConfig) error {
|
||||
m.scriptInstalled = true
|
||||
}
|
||||
|
||||
if config.IsZero() {
|
||||
if err := m.deleteTailscaleConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
stdin := new(bytes.Buffer)
|
||||
writeResolvConf(stdin, config.Nameservers, config.SearchDomains) // dns_direct.go
|
||||
stdin := new(bytes.Buffer)
|
||||
writeResolvConf(stdin, config.Nameservers, config.SearchDomains) // dns_direct.go
|
||||
|
||||
// This resolvconf implementation doesn't support exclusive
|
||||
// mode or interface priorities, so it will end up blending
|
||||
// our configuration with other sources. However, this will
|
||||
// get fixed up by the script we injected above.
|
||||
cmd := exec.Command("resolvconf", "-a", resolvconfConfigName)
|
||||
cmd.Stdin = stdin
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("running %s: %s", cmd, out)
|
||||
}
|
||||
// This resolvconf implementation doesn't support exclusive mode
|
||||
// or interface priorities, so it will end up blending our
|
||||
// configuration with other sources. However, this will get fixed
|
||||
// up by the script we injected above.
|
||||
cmd := exec.Command("resolvconf", "-a", resolvconfConfigName)
|
||||
cmd.Stdin = stdin
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("running %s: %s", cmd, out)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -173,8 +156,10 @@ func (m *resolvconfManager) GetBaseConfig() (OSConfig, error) {
|
||||
}
|
||||
|
||||
func (m *resolvconfManager) Close() error {
|
||||
if err := m.deleteTailscaleConfig(); err != nil {
|
||||
return err
|
||||
cmd := exec.Command("resolvconf", "-d", resolvconfConfigName)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("running %s: %s", cmd, out)
|
||||
}
|
||||
|
||||
if m.scriptInstalled {
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux freebsd openbsd
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
@@ -203,53 +201,15 @@ func (m directManager) backupConfig() error {
|
||||
return os.Rename(resolvConf, backupConf)
|
||||
}
|
||||
|
||||
func (m directManager) restoreBackup() error {
|
||||
if _, err := os.Stat(backupConf); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// No backup, nothing we can do.
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
owned, err := m.ownedByTailscale()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(resolvConf); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
resolvConfExists := !os.IsNotExist(err)
|
||||
|
||||
if resolvConfExists && !owned {
|
||||
// There's already a non-tailscale config in place, get rid of
|
||||
// our backup.
|
||||
os.Remove(backupConf)
|
||||
return nil
|
||||
}
|
||||
|
||||
// We own resolv.conf, and a backup exists.
|
||||
if err := os.Rename(backupConf, resolvConf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m directManager) SetDNS(config OSConfig) error {
|
||||
if config.IsZero() {
|
||||
if err := m.restoreBackup(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := m.backupConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.backupConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
writeResolvConf(buf, config.Nameservers, config.SearchDomains)
|
||||
if err := atomicfile.WriteFile(resolvConf, buf.Bytes(), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
buf := new(bytes.Buffer)
|
||||
writeResolvConf(buf, config.Nameservers, config.SearchDomains)
|
||||
if err := atomicfile.WriteFile(resolvConf, buf.Bytes(), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We might have taken over a configuration managed by resolved,
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -52,6 +51,23 @@ func NewManager(logf logger.Logf, oscfg OSConfigurator, linkMon *monitor.Mon) *M
|
||||
return m
|
||||
}
|
||||
|
||||
// forceSplitDNSForTesting alters cfg to be a split DNS configuration
|
||||
// that only captures search paths. It's intended for testing split
|
||||
// DNS until the functionality is linked up in the admin panel.
|
||||
func forceSplitDNSForTesting(cfg *Config) {
|
||||
if len(cfg.DefaultResolvers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if cfg.Routes == nil {
|
||||
cfg.Routes = map[dnsname.FQDN][]netaddr.IPPort{}
|
||||
}
|
||||
for _, search := range cfg.SearchDomains {
|
||||
cfg.Routes[search] = cfg.DefaultResolvers
|
||||
}
|
||||
cfg.DefaultResolvers = nil
|
||||
}
|
||||
|
||||
func (m *Manager) Set(cfg Config) error {
|
||||
m.logf("Set: %+v", cfg)
|
||||
|
||||
@@ -119,27 +135,7 @@ func (m *Manager) compileConfig(cfg Config) (resolver.Config, OSConfig, error) {
|
||||
var rcfg resolver.Config
|
||||
var ocfg OSConfig
|
||||
|
||||
// Workaround for
|
||||
// https://github.com/tailscale/corp/issues/1662. Even though
|
||||
// Windows natively supports split DNS, it only configures linux
|
||||
// containers using whatever the primary is, and doesn't apply
|
||||
// NRPT rules to DNS traffic coming from WSL.
|
||||
//
|
||||
// In order to make WSL work okay when the host Windows is using
|
||||
// Tailscale, we need to set up quad-100 as a "full proxy"
|
||||
// resolver, regardless of whether Windows itself can do split
|
||||
// DNS. We still make Windows do split DNS itself when it can, but
|
||||
// quad-100 will still have the full split configuration as well,
|
||||
// and so can service WSL requests correctly.
|
||||
//
|
||||
// This bool is used in a couple of places below to implement this
|
||||
// workaround.
|
||||
isWindows := runtime.GOOS == "windows"
|
||||
|
||||
// The windows check is for
|
||||
// . See also below
|
||||
// for further routing workarounds there.
|
||||
if !cfg.hasHosts() && cfg.singleResolverSet() != nil && m.os.SupportsSplitDNS() && !isWindows {
|
||||
if !cfg.hasHosts() && cfg.singleResolverSet() != nil && m.os.SupportsSplitDNS() {
|
||||
// Split DNS configuration requested, where all split domains
|
||||
// go to the same resolvers. We can let the OS do it.
|
||||
return resolver.Config{}, OSConfig{
|
||||
@@ -169,8 +165,7 @@ func (m *Manager) compileConfig(cfg Config) (resolver.Config, OSConfig, error) {
|
||||
// resolver config and blend it into our config.
|
||||
if m.os.SupportsSplitDNS() {
|
||||
ocfg.MatchDomains = cfg.matchDomains()
|
||||
}
|
||||
if !m.os.SupportsSplitDNS() || isWindows {
|
||||
} else {
|
||||
bcfg, err := m.os.GetBaseConfig()
|
||||
if err != nil {
|
||||
// Temporary hack to make OSes where split-DNS isn't fully
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/cmpver"
|
||||
)
|
||||
|
||||
type kv struct {
|
||||
@@ -57,62 +56,26 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurat
|
||||
}
|
||||
if err := dbusPing("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/DnsManager"); err != nil {
|
||||
dbg("nm", "no")
|
||||
return newResolvedManager(logf, interfaceName)
|
||||
return newResolvedManager(logf)
|
||||
}
|
||||
dbg("nm", "yes")
|
||||
if err := nmIsUsingResolved(); err != nil {
|
||||
dbg("nm-resolved", "no")
|
||||
return newResolvedManager(logf, interfaceName)
|
||||
return newResolvedManager(logf)
|
||||
}
|
||||
dbg("nm-resolved", "yes")
|
||||
|
||||
// Version of NetworkManager before 1.26.6 programmed resolved
|
||||
// incorrectly, such that NM's settings would always take
|
||||
// precedence over other settings set by other resolved
|
||||
// clients.
|
||||
//
|
||||
// If we're dealing with such a version, we have to set our
|
||||
// DNS settings through NM to have them take.
|
||||
//
|
||||
// However, versions 1.26.6 later both fixed the resolved
|
||||
// programming issue _and_ started ignoring DNS settings for
|
||||
// "unmanaged" interfaces - meaning NM 1.26.6 and later
|
||||
// actively ignore DNS configuration we give it. So, for those
|
||||
// NM versions, we can and must use resolved directly.
|
||||
old, err := nmVersionOlderThan("1.26.6")
|
||||
if err != nil {
|
||||
// Failed to figure out NM's version, can't make a correct
|
||||
// decision.
|
||||
return nil, fmt.Errorf("checking NetworkManager version: %v", err)
|
||||
}
|
||||
if old {
|
||||
dbg("nm-old", "yes")
|
||||
return newNMManager(interfaceName)
|
||||
}
|
||||
dbg("nm-old", "no")
|
||||
return newResolvedManager(logf, interfaceName)
|
||||
return newNMManager(interfaceName)
|
||||
case "resolvconf":
|
||||
dbg("rc", "resolvconf")
|
||||
if err := resolvconfSourceIsNM(bs); err == nil {
|
||||
dbg("src-is-nm", "yes")
|
||||
if err := dbusPing("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/DnsManager"); err == nil {
|
||||
dbg("nm", "yes")
|
||||
old, err := nmVersionOlderThan("1.26.6")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("checking NetworkManager version: %v", err)
|
||||
}
|
||||
if old {
|
||||
dbg("nm-old", "yes")
|
||||
return newNMManager(interfaceName)
|
||||
} else {
|
||||
dbg("nm-old", "no")
|
||||
}
|
||||
} else {
|
||||
dbg("nm", "no")
|
||||
return newNMManager(interfaceName)
|
||||
}
|
||||
} else {
|
||||
dbg("src-is-nm", "no")
|
||||
dbg("nm", "no")
|
||||
}
|
||||
dbg("src-is-nm", "no")
|
||||
if _, err := exec.LookPath("resolvconf"); err != nil {
|
||||
dbg("resolvconf", "no")
|
||||
return newDirectManager()
|
||||
@@ -126,16 +89,7 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurat
|
||||
return newDirectManager()
|
||||
}
|
||||
dbg("nm", "yes")
|
||||
old, err := nmVersionOlderThan("1.26.6")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("checking NetworkManager version: %v", err)
|
||||
}
|
||||
if old {
|
||||
dbg("nm-old", "yes")
|
||||
return newNMManager(interfaceName)
|
||||
}
|
||||
dbg("nm-old", "no")
|
||||
return newDirectManager()
|
||||
return newNMManager(interfaceName)
|
||||
default:
|
||||
dbg("rc", "unknown")
|
||||
return newDirectManager()
|
||||
@@ -181,27 +135,6 @@ func resolvconfSourceIsNM(resolvDotConf []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func nmVersionOlderThan(want string) (bool, error) {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
// DBus probably not running.
|
||||
return false, err
|
||||
}
|
||||
|
||||
nm := conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager"))
|
||||
v, err := nm.GetProperty("org.freedesktop.NetworkManager.Version")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
version, ok := v.Value().(string)
|
||||
if !ok {
|
||||
return false, fmt.Errorf("unexpected type %T for NM version", v.Value())
|
||||
}
|
||||
|
||||
return cmpver.Compare(version, want) < 0, nil
|
||||
}
|
||||
|
||||
func nmIsUsingResolved() error {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@@ -46,10 +45,6 @@ func (c *fakeOSConfigurator) GetBaseConfig() (OSConfig, error) {
|
||||
func (c *fakeOSConfigurator) Close() error { return nil }
|
||||
|
||||
func TestManager(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skipf("test's assumptions break because of https://github.com/tailscale/corp/issues/1662")
|
||||
}
|
||||
|
||||
// Note: these tests assume that it's safe to switch the
|
||||
// OSConfigurator's split-dns support on and off between Set
|
||||
// calls. Empirically this is currently true, because we reprobe
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -344,11 +343,10 @@ func (m windowsManager) getBasePrimaryResolver() (resolvers []netaddr.IP, err er
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type candidate struct {
|
||||
id winipcfg.LUID
|
||||
metric uint32
|
||||
}
|
||||
var candidates []candidate
|
||||
var (
|
||||
primary winipcfg.LUID
|
||||
best = ^uint32(0)
|
||||
)
|
||||
for _, row := range ifrows {
|
||||
if !row.Connected {
|
||||
continue
|
||||
@@ -356,57 +354,29 @@ func (m windowsManager) getBasePrimaryResolver() (resolvers []netaddr.IP, err er
|
||||
if row.InterfaceLUID == tsLUID {
|
||||
continue
|
||||
}
|
||||
candidates = append(candidates, candidate{row.InterfaceLUID, row.Metric})
|
||||
if row.Metric < best {
|
||||
primary = row.InterfaceLUID
|
||||
best = row.Metric
|
||||
}
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
if primary == 0 {
|
||||
// No resolvers set outside of Tailscale.
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
sort.Slice(candidates, func(i, j int) bool { return candidates[i].metric < candidates[j].metric })
|
||||
|
||||
for _, candidate := range candidates {
|
||||
ips, err := candidate.id.DNS()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ipLoop:
|
||||
for _, stdip := range ips {
|
||||
ip, ok := netaddr.FromStdIP(stdip)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// Skip IPv6 site-local resolvers. These are an ancient
|
||||
// and obsolete IPv6 RFC, which Windows still faithfully
|
||||
// implements. The net result is that some low-metric
|
||||
// interfaces can "have" DNS resolvers, but they're just
|
||||
// site-local resolver IPs that don't go anywhere. So, we
|
||||
// skip the site-local resolvers in order to find the
|
||||
// first interface that has real DNS servers configured.
|
||||
for _, sl := range siteLocalResolvers {
|
||||
if ip.WithZone("") == sl {
|
||||
continue ipLoop
|
||||
}
|
||||
}
|
||||
ips, err := primary.DNS()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, stdip := range ips {
|
||||
if ip, ok := netaddr.FromStdIP(stdip); ok {
|
||||
resolvers = append(resolvers, ip)
|
||||
}
|
||||
|
||||
if len(resolvers) > 0 {
|
||||
// Found some resolvers, we're done.
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return resolvers, nil
|
||||
}
|
||||
|
||||
var siteLocalResolvers = []netaddr.IP{
|
||||
netaddr.MustParseIP("fec0:0:0:ffff::1"),
|
||||
netaddr.MustParseIP("fec0:0:0:ffff::2"),
|
||||
netaddr.MustParseIP("fec0:0:0:ffff::3"),
|
||||
}
|
||||
|
||||
func isWindows7() bool {
|
||||
key, err := registry.OpenKey(registry.LOCAL_MACHINE, versionKey, registry.READ)
|
||||
if err != nil {
|
||||
|
||||
@@ -16,7 +16,6 @@ import (
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/endian"
|
||||
)
|
||||
@@ -138,53 +137,13 @@ func (m *nmManager) trySet(ctx context.Context, config OSConfig) error {
|
||||
}
|
||||
}
|
||||
|
||||
// NetworkManager wipes out IPv6 address configuration unless we
|
||||
// tell it explicitly to keep it. Read out the current interface
|
||||
// settings and mirror them out to NetworkManager.
|
||||
var addrs6 []map[string]interface{}
|
||||
addrs, _, err := interfaces.Tailscale()
|
||||
if err == nil {
|
||||
for _, a := range addrs {
|
||||
if a.Is6() {
|
||||
addrs6 = append(addrs6, map[string]interface{}{
|
||||
"address": a.String(),
|
||||
"prefix": uint32(128),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
seen := map[dnsname.FQDN]bool{}
|
||||
var search []string
|
||||
for _, dom := range config.SearchDomains {
|
||||
if seen[dom] {
|
||||
continue
|
||||
}
|
||||
seen[dom] = true
|
||||
search = append(search, dom.WithTrailingDot())
|
||||
}
|
||||
for _, dom := range config.MatchDomains {
|
||||
if seen[dom] {
|
||||
continue
|
||||
}
|
||||
seen[dom] = true
|
||||
search = append(search, "~"+dom.WithTrailingDot())
|
||||
}
|
||||
if len(config.MatchDomains) == 0 {
|
||||
// Non-split routing requested, add an all-domains match.
|
||||
search = append(search, "~.")
|
||||
}
|
||||
|
||||
// Ideally we would like to disable LLMNR and mdns on the
|
||||
// interface here, but older NetworkManagers don't understand
|
||||
// those settings and choke on them, so we don't. Both LLMNR and
|
||||
// mdns will fail since tailscale0 doesn't do multicast, so it's
|
||||
// effectively fine. We used to try and enforce LLMNR and mdns
|
||||
// settings here, but that led to #1870.
|
||||
general := settings["connection"]
|
||||
general["llmnr"] = dbus.MakeVariant(0)
|
||||
general["mdns"] = dbus.MakeVariant(0)
|
||||
|
||||
ipv4Map := settings["ipv4"]
|
||||
ipv4Map["dns"] = dbus.MakeVariant(dnsv4)
|
||||
ipv4Map["dns-search"] = dbus.MakeVariant(search)
|
||||
ipv4Map["dns-search"] = dbus.MakeVariant(config.SearchDomains)
|
||||
// We should only request priority if we have nameservers to set.
|
||||
if len(dnsv4) == 0 {
|
||||
ipv4Map["dns-priority"] = dbus.MakeVariant(lowerPriority)
|
||||
@@ -215,15 +174,12 @@ func (m *nmManager) trySet(ctx context.Context, config OSConfig) error {
|
||||
// (none of its business anyway, we handle our own default
|
||||
// routing).
|
||||
ipv6Map["method"] = dbus.MakeVariant("auto")
|
||||
if len(addrs6) > 0 {
|
||||
ipv6Map["address-data"] = dbus.MakeVariant(addrs6)
|
||||
}
|
||||
ipv6Map["ignore-auto-routes"] = dbus.MakeVariant(true)
|
||||
ipv6Map["ignore-auto-dns"] = dbus.MakeVariant(true)
|
||||
ipv6Map["never-default"] = dbus.MakeVariant(true)
|
||||
|
||||
ipv6Map["dns"] = dbus.MakeVariant(dnsv6)
|
||||
ipv6Map["dns-search"] = dbus.MakeVariant(search)
|
||||
ipv6Map["dns-search"] = dbus.MakeVariant(config.SearchDomains)
|
||||
if len(dnsv6) == 0 {
|
||||
ipv6Map["dns-priority"] = dbus.MakeVariant(lowerPriority)
|
||||
} else if len(config.MatchDomains) > 0 {
|
||||
@@ -250,7 +206,7 @@ func (m *nmManager) trySet(ctx context.Context, config OSConfig) error {
|
||||
}
|
||||
|
||||
if call := device.CallWithContext(ctx, "org.freedesktop.NetworkManager.Device.Reapply", 0, settings, version, uint32(0)); call.Err != nil {
|
||||
return fmt.Errorf("reapply: %w", call.Err)
|
||||
return fmt.Errorf("reapply: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux freebsd openbsd
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
@@ -21,20 +19,7 @@ func newOpenresolvManager() (openresolvManager, error) {
|
||||
return openresolvManager{}, nil
|
||||
}
|
||||
|
||||
func (m openresolvManager) deleteTailscaleConfig() error {
|
||||
cmd := exec.Command("resolvconf", "-f", "-d", "tailscale")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("running %s: %s", cmd, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m openresolvManager) SetDNS(config OSConfig) error {
|
||||
if config.IsZero() {
|
||||
return m.deleteTailscaleConfig()
|
||||
}
|
||||
|
||||
var stdin bytes.Buffer
|
||||
writeResolvConf(&stdin, config.Nameservers, config.SearchDomains)
|
||||
|
||||
@@ -90,5 +75,10 @@ func (m openresolvManager) GetBaseConfig() (OSConfig, error) {
|
||||
}
|
||||
|
||||
func (m openresolvManager) Close() error {
|
||||
return m.deleteTailscaleConfig()
|
||||
cmd := exec.Command("resolvconf", "-f", "-d", "tailscale")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("running %s: %s", cmd, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -52,10 +52,6 @@ type OSConfig struct {
|
||||
MatchDomains []dnsname.FQDN
|
||||
}
|
||||
|
||||
func (o OSConfig) IsZero() bool {
|
||||
return len(o.Nameservers) == 0 && len(o.SearchDomains) == 0 && len(o.MatchDomains) == 0
|
||||
}
|
||||
|
||||
func (a OSConfig) Equal(b OSConfig) bool {
|
||||
if len(a.Nameservers) != len(b.Nameservers) {
|
||||
return false
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build linux freebsd openbsd
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
|
||||
@@ -12,11 +12,11 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"golang.org/x/sys/unix"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
@@ -85,24 +85,17 @@ func isResolvedActive() bool {
|
||||
// resolvedManager uses the systemd-resolved DBus API.
|
||||
type resolvedManager struct {
|
||||
logf logger.Logf
|
||||
ifidx int
|
||||
resolved dbus.BusObject
|
||||
}
|
||||
|
||||
func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManager, error) {
|
||||
func newResolvedManager(logf logger.Logf) (*resolvedManager, error) {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
iface, err := net.InterfaceByName(interfaceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &resolvedManager{
|
||||
logf: logf,
|
||||
ifidx: iface.Index,
|
||||
resolved: conn.Object("org.freedesktop.resolve1", dbus.ObjectPath("/org/freedesktop/resolve1")),
|
||||
}, nil
|
||||
}
|
||||
@@ -112,6 +105,16 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
|
||||
defer cancel()
|
||||
|
||||
// In principle, we could persist this in the manager struct
|
||||
// if we knew that interface indices are persistent. This does not seem to be the case.
|
||||
_, iface, err := interfaces.Tailscale()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting interface index: %w", err)
|
||||
}
|
||||
if iface == nil {
|
||||
return errNotReady
|
||||
}
|
||||
|
||||
var linkNameservers = make([]resolvedLinkNameserver, len(config.Nameservers))
|
||||
for i, server := range config.Nameservers {
|
||||
ip := server.As16()
|
||||
@@ -128,9 +131,9 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
|
||||
}
|
||||
}
|
||||
|
||||
err := m.resolved.CallWithContext(
|
||||
err = m.resolved.CallWithContext(
|
||||
ctx, "org.freedesktop.resolve1.Manager.SetLinkDNS", 0,
|
||||
m.ifidx, linkNameservers,
|
||||
iface.Index, linkNameservers,
|
||||
).Store()
|
||||
if err != nil {
|
||||
return fmt.Errorf("setLinkDNS: %w", err)
|
||||
@@ -160,7 +163,7 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
|
||||
RoutingOnly: true,
|
||||
})
|
||||
}
|
||||
if len(config.MatchDomains) == 0 && len(config.Nameservers) > 0 {
|
||||
if len(config.MatchDomains) == 0 {
|
||||
// Caller requested full DNS interception, install a
|
||||
// routing-only root domain.
|
||||
linkDomains = append(linkDomains, resolvedLinkDomain{
|
||||
@@ -171,13 +174,13 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
|
||||
|
||||
err = m.resolved.CallWithContext(
|
||||
ctx, "org.freedesktop.resolve1.Manager.SetLinkDomains", 0,
|
||||
m.ifidx, linkDomains,
|
||||
iface.Index, linkDomains,
|
||||
).Store()
|
||||
if err != nil {
|
||||
return fmt.Errorf("setLinkDomains: %w", err)
|
||||
}
|
||||
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDefaultRoute", 0, m.ifidx, len(config.MatchDomains) == 0); call.Err != nil {
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDefaultRoute", 0, iface.Index, len(config.MatchDomains) == 0); call.Err != nil {
|
||||
return fmt.Errorf("setLinkDefaultRoute: %w", err)
|
||||
}
|
||||
|
||||
@@ -186,22 +189,22 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
|
||||
// or something).
|
||||
|
||||
// Disable LLMNR, we don't do multicast.
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkLLMNR", 0, m.ifidx, "no"); call.Err != nil {
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkLLMNR", 0, iface.Index, "no"); call.Err != nil {
|
||||
m.logf("[v1] failed to disable LLMNR: %v", call.Err)
|
||||
}
|
||||
|
||||
// Disable mdns.
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkMulticastDNS", 0, m.ifidx, "no"); call.Err != nil {
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkMulticastDNS", 0, iface.Index, "no"); call.Err != nil {
|
||||
m.logf("[v1] failed to disable mdns: %v", call.Err)
|
||||
}
|
||||
|
||||
// We don't support dnssec consistently right now, force it off to
|
||||
// avoid partial failures when we split DNS internally.
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDNSSEC", 0, m.ifidx, "no"); call.Err != nil {
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDNSSEC", 0, iface.Index, "no"); call.Err != nil {
|
||||
m.logf("[v1] failed to disable DNSSEC: %v", call.Err)
|
||||
}
|
||||
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDNSOverTLS", 0, m.ifidx, "no"); call.Err != nil {
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDNSOverTLS", 0, iface.Index, "no"); call.Err != nil {
|
||||
m.logf("[v1] failed to disable DoT: %v", call.Err)
|
||||
}
|
||||
|
||||
@@ -224,7 +227,15 @@ func (m *resolvedManager) Close() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
|
||||
defer cancel()
|
||||
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.RevertLink", 0, m.ifidx); call.Err != nil {
|
||||
_, iface, err := interfaces.Tailscale()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting interface index: %w", err)
|
||||
}
|
||||
if iface == nil {
|
||||
return errNotReady
|
||||
}
|
||||
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.RevertLink", 0, iface.Index); call.Err != nil {
|
||||
return fmt.Errorf("RevertLink: %w", call.Err)
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"hash/crc32"
|
||||
"math/rand"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -38,6 +39,8 @@ const (
|
||||
|
||||
var errNoUpstreams = errors.New("upstream nameservers not set")
|
||||
|
||||
var aLongTimeAgo = time.Unix(0, 1)
|
||||
|
||||
type forwardingRecord struct {
|
||||
src netaddr.IPPort
|
||||
createdAt time.Time
|
||||
@@ -300,6 +303,8 @@ type fwdConn struct {
|
||||
// logf allows a fwdConn to log.
|
||||
logf logger.Logf
|
||||
|
||||
// wg tracks the number of outstanding conn.Read and conn.Write calls.
|
||||
wg sync.WaitGroup
|
||||
// change allows calls to read to block until a the network connection has been replaced.
|
||||
change *sync.Cond
|
||||
|
||||
@@ -347,12 +352,15 @@ func (c *fwdConn) send(packet []byte, dst netaddr.IPPort) {
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
_, err := conn.WriteTo(packet, dst.UDPAddr())
|
||||
a := dst.UDPAddr()
|
||||
c.wg.Add(1)
|
||||
_, err := conn.WriteTo(packet, a)
|
||||
c.wg.Done()
|
||||
if err == nil {
|
||||
// Success
|
||||
return
|
||||
}
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
// We intentionally closed this connection.
|
||||
// It has been replaced by a new connection. Try again.
|
||||
continue
|
||||
@@ -421,12 +429,14 @@ func (c *fwdConn) read(out []byte) int {
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
c.wg.Add(1)
|
||||
n, _, err := conn.ReadFrom(out)
|
||||
c.wg.Done()
|
||||
if err == nil {
|
||||
// Success.
|
||||
return n
|
||||
}
|
||||
if errors.Is(err, net.ErrClosed) {
|
||||
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
// We intentionally closed this connection.
|
||||
// It has been replaced by a new connection. Try again.
|
||||
continue
|
||||
@@ -458,7 +468,10 @@ func (c *fwdConn) closeConnLocked() {
|
||||
if c.conn == nil {
|
||||
return
|
||||
}
|
||||
c.conn.Close() // unblocks all readers/writers, they'll pick up the next connection.
|
||||
// Unblock all readers/writers, wait for them, close the connection.
|
||||
c.conn.SetDeadline(aLongTimeAgo)
|
||||
c.wg.Wait()
|
||||
c.conn.Close()
|
||||
c.conn = nil
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ var LoginEndpointForProxyDetermination = "https://login.tailscale.com/"
|
||||
// Tailscale returns the current machine's Tailscale interface, if any.
|
||||
// If none is found, all zero values are returned.
|
||||
// A non-nil error is only returned on a problem listing the system interfaces.
|
||||
func Tailscale() ([]netaddr.IP, *net.Interface, error) {
|
||||
func Tailscale() (net.IP, *net.Interface, error) {
|
||||
ifs, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -39,18 +39,14 @@ func Tailscale() ([]netaddr.IP, *net.Interface, error) {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var tsIPs []netaddr.IP
|
||||
for _, a := range addrs {
|
||||
if ipnet, ok := a.(*net.IPNet); ok {
|
||||
nip, ok := netaddr.FromStdIP(ipnet.IP)
|
||||
if ok && tsaddr.IsTailscaleIP(nip) {
|
||||
tsIPs = append(tsIPs, nip)
|
||||
return ipnet.IP, &iface, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(tsIPs) > 0 {
|
||||
return tsIPs, &iface, nil
|
||||
}
|
||||
}
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
@@ -28,11 +28,6 @@ func DefaultRouteInterface() (string, error) {
|
||||
return iface.Name, nil
|
||||
}
|
||||
|
||||
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP2.
|
||||
func fetchRoutingTable() (rib []byte, err error) {
|
||||
return route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0)
|
||||
}
|
||||
|
||||
func DefaultRouteInterfaceIndex() (int, error) {
|
||||
// $ netstat -nr
|
||||
// Routing tables
|
||||
@@ -48,7 +43,7 @@ func DefaultRouteInterfaceIndex() (int, error) {
|
||||
// c RTF_PRCLONING Protocol-specified generate new routes on use
|
||||
// I RTF_IFSCOPE Route is associated with an interface scope
|
||||
|
||||
rib, err := fetchRoutingTable()
|
||||
rib, err := route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route.FetchRIB: %w", err)
|
||||
}
|
||||
@@ -88,7 +83,7 @@ func init() {
|
||||
}
|
||||
|
||||
func likelyHomeRouterIPDarwinFetchRIB() (ret netaddr.IP, ok bool) {
|
||||
rib, err := fetchRoutingTable()
|
||||
rib, err := route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0)
|
||||
if err != nil {
|
||||
log.Printf("routerIP/FetchRIB: %v", err)
|
||||
return ret, false
|
||||
|
||||
@@ -83,14 +83,4 @@ func likelyHomeRouterIPDarwinExec() (ret netaddr.IP, ok bool) {
|
||||
return ret, !ret.IsZero()
|
||||
}
|
||||
|
||||
func TestFetchRoutingTable(t *testing.T) {
|
||||
// Issue 1345: this used to be flaky on darwin.
|
||||
for i := 0; i < 20; i++ {
|
||||
_, err := fetchRoutingTable()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var errStopReadingNetstatTable = errors.New("found private gateway")
|
||||
|
||||
@@ -26,11 +26,11 @@ type stunStats struct {
|
||||
readIPv6 int
|
||||
}
|
||||
|
||||
func Serve(t testing.TB) (addr *net.UDPAddr, cleanupFn func()) {
|
||||
func Serve(t *testing.T) (addr *net.UDPAddr, cleanupFn func()) {
|
||||
return ServeWithPacketListener(t, nettype.Std{})
|
||||
}
|
||||
|
||||
func ServeWithPacketListener(t testing.TB, ln nettype.PacketListener) (addr *net.UDPAddr, cleanupFn func()) {
|
||||
func ServeWithPacketListener(t *testing.T, ln nettype.PacketListener) (addr *net.UDPAddr, cleanupFn func()) {
|
||||
t.Helper()
|
||||
|
||||
// TODO(crawshaw): use stats to test re-STUN logic
|
||||
@@ -52,7 +52,7 @@ func ServeWithPacketListener(t testing.TB, ln nettype.PacketListener) (addr *net
|
||||
}
|
||||
}
|
||||
|
||||
func runSTUN(t testing.TB, pc net.PacketConn, stats *stunStats, done chan<- struct{}) {
|
||||
func runSTUN(t *testing.T, pc net.PacketConn, stats *stunStats, done chan<- struct{}) {
|
||||
defer close(done)
|
||||
|
||||
var buf [64 << 10]byte
|
||||
|
||||
@@ -77,10 +77,10 @@ func Tailscale4To6Range() netaddr.IPPrefix {
|
||||
// used for ephemeral IPv6-only Tailscale nodes.
|
||||
func TailscaleEphemeral6Range() netaddr.IPPrefix {
|
||||
// This IP range has no significance, beyond being a subset of
|
||||
// TailscaleULARange. The bits from /48 to /64 were picked at
|
||||
// TailscaleULARange. The bits from /48 to /104 were picked at
|
||||
// random, with the only criterion being to not be the conflict
|
||||
// with the Tailscale4To6Range above.
|
||||
ulaEph6Range.Do(func() { mustPrefix(&ulaEph6Range.v, "fd7a:115c:a1e0:efe3::/64") })
|
||||
ulaEph6Range.Do(func() { mustPrefix(&ulaEph6Range.v, "fd7a:115c:a1e0:7234:6e44:306d:2100::/104") })
|
||||
return ulaEph6Range.v
|
||||
}
|
||||
|
||||
@@ -138,50 +138,3 @@ type onceIP struct {
|
||||
sync.Once
|
||||
v netaddr.IP
|
||||
}
|
||||
|
||||
// NewContainsIPFunc returns a func that reports whether ip is in addrs.
|
||||
//
|
||||
// It's optimized for the cases of addrs being empty and addrs
|
||||
// containing 1 or 2 single-IP prefixes (such as one IPv4 address and
|
||||
// one IPv6 address).
|
||||
//
|
||||
// Otherwise the implementation is somewhat slow.
|
||||
func NewContainsIPFunc(addrs []netaddr.IPPrefix) func(ip netaddr.IP) bool {
|
||||
// Specialize the three common cases: no address, just IPv4
|
||||
// (or just IPv6), and both IPv4 and IPv6.
|
||||
if len(addrs) == 0 {
|
||||
return func(netaddr.IP) bool { return false }
|
||||
}
|
||||
// If any addr is more than a single IP, then just do the slow
|
||||
// linear thing until
|
||||
// https://github.com/inetaf/netaddr/issues/139 is done.
|
||||
for _, a := range addrs {
|
||||
if a.IsSingleIP() {
|
||||
continue
|
||||
}
|
||||
acopy := append([]netaddr.IPPrefix(nil), addrs...)
|
||||
return func(ip netaddr.IP) bool {
|
||||
for _, a := range acopy {
|
||||
if a.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
// Fast paths for 1 and 2 IPs:
|
||||
if len(addrs) == 1 {
|
||||
a := addrs[0]
|
||||
return func(ip netaddr.IP) bool { return ip == a.IP }
|
||||
}
|
||||
if len(addrs) == 2 {
|
||||
a, b := addrs[0], addrs[1]
|
||||
return func(ip netaddr.IP) bool { return ip == a.IP || ip == b.IP }
|
||||
}
|
||||
// General case:
|
||||
m := map[netaddr.IP]bool{}
|
||||
for _, a := range addrs {
|
||||
m[a.IP] = true
|
||||
}
|
||||
return func(ip netaddr.IP) bool { return m[ip] }
|
||||
}
|
||||
|
||||
@@ -64,32 +64,3 @@ func TestIsUla(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewContainsIPFunc(t *testing.T) {
|
||||
f := NewContainsIPFunc([]netaddr.IPPrefix{netaddr.MustParseIPPrefix("10.0.0.0/8")})
|
||||
if f(netaddr.MustParseIP("8.8.8.8")) {
|
||||
t.Fatal("bad")
|
||||
}
|
||||
if !f(netaddr.MustParseIP("10.1.2.3")) {
|
||||
t.Fatal("bad")
|
||||
}
|
||||
f = NewContainsIPFunc([]netaddr.IPPrefix{netaddr.MustParseIPPrefix("10.1.2.3/32")})
|
||||
if !f(netaddr.MustParseIP("10.1.2.3")) {
|
||||
t.Fatal("bad")
|
||||
}
|
||||
f = NewContainsIPFunc([]netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.1.2.3/32"),
|
||||
netaddr.MustParseIPPrefix("::2/128"),
|
||||
})
|
||||
if !f(netaddr.MustParseIP("::2")) {
|
||||
t.Fatal("bad")
|
||||
}
|
||||
f = NewContainsIPFunc([]netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.1.2.3/32"),
|
||||
netaddr.MustParseIPPrefix("10.1.2.4/32"),
|
||||
netaddr.MustParseIPPrefix("::2/128"),
|
||||
})
|
||||
if !f(netaddr.MustParseIP("10.1.2.4")) {
|
||||
t.Fatal("bad")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ func waitInterfaceUp(iface tun.Device, timeout time.Duration, logf logger.Logf)
|
||||
iw.logf("TUN interface is up after %v", time.Since(t0))
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
break
|
||||
}
|
||||
|
||||
if iw.isUp() {
|
||||
|
||||
@@ -90,12 +90,7 @@ type Wrapper struct {
|
||||
// to discard an empty packet instead of sending it through t.outbound.
|
||||
outbound chan []byte
|
||||
|
||||
// eventsUpDown yields up and down tun.Events that arrive on a Wrapper's events channel.
|
||||
eventsUpDown chan tun.Event
|
||||
// eventsOther yields non-up-and-down tun.Events that arrive on a Wrapper's events channel.
|
||||
eventsOther chan tun.Event
|
||||
|
||||
// filter atomically stores the currently active packet filter
|
||||
// fitler stores the currently active package filter
|
||||
filter atomic.Value // of *filter.Filter
|
||||
// filterFlags control the verbosity of logging packet drops/accepts.
|
||||
filterFlags filter.RunFlags
|
||||
@@ -135,14 +130,11 @@ func Wrap(logf logger.Logf, tdev tun.Device) *Wrapper {
|
||||
closed: make(chan struct{}),
|
||||
errors: make(chan error),
|
||||
outbound: make(chan []byte),
|
||||
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,
|
||||
}
|
||||
|
||||
go tun.poll()
|
||||
go tun.pumpEvents()
|
||||
// The buffer starts out consumed.
|
||||
tun.bufferConsumed <- struct{}{}
|
||||
|
||||
@@ -168,50 +160,8 @@ func (t *Wrapper) Close() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// pumpEvents copies events from t.tdev to t.eventsUpDown and t.eventsOther.
|
||||
// pumpEvents exits when t.tdev.events or t.closed is closed.
|
||||
// pumpEvents closes t.eventsUpDown and t.eventsOther when it exits.
|
||||
func (t *Wrapper) pumpEvents() {
|
||||
defer close(t.eventsUpDown)
|
||||
defer close(t.eventsOther)
|
||||
src := t.tdev.Events()
|
||||
for {
|
||||
// Retrieve an event from the TUN device.
|
||||
var event tun.Event
|
||||
var ok bool
|
||||
select {
|
||||
case <-t.closed:
|
||||
return
|
||||
case event, ok = <-src:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Pass along event to the correct recipient.
|
||||
// Though event is a bitmask, in practice there is only ever one bit set at a time.
|
||||
dst := t.eventsOther
|
||||
if event&(tun.EventUp|tun.EventDown) != 0 {
|
||||
dst = t.eventsUpDown
|
||||
}
|
||||
select {
|
||||
case <-t.closed:
|
||||
return
|
||||
case dst <- event:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// EventsUpDown returns a TUN event channel that contains all Up and Down events.
|
||||
func (t *Wrapper) EventsUpDown() chan tun.Event {
|
||||
return t.eventsUpDown
|
||||
}
|
||||
|
||||
// Events returns a TUN event channel that contains all non-Up, non-Down events.
|
||||
// It is named Events because it is the set of events that we want to expose to wireguard-go,
|
||||
// and Events is the name specified by the wireguard-go tun.Device interface.
|
||||
func (t *Wrapper) Events() chan tun.Event {
|
||||
return t.eventsOther
|
||||
return t.tdev.Events()
|
||||
}
|
||||
|
||||
func (t *Wrapper) File() *os.File {
|
||||
@@ -457,22 +407,13 @@ func (t *Wrapper) filterIn(buf []byte) filter.Response {
|
||||
// like wireguard-go/tun.Device.Write.
|
||||
func (t *Wrapper) Write(buf []byte, offset int) (int, error) {
|
||||
if !t.disableFilter {
|
||||
if t.filterIn(buf[offset:]) != filter.Accept {
|
||||
// If we're not accepting the packet, lie to wireguard-go and pretend
|
||||
// that everything is okay with a nil error, so wireguard-go
|
||||
// doesn't log about this Write "failure".
|
||||
//
|
||||
// We return len(buf), but the ill-defined wireguard-go/tun.Device.Write
|
||||
// method doesn't specify how the offset affects the return value.
|
||||
// In fact, the Linux implementation does one of two different things depending
|
||||
// on how the /dev/net/tun was created. But fortunately the wireguard-go
|
||||
// code ignores the int return and only looks at the error:
|
||||
//
|
||||
// device/receive.go: _, err = device.tun.device.Write(....)
|
||||
//
|
||||
// TODO(bradfitz): fix upstream interface docs, implementation.
|
||||
res := t.filterIn(buf[offset:])
|
||||
if res == filter.DropSilently {
|
||||
return len(buf), nil
|
||||
}
|
||||
if res != filter.Accept {
|
||||
return 0, ErrFiltered
|
||||
}
|
||||
}
|
||||
|
||||
t.noteActivity()
|
||||
|
||||
@@ -329,14 +329,11 @@ func TestFilter(t *testing.T) {
|
||||
var filtered bool
|
||||
|
||||
if tt.dir == in {
|
||||
// Use the side effect of updating the last
|
||||
// activity atomic to determine whether the
|
||||
// data was actually filtered.
|
||||
// If it stays zero, nothing made it through
|
||||
// to the wrapped TUN.
|
||||
atomic.StoreInt64(&tun.lastActivityAtomic, 0)
|
||||
_, err = tun.Write(tt.data, 0)
|
||||
filtered = atomic.LoadInt64(&tun.lastActivityAtomic) == 0
|
||||
if err == ErrFiltered {
|
||||
filtered = true
|
||||
err = nil
|
||||
}
|
||||
} else {
|
||||
chtun.Outbound <- tt.data
|
||||
n, err = tun.Read(buf[:], 0)
|
||||
|
||||
@@ -26,13 +26,6 @@ func DefaultTailscaledSocket() string {
|
||||
if runtime.GOOS == "darwin" {
|
||||
return "/var/run/tailscaled.socket"
|
||||
}
|
||||
if runtime.GOOS == "linux" {
|
||||
// TODO(crawshaw): does this path change with DSM7?
|
||||
const synologySock = "/volume1/@appstore/Tailscale/var/tailscaled.sock" // SYNOPKG_PKGDEST in scripts/installer
|
||||
if fi, err := os.Stat(filepath.Dir(synologySock)); err == nil && fi.IsDir() {
|
||||
return synologySock
|
||||
}
|
||||
}
|
||||
if fi, err := os.Stat("/var/run"); err == nil && fi.IsDir() {
|
||||
return "/var/run/tailscale/tailscaled.sock"
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func listPorts() (List, error) {
|
||||
}
|
||||
|
||||
func addProcesses(pl []Port) ([]Port, error) {
|
||||
//lint:ignore SA1019 OpenCurrentProcessToken instead of GetCurrentProcessToken,
|
||||
// OpenCurrentProcessToken instead of GetCurrentProcessToken,
|
||||
// as GetCurrentProcessToken only works on Windows 8+.
|
||||
tok, err := windows.OpenCurrentProcessToken()
|
||||
if err != nil {
|
||||
|
||||
@@ -11,6 +11,11 @@ import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func path(vendor, name string, port uint16) string {
|
||||
return fmt.Sprintf("127.0.0.1:%v", port)
|
||||
}
|
||||
|
||||
// TODO(apenwarr): handle magic cookie auth
|
||||
func connect(path string, port uint16) (net.Conn, error) {
|
||||
pipe, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port))
|
||||
if err != nil {
|
||||
@@ -31,7 +36,7 @@ func setFlags(network, address string, c syscall.RawConn) error {
|
||||
// built on top of an API that's a disaster. So for now we'll hack it by
|
||||
// just always using a TCP session on a fixed port on localhost. As a
|
||||
// result, on Windows we ignore the vendor and name strings.
|
||||
// NOTE(bradfitz): Jason did a new pipe package: https://go-review.googlesource.com/c/sys/+/299009
|
||||
// TODO(apenwarr): handle magic cookie auth
|
||||
func listen(path string, port uint16) (_ net.Listener, gotPort uint16, _ error) {
|
||||
lc := net.ListenConfig{
|
||||
Control: setFlags,
|
||||
|
||||
@@ -61,12 +61,8 @@ func LocalTCPPortAndToken() (port int, token string, err error) {
|
||||
|
||||
// PlatformUsesPeerCreds reports whether the current platform uses peer credentials
|
||||
// to authenticate connections.
|
||||
func PlatformUsesPeerCreds() bool { return GOOSUsesPeerCreds(runtime.GOOS) }
|
||||
|
||||
// GOOSUsesPeerCreds is like PlatformUsesPeerCreds but takes a
|
||||
// runtime.GOOS value instead of using the current one.
|
||||
func GOOSUsesPeerCreds(goos string) bool {
|
||||
switch goos {
|
||||
func PlatformUsesPeerCreds() bool {
|
||||
switch runtime.GOOS {
|
||||
case "linux", "darwin", "freebsd":
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,414 +0,0 @@
|
||||
#!/bin/sh
|
||||
# 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 script detects the current operating system, and installs
|
||||
# Tailscale according to that OS's conventions.
|
||||
|
||||
set -eu
|
||||
|
||||
RELEASE="${TAILSCALE_RELEASE:-stable}"
|
||||
|
||||
# All the code is wrapped in a main function that gets called at the
|
||||
# bottom of the file, so that a truncated partial download doesn't end
|
||||
# up executing half a script.
|
||||
main() {
|
||||
# Step 1: detect the current linux distro, version, and packaging system.
|
||||
#
|
||||
# We rely on a combination of 'uname' and /etc/os-release to find
|
||||
# an OS name and version, and from there work out what
|
||||
# installation method we should be using.
|
||||
#
|
||||
# The end result of this step is that the following three
|
||||
# variables are populated, if detection was successful.
|
||||
OS=""
|
||||
VERSION=""
|
||||
PACKAGETYPE=""
|
||||
|
||||
if [ -f /etc/os-release ]; then
|
||||
# /etc/os-release populates a number of shell variables. We care about the following:
|
||||
# - ID: the short name of the OS (e.g. "debian", "freebsd")
|
||||
# - VERSION_ID: the numeric release version for the OS, if any (e.g. "18.04")
|
||||
# - VERSION_CODENAME: the codename of the OS release, if any (e.g. "buster")
|
||||
. /etc/os-release
|
||||
case "$ID" in
|
||||
ubuntu)
|
||||
OS="$ID"
|
||||
VERSION="$VERSION_CODENAME"
|
||||
PACKAGETYPE="apt"
|
||||
;;
|
||||
debian)
|
||||
OS="$ID"
|
||||
VERSION="$VERSION_CODENAME"
|
||||
PACKAGETYPE="apt"
|
||||
;;
|
||||
raspbian)
|
||||
OS="$ID"
|
||||
VERSION="$VERSION_CODENAME"
|
||||
PACKAGETYPE="apt"
|
||||
;;
|
||||
centos)
|
||||
OS="$ID"
|
||||
VERSION="$VERSION_ID"
|
||||
PACKAGETYPE="dnf"
|
||||
if [ "$VERSION" = "7" ]; then
|
||||
PACKAGETYPE="yum"
|
||||
fi
|
||||
;;
|
||||
rhel)
|
||||
OS="$ID"
|
||||
VERSION="$(echo "$VERSION_ID" | cut -f1 -d.)"
|
||||
PACKAGETYPE="dnf"
|
||||
;;
|
||||
fedora)
|
||||
OS="$ID"
|
||||
VERSION=""
|
||||
PACKAGETYPE="dnf"
|
||||
;;
|
||||
amzn)
|
||||
OS="amazon-linux"
|
||||
VERSION="$VERSION_ID"
|
||||
PACKAGETYPE="yum"
|
||||
;;
|
||||
opensuse-leap)
|
||||
OS="opensuse"
|
||||
VERSION="leap/$VERSION_ID"
|
||||
PACKAGETYPE="zypper"
|
||||
;;
|
||||
opensuse-tumbleweed)
|
||||
OS="opensuse"
|
||||
VERSION="tumbleweed"
|
||||
PACKAGETYPE="zypper"
|
||||
;;
|
||||
arch)
|
||||
OS="$ID"
|
||||
VERSION="" # rolling release
|
||||
PACKAGETYPE="pacman"
|
||||
;;
|
||||
manjaro)
|
||||
OS="$ID"
|
||||
VERSION="" # rolling release
|
||||
PACKAGETYPE="pacman"
|
||||
;;
|
||||
alpine)
|
||||
OS="$ID"
|
||||
VERSION="$(echo $PRETTY_NAME | cut -d' ' -f3)"
|
||||
PACKAGETYPE="apk"
|
||||
;;
|
||||
nixos)
|
||||
echo "Please add Tailscale to your NixOS configuration directly:"
|
||||
echo
|
||||
echo "services.tailscale.enable = true;"
|
||||
exit 1
|
||||
;;
|
||||
void)
|
||||
OS="$ID"
|
||||
VERSION="" # rolling release
|
||||
PACKAGETYPE="xbps"
|
||||
;;
|
||||
gentoo)
|
||||
OS="$ID"
|
||||
VERSION="" # rolling release
|
||||
PACKAGETYPE="emerge"
|
||||
;;
|
||||
freebsd)
|
||||
OS="$ID"
|
||||
VERSION="$(echo "$VERSION_ID" | cut -f1 -d.)"
|
||||
PACKAGETYPE="pkg"
|
||||
;;
|
||||
# TODO: wsl?
|
||||
# TODO: synology? qnap?
|
||||
esac
|
||||
fi
|
||||
|
||||
# If we failed to detect something through os-release, consult
|
||||
# uname and try to infer things from that.
|
||||
if [ -z "$OS" ]; then
|
||||
if type uname >/dev/null 2>&1; then
|
||||
case "$(uname)" in
|
||||
FreeBSD)
|
||||
# FreeBSD before 12.2 doesn't have
|
||||
# /etc/os-release, so we wouldn't have found it in
|
||||
# the os-release probing above.
|
||||
OS="freebsd"
|
||||
VERSION="$(freebsd-version | cut -f1 -d.)"
|
||||
PACKAGETYPE="pkg"
|
||||
;;
|
||||
OpenBSD)
|
||||
OS="openbsd"
|
||||
VERSION="$(uname -r)"
|
||||
PACKAGETYPE=""
|
||||
;;
|
||||
Darwin)
|
||||
OS="macos"
|
||||
VERSION="$(sw_vers -productVersion | cut -f1-2 -d.)"
|
||||
PACKAGETYPE="appstore"
|
||||
;;
|
||||
Linux)
|
||||
OS="other-linux"
|
||||
VERSION=""
|
||||
PACKAGETYPE=""
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 2: having detected an OS we support, is it one of the
|
||||
# versions we support?
|
||||
OS_UNSUPPORTED=
|
||||
case "$OS" in
|
||||
ubuntu)
|
||||
if [ "$VERSION" != "xenial" ] && \
|
||||
[ "$VERSION" != "bionic" ] && \
|
||||
[ "$VERSION" != "eoan" ] && \
|
||||
[ "$VERSION" != "focal" ] && \
|
||||
[ "$VERSION" != "groovy" ] && \
|
||||
[ "$VERSION" != "hirsute" ]
|
||||
then
|
||||
OS_UNSUPPORTED=1
|
||||
fi
|
||||
;;
|
||||
debian)
|
||||
if [ "$VERSION" != "stretch" ] && \
|
||||
[ "$VERSION" != "buster" ] && \
|
||||
[ "$VERSION" != "bullseye" ] && \
|
||||
[ "$VERSION" != "sid" ]
|
||||
then
|
||||
OS_UNSUPPORTED=1
|
||||
fi
|
||||
;;
|
||||
raspbian)
|
||||
if [ "$VERSION" != "buster" ]
|
||||
then
|
||||
OS_UNSUPPORTED=1
|
||||
fi
|
||||
;;
|
||||
fedora)
|
||||
# We support every fedora release currently in use.
|
||||
# No checking needed.
|
||||
;;
|
||||
centos)
|
||||
if [ "$VERSION" != "7" ] && \
|
||||
[ "$VERSION" != "8" ]
|
||||
then
|
||||
OS_UNSUPPORTED=1
|
||||
fi
|
||||
;;
|
||||
rhel)
|
||||
if [ "$VERSION" != "8" ]
|
||||
then
|
||||
OS_UNSUPPORTED=1
|
||||
fi
|
||||
;;
|
||||
amazon-linux)
|
||||
if [ "$VERSION" != "2" ]
|
||||
then
|
||||
OS_UNSUPPORTED=1
|
||||
fi
|
||||
;;
|
||||
opensuse)
|
||||
if [ "$VERSION" != "leap/15.1" ] && \
|
||||
[ "$VERSION" != "leap/15.2" ] && \
|
||||
[ "$VERSION" != "tumbleweed" ]
|
||||
then
|
||||
OS_UNSUPPORTED=1
|
||||
fi
|
||||
;;
|
||||
arch)
|
||||
# Rolling release, no version checking needed.
|
||||
;;
|
||||
manjaro)
|
||||
# Rolling release, no version checking needed.
|
||||
;;
|
||||
alpine)
|
||||
if [ "$VERSION" != "edge" ]
|
||||
then
|
||||
OS_UNSUPPORTED=1
|
||||
fi
|
||||
;;
|
||||
void)
|
||||
# Rolling release, no version checking needed.
|
||||
;;
|
||||
gentoo)
|
||||
# Rolling release, no version checking needed.
|
||||
;;
|
||||
freebsd)
|
||||
if [ "$VERSION" != "12" ] && \
|
||||
[ "$VERSION" != "13" ]
|
||||
then
|
||||
OS_UNSUPPORTED=1
|
||||
fi
|
||||
;;
|
||||
openbsd)
|
||||
OS_UNSUPPORTED=1
|
||||
;;
|
||||
macos)
|
||||
# We delegate macOS installation to the app store, it will
|
||||
# perform version checks for us.
|
||||
;;
|
||||
other-linux)
|
||||
OS_UNSUPPORTED=1
|
||||
;;
|
||||
*)
|
||||
OS_UNSUPPORTED=1
|
||||
;;
|
||||
esac
|
||||
if [ "$OS_UNSUPPORTED" = "1" ]; then
|
||||
case "$OS" in
|
||||
other-linux)
|
||||
echo "Couldn't determine what kind of Linux is running."
|
||||
echo "You could try the static binaries at:"
|
||||
echo "https://pkgs.tailscale.com/stable/#static"
|
||||
;;
|
||||
"")
|
||||
echo "Couldn't determine what operating system you're running."
|
||||
;;
|
||||
*)
|
||||
echo "$OS $VERSION isn't supported by this script yet."
|
||||
;;
|
||||
esac
|
||||
echo
|
||||
echo "If you'd like us to support your system better, please email support@tailscale.com"
|
||||
echo "and tell us what OS you're running."
|
||||
echo
|
||||
echo "Please include the following information we gathered from your system:"
|
||||
echo
|
||||
echo "OS=$OS"
|
||||
echo "VERSION=$VERSION"
|
||||
echo "PACKAGETYPE=$PACKAGETYPE"
|
||||
if type uname >/dev/null 2>&1; then
|
||||
echo "UNAME=$(uname -a)"
|
||||
else
|
||||
echo "UNAME="
|
||||
fi
|
||||
echo
|
||||
if [ -f /etc/os-release ]; then
|
||||
cat /etc/os-release
|
||||
else
|
||||
echo "No /etc/os-release"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 3: work out if we can run privileged commands, and if so,
|
||||
# how.
|
||||
CAN_ROOT=
|
||||
SUDO=
|
||||
if [ "$(id -u)" = 0 ]; then
|
||||
CAN_ROOT=1
|
||||
SUDO=""
|
||||
elif type sudo >/dev/null; then
|
||||
CAN_ROOT=1
|
||||
SUDO="sudo"
|
||||
elif type doas >/dev/null; then
|
||||
CAN_ROOT=1
|
||||
SUDO="doas"
|
||||
fi
|
||||
if [ "$CAN_ROOT" != "1" ]; then
|
||||
echo "This installer needs to run commands as root."
|
||||
echo "We tried looking for 'sudo' and 'doas', but couldn't find them."
|
||||
echo "Either re-run this script as root, or set up sudo/doas."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# Step 4: run the installation.
|
||||
echo "Installing Tailscale for $OS $VERSION, using method $PACKAGETYPE"
|
||||
case "$PACKAGETYPE" in
|
||||
apt)
|
||||
# Ideally we want to use curl, but on some installs we
|
||||
# only have wget. Detect and use what's available.
|
||||
CURL=
|
||||
if type curl >/dev/null; then
|
||||
CURL="curl -fsSL"
|
||||
elif type wget >/dev/null; then
|
||||
CURL="wget -q -O-"
|
||||
fi
|
||||
if [ -z "$CURL" ]; then
|
||||
echo "The installer needs either curl or wget to download files."
|
||||
echo "Please install either curl or wget to proceed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# TODO: use newfangled per-repo signature scheme
|
||||
set -x
|
||||
$CURL "https://pkgs.tailscale.com/$RELEASE/$OS/$VERSION.gpg" | $SUDO apt-key add -
|
||||
$CURL "https://pkgs.tailscale.com/$RELEASE/$OS/$VERSION.list" | $SUDO tee /etc/apt/sources.list.d/tailscale.list
|
||||
$SUDO apt-get update
|
||||
$SUDO apt-get install tailscale
|
||||
set +x
|
||||
;;
|
||||
yum)
|
||||
set -x
|
||||
$SUDO yum install -y yum-utils
|
||||
$SUDO yum-config-manager --add-repo "https://pkgs.tailscale.com/$RELEASE/$OS/$VERSION/tailscale.repo"
|
||||
$SUDO yum install -y tailscale
|
||||
$SUDO systemctl enable --now tailscaled
|
||||
set +x
|
||||
;;
|
||||
dnf)
|
||||
set -x
|
||||
$SUDO dnf config-manager --add-repo "https://pkgs.tailscale.com/$RELEASE/$OS/$VERSION/tailscale.repo"
|
||||
$SUDO dnf install -y tailscale
|
||||
$SUDO systemctl enable --now tailscaled
|
||||
set +x
|
||||
;;
|
||||
zypper)
|
||||
set -x
|
||||
$SUDO rpm --import https://pkgs.tailscale.com/$RELEASE/$OS/$VERSION/repo.gpg
|
||||
$SUDO zypper ar -g -r "https://pkgs.tailscale.com/$RELEASE/$OS/$VERSION/tailscale.repo"
|
||||
$SUDO zypper ref -r tailscale-stable
|
||||
$SUDO zypper in -y tailscale
|
||||
$SUDO systemctl enable --now tailscaled
|
||||
set +x
|
||||
;;
|
||||
pacman)
|
||||
set -x
|
||||
$SUDO pacman -S --noconfirm tailscale
|
||||
$SUDO systemctl enable --now tailscaled
|
||||
set +x
|
||||
;;
|
||||
apk)
|
||||
set -x
|
||||
$SUDO apk add tailscale
|
||||
$SUDO rc-update add tailscale
|
||||
$SUDO service tailscale start
|
||||
set +x
|
||||
;;
|
||||
xbps)
|
||||
set -x
|
||||
$SUDO xbps-install tailscale
|
||||
set +x
|
||||
;;
|
||||
emerge)
|
||||
set -x
|
||||
$SUDO emerge net-vpn/tailscale
|
||||
set +x
|
||||
;;
|
||||
appstore)
|
||||
set -x
|
||||
open "https://apps.apple.com/us/app/tailscale/id1475387142"
|
||||
set +x
|
||||
;;
|
||||
pkg)
|
||||
set -x
|
||||
$SUDO pkg install -y tailscale
|
||||
set +x
|
||||
;;
|
||||
*)
|
||||
echo "unexpected: unknown package type $PACKAGETYPE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Installation complete! Log in to start using Tailscale by running:"
|
||||
echo
|
||||
if [ -z "$SUDO" ]; then
|
||||
echo "tailscale up"
|
||||
else
|
||||
echo "$SUDO tailscale up"
|
||||
fi
|
||||
}
|
||||
|
||||
main
|
||||
@@ -6,6 +6,7 @@ package syncs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -48,11 +49,11 @@ func TestWatchContended(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestWatchMultipleValues(t *testing.T) {
|
||||
if cibuild.On() {
|
||||
if cibuild.On() && runtime.GOOS == "windows" {
|
||||
// On the CI machine, it sometimes takes 500ms to start a new goroutine.
|
||||
// When this happens, we don't get enough events quickly enough.
|
||||
// Nothing's wrong, and it's not worth working around. Just skip the test.
|
||||
t.Skip("flaky on CI")
|
||||
t.Skip("flaky on Windows CI")
|
||||
}
|
||||
mu := new(sync.Mutex)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
@@ -7,7 +7,7 @@ package tailcfg
|
||||
//go:generate go run tailscale.com/cmd/cloner --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse --clonefunc=true --output=tailcfg_clone.go
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
@@ -39,10 +39,7 @@ import (
|
||||
// 14: 2021-04-07: client understands DNSConfig.Routes and DNSConfig.Resolvers
|
||||
// 15: 2021-04-12: client treats nil MapResponse.DNSConfig as meaning unchanged
|
||||
// 16: 2021-04-15: client understands Node.Online, MapResponse.OnlineChange
|
||||
// 17: 2021-04-18: MapResponse.Domain empty means unchanged
|
||||
// 18: 2021-04-19: MapResponse.Node nil means unchanged (all fields now omitempty)
|
||||
// 19: 2021-04-21: MapResponse.Debug.SleepSeconds
|
||||
const CurrentMapRequestVersion = 19
|
||||
const CurrentMapRequestVersion = 16
|
||||
|
||||
type StableID string
|
||||
|
||||
@@ -679,7 +676,7 @@ func (et EndpointType) String() string {
|
||||
|
||||
// Endpoint is an endpoint IPPort and an associated type.
|
||||
// It doesn't currently go over the wire as is but is instead
|
||||
// broken up into two parallel slices in MapRequest, for compatibility
|
||||
// broken up into two parallel slices in MapReqeust, for compatibility
|
||||
// reasons. But this type is used in the codebase.
|
||||
type Endpoint struct {
|
||||
Addr netaddr.IPPort
|
||||
@@ -850,11 +847,6 @@ type DNSConfig struct {
|
||||
// Map keys must be fully-qualified DNS name suffixes, with a
|
||||
// trailing dot but no leading dot.
|
||||
Routes map[string][]DNSResolver `json:",omitempty"`
|
||||
// FallbackResolvers is like Resolvers, but is only used if a
|
||||
// split DNS configuration is requested in a configuration that
|
||||
// doesn't work yet without explicit default resolvers.
|
||||
// https://github.com/tailscale/tailscale/issues/1743
|
||||
FallbackResolvers []DNSResolver `json:",omitempty"`
|
||||
// Domains are the search domains to use.
|
||||
// Search domains must be FQDNs, but *without* the trailing dot.
|
||||
Domains []string `json:",omitempty"`
|
||||
@@ -871,6 +863,8 @@ type DNSConfig struct {
|
||||
Nameservers []netaddr.IP `json:",omitempty"`
|
||||
|
||||
// PerDomain is not set by the control server, and does nothing.
|
||||
// TODO(danderson): revise DNS configuration to make this useful
|
||||
// again.
|
||||
PerDomain bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
@@ -900,14 +894,8 @@ type MapResponse struct {
|
||||
PingRequest *PingRequest `json:",omitempty"`
|
||||
|
||||
// Networking
|
||||
|
||||
// Node describes the node making the map request.
|
||||
// Starting with MapRequest.Version 18, nil means unchanged.
|
||||
Node *Node `json:",omitempty"`
|
||||
|
||||
// DERPMap describe the set of DERP servers available.
|
||||
// A nil value means unchanged.
|
||||
DERPMap *DERPMap `json:",omitempty"`
|
||||
Node *Node
|
||||
DERPMap *DERPMap `json:",omitempty"` // if non-empty, a change in the DERP map.
|
||||
|
||||
// Peers, if non-empty, is the complete list of peers.
|
||||
// It will be set in the first MapResponse for a long-polled request/response.
|
||||
@@ -951,8 +939,7 @@ type MapResponse struct {
|
||||
// "foo@gmail.com" (for siloed users on shared email
|
||||
// providers). Its exact form should not be depended on; new
|
||||
// forms are coming later.
|
||||
// If empty, the value is unchanged.
|
||||
Domain string `json:",omitempty"`
|
||||
Domain string
|
||||
|
||||
// CollectServices reports whether this node's Tailnet has
|
||||
// requested that info about services be included in HostInfo.
|
||||
@@ -966,12 +953,9 @@ type MapResponse struct {
|
||||
// previously streamed non-nil MapResponse.PacketFilter within
|
||||
// the same HTTP response. A non-nil but empty list always means
|
||||
// no PacketFilter (that is, to block everything).
|
||||
PacketFilter []FilterRule `json:",omitempty"`
|
||||
PacketFilter []FilterRule
|
||||
|
||||
// UserProfiles are the user profiles of nodes in the network.
|
||||
// As as of 1.1.541 (mapver 5), this contains new or updated
|
||||
// user profiles only.
|
||||
UserProfiles []UserProfile `json:",omitempty"`
|
||||
UserProfiles []UserProfile // as of 1.1.541 (mapver 5): may be new or updated user profiles only
|
||||
|
||||
// Debug is normally nil, except for when the control server
|
||||
// is setting debug settings on a node.
|
||||
@@ -1013,12 +997,6 @@ type Debug struct {
|
||||
// GoroutineDumpURL, if non-empty, requests that the client do
|
||||
// a one-time dump of its active goroutines to the given URL.
|
||||
GoroutineDumpURL string `json:",omitempty"`
|
||||
|
||||
// SleepSeconds requests that the client sleep for the
|
||||
// provided number of seconds.
|
||||
// The client can (and should) limit the value (such as 5
|
||||
// minutes).
|
||||
SleepSeconds float64 `json:",omitempty"`
|
||||
}
|
||||
|
||||
func (k MachineKey) String() string { return fmt.Sprintf("mkey:%x", k[:]) }
|
||||
@@ -1027,10 +1005,9 @@ func (k MachineKey) HexString() string { return fmt.Sprintf("%x",
|
||||
func (k *MachineKey) UnmarshalText(text []byte) error { return keyUnmarshalText(k[:], "mkey:", text) }
|
||||
|
||||
func keyMarshalText(prefix string, k [32]byte) []byte {
|
||||
buf := make([]byte, len(prefix)+64)
|
||||
copy(buf, prefix)
|
||||
hex.Encode(buf[len(prefix):], k[:])
|
||||
return buf
|
||||
buf := bytes.NewBuffer(make([]byte, 0, len(prefix)+64))
|
||||
fmt.Fprintf(buf, "%s%x", prefix, k[:])
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func keyUnmarshalText(dst []byte, prefix string, text []byte) error {
|
||||
|
||||
@@ -198,10 +198,6 @@ func (src *DNSConfig) Clone() *DNSConfig {
|
||||
dst.Routes[k] = append([]DNSResolver{}, src.Routes[k]...)
|
||||
}
|
||||
}
|
||||
dst.FallbackResolvers = make([]DNSResolver, len(src.FallbackResolvers))
|
||||
for i := range dst.FallbackResolvers {
|
||||
dst.FallbackResolvers[i] = *src.FallbackResolvers[i].Clone()
|
||||
}
|
||||
dst.Domains = append(src.Domains[:0:0], src.Domains...)
|
||||
dst.Nameservers = append(src.Nameservers[:0:0], src.Nameservers...)
|
||||
return dst
|
||||
@@ -210,13 +206,12 @@ func (src *DNSConfig) Clone() *DNSConfig {
|
||||
// A compilation failure here means this code must be regenerated, with command:
|
||||
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse
|
||||
var _DNSConfigNeedsRegeneration = DNSConfig(struct {
|
||||
Resolvers []DNSResolver
|
||||
Routes map[string][]DNSResolver
|
||||
FallbackResolvers []DNSResolver
|
||||
Domains []string
|
||||
Proxied bool
|
||||
Nameservers []netaddr.IP
|
||||
PerDomain bool
|
||||
Resolvers []DNSResolver
|
||||
Routes map[string][]DNSResolver
|
||||
Domains []string
|
||||
Proxied bool
|
||||
Nameservers []netaddr.IP
|
||||
PerDomain bool
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of DNSResolver.
|
||||
|
||||
@@ -518,13 +518,3 @@ func TestEndpointTypeMarshal(t *testing.T) {
|
||||
t.Errorf("got %s; want %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
var sinkBytes []byte
|
||||
|
||||
func BenchmarkKeyMarshalText(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
var k [32]byte
|
||||
for i := 0; i < b.N; i++ {
|
||||
sinkBytes = keyMarshalText("prefix", k)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The tshello server demonstrates how to use Tailscale as a library.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/tsnet"
|
||||
)
|
||||
|
||||
func main() {
|
||||
s := new(tsnet.Server)
|
||||
ln, err := s.Listen("tcp", ":80")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Fatal(http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
who, ok := s.WhoIs(r.RemoteAddr)
|
||||
if !ok {
|
||||
http.Error(w, "WhoIs failed", 500)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "<html><body><h1>Hello, world!</h1>\n")
|
||||
fmt.Fprintf(w, "<p>You are <b>%s</b> from <b>%s</b> (%s)</p>",
|
||||
html.EscapeString(who.UserProfile.LoginName),
|
||||
html.EscapeString(firstLabel(who.Node.ComputedName)),
|
||||
r.RemoteAddr)
|
||||
})))
|
||||
}
|
||||
|
||||
func firstLabel(s string) string {
|
||||
if i := strings.Index(s, "."); i != -1 {
|
||||
return s[:i]
|
||||
}
|
||||
return s
|
||||
}
|
||||
274
tsnet/tsnet.go
274
tsnet/tsnet.go
@@ -1,274 +0,0 @@
|
||||
// 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 tsnet provides Tailscale as a library.
|
||||
//
|
||||
// It is an experimental work in progress.
|
||||
package tsnet
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
)
|
||||
|
||||
// Server is an embedded Tailscale server.
|
||||
//
|
||||
// Its exported fields may be changed until the first call to Listen.
|
||||
type Server struct {
|
||||
// Dir specifies the name of the directory to use for
|
||||
// state. If empty, a directory is selected automatically
|
||||
// under os.UserConfigDir (https://golang.org/pkg/os/#UserConfigDir).
|
||||
// based on the name of the binary.
|
||||
Dir string
|
||||
|
||||
// Hostname is the hostname to present to the control server.
|
||||
// If empty, the binary name is used.l
|
||||
Hostname string
|
||||
|
||||
// Logf, if non-nil, specifies the logger to use. By default,
|
||||
// log.Printf is used.
|
||||
Logf logger.Logf
|
||||
|
||||
initOnce sync.Once
|
||||
initErr error
|
||||
lb *ipnlocal.LocalBackend
|
||||
// the state directory
|
||||
dir string
|
||||
hostname string
|
||||
|
||||
mu sync.Mutex
|
||||
listeners map[listenKey]*listener
|
||||
}
|
||||
|
||||
// WhoIs reports the node and user who owns the node with the given
|
||||
// address. The addr may be an ip:port (as from an
|
||||
// http.Request.RemoteAddr) or just an IP address.
|
||||
func (s *Server) WhoIs(addr string) (w *apitype.WhoIsResponse, ok bool) {
|
||||
ipp, err := netaddr.ParseIPPort(addr)
|
||||
if err != nil {
|
||||
ip, err := netaddr.ParseIP(addr)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
ipp.IP = ip
|
||||
}
|
||||
n, up, ok := s.lb.WhoIs(ipp)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return &apitype.WhoIsResponse{
|
||||
Node: n,
|
||||
UserProfile: &up,
|
||||
}, true
|
||||
}
|
||||
|
||||
func (s *Server) doInit() {
|
||||
if err := s.start(); err != nil {
|
||||
s.initErr = fmt.Errorf("tsnet: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) start() error {
|
||||
if v, _ := strconv.ParseBool(os.Getenv("TAILSCALE_USE_WIP_CODE")); !v {
|
||||
return errors.New("code disabled without environment variable TAILSCALE_USE_WIP_CODE set true")
|
||||
}
|
||||
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
prog := strings.TrimSuffix(strings.ToLower(filepath.Base(exe)), ".exe")
|
||||
|
||||
s.hostname = s.Hostname
|
||||
if s.hostname == "" {
|
||||
s.hostname = prog
|
||||
}
|
||||
|
||||
s.dir = s.Dir
|
||||
if s.dir == "" {
|
||||
confDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.dir = filepath.Join(confDir, "tslib-"+prog)
|
||||
if err := os.MkdirAll(s.dir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if fi, err := os.Stat(s.dir); err != nil {
|
||||
return err
|
||||
} else if !fi.IsDir() {
|
||||
return fmt.Errorf("%v is not a directory", s.dir)
|
||||
}
|
||||
|
||||
logf := s.Logf
|
||||
if logf == nil {
|
||||
logf = log.Printf
|
||||
}
|
||||
|
||||
// TODO(bradfitz): start logtail? don't use filch, perhaps?
|
||||
// only upload plumbed Logf?
|
||||
|
||||
linkMon, err := monitor.New(logf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
ListenPort: 0,
|
||||
LinkMonitor: linkMon,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tunDev, magicConn, ok := eng.(wgengine.InternalsGetter).GetInternals()
|
||||
if !ok {
|
||||
return fmt.Errorf("%T is not a wgengine.InternalsGetter", eng)
|
||||
}
|
||||
|
||||
ns, err := netstack.Create(logf, tunDev, eng, magicConn, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("netstack.Create: %w", err)
|
||||
}
|
||||
ns.ForwardTCPIn = s.forwardTCP
|
||||
if err := ns.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start netstack: %w", err)
|
||||
}
|
||||
|
||||
statePath := filepath.Join(s.dir, "tailscaled.state")
|
||||
store, err := ipn.NewFileStore(statePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logid := "tslib-TODO"
|
||||
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, eng)
|
||||
if err != nil {
|
||||
return fmt.Errorf("NewLocalBackend: %v", err)
|
||||
}
|
||||
s.lb = lb
|
||||
lb.SetDecompressor(func() (controlclient.Decompressor, error) {
|
||||
return smallzstd.NewDecoder(nil)
|
||||
})
|
||||
prefs := ipn.NewPrefs()
|
||||
prefs.Hostname = s.hostname
|
||||
prefs.WantRunning = true
|
||||
err = lb.Start(ipn.Options{
|
||||
StateKey: ipn.GlobalDaemonStateKey,
|
||||
UpdatePrefs: prefs,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting backend: %w", err)
|
||||
}
|
||||
if os.Getenv("TS_LOGIN") == "1" {
|
||||
s.lb.StartLoginInteractive()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) forwardTCP(c net.Conn, port uint16) {
|
||||
s.mu.Lock()
|
||||
ln, ok := s.listeners[listenKey{"tcp", "", fmt.Sprint(port)}]
|
||||
s.mu.Unlock()
|
||||
if !ok {
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
t := time.NewTimer(time.Second)
|
||||
defer t.Stop()
|
||||
select {
|
||||
case ln.conn <- c:
|
||||
case <-t.C:
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Listen(network, addr string) (net.Listener, error) {
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tsnet: %w", err)
|
||||
}
|
||||
|
||||
s.initOnce.Do(s.doInit)
|
||||
if s.initErr != nil {
|
||||
return nil, s.initErr
|
||||
}
|
||||
|
||||
key := listenKey{network, host, port}
|
||||
ln := &listener{
|
||||
s: s,
|
||||
key: key,
|
||||
addr: addr,
|
||||
|
||||
conn: make(chan net.Conn),
|
||||
}
|
||||
s.mu.Lock()
|
||||
if s.listeners == nil {
|
||||
s.listeners = map[listenKey]*listener{}
|
||||
}
|
||||
if _, ok := s.listeners[key]; ok {
|
||||
s.mu.Unlock()
|
||||
return nil, fmt.Errorf("tsnet: listener already open for %s, %s", network, addr)
|
||||
}
|
||||
s.listeners[key] = ln
|
||||
s.mu.Unlock()
|
||||
return ln, nil
|
||||
}
|
||||
|
||||
type listenKey struct {
|
||||
network string
|
||||
host string
|
||||
port string
|
||||
}
|
||||
|
||||
type listener struct {
|
||||
s *Server
|
||||
key listenKey
|
||||
addr string
|
||||
conn chan net.Conn
|
||||
}
|
||||
|
||||
func (ln *listener) Accept() (net.Conn, error) {
|
||||
c, ok := <-ln.conn
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("tsnet: %w", net.ErrClosed)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (ln *listener) Addr() net.Addr { return addr{ln} }
|
||||
func (ln *listener) Close() error {
|
||||
ln.s.mu.Lock()
|
||||
defer ln.s.mu.Unlock()
|
||||
if v, ok := ln.s.listeners[ln.key]; ok && v == ln {
|
||||
delete(ln.s.listeners, ln.key)
|
||||
close(ln.conn)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type addr struct{ ln *listener }
|
||||
|
||||
func (a addr) Network() string { return a.ln.key.network }
|
||||
func (a addr) String() string { return a.ln.addr }
|
||||
6
tstest/doc.go
Normal file
6
tstest/doc.go
Normal file
@@ -0,0 +1,6 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package tstest provides utilities for use in unit tests.
|
||||
package tstest
|
||||
@@ -1,654 +0,0 @@
|
||||
// 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 integration contains Tailscale integration tests.
|
||||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
crand "crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/stun/stuntest"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/tstest/integration/testcontrol"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/nettype"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var verbose = flag.Bool("verbose", false, "verbose debug logs")
|
||||
|
||||
var mainError atomic.Value // of error
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
v := m.Run()
|
||||
if v != 0 {
|
||||
os.Exit(v)
|
||||
}
|
||||
if err, ok := mainError.Load().(error); ok {
|
||||
fmt.Fprintf(os.Stderr, "FAIL: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func TestOneNodeUp_NoAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := buildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
defer env.Close()
|
||||
|
||||
n1 := newTestNode(t, env)
|
||||
|
||||
d1 := n1.StartDaemon(t)
|
||||
defer d1.Kill()
|
||||
|
||||
n1.AwaitListening(t)
|
||||
|
||||
st := n1.MustStatus(t)
|
||||
t.Logf("Status: %s", st.BackendState)
|
||||
|
||||
if err := tstest.WaitFor(20*time.Second, func() error {
|
||||
const sub = `Program starting: `
|
||||
if !env.LogCatcher.logsContains(mem.S(sub)) {
|
||||
return fmt.Errorf("log catcher didn't see %#q; got %s", sub, env.LogCatcher.logsString())
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
n1.MustUp()
|
||||
|
||||
if d, _ := time.ParseDuration(os.Getenv("TS_POST_UP_SLEEP")); d > 0 {
|
||||
t.Logf("Sleeping for %v to give 'up' time to misbehave (https://github.com/tailscale/tailscale/issues/1840) ...", d)
|
||||
time.Sleep(d)
|
||||
}
|
||||
|
||||
t.Logf("Got IP: %v", n1.AwaitIP(t))
|
||||
n1.AwaitRunning(t)
|
||||
|
||||
d1.MustCleanShutdown(t)
|
||||
|
||||
t.Logf("number of HTTP logcatcher requests: %v", env.LogCatcher.numRequests())
|
||||
}
|
||||
|
||||
func TestOneNodeUp_Auth(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := buildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
defer env.Close()
|
||||
env.Control.RequireAuth = true
|
||||
|
||||
n1 := newTestNode(t, env)
|
||||
d1 := n1.StartDaemon(t)
|
||||
defer d1.Kill()
|
||||
|
||||
n1.AwaitListening(t)
|
||||
|
||||
st := n1.MustStatus(t)
|
||||
t.Logf("Status: %s", st.BackendState)
|
||||
|
||||
t.Logf("Running up --login-server=%s ...", env.ControlServer.URL)
|
||||
|
||||
cmd := n1.Tailscale("up", "--login-server="+env.ControlServer.URL)
|
||||
var authCountAtomic int32
|
||||
cmd.Stdout = &authURLParserWriter{fn: func(urlStr string) error {
|
||||
if env.Control.CompleteAuth(urlStr) {
|
||||
atomic.AddInt32(&authCountAtomic, 1)
|
||||
t.Logf("completed auth path %s", urlStr)
|
||||
return nil
|
||||
}
|
||||
err := fmt.Errorf("Failed to complete auth path to %q", urlStr)
|
||||
t.Log(err)
|
||||
return err
|
||||
}}
|
||||
cmd.Stderr = cmd.Stdout
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("up: %v", err)
|
||||
}
|
||||
t.Logf("Got IP: %v", n1.AwaitIP(t))
|
||||
|
||||
n1.AwaitRunning(t)
|
||||
|
||||
if n := atomic.LoadInt32(&authCountAtomic); n != 1 {
|
||||
t.Errorf("Auth URLs completed = %d; want 1", n)
|
||||
}
|
||||
|
||||
d1.MustCleanShutdown(t)
|
||||
|
||||
}
|
||||
|
||||
func TestTwoNodes(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := buildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
defer env.Close()
|
||||
|
||||
// Create two nodes:
|
||||
n1 := newTestNode(t, env)
|
||||
d1 := n1.StartDaemon(t)
|
||||
defer d1.Kill()
|
||||
|
||||
n2 := newTestNode(t, env)
|
||||
d2 := n2.StartDaemon(t)
|
||||
defer d2.Kill()
|
||||
|
||||
n1.AwaitListening(t)
|
||||
n2.AwaitListening(t)
|
||||
n1.MustUp()
|
||||
n2.MustUp()
|
||||
n1.AwaitRunning(t)
|
||||
n2.AwaitRunning(t)
|
||||
|
||||
if err := tstest.WaitFor(2*time.Second, func() error {
|
||||
st := n1.MustStatus(t)
|
||||
if len(st.Peer) == 0 {
|
||||
return errors.New("no peers")
|
||||
}
|
||||
if len(st.Peer) > 1 {
|
||||
return fmt.Errorf("got %d peers; want 1", len(st.Peer))
|
||||
}
|
||||
peer := st.Peer[st.Peers()[0]]
|
||||
if peer.ID == st.Self.ID {
|
||||
return errors.New("peer is self")
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
d1.MustCleanShutdown(t)
|
||||
d2.MustCleanShutdown(t)
|
||||
}
|
||||
|
||||
// testBinaries are the paths to a tailscaled and tailscale binary.
|
||||
// These can be shared by multiple nodes.
|
||||
type testBinaries struct {
|
||||
dir string // temp dir for tailscale & tailscaled
|
||||
daemon string // tailscaled
|
||||
cli string // tailscale
|
||||
}
|
||||
|
||||
// buildTestBinaries builds tailscale and tailscaled, failing the test
|
||||
// if they fail to compile.
|
||||
func buildTestBinaries(t testing.TB) *testBinaries {
|
||||
td := t.TempDir()
|
||||
build(t, td, "tailscale.com/cmd/tailscaled", "tailscale.com/cmd/tailscale")
|
||||
return &testBinaries{
|
||||
dir: td,
|
||||
daemon: filepath.Join(td, "tailscaled"+exe()),
|
||||
cli: filepath.Join(td, "tailscale"+exe()),
|
||||
}
|
||||
}
|
||||
|
||||
// testEnv contains the test environment (set of servers) used by one
|
||||
// or more nodes.
|
||||
type testEnv struct {
|
||||
t testing.TB
|
||||
Binaries *testBinaries
|
||||
|
||||
LogCatcher *logCatcher
|
||||
LogCatcherServer *httptest.Server
|
||||
|
||||
Control *testcontrol.Server
|
||||
ControlServer *httptest.Server
|
||||
|
||||
TrafficTrap *trafficTrap
|
||||
TrafficTrapServer *httptest.Server
|
||||
|
||||
derpShutdown func()
|
||||
}
|
||||
|
||||
// newTestEnv starts a bunch of services and returns a new test
|
||||
// environment.
|
||||
//
|
||||
// Call Close to shut everything down.
|
||||
func newTestEnv(t testing.TB, bins *testBinaries) *testEnv {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("not tested/working on Windows yet")
|
||||
}
|
||||
derpMap, derpShutdown := runDERPAndStun(t, logger.Discard)
|
||||
logc := new(logCatcher)
|
||||
control := &testcontrol.Server{
|
||||
DERPMap: derpMap,
|
||||
}
|
||||
trafficTrap := new(trafficTrap)
|
||||
e := &testEnv{
|
||||
t: t,
|
||||
Binaries: bins,
|
||||
LogCatcher: logc,
|
||||
LogCatcherServer: httptest.NewServer(logc),
|
||||
Control: control,
|
||||
ControlServer: httptest.NewServer(control),
|
||||
TrafficTrap: trafficTrap,
|
||||
TrafficTrapServer: httptest.NewServer(trafficTrap),
|
||||
derpShutdown: derpShutdown,
|
||||
}
|
||||
e.Control.BaseURL = e.ControlServer.URL
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *testEnv) Close() error {
|
||||
if err := e.TrafficTrap.Err(); err != nil {
|
||||
e.t.Errorf("traffic trap: %v", err)
|
||||
e.t.Logf("logs: %s", e.LogCatcher.logsString())
|
||||
}
|
||||
|
||||
e.LogCatcherServer.Close()
|
||||
e.TrafficTrapServer.Close()
|
||||
e.ControlServer.Close()
|
||||
e.derpShutdown()
|
||||
return nil
|
||||
}
|
||||
|
||||
// testNode is a machine with a tailscale & tailscaled.
|
||||
// Currently, the test is simplistic and user==node==machine.
|
||||
// That may grow complexity later to test more.
|
||||
type testNode struct {
|
||||
env *testEnv
|
||||
|
||||
dir string // temp dir for sock & state
|
||||
sockFile string
|
||||
stateFile string
|
||||
}
|
||||
|
||||
// newTestNode allocates a temp directory for a new test node.
|
||||
// The node is not started automatically.
|
||||
func newTestNode(t *testing.T, env *testEnv) *testNode {
|
||||
dir := t.TempDir()
|
||||
return &testNode{
|
||||
env: env,
|
||||
dir: dir,
|
||||
sockFile: filepath.Join(dir, "tailscale.sock"),
|
||||
stateFile: filepath.Join(dir, "tailscale.state"),
|
||||
}
|
||||
}
|
||||
|
||||
type Daemon struct {
|
||||
Process *os.Process
|
||||
}
|
||||
|
||||
func (d *Daemon) Kill() {
|
||||
d.Process.Kill()
|
||||
}
|
||||
|
||||
func (d *Daemon) MustCleanShutdown(t testing.TB) {
|
||||
d.Process.Signal(os.Interrupt)
|
||||
ps, err := d.Process.Wait()
|
||||
if err != nil {
|
||||
t.Fatalf("tailscaled Wait: %v", err)
|
||||
}
|
||||
if ps.ExitCode() != 0 {
|
||||
t.Errorf("tailscaled ExitCode = %d; want 0", ps.ExitCode())
|
||||
}
|
||||
}
|
||||
|
||||
// StartDaemon starts the node's tailscaled, failing if it fails to
|
||||
// start.
|
||||
func (n *testNode) StartDaemon(t testing.TB) *Daemon {
|
||||
cmd := exec.Command(n.env.Binaries.daemon,
|
||||
"--tun=userspace-networking",
|
||||
"--state="+n.stateFile,
|
||||
"--socket="+n.sockFile,
|
||||
)
|
||||
cmd.Env = append(os.Environ(),
|
||||
"TS_LOG_TARGET="+n.env.LogCatcherServer.URL,
|
||||
"HTTP_PROXY="+n.env.TrafficTrapServer.URL,
|
||||
"HTTPS_PROXY="+n.env.TrafficTrapServer.URL,
|
||||
)
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("starting tailscaled: %v", err)
|
||||
}
|
||||
return &Daemon{
|
||||
Process: cmd.Process,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *testNode) MustUp() {
|
||||
t := n.env.t
|
||||
t.Logf("Running up --login-server=%s ...", n.env.ControlServer.URL)
|
||||
if err := n.Tailscale("up", "--login-server="+n.env.ControlServer.URL).Run(); err != nil {
|
||||
t.Fatalf("up: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// AwaitListening waits for the tailscaled to be serving local clients
|
||||
// over its localhost IPC mechanism. (Unix socket, etc)
|
||||
func (n *testNode) AwaitListening(t testing.TB) {
|
||||
if err := tstest.WaitFor(20*time.Second, func() (err error) {
|
||||
c, err := safesocket.Connect(n.sockFile, 41112)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Close()
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *testNode) AwaitIP(t testing.TB) (ips string) {
|
||||
t.Helper()
|
||||
if err := tstest.WaitFor(20*time.Second, func() error {
|
||||
out, err := n.Tailscale("ip").Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ips = string(out)
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatalf("awaiting an IP address: %v", err)
|
||||
}
|
||||
if ips == "" {
|
||||
t.Fatalf("returned IP address was blank")
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
func (n *testNode) AwaitRunning(t testing.TB) {
|
||||
t.Helper()
|
||||
if err := tstest.WaitFor(20*time.Second, func() error {
|
||||
st, err := n.Status()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if st.BackendState != "Running" {
|
||||
return fmt.Errorf("in state %q", st.BackendState)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatalf("failure/timeout waiting for transition to Running status: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tailscale returns a command that runs the tailscale CLI with the provided arguments.
|
||||
// It does not start the process.
|
||||
func (n *testNode) Tailscale(arg ...string) *exec.Cmd {
|
||||
cmd := exec.Command(n.env.Binaries.cli, "--socket="+n.sockFile)
|
||||
cmd.Args = append(cmd.Args, arg...)
|
||||
cmd.Dir = n.dir
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (n *testNode) Status() (*ipnstate.Status, error) {
|
||||
out, err := n.Tailscale("status", "--json").CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("running tailscale status: %v, %s", err, out)
|
||||
}
|
||||
st := new(ipnstate.Status)
|
||||
if err := json.Unmarshal(out, st); err != nil {
|
||||
return nil, fmt.Errorf("decoding tailscale status JSON: %w", err)
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
|
||||
func (n *testNode) MustStatus(tb testing.TB) *ipnstate.Status {
|
||||
tb.Helper()
|
||||
st, err := n.Status()
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
func exe() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return ".exe"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func findGo(t testing.TB) string {
|
||||
goBin := filepath.Join(runtime.GOROOT(), "bin", "go"+exe())
|
||||
if fi, err := os.Stat(goBin); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
t.Fatalf("failed to find go at %v", goBin)
|
||||
}
|
||||
t.Fatalf("looking for go binary: %v", err)
|
||||
} else if !fi.Mode().IsRegular() {
|
||||
t.Fatalf("%v is unexpected %v", goBin, fi.Mode())
|
||||
}
|
||||
return goBin
|
||||
}
|
||||
|
||||
// buildMu limits our use of "go build" to one at a time, so we don't
|
||||
// fight Go's built-in caching trying to do the same build concurrently.
|
||||
var buildMu sync.Mutex
|
||||
|
||||
func build(t testing.TB, outDir string, targets ...string) {
|
||||
buildMu.Lock()
|
||||
defer buildMu.Unlock()
|
||||
|
||||
t0 := time.Now()
|
||||
defer func() { t.Logf("built %s in %v", targets, time.Since(t0).Round(time.Millisecond)) }()
|
||||
|
||||
goBin := findGo(t)
|
||||
cmd := exec.Command(goBin, "install")
|
||||
if version.IsRace() {
|
||||
cmd.Args = append(cmd.Args, "-race")
|
||||
}
|
||||
cmd.Args = append(cmd.Args, targets...)
|
||||
cmd.Env = append(os.Environ(), "GOARCH="+runtime.GOARCH, "GOBIN="+outDir)
|
||||
errOut, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if strings.Contains(string(errOut), "when GOBIN is set") {
|
||||
// Fallback slow path for cross-compiled binaries.
|
||||
for _, target := range targets {
|
||||
outFile := filepath.Join(outDir, path.Base(target)+exe())
|
||||
cmd := exec.Command(goBin, "build", "-o", outFile, target)
|
||||
cmd.Env = append(os.Environ(), "GOARCH="+runtime.GOARCH)
|
||||
if errOut, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("failed to build %v with %v: %v, %s", target, goBin, err, errOut)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Fatalf("failed to build %v with %v: %v, %s", targets, goBin, err, errOut)
|
||||
}
|
||||
|
||||
// logCatcher is a minimal logcatcher for the logtail upload client.
|
||||
type logCatcher struct {
|
||||
mu sync.Mutex
|
||||
buf bytes.Buffer
|
||||
gotErr error
|
||||
reqs int
|
||||
}
|
||||
|
||||
func (lc *logCatcher) logsContains(sub mem.RO) bool {
|
||||
lc.mu.Lock()
|
||||
defer lc.mu.Unlock()
|
||||
return mem.Contains(mem.B(lc.buf.Bytes()), sub)
|
||||
}
|
||||
|
||||
func (lc *logCatcher) numRequests() int {
|
||||
lc.mu.Lock()
|
||||
defer lc.mu.Unlock()
|
||||
return lc.reqs
|
||||
}
|
||||
|
||||
func (lc *logCatcher) logsString() string {
|
||||
lc.mu.Lock()
|
||||
defer lc.mu.Unlock()
|
||||
return lc.buf.String()
|
||||
}
|
||||
|
||||
func (lc *logCatcher) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
var body io.Reader = r.Body
|
||||
if r.Header.Get("Content-Encoding") == "zstd" {
|
||||
var err error
|
||||
body, err = smallzstd.NewDecoder(body)
|
||||
if err != nil {
|
||||
log.Printf("bad caught zstd: %v", err)
|
||||
http.Error(w, err.Error(), 400)
|
||||
return
|
||||
}
|
||||
}
|
||||
bodyBytes, _ := ioutil.ReadAll(body)
|
||||
|
||||
type Entry struct {
|
||||
Logtail struct {
|
||||
ClientTime time.Time `json:"client_time"`
|
||||
ServerTime time.Time `json:"server_time"`
|
||||
Error struct {
|
||||
BadData string `json:"bad_data"`
|
||||
} `json:"error"`
|
||||
} `json:"logtail"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
var jreq []Entry
|
||||
var err error
|
||||
if len(bodyBytes) > 0 && bodyBytes[0] == '[' {
|
||||
err = json.Unmarshal(bodyBytes, &jreq)
|
||||
} else {
|
||||
var ent Entry
|
||||
err = json.Unmarshal(bodyBytes, &ent)
|
||||
jreq = append(jreq, ent)
|
||||
}
|
||||
|
||||
lc.mu.Lock()
|
||||
defer lc.mu.Unlock()
|
||||
lc.reqs++
|
||||
if lc.gotErr == nil && err != nil {
|
||||
lc.gotErr = err
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(&lc.buf, "error from %s of %#q: %v\n", r.Method, bodyBytes, err)
|
||||
} else {
|
||||
for _, ent := range jreq {
|
||||
fmt.Fprintf(&lc.buf, "%s\n", strings.TrimSpace(ent.Text))
|
||||
if *verbose {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", strings.TrimSpace(ent.Text))
|
||||
}
|
||||
}
|
||||
}
|
||||
w.WriteHeader(200) // must have no content, but not a 204
|
||||
}
|
||||
|
||||
// trafficTrap is an HTTP proxy handler to note whether any
|
||||
// HTTP traffic tries to leave localhost from tailscaled. We don't
|
||||
// expect any, so any request triggers a failure.
|
||||
type trafficTrap struct {
|
||||
atomicErr atomic.Value // of error
|
||||
}
|
||||
|
||||
func (tt *trafficTrap) Err() error {
|
||||
if err, ok := tt.atomicErr.Load().(error); ok {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tt *trafficTrap) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
var got bytes.Buffer
|
||||
r.Write(&got)
|
||||
err := fmt.Errorf("unexpected HTTP proxy via proxy: %s", got.Bytes())
|
||||
mainError.Store(err)
|
||||
if tt.Err() == nil {
|
||||
// Best effort at remembering the first request.
|
||||
tt.atomicErr.Store(err)
|
||||
}
|
||||
log.Printf("Error: %v", err)
|
||||
w.WriteHeader(403)
|
||||
}
|
||||
|
||||
func runDERPAndStun(t testing.TB, logf logger.Logf) (derpMap *tailcfg.DERPMap, cleanup func()) {
|
||||
var serverPrivateKey key.Private
|
||||
if _, err := crand.Read(serverPrivateKey[:]); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
d := derp.NewServer(serverPrivateKey, logf)
|
||||
|
||||
httpsrv := httptest.NewUnstartedServer(derphttp.Handler(d))
|
||||
httpsrv.Config.ErrorLog = logger.StdLogger(logf)
|
||||
httpsrv.Config.TLSNextProto = make(map[string]func(*http.Server, *tls.Conn, http.Handler))
|
||||
httpsrv.StartTLS()
|
||||
|
||||
stunAddr, stunCleanup := stuntest.ServeWithPacketListener(t, nettype.Std{})
|
||||
|
||||
m := &tailcfg.DERPMap{
|
||||
Regions: map[int]*tailcfg.DERPRegion{
|
||||
1: {
|
||||
RegionID: 1,
|
||||
RegionCode: "test",
|
||||
Nodes: []*tailcfg.DERPNode{
|
||||
{
|
||||
Name: "t1",
|
||||
RegionID: 1,
|
||||
HostName: "127.0.0.1", // to bypass HTTP proxy
|
||||
IPv4: "127.0.0.1",
|
||||
IPv6: "none",
|
||||
STUNPort: stunAddr.Port,
|
||||
DERPTestPort: httpsrv.Listener.Addr().(*net.TCPAddr).Port,
|
||||
STUNTestIP: stunAddr.IP.String(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
cleanup = func() {
|
||||
httpsrv.CloseClientConnections()
|
||||
httpsrv.Close()
|
||||
d.Close()
|
||||
stunCleanup()
|
||||
}
|
||||
|
||||
return m, cleanup
|
||||
}
|
||||
|
||||
type authURLParserWriter struct {
|
||||
buf bytes.Buffer
|
||||
fn func(urlStr string) error
|
||||
}
|
||||
|
||||
var authURLRx = regexp.MustCompile(`(https?://\S+/auth/\S+)`)
|
||||
|
||||
func (w *authURLParserWriter) Write(p []byte) (n int, err error) {
|
||||
n, err = w.buf.Write(p)
|
||||
m := authURLRx.FindSubmatch(w.buf.Bytes())
|
||||
if m != nil {
|
||||
urlStr := string(m[1])
|
||||
w.buf.Reset() // so it's not matched again
|
||||
if err := w.fn(urlStr); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
@@ -1,689 +0,0 @@
|
||||
// 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 testcontrol contains a minimal control plane server for testing purposes.
|
||||
package testcontrol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
crand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/compress/zstd"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/derp/derpmap"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
// Server is a control plane server. Its zero value is ready for use.
|
||||
// Everything is stored in-memory in one tailnet.
|
||||
type Server struct {
|
||||
Logf logger.Logf // nil means to use the log package
|
||||
DERPMap *tailcfg.DERPMap // nil means to use prod DERP map
|
||||
RequireAuth bool
|
||||
BaseURL string // must be set to e.g. "http://127.0.0.1:1234" with no trailing URL
|
||||
Verbose bool
|
||||
|
||||
initMuxOnce sync.Once
|
||||
mux *http.ServeMux
|
||||
|
||||
mu sync.Mutex
|
||||
pubKey wgkey.Key
|
||||
privKey wgkey.Private
|
||||
nodes map[tailcfg.NodeKey]*tailcfg.Node
|
||||
users map[tailcfg.NodeKey]*tailcfg.User
|
||||
logins map[tailcfg.NodeKey]*tailcfg.Login
|
||||
updates map[tailcfg.NodeID]chan updateType
|
||||
authPath map[string]*AuthPath
|
||||
nodeKeyAuthed map[tailcfg.NodeKey]bool // key => true once authenticated
|
||||
}
|
||||
|
||||
type AuthPath struct {
|
||||
nodeKey tailcfg.NodeKey
|
||||
|
||||
closeOnce sync.Once
|
||||
ch chan struct{}
|
||||
success bool
|
||||
}
|
||||
|
||||
func (ap *AuthPath) completeSuccessfully() {
|
||||
ap.success = true
|
||||
close(ap.ch)
|
||||
}
|
||||
|
||||
// CompleteSuccessfully completes the login path successfully, as if
|
||||
// the user did the whole auth dance.
|
||||
func (ap *AuthPath) CompleteSuccessfully() {
|
||||
ap.closeOnce.Do(ap.completeSuccessfully)
|
||||
}
|
||||
|
||||
func (s *Server) logf(format string, a ...interface{}) {
|
||||
if s.Logf != nil {
|
||||
s.Logf(format, a...)
|
||||
} else {
|
||||
log.Printf(format, a...)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) initMux() {
|
||||
s.mux = http.NewServeMux()
|
||||
s.mux.HandleFunc("/", s.serveUnhandled)
|
||||
s.mux.HandleFunc("/key", s.serveKey)
|
||||
s.mux.HandleFunc("/machine/", s.serveMachine)
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.initMuxOnce.Do(s.initMux)
|
||||
s.mux.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (s *Server) serveUnhandled(w http.ResponseWriter, r *http.Request) {
|
||||
var got bytes.Buffer
|
||||
r.Write(&got)
|
||||
go panic(fmt.Sprintf("testcontrol.Server received unhandled request: %s", got.Bytes()))
|
||||
}
|
||||
|
||||
func (s *Server) publicKey() wgkey.Key {
|
||||
pub, _ := s.keyPair()
|
||||
return pub
|
||||
}
|
||||
|
||||
func (s *Server) privateKey() wgkey.Private {
|
||||
_, priv := s.keyPair()
|
||||
return priv
|
||||
}
|
||||
|
||||
func (s *Server) keyPair() (pub wgkey.Key, priv wgkey.Private) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.pubKey.IsZero() {
|
||||
var err error
|
||||
s.privKey, err = wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
go panic(err) // bring down test, even if in http.Handler
|
||||
}
|
||||
s.pubKey = s.privKey.Public()
|
||||
}
|
||||
return s.pubKey, s.privKey
|
||||
}
|
||||
|
||||
func (s *Server) serveKey(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(200)
|
||||
io.WriteString(w, s.publicKey().HexString())
|
||||
}
|
||||
|
||||
func (s *Server) serveMachine(w http.ResponseWriter, r *http.Request) {
|
||||
mkeyStr := strings.TrimPrefix(r.URL.Path, "/machine/")
|
||||
rem := ""
|
||||
if i := strings.IndexByte(mkeyStr, '/'); i != -1 {
|
||||
rem = mkeyStr[i:]
|
||||
mkeyStr = mkeyStr[:i]
|
||||
}
|
||||
|
||||
key, err := wgkey.ParseHex(mkeyStr)
|
||||
if err != nil {
|
||||
http.Error(w, "bad machine key hex", 400)
|
||||
return
|
||||
}
|
||||
mkey := tailcfg.MachineKey(key)
|
||||
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "POST required", 400)
|
||||
return
|
||||
}
|
||||
|
||||
switch rem {
|
||||
case "":
|
||||
s.serveRegister(w, r, mkey)
|
||||
case "/map":
|
||||
s.serveMap(w, r, mkey)
|
||||
default:
|
||||
s.serveUnhandled(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Node returns the node for nodeKey. It's always nil or cloned memory.
|
||||
func (s *Server) Node(nodeKey tailcfg.NodeKey) *tailcfg.Node {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.nodes[nodeKey].Clone()
|
||||
}
|
||||
|
||||
func (s *Server) AllNodes() (nodes []*tailcfg.Node) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for _, n := range s.nodes {
|
||||
nodes = append(nodes, n.Clone())
|
||||
}
|
||||
sort.Slice(nodes, func(i, j int) bool {
|
||||
return nodes[i].StableID < nodes[j].StableID
|
||||
})
|
||||
return nodes
|
||||
}
|
||||
|
||||
func (s *Server) getUser(nodeKey tailcfg.NodeKey) (*tailcfg.User, *tailcfg.Login) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.users == nil {
|
||||
s.users = map[tailcfg.NodeKey]*tailcfg.User{}
|
||||
}
|
||||
if s.logins == nil {
|
||||
s.logins = map[tailcfg.NodeKey]*tailcfg.Login{}
|
||||
}
|
||||
if u, ok := s.users[nodeKey]; ok {
|
||||
return u, s.logins[nodeKey]
|
||||
}
|
||||
id := tailcfg.UserID(len(s.users) + 1)
|
||||
domain := "fake-control.example.net"
|
||||
loginName := fmt.Sprintf("user-%d@%s", id, domain)
|
||||
displayName := fmt.Sprintf("User %d", id)
|
||||
login := &tailcfg.Login{
|
||||
ID: tailcfg.LoginID(id),
|
||||
Provider: "testcontrol",
|
||||
LoginName: loginName,
|
||||
DisplayName: displayName,
|
||||
ProfilePicURL: "https://tailscale.com/static/images/marketing/team-carney.jpg",
|
||||
Domain: domain,
|
||||
}
|
||||
user := &tailcfg.User{
|
||||
ID: id,
|
||||
LoginName: loginName,
|
||||
DisplayName: displayName,
|
||||
Domain: domain,
|
||||
Logins: []tailcfg.LoginID{login.ID},
|
||||
}
|
||||
s.users[nodeKey] = user
|
||||
s.logins[nodeKey] = login
|
||||
return user, login
|
||||
}
|
||||
|
||||
// authPathDone returns a close-only struct that's closed when the
|
||||
// authPath ("/auth/XXXXXX") has authenticated.
|
||||
func (s *Server) authPathDone(authPath string) <-chan struct{} {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if a, ok := s.authPath[authPath]; ok {
|
||||
return a.ch
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) addAuthPath(authPath string, nodeKey tailcfg.NodeKey) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.authPath == nil {
|
||||
s.authPath = map[string]*AuthPath{}
|
||||
}
|
||||
s.authPath[authPath] = &AuthPath{
|
||||
ch: make(chan struct{}),
|
||||
nodeKey: nodeKey,
|
||||
}
|
||||
}
|
||||
|
||||
// CompleteAuth marks the provided path or URL (containing
|
||||
// "/auth/...") as successfully authenticated, unblocking any
|
||||
// requests blocked on that in serveRegister.
|
||||
func (s *Server) CompleteAuth(authPathOrURL string) bool {
|
||||
i := strings.Index(authPathOrURL, "/auth/")
|
||||
if i == -1 {
|
||||
return false
|
||||
}
|
||||
authPath := authPathOrURL[i:]
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
ap, ok := s.authPath[authPath]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if ap.nodeKey.IsZero() {
|
||||
panic("zero AuthPath.NodeKey")
|
||||
}
|
||||
if s.nodeKeyAuthed == nil {
|
||||
s.nodeKeyAuthed = map[tailcfg.NodeKey]bool{}
|
||||
}
|
||||
s.nodeKeyAuthed[ap.nodeKey] = true
|
||||
ap.CompleteSuccessfully()
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey tailcfg.MachineKey) {
|
||||
var req tailcfg.RegisterRequest
|
||||
if err := s.decode(mkey, r.Body, &req); err != nil {
|
||||
panic(fmt.Sprintf("serveRegister: decode: %v", err))
|
||||
}
|
||||
if req.Version != 1 {
|
||||
panic(fmt.Sprintf("serveRegister: unsupported version: %d", req.Version))
|
||||
}
|
||||
if req.NodeKey.IsZero() {
|
||||
panic("serveRegister: request has zero node key")
|
||||
}
|
||||
if s.Verbose {
|
||||
j, _ := json.MarshalIndent(req, "", "\t")
|
||||
log.Printf("Got %T: %s", req, j)
|
||||
}
|
||||
|
||||
// If this is a followup request, wait until interactive followup URL visit complete.
|
||||
if req.Followup != "" {
|
||||
followupURL, err := url.Parse(req.Followup)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
doneCh := s.authPathDone(followupURL.Path)
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
case <-doneCh:
|
||||
}
|
||||
// TODO(bradfitz): support a side test API to mark an
|
||||
// auth as failued so we can send an error response in
|
||||
// some follow-ups? For now all are successes.
|
||||
}
|
||||
|
||||
user, login := s.getUser(req.NodeKey)
|
||||
s.mu.Lock()
|
||||
if s.nodes == nil {
|
||||
s.nodes = map[tailcfg.NodeKey]*tailcfg.Node{}
|
||||
}
|
||||
|
||||
machineAuthorized := true // TODO: add Server.RequireMachineAuth
|
||||
|
||||
s.nodes[req.NodeKey] = &tailcfg.Node{
|
||||
ID: tailcfg.NodeID(user.ID),
|
||||
StableID: tailcfg.StableNodeID(fmt.Sprintf("TESTCTRL%08x", int(user.ID))),
|
||||
User: user.ID,
|
||||
Machine: mkey,
|
||||
Key: req.NodeKey,
|
||||
MachineAuthorized: machineAuthorized,
|
||||
}
|
||||
requireAuth := s.RequireAuth
|
||||
if requireAuth && s.nodeKeyAuthed[req.NodeKey] {
|
||||
requireAuth = false
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
authURL := ""
|
||||
if requireAuth {
|
||||
randHex := make([]byte, 10)
|
||||
crand.Read(randHex)
|
||||
authPath := fmt.Sprintf("/auth/%x", randHex)
|
||||
s.addAuthPath(authPath, req.NodeKey)
|
||||
authURL = s.BaseURL + authPath
|
||||
}
|
||||
|
||||
res, err := s.encode(mkey, false, tailcfg.RegisterResponse{
|
||||
User: *user,
|
||||
Login: *login,
|
||||
NodeKeyExpired: false,
|
||||
MachineAuthorized: machineAuthorized,
|
||||
AuthURL: authURL,
|
||||
})
|
||||
if err != nil {
|
||||
go panic(fmt.Sprintf("serveRegister: encode: %v", err))
|
||||
}
|
||||
w.WriteHeader(200)
|
||||
w.Write(res)
|
||||
}
|
||||
|
||||
// updateType indicates why a long-polling map request is being woken
|
||||
// up for an update.
|
||||
type updateType int
|
||||
|
||||
const (
|
||||
// updatePeerChanged is an update that a peer has changed.
|
||||
updatePeerChanged updateType = iota + 1
|
||||
|
||||
// updateSelfChanged is an update that the node changed itself
|
||||
// via a lite endpoint update. These ones are never dup-suppressed,
|
||||
// as the client is expecting an answer regardless.
|
||||
updateSelfChanged
|
||||
)
|
||||
|
||||
func (s *Server) updateLocked(source string, peers []tailcfg.NodeID) {
|
||||
for _, peer := range peers {
|
||||
sendUpdate(s.updates[peer], updatePeerChanged)
|
||||
}
|
||||
}
|
||||
|
||||
// sendUpdate sends updateType to dst if dst is non-nil and
|
||||
// has capacity.
|
||||
func sendUpdate(dst chan<- updateType, updateType updateType) {
|
||||
if dst == nil {
|
||||
return
|
||||
}
|
||||
// The dst channel has a buffer size of 1.
|
||||
// If we fail to insert an update into the buffer that
|
||||
// means there is already an update pending.
|
||||
select {
|
||||
case dst <- updateType:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) UpdateNode(n *tailcfg.Node) (peersToUpdate []tailcfg.NodeID) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if n.Key.IsZero() {
|
||||
panic("zero nodekey")
|
||||
}
|
||||
s.nodes[n.Key] = n.Clone()
|
||||
for _, n2 := range s.nodes {
|
||||
if n.ID != n2.ID {
|
||||
peersToUpdate = append(peersToUpdate, n2.ID)
|
||||
}
|
||||
}
|
||||
return peersToUpdate
|
||||
}
|
||||
|
||||
func (s *Server) serveMap(w http.ResponseWriter, r *http.Request, mkey tailcfg.MachineKey) {
|
||||
ctx := r.Context()
|
||||
|
||||
req := new(tailcfg.MapRequest)
|
||||
if err := s.decode(mkey, r.Body, req); err != nil {
|
||||
go panic(fmt.Sprintf("bad map request: %v", err))
|
||||
}
|
||||
|
||||
jitter := time.Duration(rand.Intn(8000)) * time.Millisecond
|
||||
keepAlive := 50*time.Second + jitter
|
||||
|
||||
node := s.Node(req.NodeKey)
|
||||
if node == nil {
|
||||
http.Error(w, "node not found", 400)
|
||||
return
|
||||
}
|
||||
if node.Machine != mkey {
|
||||
http.Error(w, "node doesn't match machine key", 400)
|
||||
return
|
||||
}
|
||||
|
||||
var peersToUpdate []tailcfg.NodeID
|
||||
if !req.ReadOnly {
|
||||
endpoints := filterInvalidIPv6Endpoints(req.Endpoints)
|
||||
node.Endpoints = endpoints
|
||||
node.DiscoKey = req.DiscoKey
|
||||
peersToUpdate = s.UpdateNode(node)
|
||||
}
|
||||
|
||||
nodeID := node.ID
|
||||
|
||||
s.mu.Lock()
|
||||
updatesCh := make(chan updateType, 1)
|
||||
oldUpdatesCh := s.updates[nodeID]
|
||||
if breakSameNodeMapResponseStreams(req) {
|
||||
if oldUpdatesCh != nil {
|
||||
close(oldUpdatesCh)
|
||||
}
|
||||
if s.updates == nil {
|
||||
s.updates = map[tailcfg.NodeID]chan updateType{}
|
||||
}
|
||||
s.updates[nodeID] = updatesCh
|
||||
} else {
|
||||
sendUpdate(oldUpdatesCh, updateSelfChanged)
|
||||
}
|
||||
s.updateLocked("serveMap", peersToUpdate)
|
||||
s.mu.Unlock()
|
||||
|
||||
// ReadOnly implies no streaming, as it doesn't
|
||||
// register an updatesCh to get updates.
|
||||
streaming := req.Stream && !req.ReadOnly
|
||||
compress := req.Compress != ""
|
||||
|
||||
w.WriteHeader(200)
|
||||
for {
|
||||
res, err := s.MapResponse(req)
|
||||
if err != nil {
|
||||
// TODO: log
|
||||
return
|
||||
}
|
||||
if res == nil {
|
||||
return // done
|
||||
}
|
||||
// TODO: add minner if/when needed
|
||||
resBytes, err := json.Marshal(res)
|
||||
if err != nil {
|
||||
s.logf("json.Marshal: %v", err)
|
||||
return
|
||||
}
|
||||
if err := s.sendMapMsg(w, mkey, compress, resBytes); err != nil {
|
||||
return
|
||||
}
|
||||
if !streaming {
|
||||
return
|
||||
}
|
||||
keepAliveLoop:
|
||||
for {
|
||||
var keepAliveTimer *time.Timer
|
||||
var keepAliveTimerCh <-chan time.Time
|
||||
if keepAlive > 0 {
|
||||
keepAliveTimer = time.NewTimer(keepAlive)
|
||||
keepAliveTimerCh = keepAliveTimer.C
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if keepAliveTimer != nil {
|
||||
keepAliveTimer.Stop()
|
||||
}
|
||||
return
|
||||
case _, ok := <-updatesCh:
|
||||
if !ok {
|
||||
// replaced by new poll request
|
||||
return
|
||||
}
|
||||
break keepAliveLoop
|
||||
case <-keepAliveTimerCh:
|
||||
if err := s.sendMapMsg(w, mkey, compress, keepAliveMsg); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var keepAliveMsg = &struct {
|
||||
KeepAlive bool
|
||||
}{
|
||||
KeepAlive: true,
|
||||
}
|
||||
|
||||
var prodDERPMap = derpmap.Prod()
|
||||
|
||||
// MapResponse generates a MapResponse for a MapRequest.
|
||||
//
|
||||
// No updates to s are done here.
|
||||
func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse, err error) {
|
||||
node := s.Node(req.NodeKey)
|
||||
if node == nil {
|
||||
// node key rotated away (once test server supports that)
|
||||
return nil, nil
|
||||
}
|
||||
derpMap := s.DERPMap
|
||||
if derpMap == nil {
|
||||
derpMap = prodDERPMap
|
||||
}
|
||||
user, _ := s.getUser(req.NodeKey)
|
||||
res = &tailcfg.MapResponse{
|
||||
Node: node,
|
||||
DERPMap: derpMap,
|
||||
Domain: string(user.Domain),
|
||||
CollectServices: "true",
|
||||
PacketFilter: tailcfg.FilterAllowAll,
|
||||
}
|
||||
for _, p := range s.AllNodes() {
|
||||
if p.StableID != node.StableID {
|
||||
res.Peers = append(res.Peers, p)
|
||||
}
|
||||
}
|
||||
|
||||
res.Node.Addresses = []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix(fmt.Sprintf("100.64.%d.%d/32", uint8(node.ID>>8), uint8(node.ID))),
|
||||
}
|
||||
res.Node.AllowedIPs = res.Node.Addresses
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (s *Server) sendMapMsg(w http.ResponseWriter, mkey tailcfg.MachineKey, compress bool, msg interface{}) error {
|
||||
resBytes, err := s.encode(mkey, compress, msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(resBytes) > 16<<20 {
|
||||
return fmt.Errorf("map message too big: %d", len(resBytes))
|
||||
}
|
||||
var siz [4]byte
|
||||
binary.LittleEndian.PutUint32(siz[:], uint32(len(resBytes)))
|
||||
if _, err := w.Write(siz[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := w.Write(resBytes); err != nil {
|
||||
return err
|
||||
}
|
||||
if f, ok := w.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
} else {
|
||||
s.logf("[unexpected] ResponseWriter %T is not a Flusher", w)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) decode(mkey tailcfg.MachineKey, r io.Reader, v interface{}) error {
|
||||
if c, _ := r.(io.Closer); c != nil {
|
||||
defer c.Close()
|
||||
}
|
||||
const msgLimit = 1 << 20
|
||||
msg, err := ioutil.ReadAll(io.LimitReader(r, msgLimit))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(msg) == msgLimit {
|
||||
return errors.New("encrypted message too long")
|
||||
}
|
||||
|
||||
var nonce [24]byte
|
||||
if len(msg) < len(nonce)+1 {
|
||||
return errors.New("missing nonce")
|
||||
}
|
||||
copy(nonce[:], msg)
|
||||
msg = msg[len(nonce):]
|
||||
|
||||
priv := s.privateKey()
|
||||
pub, pri := (*[32]byte)(&mkey), (*[32]byte)(&priv)
|
||||
decrypted, ok := box.Open(nil, msg, &nonce, pub, pri)
|
||||
if !ok {
|
||||
return errors.New("can't decrypt request")
|
||||
}
|
||||
return json.Unmarshal(decrypted, v)
|
||||
}
|
||||
|
||||
var zstdEncoderPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
encoder, err := smallzstd.NewEncoder(nil, zstd.WithEncoderLevel(zstd.SpeedFastest))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return encoder
|
||||
},
|
||||
}
|
||||
|
||||
func (s *Server) encode(mkey tailcfg.MachineKey, compress bool, v interface{}) (b []byte, err error) {
|
||||
var isBytes bool
|
||||
if b, isBytes = v.([]byte); !isBytes {
|
||||
b, err = json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if compress {
|
||||
encoder := zstdEncoderPool.Get().(*zstd.Encoder)
|
||||
b = encoder.EncodeAll(b, nil)
|
||||
encoder.Close()
|
||||
zstdEncoderPool.Put(encoder)
|
||||
}
|
||||
var nonce [24]byte
|
||||
if _, err := io.ReadFull(crand.Reader, nonce[:]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
priv := s.privateKey()
|
||||
pub, pri := (*[32]byte)(&mkey), (*[32]byte)(&priv)
|
||||
msgData := box.Seal(nonce[:], b, &nonce, pub, pri)
|
||||
return msgData, nil
|
||||
}
|
||||
|
||||
// filterInvalidIPv6Endpoints removes invalid IPv6 endpoints from eps,
|
||||
// modify the slice in place, returning the potentially smaller subset (aliasing
|
||||
// the original memory).
|
||||
//
|
||||
// Two types of IPv6 endpoints are considered invalid: link-local
|
||||
// addresses, and anything with a zone.
|
||||
func filterInvalidIPv6Endpoints(eps []string) []string {
|
||||
clean := eps[:0]
|
||||
for _, ep := range eps {
|
||||
if keepClientEndpoint(ep) {
|
||||
clean = append(clean, ep)
|
||||
}
|
||||
}
|
||||
return clean
|
||||
}
|
||||
|
||||
func keepClientEndpoint(ep string) bool {
|
||||
ipp, err := netaddr.ParseIPPort(ep)
|
||||
if err != nil {
|
||||
// Shouldn't have made it this far if we unmarshalled
|
||||
// the incoming JSON response.
|
||||
return false
|
||||
}
|
||||
ip := ipp.IP
|
||||
if ip.Zone() != "" {
|
||||
return false
|
||||
}
|
||||
if ip.Is6() && ip.IsLinkLocalUnicast() {
|
||||
// We let clients send these for now, but
|
||||
// tailscaled doesn't know how to use them yet
|
||||
// so we filter them out for now. A future
|
||||
// MapRequest.Version might signal that
|
||||
// clients know how to use them (e.g. try all
|
||||
// local scopes).
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// breakSameNodeMapResponseStreams reports whether req should break a
|
||||
// prior long-polling MapResponse stream (if active) from the same
|
||||
// node ID.
|
||||
func breakSameNodeMapResponseStreams(req *tailcfg.MapRequest) bool {
|
||||
if req.ReadOnly {
|
||||
// Don't register our updatesCh for closability
|
||||
// nor close another peer's if we're a read-only request.
|
||||
return false
|
||||
}
|
||||
if !req.Stream && req.OmitPeers {
|
||||
// Likewise, if we're not streaming and not asking for peers,
|
||||
// (but still mutable, without Readonly set), consider this an endpoint
|
||||
// update request only, and don't close any existing map response
|
||||
// for this nodeID. It's likely the same client with a built-up
|
||||
// compression context. We want to let them update their
|
||||
// new endpoints with us without breaking that other long-running
|
||||
// map response.
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -107,15 +107,6 @@ func (lt *LogLineTracker) Check() []string {
|
||||
return notSeen
|
||||
}
|
||||
|
||||
// Reset forgets everything that it's seen.
|
||||
func (lt *LogLineTracker) Reset() {
|
||||
lt.mu.Lock()
|
||||
defer lt.mu.Unlock()
|
||||
for _, line := range lt.listenFor {
|
||||
lt.seen[line] = false
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes lt. After calling Close, calls to Logf become no-ops.
|
||||
func (lt *LogLineTracker) Close() {
|
||||
lt.mu.Lock()
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package tstest provides utilities for use in unit tests.
|
||||
package tstest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// WaitFor retries try for up to maxWait.
|
||||
// It returns nil once try returns nil the first time.
|
||||
// If maxWait passes without success, it returns try's last error.
|
||||
func WaitFor(maxWait time.Duration, try func() error) error {
|
||||
bo := backoff.NewBackoff("wait-for", logger.Discard, maxWait/4)
|
||||
deadline := time.Now().Add(maxWait)
|
||||
var err error
|
||||
for time.Now().Before(deadline) {
|
||||
err = try()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
bo.BackOff(context.Background(), err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// Logf is the basic Tailscale logger type: a printf-like func.
|
||||
@@ -55,53 +57,45 @@ func Discard(string, ...interface{}) {}
|
||||
// limitData is used to keep track of each format string's associated
|
||||
// rate-limiting data.
|
||||
type limitData struct {
|
||||
bucket *tokenBucket // the token bucket associated with this string
|
||||
nBlocked int // number of messages skipped
|
||||
ele *list.Element // list element used to access this string in the cache
|
||||
lim *rate.Limiter // the token bucket associated with this string
|
||||
msgBlocked bool // whether a "duplicate error" message has already been logged
|
||||
ele *list.Element // list element used to access this string in the cache
|
||||
}
|
||||
|
||||
var disableRateLimit = os.Getenv("TS_DEBUG_LOG_RATE") == "all"
|
||||
|
||||
// rateFree are format string substrings that are exempt from rate limiting.
|
||||
// Things should not be added to this unless they're already limited otherwise
|
||||
// or are critical for generating important stats from the logs.
|
||||
// Things should not be added to this unless they're already limited otherwise.
|
||||
var rateFree = []string{
|
||||
"magicsock: disco: ",
|
||||
"magicsock: ParseEndpoint:",
|
||||
// grinder stats lines
|
||||
"SetPrefs: %v",
|
||||
"peer keys: %s",
|
||||
"v%v peers: %v",
|
||||
"magicsock: CreateEndpoint:",
|
||||
}
|
||||
|
||||
// RateLimitedFn is a wrapper for RateLimitedFnWithClock that includes the
|
||||
// current time automatically. This is mainly for backward compatibility.
|
||||
// RateLimitedFn returns a rate-limiting Logf wrapping the given logf.
|
||||
// Messages are allowed through at a maximum of one message every f (where f is a time.Duration), in
|
||||
// bursts of up to burst messages at a time. Up to maxCache strings will be held at a time.
|
||||
func RateLimitedFn(logf Logf, f time.Duration, burst int, maxCache int) Logf {
|
||||
return RateLimitedFnWithClock(logf, f, burst, maxCache, time.Now)
|
||||
}
|
||||
|
||||
// RateLimitedFnWithClock returns a rate-limiting Logf wrapping the given
|
||||
// logf. Messages are allowed through at a maximum of one message every f
|
||||
// (where f is a time.Duration), in bursts of up to burst messages at a
|
||||
// time. Up to maxCache format strings will be tracked separately.
|
||||
// timeNow is a function that returns the current time, used for calculating
|
||||
// rate limits.
|
||||
func RateLimitedFnWithClock(logf Logf, f time.Duration, burst int, maxCache int, timeNow func() time.Time) Logf {
|
||||
if disableRateLimit {
|
||||
return logf
|
||||
}
|
||||
r := rate.Every(f)
|
||||
var (
|
||||
mu sync.Mutex
|
||||
msgLim = make(map[string]*limitData) // keyed by logf format
|
||||
msgCache = list.New() // a rudimentary LRU that limits the size of the map
|
||||
)
|
||||
|
||||
return func(format string, args ...interface{}) {
|
||||
// Shortcut for formats with no rate limit
|
||||
type verdict int
|
||||
const (
|
||||
allow verdict = iota
|
||||
warn
|
||||
block
|
||||
)
|
||||
|
||||
judge := func(format string) verdict {
|
||||
for _, sub := range rateFree {
|
||||
if strings.Contains(format, sub) {
|
||||
logf(format, args...)
|
||||
return
|
||||
return allow
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,8 +106,8 @@ func RateLimitedFnWithClock(logf Logf, f time.Duration, burst int, maxCache int,
|
||||
msgCache.MoveToFront(rl.ele)
|
||||
} else {
|
||||
rl = &limitData{
|
||||
bucket: newTokenBucket(f, burst, timeNow()),
|
||||
ele: msgCache.PushFront(format),
|
||||
lim: rate.NewLimiter(r, burst),
|
||||
ele: msgCache.PushFront(format),
|
||||
}
|
||||
msgLim[format] = rl
|
||||
if msgCache.Len() > maxCache {
|
||||
@@ -121,39 +115,24 @@ func RateLimitedFnWithClock(logf Logf, f time.Duration, burst int, maxCache int,
|
||||
msgCache.Remove(msgCache.Back())
|
||||
}
|
||||
}
|
||||
|
||||
rl.bucket.AdvanceTo(timeNow())
|
||||
|
||||
// Make sure there's enough room for at least a few
|
||||
// more logs before we unblock, so we don't alternate
|
||||
// between blocking and unblocking.
|
||||
if rl.nBlocked > 0 && rl.bucket.remaining >= 2 {
|
||||
// Only print this if we dropped more than 1
|
||||
// message. Otherwise we'd *increase* the total
|
||||
// number of log lines printed.
|
||||
if rl.nBlocked > 1 {
|
||||
logf("[RATELIMIT] format(%q) (%d dropped)",
|
||||
format, rl.nBlocked-1)
|
||||
}
|
||||
rl.nBlocked = 0
|
||||
if rl.lim.Allow() {
|
||||
rl.msgBlocked = false
|
||||
return allow
|
||||
}
|
||||
if rl.nBlocked == 0 && rl.bucket.Get() {
|
||||
if !rl.msgBlocked {
|
||||
rl.msgBlocked = true
|
||||
return warn
|
||||
}
|
||||
return block
|
||||
}
|
||||
|
||||
return func(format string, args ...interface{}) {
|
||||
switch judge(format) {
|
||||
case allow:
|
||||
logf(format, args...)
|
||||
if rl.bucket.remaining == 0 {
|
||||
// Enter "blocked" mode immediately after
|
||||
// reaching the burst limit. We want to
|
||||
// always accompany the format() message
|
||||
// with an example of the format, which is
|
||||
// effectively the same as printing the
|
||||
// message anyway. But this way they can
|
||||
// be on two separate lines and we don't
|
||||
// corrupt the original message.
|
||||
logf("[RATELIMIT] format(%q)", format)
|
||||
rl.nBlocked = 1
|
||||
}
|
||||
return
|
||||
} else {
|
||||
rl.nBlocked++
|
||||
case warn:
|
||||
// For the warning, log the specific format string
|
||||
logf("[RATE LIMITED] format string \"%s\" (example: \"%s\")", format, strings.TrimSpace(fmt.Sprintf(format, args...)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -184,6 +163,7 @@ func LogOnChange(logf Logf, maxInterval time.Duration, timeNow func() time.Time)
|
||||
// as it might contain formatting directives.)
|
||||
logf(format, args...)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ArgWriter is a fmt.Formatter that can be passed to any Logf func to
|
||||
|
||||
@@ -44,27 +44,16 @@ func TestRateLimiter(t *testing.T) {
|
||||
"boring string with constant formatting (constant)",
|
||||
"templated format string no. 0",
|
||||
"boring string with constant formatting (constant)",
|
||||
"[RATELIMIT] format(\"boring string with constant formatting %s\")",
|
||||
"templated format string no. 1",
|
||||
"[RATELIMIT] format(\"templated format string no. %d\")",
|
||||
"[RATE LIMITED] format string \"boring string with constant formatting %s\" (example: \"boring string with constant formatting (constant)\")",
|
||||
"[RATE LIMITED] format string \"templated format string no. %d\" (example: \"templated format string no. 2\")",
|
||||
"Make sure this string makes it through the rest (that are blocked) 4",
|
||||
"4 shouldn't get filtered.",
|
||||
"hello 1",
|
||||
"hello 2",
|
||||
"[RATELIMIT] format(\"hello %v\")",
|
||||
"[RATELIMIT] format(\"hello %v\") (2 dropped)",
|
||||
"hello 5",
|
||||
"hello 6",
|
||||
"[RATELIMIT] format(\"hello %v\")",
|
||||
"hello 7",
|
||||
}
|
||||
|
||||
var now time.Time
|
||||
nowf := func() time.Time { return now }
|
||||
|
||||
testsRun := 0
|
||||
lgtest := logTester(want, t, &testsRun)
|
||||
lg := RateLimitedFnWithClock(lgtest, 1*time.Minute, 2, 50, nowf)
|
||||
lg := RateLimitedFn(lgtest, 1*time.Minute, 2, 50)
|
||||
var prefixed Logf
|
||||
for i := 0; i < 10; i++ {
|
||||
lg("boring string with constant formatting %s", "(constant)")
|
||||
@@ -75,19 +64,6 @@ func TestRateLimiter(t *testing.T) {
|
||||
prefixed(" shouldn't get filtered.")
|
||||
}
|
||||
}
|
||||
|
||||
lg("hello %v", 1)
|
||||
lg("hello %v", 2) // printed, but rate limit starts
|
||||
lg("hello %v", 3) // rate limited (not printed)
|
||||
now = now.Add(1 * time.Minute)
|
||||
lg("hello %v", 4) // still limited (not printed)
|
||||
now = now.Add(1 * time.Minute)
|
||||
lg("hello %v", 5) // restriction lifted; prints drop count + message
|
||||
|
||||
lg("hello %v", 6) // printed, but rate limit starts
|
||||
now = now.Add(2 * time.Minute)
|
||||
lg("hello %v", 7) // restriction lifted; no drop count needed
|
||||
|
||||
if testsRun < len(want) {
|
||||
t.Fatalf("Tests after %s weren't logged.", want[testsRun])
|
||||
}
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
// 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 logger
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// tokenBucket is a simple token bucket style rate limiter.
|
||||
|
||||
// It's similar in function to golang.org/x/time/rate.Limiter, which we
|
||||
// can't use because:
|
||||
// - It doesn't give access to the number of accumulated tokens, which we
|
||||
// need for implementing hysteresis;
|
||||
// - It doesn't let us provide our own time function, which we need for
|
||||
// implementing proper unit tests.
|
||||
// rate.Limiter is also much more complex than necessary, but that wouldn't
|
||||
// be enough to disqualify it on its own.
|
||||
//
|
||||
// Unlike rate.Limiter, this token bucket does not attempt to
|
||||
// do any locking of its own. Don't try to access it re-entrantly.
|
||||
// That's fine inside this types/logger package because we already have
|
||||
// locking at a higher level.
|
||||
type tokenBucket struct {
|
||||
remaining int
|
||||
max int
|
||||
tick time.Duration
|
||||
t time.Time
|
||||
}
|
||||
|
||||
func newTokenBucket(tick time.Duration, max int, now time.Time) *tokenBucket {
|
||||
return &tokenBucket{max, max, tick, now}
|
||||
}
|
||||
|
||||
func (tb *tokenBucket) Get() bool {
|
||||
if tb.remaining > 0 {
|
||||
tb.remaining--
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (tb *tokenBucket) Refund(n int) {
|
||||
b := tb.remaining + n
|
||||
if b > tb.max {
|
||||
tb.remaining = tb.max
|
||||
} else {
|
||||
tb.remaining = b
|
||||
}
|
||||
}
|
||||
|
||||
func (tb *tokenBucket) AdvanceTo(t time.Time) {
|
||||
diff := t.Sub(tb.t)
|
||||
|
||||
// only use up whole ticks. The remainder will be used up
|
||||
// next time.
|
||||
ticks := int(diff / tb.tick)
|
||||
tb.t = tb.t.Add(time.Duration(ticks) * tb.tick)
|
||||
|
||||
tb.Refund(ticks)
|
||||
}
|
||||
@@ -31,8 +31,8 @@ type NetworkMap struct {
|
||||
Expiry time.Time
|
||||
// Name is the DNS name assigned to this node.
|
||||
Name string
|
||||
Addresses []netaddr.IPPrefix // same as tailcfg.Node.Addresses (IP addresses of this Node directly)
|
||||
LocalPort uint16 // used for debugging
|
||||
Addresses []netaddr.IPPrefix
|
||||
LocalPort uint16 // used for debugging
|
||||
MachineStatus tailcfg.MachineStatus
|
||||
MachineKey tailcfg.MachineKey
|
||||
Peers []*tailcfg.Node // sorted by Node.ID
|
||||
@@ -57,8 +57,11 @@ type NetworkMap struct {
|
||||
|
||||
User tailcfg.UserID
|
||||
Domain string
|
||||
|
||||
// TODO(crawshaw): reduce UserProfiles to []tailcfg.UserProfile?
|
||||
// There are lots of ways to slice this data, leave it up to users.
|
||||
UserProfiles map[tailcfg.UserID]tailcfg.UserProfile
|
||||
// TODO(crawshaw): Groups []tailcfg.Group
|
||||
// TODO(crawshaw): Capabilities []tailcfg.Capability
|
||||
}
|
||||
|
||||
// MagicDNSSuffix returns the domain's MagicDNS suffix (even if
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
const Size = 32
|
||||
|
||||
// A Key is a curve25519 key.
|
||||
// It is used by WireGuard to represent public and preshared keys.
|
||||
// It is used by WireGuard to represent public keys.
|
||||
type Key [Size]byte
|
||||
|
||||
// NewPreshared generates a new random Key.
|
||||
@@ -78,16 +78,8 @@ func (k Key) HexString() string { return hex.EncodeToString(k[:]) }
|
||||
func (k Key) Equal(k2 Key) bool { return subtle.ConstantTimeCompare(k[:], k2[:]) == 1 }
|
||||
|
||||
func (k *Key) ShortString() string {
|
||||
// The goal here is to generate "[" + base64.StdEncoding.EncodeToString(k[:])[:5] + "]".
|
||||
// Since we only care about the first 5 characters, it suffices to encode the first 4 bytes of k.
|
||||
// Encoding those 4 bytes requires 8 bytes.
|
||||
// Make dst have size 9, to fit the leading '[' plus those 8 bytes.
|
||||
// We slice the unused ones away at the end.
|
||||
dst := make([]byte, 9)
|
||||
dst[0] = '['
|
||||
base64.StdEncoding.Encode(dst[1:], k[:4])
|
||||
dst[6] = ']'
|
||||
return string(dst[:7])
|
||||
long := k.Base64()
|
||||
return "[" + long[0:5] + "]"
|
||||
}
|
||||
|
||||
func (k *Key) IsZero() bool {
|
||||
@@ -98,12 +90,14 @@ func (k *Key) IsZero() bool {
|
||||
return subtle.ConstantTimeCompare(zeros[:], k[:]) == 1
|
||||
}
|
||||
|
||||
func (k Key) MarshalJSON() ([]byte, error) {
|
||||
buf := make([]byte, 2+len(k)*2)
|
||||
buf[0] = '"'
|
||||
hex.Encode(buf[1:], k[:])
|
||||
buf[len(buf)-1] = '"'
|
||||
return buf, nil
|
||||
func (k *Key) MarshalJSON() ([]byte, error) {
|
||||
if k == nil {
|
||||
return []byte("null"), nil
|
||||
}
|
||||
// TODO(josharian): use encoding/hex instead?
|
||||
buf := new(bytes.Buffer)
|
||||
fmt.Fprintf(buf, `"%x"`, k[:])
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (k *Key) UnmarshalJSON(b []byte) error {
|
||||
@@ -114,10 +108,11 @@ func (k *Key) UnmarshalJSON(b []byte) error {
|
||||
return errors.New("wgkey.Key: UnmarshalJSON not given a string")
|
||||
}
|
||||
b = b[1 : len(b)-1]
|
||||
if len(b) != 2*Size {
|
||||
return fmt.Errorf("wgkey.Key: UnmarshalJSON input wrong size: %d", len(b))
|
||||
key, err := ParseHex(string(b))
|
||||
if err != nil {
|
||||
return fmt.Errorf("wgkey.Key: UnmarshalJSON: %v", err)
|
||||
}
|
||||
hex.Decode(k[:], b)
|
||||
copy(k[:], key[:])
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user