Compare commits
43 Commits
bradfitz/m
...
naman/nets
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd4e7e51a7 | ||
|
|
6bb159d5fa | ||
|
|
91c9c33036 | ||
|
|
7d8f082ff7 | ||
|
|
7689213aaa | ||
|
|
6fd9e28bd0 | ||
|
|
89c81c26c5 | ||
|
|
4be26b269f | ||
|
|
ca283ac899 | ||
|
|
48d4f14652 | ||
|
|
53213114ec | ||
|
|
3b1ab78954 | ||
|
|
f99e63bb17 | ||
|
|
158328ba24 | ||
|
|
1e5c608fae | ||
|
|
28ba20d733 | ||
|
|
3d0599fca0 | ||
|
|
48e30bb8de | ||
|
|
a2a2c0ce1c | ||
|
|
b1e624ef04 | ||
|
|
98714e784b | ||
|
|
15ceacc4c5 | ||
|
|
f42ded7acf | ||
|
|
a58fbb4da9 | ||
|
|
36fa29feec | ||
|
|
8570f82c8b | ||
|
|
7f8519c88f | ||
|
|
cad8df500c | ||
|
|
0d1550898e | ||
|
|
f72a120016 | ||
|
|
71b7e48547 | ||
|
|
e9d24341e0 | ||
|
|
97204fdc52 | ||
|
|
8f3e453356 | ||
|
|
3739cf22b0 | ||
|
|
5092cffd1f | ||
|
|
aef3c0350c | ||
|
|
6d64107f26 | ||
|
|
49808ae6ea | ||
|
|
4df6e62fbc | ||
|
|
f1d45bc4bb | ||
|
|
4948ff6ecb | ||
|
|
eb6115e295 |
48
.github/workflows/linux-race.yml
vendored
Normal file
48
.github/workflows/linux-race.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
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 with -race flag on linux
|
||||
run: go test -race ./...
|
||||
|
||||
- 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'
|
||||
|
||||
52
.github/workflows/windows-race.yml
vendored
Normal file
52
.github/workflows/windows-race.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
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
|
||||
run: go test -race ./...
|
||||
|
||||
- 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'
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"text/tabwriter"
|
||||
@@ -31,15 +32,37 @@ import (
|
||||
// CLI based on os.Args, GOOS, the context the process is running in
|
||||
// (pty, parent PID), etc.
|
||||
func ActLikeCLI() bool {
|
||||
if len(os.Args) < 2 {
|
||||
// This function is only used on macOS.
|
||||
if runtime.GOOS != "darwin" {
|
||||
return false
|
||||
}
|
||||
switch os.Args[1] {
|
||||
case "up", "down", "status", "netcheck", "ping", "version",
|
||||
"debug",
|
||||
"-V", "--version", "-h", "--help":
|
||||
|
||||
// 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 {
|
||||
return true
|
||||
}
|
||||
|
||||
// If our parent is launchd, we're definitely not
|
||||
// being run as a CLI.
|
||||
if os.Getppid() == 1 {
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,18 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/preftype"
|
||||
)
|
||||
|
||||
// Test that checkForAccidentalSettingReverts's updateMaskedPrefsFromUpFlag can handle
|
||||
@@ -39,6 +47,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "bare_up_means_up",
|
||||
flagSet: f(),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
Hostname: "foo",
|
||||
},
|
||||
@@ -54,15 +63,18 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "losing_hostname",
|
||||
flagSet: f("accept-dns"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
Hostname: "foo",
|
||||
CorpDNS: true,
|
||||
},
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
CorpDNS: true,
|
||||
},
|
||||
ControlURLSet: true,
|
||||
WantRunningSet: true,
|
||||
CorpDNSSet: true,
|
||||
},
|
||||
@@ -72,14 +84,17 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "hostname_changing_explicitly",
|
||||
flagSet: f("hostname"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
Hostname: "foo",
|
||||
},
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
Hostname: "bar",
|
||||
},
|
||||
ControlURLSet: true,
|
||||
WantRunningSet: true,
|
||||
HostnameSet: true,
|
||||
},
|
||||
@@ -89,19 +104,50 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
name: "hostname_changing_empty_explicitly",
|
||||
flagSet: f("hostname"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
Hostname: "foo",
|
||||
},
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
Hostname: "",
|
||||
},
|
||||
ControlURLSet: true,
|
||||
WantRunningSet: true,
|
||||
HostnameSet: true,
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "empty_slice_equals_nil_slice",
|
||||
flagSet: f("hostname"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{},
|
||||
},
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AdvertiseRoutes: nil,
|
||||
},
|
||||
ControlURLSet: 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",
|
||||
flagSet: f("authkey"),
|
||||
curPrefs: ipn.NewPrefs(),
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: *defaultPrefsFromUpArgs(t),
|
||||
WantRunningSet: true,
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -115,3 +161,176 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func defaultPrefsFromUpArgs(t testing.TB) *ipn.Prefs {
|
||||
upFlagSet.Parse(nil) // populates upArgs
|
||||
if upFlagSet.Lookup("netfilter-mode") == nil && upArgs.netfilterMode == "" {
|
||||
// This flag is not compiled on on-Linux platforms,
|
||||
// but prefsFromUpArgs requires it be populated.
|
||||
upArgs.netfilterMode = defaultNetfilterMode()
|
||||
}
|
||||
prefs, err := prefsFromUpArgs(upArgs, logger.Discard, new(ipnstate.Status), "linux")
|
||||
if err != nil {
|
||||
t.Fatalf("defaultPrefsFromUpArgs: %v", err)
|
||||
}
|
||||
prefs.WantRunning = true
|
||||
return prefs
|
||||
}
|
||||
|
||||
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: "zero",
|
||||
goos: "windows",
|
||||
args: upArgsT{},
|
||||
want: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
NoSNAT: true,
|
||||
NetfilterMode: preftype.NetfilterOn, // silly, but default from ipn.NewPref currently
|
||||
},
|
||||
},
|
||||
{
|
||||
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: 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,
|
||||
)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,7 +22,10 @@ import (
|
||||
"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"
|
||||
)
|
||||
@@ -55,7 +58,7 @@ var upFlagSet = (func() *flag.FlagSet {
|
||||
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", "https://login.tailscale.com", "base URL of control server")
|
||||
upf.StringVar(&upArgs.server, "login-server", ipn.DefaultControlURL, "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")
|
||||
@@ -67,6 +70,9 @@ var upFlagSet = (func() *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.PlatformUsesPeerCreds() {
|
||||
upf.StringVar(&upArgs.opUser, "operator", "", "Unix username to allow to operate on tailscaled without sudo")
|
||||
}
|
||||
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)")
|
||||
@@ -84,7 +90,7 @@ func defaultNetfilterMode() string {
|
||||
return "on"
|
||||
}
|
||||
|
||||
var upArgs struct {
|
||||
type upArgsT struct {
|
||||
reset bool
|
||||
server string
|
||||
acceptRoutes bool
|
||||
@@ -102,8 +108,11 @@ var upArgs struct {
|
||||
netfilterMode string
|
||||
authKey string
|
||||
hostname string
|
||||
opUser string
|
||||
}
|
||||
|
||||
var upArgs upArgsT
|
||||
|
||||
func warnf(format string, args ...interface{}) {
|
||||
fmt.Printf("Warning: "+format+"\n", args...)
|
||||
}
|
||||
@@ -113,6 +122,120 @@ 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) {
|
||||
routeMap := map[netaddr.IPPrefix]bool{}
|
||||
var default4, default6 bool
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
|
||||
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)
|
||||
}
|
||||
if ipp != ipp.Masked() {
|
||||
return nil, fmt.Errorf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
|
||||
}
|
||||
if ipp == ipv4default {
|
||||
default4 = true
|
||||
} else if ipp == ipv6default {
|
||||
default6 = true
|
||||
}
|
||||
routeMap[ipp] = true
|
||||
}
|
||||
if default4 && !default6 {
|
||||
return nil, fmt.Errorf("%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)
|
||||
}
|
||||
}
|
||||
if upArgs.advertiseDefaultRoute {
|
||||
routeMap[netaddr.MustParseIPPrefix("0.0.0.0/0")] = true
|
||||
routeMap[netaddr.MustParseIPPrefix("::/0")] = true
|
||||
}
|
||||
routes := make([]netaddr.IPPrefix, 0, len(routeMap))
|
||||
for r := range routeMap {
|
||||
routes = append(routes, r)
|
||||
}
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
if routes[i].Bits != routes[j].Bits {
|
||||
return routes[i].Bits < routes[j].Bits
|
||||
}
|
||||
return routes[i].IP.Less(routes[j].IP)
|
||||
})
|
||||
|
||||
var exitNodeIP netaddr.IP
|
||||
if upArgs.exitNodeIP != "" {
|
||||
var err error
|
||||
exitNodeIP, err = netaddr.ParseIP(upArgs.exitNodeIP)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("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")
|
||||
}
|
||||
|
||||
if upArgs.exitNodeIP != "" {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tags []string
|
||||
if upArgs.advertiseTags != "" {
|
||||
tags = strings.Split(upArgs.advertiseTags, ",")
|
||||
for _, tag := range tags {
|
||||
err := tailcfg.CheckTag(tag)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tag: %q: %s", tag, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(upArgs.hostname) > 256 {
|
||||
return nil, fmt.Errorf("hostname too long: %d bytes (max 256)", len(upArgs.hostname))
|
||||
}
|
||||
|
||||
prefs := ipn.NewPrefs()
|
||||
prefs.ControlURL = upArgs.server
|
||||
prefs.WantRunning = true
|
||||
prefs.RouteAll = upArgs.acceptRoutes
|
||||
prefs.ExitNodeIP = exitNodeIP
|
||||
prefs.ExitNodeAllowLANAccess = upArgs.exitNodeAllowLANAccess
|
||||
prefs.CorpDNS = upArgs.acceptDNS
|
||||
prefs.AllowSingleHosts = upArgs.singleRoutes
|
||||
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" {
|
||||
switch upArgs.netfilterMode {
|
||||
case "on":
|
||||
prefs.NetfilterMode = preftype.NetfilterOn
|
||||
case "nodivert":
|
||||
prefs.NetfilterMode = preftype.NetfilterNoDivert
|
||||
warnf("netfilter=nodivert; add iptables calls to ts-* chains manually.")
|
||||
case "off":
|
||||
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)
|
||||
@@ -136,114 +259,16 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
routeMap := map[netaddr.IPPrefix]bool{}
|
||||
var default4, default6 bool
|
||||
if upArgs.advertiseRoutes != "" {
|
||||
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
|
||||
for _, s := range advroutes {
|
||||
ipp, err := netaddr.ParseIPPrefix(s)
|
||||
if err != nil {
|
||||
fatalf("%q is not a valid IP address or CIDR prefix", s)
|
||||
}
|
||||
if ipp != ipp.Masked() {
|
||||
fatalf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
|
||||
}
|
||||
if ipp == ipv4default {
|
||||
default4 = true
|
||||
} else if ipp == ipv6default {
|
||||
default6 = true
|
||||
}
|
||||
routeMap[ipp] = true
|
||||
}
|
||||
if default4 && !default6 {
|
||||
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv4default, ipv6default)
|
||||
} else if default6 && !default4 {
|
||||
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv6default, ipv4default)
|
||||
}
|
||||
prefs, err := prefsFromUpArgs(upArgs, warnf, st, runtime.GOOS)
|
||||
if err != nil {
|
||||
fatalf("%s", err)
|
||||
}
|
||||
if upArgs.advertiseDefaultRoute {
|
||||
routeMap[netaddr.MustParseIPPrefix("0.0.0.0/0")] = true
|
||||
routeMap[netaddr.MustParseIPPrefix("::/0")] = true
|
||||
}
|
||||
if len(routeMap) > 0 {
|
||||
|
||||
if len(prefs.AdvertiseRoutes) > 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)
|
||||
}
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
if routes[i].Bits != routes[j].Bits {
|
||||
return routes[i].Bits < routes[j].Bits
|
||||
}
|
||||
return routes[i].IP.Less(routes[j].IP)
|
||||
})
|
||||
|
||||
var exitNodeIP netaddr.IP
|
||||
if upArgs.exitNodeIP != "" {
|
||||
var err error
|
||||
exitNodeIP, err = netaddr.ParseIP(upArgs.exitNodeIP)
|
||||
if err != nil {
|
||||
fatalf("invalid IP address %q for --exit-node: %v", upArgs.exitNodeIP, err)
|
||||
}
|
||||
} else if upArgs.exitNodeAllowLANAccess {
|
||||
fatalf("--exit-node-allow-lan-access can only be used with --exit-node")
|
||||
}
|
||||
|
||||
if !exitNodeIP.IsZero() {
|
||||
for _, ip := range st.TailscaleIPs {
|
||||
if exitNodeIP == ip {
|
||||
fatalf("cannot use %s as the exit node as it is a local IP address to this machine, did you mean --advertise-exit-node?", exitNodeIP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tags []string
|
||||
if upArgs.advertiseTags != "" {
|
||||
tags = strings.Split(upArgs.advertiseTags, ",")
|
||||
for _, tag := range tags {
|
||||
err := tailcfg.CheckTag(tag)
|
||||
if err != nil {
|
||||
fatalf("tag: %q: %s", tag, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(upArgs.hostname) > 256 {
|
||||
fatalf("hostname too long: %d bytes (max 256)", len(upArgs.hostname))
|
||||
}
|
||||
|
||||
prefs := ipn.NewPrefs()
|
||||
prefs.ControlURL = upArgs.server
|
||||
prefs.WantRunning = true
|
||||
prefs.RouteAll = upArgs.acceptRoutes
|
||||
prefs.ExitNodeIP = exitNodeIP
|
||||
prefs.ExitNodeAllowLANAccess = upArgs.exitNodeAllowLANAccess
|
||||
prefs.CorpDNS = upArgs.acceptDNS
|
||||
prefs.AllowSingleHosts = upArgs.singleRoutes
|
||||
prefs.ShieldsUp = upArgs.shieldsUp
|
||||
prefs.AdvertiseRoutes = routes
|
||||
prefs.AdvertiseTags = tags
|
||||
prefs.NoSNAT = !upArgs.snat
|
||||
prefs.Hostname = upArgs.hostname
|
||||
prefs.ForceDaemon = upArgs.forceDaemon
|
||||
|
||||
if runtime.GOOS == "linux" {
|
||||
switch upArgs.netfilterMode {
|
||||
case "on":
|
||||
prefs.NetfilterMode = preftype.NetfilterOn
|
||||
case "nodivert":
|
||||
prefs.NetfilterMode = preftype.NetfilterNoDivert
|
||||
warnf("netfilter=nodivert; add iptables calls to ts-* chains manually.")
|
||||
case "off":
|
||||
prefs.NetfilterMode = preftype.NetfilterOff
|
||||
warnf("netfilter=off; configure iptables yourself.")
|
||||
default:
|
||||
fatalf("invalid value --netfilter-mode: %q", upArgs.netfilterMode)
|
||||
}
|
||||
}
|
||||
|
||||
curPrefs, err := tailscale.GetPrefs(ctx)
|
||||
if err != nil {
|
||||
@@ -428,6 +453,7 @@ func init() {
|
||||
addPrefFlagMapping("exit-node", "ExitNodeIP", "ExitNodeIP")
|
||||
addPrefFlagMapping("exit-node-allow-lan-access", "ExitNodeAllowLANAccess")
|
||||
addPrefFlagMapping("unattended", "ForceDaemon")
|
||||
addPrefFlagMapping("operator", "OperatorUser")
|
||||
}
|
||||
|
||||
func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||
@@ -456,7 +482,7 @@ func updateMaskedPrefsFromUpFlag(mp *ipn.MaskedPrefs, flagName string) {
|
||||
case "advertise-exit-node":
|
||||
// This pref is a shorthand for advertise-routes.
|
||||
default:
|
||||
panic("internal error: unhandled flag " + flagName)
|
||||
panic(fmt.Sprintf("internal error: unhandled flag %q", flagName))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,6 +506,10 @@ func checkForAccidentalSettingReverts(flagSet map[string]bool, curPrefs *ipn.Pre
|
||||
// mean bringing the network up without any changes.
|
||||
return nil
|
||||
}
|
||||
if curPrefs.ControlURL == "" {
|
||||
// Don't validate things on initial "up" before a control URL has been set.
|
||||
return nil
|
||||
}
|
||||
curWithExplicitEdits := curPrefs.Clone()
|
||||
curWithExplicitEdits.ApplyEdits(mp)
|
||||
|
||||
@@ -501,8 +531,16 @@ func checkForAccidentalSettingReverts(flagSet map[string]bool, curPrefs *ipn.Pre
|
||||
continue
|
||||
}
|
||||
// Get explicit value and implicit value
|
||||
evi, ivi := ev.Field(i).Interface(), iv.Field(i).Interface()
|
||||
if reflect.DeepEqual(evi, ivi) {
|
||||
ex, im := ev.Field(i), iv.Field(i)
|
||||
switch ex.Kind() {
|
||||
case reflect.String, reflect.Slice:
|
||||
if ex.Kind() == reflect.Slice && ex.Len() == 0 && im.Len() == 0 {
|
||||
// Treat nil and non-nil empty slices as equivalent.
|
||||
continue
|
||||
}
|
||||
}
|
||||
exi, imi := ex.Interface(), im.Interface()
|
||||
if reflect.DeepEqual(exi, imi) {
|
||||
continue
|
||||
}
|
||||
switch flagName {
|
||||
@@ -515,7 +553,7 @@ func checkForAccidentalSettingReverts(flagSet map[string]bool, curPrefs *ipn.Pre
|
||||
}
|
||||
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)))
|
||||
flagName, fmtSettingVal(imi), fmtSettingVal(exi)))
|
||||
}
|
||||
}
|
||||
return multierror.New(errs)
|
||||
|
||||
@@ -246,7 +246,7 @@ 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 tailscaleUp(ctx context.Context) (authURL string, retErr error) {
|
||||
prefs := ipn.NewPrefs()
|
||||
prefs.ControlURL = "https://login.tailscale.com"
|
||||
prefs.ControlURL = ipn.DefaultControlURL
|
||||
prefs.WantRunning = true
|
||||
prefs.CorpDNS = true
|
||||
prefs.AllowSingleHosts = true
|
||||
|
||||
@@ -178,7 +178,8 @@ 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 from tailscale.com/cmd/tailscaled+
|
||||
W golang.org/x/sys/windows/svc/mgr 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+
|
||||
|
||||
119
cmd/tailscaled/install_windows.go
Normal file
119
cmd/tailscaled/install_windows.go
Normal file
@@ -0,0 +1,119 @@
|
||||
// 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"
|
||||
)
|
||||
|
||||
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) {
|
||||
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,10 +29,12 @@ 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"
|
||||
@@ -239,30 +241,31 @@ func run() error {
|
||||
srv := &socks5.Server{
|
||||
Logf: logger.WithPrefix(logf, "socks5: "),
|
||||
}
|
||||
// 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) {
|
||||
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) {
|
||||
return ns.DialContextTCP(ctx, addr)
|
||||
}
|
||||
} else {
|
||||
var mu sync.Mutex
|
||||
var dns netstack.DNSMap
|
||||
e.AddNetworkMapCallback(func(nm *netmap.NetworkMap) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
dns = netstack.DNSMapFromNetworkMap(nm)
|
||||
})
|
||||
srv.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
ipp, err := dns.Resolve(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, network, ipp.String())
|
||||
}
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, network, ipp.String())
|
||||
}
|
||||
go func() {
|
||||
log.Fatalf("SOCKS5 server exited: %v", srv.Serve(socksListener))
|
||||
@@ -295,7 +298,7 @@ func run() error {
|
||||
Port: 41112,
|
||||
StatePath: args.statepath,
|
||||
AutostartStateKey: globalStateKey,
|
||||
SurviveDisconnects: true,
|
||||
SurviveDisconnects: runtime.GOOS != "windows",
|
||||
DebugMux: debugMux,
|
||||
}
|
||||
err = ipnserver.Run(ctx, logf, pol.PublicID.String(), ipnserver.FixedEngine(e), opts)
|
||||
|
||||
@@ -159,18 +159,16 @@ func beFirewallKillswitch() bool {
|
||||
|
||||
func startIPNServer(ctx context.Context, logid string) error {
|
||||
var logf logger.Logf = log.Printf
|
||||
var eng wgengine.Engine
|
||||
var err error
|
||||
|
||||
getEngine := func() (wgengine.Engine, error) {
|
||||
getEngineRaw := func() (wgengine.Engine, error) {
|
||||
dev, devName, err := tstun.New(logf, "Tailscale")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("TUN: %w", err)
|
||||
}
|
||||
r, err := router.New(logf, dev)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Router: %w", err)
|
||||
}
|
||||
if wrapNetstack {
|
||||
r = netstack.NewSubnetRouterWrapper(r)
|
||||
@@ -179,7 +177,7 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
if err != nil {
|
||||
r.Close()
|
||||
dev.Close()
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("DNS: %w", err)
|
||||
}
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
Tun: dev,
|
||||
@@ -190,7 +188,7 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
if err != nil {
|
||||
r.Close()
|
||||
dev.Close()
|
||||
return nil, err
|
||||
return nil, fmt.Errorf("Engine: %w", err)
|
||||
}
|
||||
onlySubnets := true
|
||||
if wrapNetstack {
|
||||
@@ -199,60 +197,82 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
return wgengine.NewWatchdog(eng), nil
|
||||
}
|
||||
|
||||
if msg := os.Getenv("TS_DEBUG_WIN_FAIL"); msg != "" {
|
||||
err = fmt.Errorf("pretending to be a service failure: %v", msg)
|
||||
} else {
|
||||
// We have a bunch of bug reports of wgengine.NewUserspaceEngine returning a few different errors,
|
||||
// all intermittently. A few times I (Brad) have also seen sporadic failures that simply
|
||||
// restarting fixed. So try a few times.
|
||||
for try := 1; try <= 5; try++ {
|
||||
if try > 1 {
|
||||
// Only sleep a bit. Don't do some massive backoff because
|
||||
// the frontend GUI has a 30 second timeout on connecting to us,
|
||||
// but even 5 seconds is too long for them to get any results.
|
||||
// 5 tries * 1 second each seems fine.
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
eng, err = getEngine()
|
||||
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 err != nil {
|
||||
logf("wgengine.NewUserspaceEngine: (try %v) %v", try, err)
|
||||
continue
|
||||
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)
|
||||
}
|
||||
}
|
||||
if try > 1 {
|
||||
logf("wgengine.NewUserspaceEngine: ended up working on try %v", try)
|
||||
timer := time.NewTimer(5 * time.Second)
|
||||
engErrc <- engineOrError{eng, err}
|
||||
if err == nil {
|
||||
timer.Stop()
|
||||
return
|
||||
}
|
||||
break
|
||||
<-timer.C
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
// Log the error, but don't fatalf. We want to
|
||||
// propagate the error message to the UI frontend. So
|
||||
// we continue and tell the ipnserver to return that
|
||||
// in a Notify message.
|
||||
logf("wgengine.NewUserspaceEngine: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
opts := ipnserver.Options{
|
||||
Port: 41112,
|
||||
SurviveDisconnects: false,
|
||||
StatePath: args.statepath,
|
||||
}
|
||||
if err != nil {
|
||||
// Return nicer errors to users, annotated with logids, which helps
|
||||
// when they file bugs.
|
||||
rawGetEngine := getEngine // raw == without verbose logid-containing error
|
||||
getEngine = func() (wgengine.Engine, error) {
|
||||
eng, err := rawGetEngine()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("wgengine.NewUserspaceEngine: %v\n\nlogid: %v", err, logid)
|
||||
}
|
||||
return eng, nil
|
||||
|
||||
// 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 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)
|
||||
}
|
||||
} 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
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -49,7 +48,6 @@ import (
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/systemd"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
@@ -729,11 +727,11 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
}
|
||||
}()
|
||||
|
||||
var lastDNSConfig = new(tailcfg.DNSConfig)
|
||||
var lastDERPMap *tailcfg.DERPMap
|
||||
var lastUserProfile = map[tailcfg.UserID]tailcfg.UserProfile{}
|
||||
var lastParsedPacketFilter []filter.Match
|
||||
var collectServices bool
|
||||
sess := newMapSession(persist.PrivateNodeKey)
|
||||
sess.logf = c.logf
|
||||
sess.vlogf = vlogf
|
||||
sess.machinePubKey = machinePubKey
|
||||
sess.keepSharerAndUserSplit = c.keepSharerAndUserSplit
|
||||
|
||||
// If allowStream, then the server will use an HTTP long poll to
|
||||
// return incremental results. There is always one response right
|
||||
@@ -742,7 +740,6 @@ 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
|
||||
@@ -789,16 +786,6 @@ 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)
|
||||
@@ -809,17 +796,34 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
setControlAtomic(&controlUseDERPRoute, resp.Debug.DERPRoute)
|
||||
setControlAtomic(&controlTrimWGConfig, resp.Debug.TrimWGConfig)
|
||||
}
|
||||
|
||||
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 {
|
||||
filtered := resp.Peers[:0]
|
||||
for _, p := range resp.Peers {
|
||||
if !p.DiscoKey.IsZero() {
|
||||
filtered = append(filtered, p)
|
||||
anyOld, numDisco := false, 0
|
||||
for _, p := range nm.Peers {
|
||||
if p.DiscoKey.IsZero() {
|
||||
anyOld = true
|
||||
} else {
|
||||
numDisco++
|
||||
}
|
||||
}
|
||||
resp.Peers = filtered
|
||||
if anyOld {
|
||||
filtered := make([]*tailcfg.Node, 0, numDisco)
|
||||
for _, p := range nm.Peers {
|
||||
if !p.DiscoKey.IsZero() {
|
||||
filtered = append(filtered, p)
|
||||
}
|
||||
}
|
||||
nm.Peers = filtered
|
||||
}
|
||||
}
|
||||
if Debug.StripEndpoints {
|
||||
for _, p := range resp.Peers {
|
||||
@@ -830,18 +834,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
|
||||
}
|
||||
}
|
||||
if Debug.StripCaps {
|
||||
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
|
||||
nm.SelfNode.Capabilities = nil
|
||||
}
|
||||
|
||||
// Get latest localPort. This might've changed if
|
||||
@@ -849,67 +842,9 @@ 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()
|
||||
localPort = c.localPort
|
||||
nm.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
|
||||
@@ -1104,124 +1039,6 @@ 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
|
||||
|
||||
@@ -6,169 +6,13 @@ 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"}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
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
|
||||
}
|
||||
282
control/controlclient/map.go
Normal file
282
control/controlclient/map.go
Normal file
@@ -0,0 +1,282 @@
|
||||
// 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
|
||||
}
|
||||
311
control/controlclient/map_test.go
Normal file
311
control/controlclient/map_test.go
Normal file
@@ -0,0 +1,311 @@
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
2
go.mod
2
go.mod
@@ -24,7 +24,7 @@ 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-20210403171604-17614717a9b5
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210419202603-b32acd8f0292
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
|
||||
|
||||
2
go.sum
2
go.sum
@@ -139,6 +139,8 @@ github.com/tailscale/wireguard-go v0.0.0-20210402193818-fc309421dd43 h1:SRUknVD6
|
||||
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/tailscale/wireguard-go v0.0.0-20210419202603-b32acd8f0292 h1:rKgYi0k3TNqEz5f7sc6zNeufZcnxm1Efd6bb39cGGkY=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210419202603-b32acd8f0292/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=
|
||||
|
||||
@@ -95,12 +95,15 @@ type PartialFile struct {
|
||||
DeclaredSize int64 // or -1 if unknown
|
||||
Received int64 // bytes copied thus far
|
||||
|
||||
// FinalPath is non-empty when the final has been completely
|
||||
// written and renamed into place. This is then the complete
|
||||
// path to the file post-rename. This is only set in
|
||||
// "direct" file mode where the peerapi isn't being used; see
|
||||
// LocalBackend.SetDirectFileRoot.
|
||||
FinalPath string `json:",omitempty"`
|
||||
// PartialPath is set non-empty in "direct" file mode to the
|
||||
// in-progress '*.partial' file's path when the peerapi isn't
|
||||
// being used; see LocalBackend.SetDirectFileRoot.
|
||||
PartialPath string `json:",omitempty"`
|
||||
|
||||
// Done is set in "direct" mode when the partial file has been
|
||||
// closed and is ready for the caller to rename away the
|
||||
// ".partial" suffix.
|
||||
Done bool `json:",omitempty"`
|
||||
}
|
||||
|
||||
// StateKey is an opaque identifier for a set of LocalBackend state
|
||||
|
||||
@@ -19,7 +19,7 @@ type FakeBackend struct {
|
||||
}
|
||||
|
||||
func (b *FakeBackend) Start(opts Options) error {
|
||||
b.serverURL = opts.Prefs.ControlURL
|
||||
b.serverURL = opts.Prefs.ControlURLOrDefault()
|
||||
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.ControlURL + "/admin/machines"
|
||||
return h.prefsCache.ControlURLOrDefault() + "/admin/machines"
|
||||
}
|
||||
|
||||
func (h *Handle) StartLoginInteractive() {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
@@ -231,7 +232,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 {
|
||||
if runtime.GOOS == "windows" && b.netMap != nil && b.state == ipn.Running {
|
||||
want := len(b.netMap.Addresses)
|
||||
b.logf("linkChange: peerAPIListeners too low; trying again")
|
||||
if len(b.peerAPIListeners) < want {
|
||||
@@ -436,6 +437,15 @@ 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
|
||||
@@ -637,7 +647,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
}
|
||||
|
||||
b.inServerMode = b.prefs.ForceDaemon
|
||||
b.serverURL = b.prefs.ControlURL
|
||||
b.serverURL = b.prefs.ControlURLOrDefault()
|
||||
hostinfo.RoutableIPs = append(hostinfo.RoutableIPs, b.prefs.AdvertiseRoutes...)
|
||||
hostinfo.RequestTags = append(hostinfo.RequestTags, b.prefs.AdvertiseTags...)
|
||||
if b.inServerMode || runtime.GOOS == "windows" {
|
||||
@@ -1571,14 +1581,18 @@ func (b *LocalBackend) authReconfig() {
|
||||
|
||||
// If CorpDNS is false, dcfg remains the zero value.
|
||||
if uc.CorpDNS {
|
||||
for _, resolver := range nm.DNS.Resolvers {
|
||||
res, err := parseResolver(resolver)
|
||||
if err != nil {
|
||||
b.logf(err.Error())
|
||||
continue
|
||||
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)
|
||||
}
|
||||
dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, res)
|
||||
}
|
||||
|
||||
addDefault(nm.DNS.Resolvers)
|
||||
if len(nm.DNS.Routes) > 0 {
|
||||
dcfg.Routes = map[dnsname.FQDN][]netaddr.IPPort{}
|
||||
}
|
||||
@@ -1603,7 +1617,6 @@ 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
|
||||
@@ -1619,12 +1632,34 @@ func (b *LocalBackend) authReconfig() {
|
||||
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 len(dcfg.Routes) == 0 && len(dcfg.Hosts) == 0 && len(dcfg.AuthoritativeSuffixes) == 0:
|
||||
// No settings requiring split DNS, no problem.
|
||||
case (version.OS() == "iOS" || version.OS() == "macOS") && !uc.ExitNodeID.IsZero():
|
||||
// On Apple OSes, if your NetworkExtension provides a
|
||||
// default route, underlying primary resolvers are
|
||||
// automatically removed, so we MUST provide a set of
|
||||
// resolvers capable of resolving the entire world.
|
||||
// https://github.com/tailscale/tailscale/issues/1713
|
||||
addDefault(nm.DNS.FallbackResolvers)
|
||||
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)
|
||||
@@ -1771,7 +1806,19 @@ func magicDNSRootDomains(nm *netmap.NetworkMap) []dnsname.FQDN {
|
||||
// TODO: propagate error
|
||||
return nil
|
||||
}
|
||||
return []dnsname.FQDN{fqdn}
|
||||
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 nil
|
||||
}
|
||||
@@ -1851,16 +1898,19 @@ func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router
|
||||
if !default6 {
|
||||
rs.Routes = append(rs.Routes, ipv6Default)
|
||||
}
|
||||
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()...)
|
||||
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()...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2044,6 +2094,30 @@ 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.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.
|
||||
@@ -2067,7 +2141,7 @@ func (b *LocalBackend) logout(ctx context.Context, sync bool) error {
|
||||
|
||||
b.EditPrefs(&ipn.MaskedPrefs{
|
||||
WantRunningSet: true,
|
||||
Prefs: ipn.Prefs{WantRunning: true},
|
||||
Prefs: ipn.Prefs{WantRunning: false},
|
||||
})
|
||||
|
||||
if cc == nil {
|
||||
@@ -2167,6 +2241,27 @@ 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.
|
||||
|
||||
@@ -22,6 +22,8 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
@@ -50,15 +52,46 @@ type peerAPIServer struct {
|
||||
|
||||
const partialSuffix = ".partial"
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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 == "." ||
|
||||
strings.ContainsAny(clean, `/\`) ||
|
||||
clean == "." || clean == ".." ||
|
||||
strings.HasSuffix(clean, partialSuffix) {
|
||||
return "", false
|
||||
}
|
||||
return filepath.Join(s.rootDir, strings.ReplaceAll(url.PathEscape(baseName), ":", "%3a")), true
|
||||
for _, r := range baseName {
|
||||
if !validFilenameRune(r) {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
return filepath.Join(s.rootDir, baseName), true
|
||||
}
|
||||
|
||||
// hasFilesWaiting reports whether any files are buffered in the
|
||||
@@ -290,7 +323,6 @@ func (pln *peerAPIListener) serve() {
|
||||
remoteAddr: ipp,
|
||||
peerNode: peerNode,
|
||||
peerUser: peerUser,
|
||||
lb: pln.lb,
|
||||
}
|
||||
httpServer := &http.Server{
|
||||
Handler: h,
|
||||
@@ -324,7 +356,6 @@ 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{}) {
|
||||
@@ -333,7 +364,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.put(w, r)
|
||||
h.handlePeerPut(w, r)
|
||||
return
|
||||
}
|
||||
who := h.peerUser.DisplayName
|
||||
@@ -350,21 +381,22 @@ This is my Tailscale device. Your device is %v.
|
||||
}
|
||||
|
||||
type incomingFile struct {
|
||||
name string // "foo.jpg"
|
||||
started time.Time
|
||||
size int64 // or -1 if unknown; never 0
|
||||
w io.Writer // underlying writer
|
||||
ph *peerAPIHandler
|
||||
name string // "foo.jpg"
|
||||
started time.Time
|
||||
size int64 // or -1 if unknown; never 0
|
||||
w io.Writer // underlying writer
|
||||
ph *peerAPIHandler
|
||||
partialPath string // non-empty in direct mode
|
||||
|
||||
mu sync.Mutex
|
||||
copied int64
|
||||
done bool
|
||||
lastNotify time.Time
|
||||
finalPath string // non-empty in direct mode, when file is done
|
||||
}
|
||||
|
||||
func (f *incomingFile) markAndNotifyDone(finalPath string) {
|
||||
func (f *incomingFile) markAndNotifyDone() {
|
||||
f.mu.Lock()
|
||||
f.finalPath = finalPath
|
||||
f.done = true
|
||||
f.mu.Unlock()
|
||||
b := f.ph.ps.b
|
||||
b.sendFileNotify()
|
||||
@@ -401,11 +433,12 @@ func (f *incomingFile) PartialFile() ipn.PartialFile {
|
||||
Started: f.started,
|
||||
DeclaredSize: f.size,
|
||||
Received: f.copied,
|
||||
FinalPath: f.finalPath,
|
||||
PartialPath: f.partialPath,
|
||||
Done: f.done,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) put(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.isSelf {
|
||||
http.Error(w, "not owner", http.StatusForbidden)
|
||||
return
|
||||
@@ -415,14 +448,32 @@ func (h *peerAPIHandler) put(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if r.Method != "PUT" {
|
||||
http.Error(w, "not method PUT", http.StatusMethodNotAllowed)
|
||||
http.Error(w, "expected method PUT", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if h.ps.rootDir == "" {
|
||||
http.Error(w, "no rootdir", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
baseName := path.Base(r.URL.Path)
|
||||
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
|
||||
}
|
||||
dstFile, ok := h.ps.diskPath(baseName)
|
||||
if !ok {
|
||||
http.Error(w, "bad filename", 400)
|
||||
@@ -453,6 +504,9 @@ func (h *peerAPIHandler) put(w http.ResponseWriter, r *http.Request) {
|
||||
w: f,
|
||||
ph: h,
|
||||
}
|
||||
if h.ps.directFileMode {
|
||||
inFile.partialPath = dstFile
|
||||
}
|
||||
h.ps.b.registerIncomingFile(inFile, true)
|
||||
defer h.ps.b.registerIncomingFile(inFile, false)
|
||||
n, err := io.Copy(inFile, r.Body)
|
||||
@@ -470,14 +524,8 @@ func (h *peerAPIHandler) put(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if h.ps.directFileMode {
|
||||
finalPath := strings.TrimSuffix(dstFile, partialSuffix)
|
||||
if err := os.Rename(dstFile, finalPath); err != nil {
|
||||
h.logf("Rename error: %v", err)
|
||||
http.Error(w, "error renaming file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if inFile != nil { // non-zero length; TODO: notify even for zero length
|
||||
inFile.markAndNotifyDone(finalPath)
|
||||
inFile.markAndNotifyDone()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
424
ipn/ipnlocal/peerapi_test.go
Normal file
424
ipn/ipnlocal/peerapi_test.go
Normal file
@@ -0,0 +1,424 @@
|
||||
// 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"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
|
||||
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 TestHandlePeerPut(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: "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_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 caps []string
|
||||
if tt.capSharing {
|
||||
caps = append(caps, tailcfg.CapabilityFileSharing)
|
||||
}
|
||||
var e peerAPITestEnv
|
||||
lb := &LocalBackend{
|
||||
netMap: &netmap.NetworkMap{
|
||||
SelfNode: &tailcfg.Node{
|
||||
Capabilities: caps,
|
||||
},
|
||||
},
|
||||
logf: e.logf,
|
||||
}
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -287,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, logf) {
|
||||
if isReadonlyConn(ci, s.b.OperatorUserID(), logf) {
|
||||
ctx = ipn.ReadonlyContextOf(ctx)
|
||||
}
|
||||
|
||||
@@ -313,7 +313,7 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
}
|
||||
}
|
||||
|
||||
func isReadonlyConn(ci connIdentity, logf logger.Logf) bool {
|
||||
func isReadonlyConn(ci connIdentity, operatorUID string, 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.
|
||||
@@ -342,6 +342,10 @@ func isReadonlyConn(ci connIdentity, 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":
|
||||
@@ -435,7 +439,7 @@ func (s *server) localAPIPermissions(ci connIdentity) (read, write bool) {
|
||||
return false, false
|
||||
}
|
||||
if ci.IsUnixSock {
|
||||
return true, !isReadonlyConn(ci, logger.Discard)
|
||||
return true, !isReadonlyConn(ci, s.b.OperatorUserID(), logger.Discard)
|
||||
}
|
||||
return false, false
|
||||
}
|
||||
@@ -472,9 +476,7 @@ func (s *server) addConn(c net.Conn, isHTTP bool) (ci connIdentity, err error) {
|
||||
defer func() {
|
||||
if doReset {
|
||||
s.logf("identity changed; resetting server")
|
||||
s.bsMu.Lock()
|
||||
s.bs.Reset(context.TODO())
|
||||
s.bsMu.Unlock()
|
||||
s.b.ResetForClientDisconnect()
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -524,9 +526,7 @@ func (s *server) removeAndCloseConn(c net.Conn) {
|
||||
s.logf("client disconnected; staying alive in server mode")
|
||||
} else {
|
||||
s.logf("client disconnected; stopping server")
|
||||
s.bsMu.Lock()
|
||||
s.bs.Reset(context.TODO())
|
||||
s.bsMu.Unlock()
|
||||
s.b.ResetForClientDisconnect()
|
||||
}
|
||||
}
|
||||
c.Close()
|
||||
@@ -592,6 +592,7 @@ func (s *server) writeToClients(b []byte) {
|
||||
// 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)
|
||||
|
||||
@@ -652,46 +653,6 @@ 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 {
|
||||
@@ -699,7 +660,6 @@ 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 {
|
||||
|
||||
@@ -143,11 +143,6 @@ 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"
|
||||
@@ -211,12 +206,6 @@ 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)
|
||||
|
||||
44
ipn/prefs.go
44
ipn/prefs.go
@@ -25,9 +25,18 @@ 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.
|
||||
ControlURL string
|
||||
|
||||
// RouteAll specifies whether to accept subnets advertised by
|
||||
@@ -144,6 +153,10 @@ 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.
|
||||
@@ -174,6 +187,7 @@ 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
|
||||
@@ -258,12 +272,15 @@ func (p *Prefs) pretty(goos string) string {
|
||||
if goos == "linux" {
|
||||
fmt.Fprintf(&sb, "nf=%v ", p.NetfilterMode)
|
||||
}
|
||||
if p.ControlURL != "" && p.ControlURL != "https://login.tailscale.com" {
|
||||
if p.ControlURL != "" && p.ControlURL != DefaultControlURL {
|
||||
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 {
|
||||
@@ -302,6 +319,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
|
||||
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 &&
|
||||
@@ -335,12 +353,19 @@ 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{
|
||||
// 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",
|
||||
// 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: "",
|
||||
|
||||
RouteAll: true,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
@@ -349,6 +374,15 @@ 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.
|
||||
|
||||
@@ -51,5 +51,6 @@ var _PrefsNeedsRegeneration = Prefs(struct {
|
||||
AdvertiseRoutes []netaddr.IPPrefix
|
||||
NoSNAT bool
|
||||
NetfilterMode preftype.NetfilterMode
|
||||
OperatorUser string
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
|
||||
@@ -33,7 +33,28 @@ func fieldsOf(t reflect.Type) (fields []string) {
|
||||
func TestPrefsEqual(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
|
||||
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "ExitNodeID", "ExitNodeIP", "ExitNodeAllowLANAccess", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"}
|
||||
prefsHandles := []string{
|
||||
"ControlURL",
|
||||
"RouteAll",
|
||||
"AllowSingleHosts",
|
||||
"ExitNodeID",
|
||||
"ExitNodeIP",
|
||||
"ExitNodeAllowLANAccess",
|
||||
"CorpDNS",
|
||||
"WantRunning",
|
||||
"ShieldsUp",
|
||||
"AdvertiseTags",
|
||||
"Hostname",
|
||||
"OSVersion",
|
||||
"DeviceModel",
|
||||
"NotepadURLs",
|
||||
"ForceDaemon",
|
||||
"AdvertiseRoutes",
|
||||
"NoSNAT",
|
||||
"NetfilterMode",
|
||||
"OperatorUser",
|
||||
"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)
|
||||
|
||||
@@ -89,6 +89,15 @@ 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")
|
||||
@@ -101,18 +110,24 @@ func (m *resolvconfManager) SetDNS(config OSConfig) error {
|
||||
m.scriptInstalled = true
|
||||
}
|
||||
|
||||
stdin := new(bytes.Buffer)
|
||||
writeResolvConf(stdin, config.Nameservers, config.SearchDomains) // dns_direct.go
|
||||
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
|
||||
|
||||
// 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
|
||||
@@ -156,10 +171,8 @@ func (m *resolvconfManager) GetBaseConfig() (OSConfig, error) {
|
||||
}
|
||||
|
||||
func (m *resolvconfManager) Close() error {
|
||||
cmd := exec.Command("resolvconf", "-d", resolvconfConfigName)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("running %s: %s", cmd, out)
|
||||
if err := m.deleteTailscaleConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if m.scriptInstalled {
|
||||
|
||||
@@ -201,15 +201,53 @@ func (m directManager) backupConfig() error {
|
||||
return os.Rename(resolvConf, backupConf)
|
||||
}
|
||||
|
||||
func (m directManager) SetDNS(config OSConfig) error {
|
||||
if err := m.backupConfig(); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
writeResolvConf(buf, config.Nameservers, config.SearchDomains)
|
||||
if err := atomicfile.WriteFile(resolvConf, buf.Bytes(), 0644); 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
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -51,23 +51,6 @@ 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)
|
||||
|
||||
|
||||
@@ -137,13 +137,34 @@ func (m *nmManager) trySet(ctx context.Context, config OSConfig) error {
|
||||
}
|
||||
}
|
||||
|
||||
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, "~.")
|
||||
}
|
||||
|
||||
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(config.SearchDomains)
|
||||
ipv4Map["dns-search"] = dbus.MakeVariant(search)
|
||||
// We should only request priority if we have nameservers to set.
|
||||
if len(dnsv4) == 0 {
|
||||
ipv4Map["dns-priority"] = dbus.MakeVariant(lowerPriority)
|
||||
@@ -179,7 +200,7 @@ func (m *nmManager) trySet(ctx context.Context, config OSConfig) error {
|
||||
ipv6Map["never-default"] = dbus.MakeVariant(true)
|
||||
|
||||
ipv6Map["dns"] = dbus.MakeVariant(dnsv6)
|
||||
ipv6Map["dns-search"] = dbus.MakeVariant(config.SearchDomains)
|
||||
ipv6Map["dns-search"] = dbus.MakeVariant(search)
|
||||
if len(dnsv6) == 0 {
|
||||
ipv6Map["dns-priority"] = dbus.MakeVariant(lowerPriority)
|
||||
} else if len(config.MatchDomains) > 0 {
|
||||
|
||||
@@ -19,7 +19,20 @@ 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)
|
||||
|
||||
@@ -75,10 +88,5 @@ func (m openresolvManager) GetBaseConfig() (OSConfig, error) {
|
||||
}
|
||||
|
||||
func (m openresolvManager) Close() 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
|
||||
return m.deleteTailscaleConfig()
|
||||
}
|
||||
|
||||
@@ -52,6 +52,10 @@ 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
|
||||
|
||||
@@ -163,7 +163,7 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
|
||||
RoutingOnly: true,
|
||||
})
|
||||
}
|
||||
if len(config.MatchDomains) == 0 {
|
||||
if len(config.MatchDomains) == 0 && len(config.Nameservers) > 0 {
|
||||
// Caller requested full DNS interception, install a
|
||||
// routing-only root domain.
|
||||
linkDomains = append(linkDomains, resolvedLinkDomain{
|
||||
|
||||
@@ -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 /104 were picked at
|
||||
// TailscaleULARange. The bits from /48 to /64 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:7234:6e44:306d:2100::/104") })
|
||||
ulaEph6Range.Do(func() { mustPrefix(&ulaEph6Range.v, "fd7a:115c:a1e0:efe3::/64") })
|
||||
return ulaEph6Range.v
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ 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 {
|
||||
@@ -36,7 +35,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.
|
||||
// TODO(apenwarr): handle magic cookie auth
|
||||
// NOTE(bradfitz): Jason did a new pipe package: https://go-review.googlesource.com/c/sys/+/299009
|
||||
func listen(path string, port uint16) (_ net.Listener, gotPort uint16, _ error) {
|
||||
lc := net.ListenConfig{
|
||||
Control: setFlags,
|
||||
|
||||
@@ -39,7 +39,9 @@ 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
|
||||
const CurrentMapRequestVersion = 16
|
||||
// 17: 2021-04-18: MapResponse.Domain empty means unchanged
|
||||
// 18: 2021-04-19: MapResponse.Node nil means unchanged (all fields now omitempty)
|
||||
const CurrentMapRequestVersion = 18
|
||||
|
||||
type StableID string
|
||||
|
||||
@@ -847,6 +849,11 @@ 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"`
|
||||
@@ -863,8 +870,6 @@ 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"`
|
||||
}
|
||||
|
||||
@@ -894,8 +899,14 @@ type MapResponse struct {
|
||||
PingRequest *PingRequest `json:",omitempty"`
|
||||
|
||||
// Networking
|
||||
Node *Node
|
||||
DERPMap *DERPMap `json:",omitempty"` // if non-empty, a change in the DERP map.
|
||||
|
||||
// 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"`
|
||||
|
||||
// Peers, if non-empty, is the complete list of peers.
|
||||
// It will be set in the first MapResponse for a long-polled request/response.
|
||||
@@ -939,7 +950,8 @@ 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.
|
||||
Domain string
|
||||
// If empty, the value is unchanged.
|
||||
Domain string `json:",omitempty"`
|
||||
|
||||
// CollectServices reports whether this node's Tailnet has
|
||||
// requested that info about services be included in HostInfo.
|
||||
@@ -953,9 +965,12 @@ 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
|
||||
PacketFilter []FilterRule `json:",omitempty"`
|
||||
|
||||
UserProfiles []UserProfile // as of 1.1.541 (mapver 5): may be new or updated user profiles only
|
||||
// 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"`
|
||||
|
||||
// Debug is normally nil, except for when the control server
|
||||
// is setting debug settings on a node.
|
||||
|
||||
@@ -198,6 +198,10 @@ 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
|
||||
@@ -206,12 +210,13 @@ 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
|
||||
Domains []string
|
||||
Proxied bool
|
||||
Nameservers []netaddr.IP
|
||||
PerDomain bool
|
||||
Resolvers []DNSResolver
|
||||
Routes map[string][]DNSResolver
|
||||
FallbackResolvers []DNSResolver
|
||||
Domains []string
|
||||
Proxied bool
|
||||
Nameservers []netaddr.IP
|
||||
PerDomain bool
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of DNSResolver.
|
||||
|
||||
@@ -57,11 +57,8 @@ 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
|
||||
|
||||
@@ -8,15 +8,13 @@ import "runtime"
|
||||
|
||||
// IsMobile reports whether this is a mobile client build.
|
||||
func IsMobile() bool {
|
||||
// Good enough heuristic for now, at least until Apple makes
|
||||
// ARM laptops...
|
||||
return runtime.GOOS == "android" || isIOS
|
||||
return runtime.GOOS == "android" || runtime.GOOS == "ios"
|
||||
}
|
||||
|
||||
// OS returns runtime.GOOS, except instead of returning "darwin" it
|
||||
// returns "iOS" or "macOS".
|
||||
func OS() string {
|
||||
if isIOS {
|
||||
if runtime.GOOS == "ios" {
|
||||
return "iOS"
|
||||
}
|
||||
if runtime.GOOS == "darwin" {
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build ios
|
||||
|
||||
package version
|
||||
|
||||
const isIOS = true
|
||||
@@ -1,9 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !ios
|
||||
|
||||
package version
|
||||
|
||||
const isIOS = false
|
||||
@@ -2595,7 +2595,8 @@ func (c *Conn) initialBind() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Conn) listenPacket(ctx context.Context, network, addr string) (net.PacketConn, error) {
|
||||
func (c *Conn) listenPacket(ctx context.Context, network, host string, port uint16) (net.PacketConn, error) {
|
||||
addr := net.JoinHostPort(host, fmt.Sprint(port))
|
||||
if c.packetListener != nil {
|
||||
return c.packetListener.ListenPacket(ctx, network, addr)
|
||||
}
|
||||
@@ -2614,13 +2615,13 @@ func (c *Conn) bind1(ruc **RebindingUDPConn, which string) error {
|
||||
var err error
|
||||
listenCtx := context.Background() // unused without DNS name to resolve
|
||||
if c.port == 0 && DefaultPort != 0 {
|
||||
pc, err = c.listenPacket(listenCtx, which, net.JoinHostPort(host, fmt.Sprint(DefaultPort)))
|
||||
pc, err = c.listenPacket(listenCtx, which, host, DefaultPort)
|
||||
if err != nil {
|
||||
c.logf("magicsock: bind: default port %s/%v unavailable; picking random", which, DefaultPort)
|
||||
}
|
||||
}
|
||||
if pc == nil {
|
||||
pc, err = c.listenPacket(listenCtx, which, net.JoinHostPort(host, fmt.Sprint(c.port)))
|
||||
pc, err = c.listenPacket(listenCtx, which, host, c.port)
|
||||
}
|
||||
if err != nil {
|
||||
c.logf("magicsock: bind(%s/%v): %v", which, c.port, err)
|
||||
@@ -2648,30 +2649,30 @@ func (c *Conn) Rebind() {
|
||||
if err := c.pconn4.pconn.Close(); err != nil {
|
||||
c.logf("magicsock: link change close failed: %v", err)
|
||||
}
|
||||
packetConn, err := c.listenPacket(listenCtx, "udp4", net.JoinHostPort(host, fmt.Sprint(c.port)))
|
||||
packetConn, err := c.listenPacket(listenCtx, "udp4", host, c.port)
|
||||
if err != nil {
|
||||
c.logf("magicsock: link change unable to bind fixed port %d: %v, falling back to random port", c.port, err)
|
||||
packetConn, err = c.listenPacket(listenCtx, "udp4", net.JoinHostPort(host, "0"))
|
||||
packetConn, err = c.listenPacket(listenCtx, "udp4", host, 0)
|
||||
if err != nil {
|
||||
c.logf("magicsock: link change failed to bind random port: %v", err)
|
||||
c.pconn4.mu.Unlock()
|
||||
return
|
||||
}
|
||||
newPort := c.pconn4.localAddrLocked().Port
|
||||
newPort := packetConn.LocalAddr().(*net.UDPAddr).Port
|
||||
c.logf("magicsock: link change rebound port: from %v to %v (failed to get %v)", oldPort, newPort, c.port)
|
||||
} else {
|
||||
c.logf("magicsock: link change rebound port: %d", c.port)
|
||||
c.logf("magicsock: link change rebound port from %d to %d", oldPort, c.port)
|
||||
}
|
||||
c.pconn4.pconn = packetConn.(*net.UDPConn)
|
||||
c.pconn4.pconn = packetConn
|
||||
c.pconn4.mu.Unlock()
|
||||
} else {
|
||||
c.logf("magicsock: link change, binding new port")
|
||||
packetConn, err := c.listenPacket(listenCtx, "udp4", host+":0")
|
||||
packetConn, err := c.listenPacket(listenCtx, "udp4", host, 0)
|
||||
if err != nil {
|
||||
c.logf("magicsock: link change failed to bind new port: %v", err)
|
||||
return
|
||||
}
|
||||
c.pconn4.Reset(packetConn.(*net.UDPConn))
|
||||
c.pconn4.Reset(packetConn)
|
||||
}
|
||||
c.portMapper.SetLocalPort(c.LocalPort())
|
||||
|
||||
@@ -2766,8 +2767,8 @@ type RebindingUDPConn struct {
|
||||
pconn net.PacketConn
|
||||
}
|
||||
|
||||
// currentConn returns c's current pconn and whether it is (fake) closed.
|
||||
func (c *RebindingUDPConn) currentConn() (pconn net.PacketConn) {
|
||||
// currentConn returns c's current pconn.
|
||||
func (c *RebindingUDPConn) currentConn() net.PacketConn {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.pconn
|
||||
@@ -2784,7 +2785,7 @@ func (c *RebindingUDPConn) Reset(pconn net.PacketConn) {
|
||||
}
|
||||
}
|
||||
|
||||
// ReadFromNetaddr reads a packet from c into b.
|
||||
// ReadFrom reads a packet from c into b.
|
||||
// It returns the number of bytes copied and the source address.
|
||||
func (c *RebindingUDPConn) ReadFrom(b []byte) (int, net.Addr, error) {
|
||||
for {
|
||||
|
||||
@@ -393,12 +393,10 @@ func (ns *Impl) injectInbound(p *packet.Parsed, t *tstun.Wrapper) filter.Respons
|
||||
}
|
||||
|
||||
func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
|
||||
if debugNetstack {
|
||||
// Kinda ugly:
|
||||
// ForwarderRequest: &{{{{0 0}}} 0xc0001c30b0 0xc0004c3d40 {1240 6 true 826109390 0 true}
|
||||
ns.logf("[v2] ForwarderRequest: %v", r)
|
||||
}
|
||||
reqDetails := r.ID()
|
||||
if debugNetstack {
|
||||
ns.logf("[v2] TCP ForwarderRequest: %s", stringifyTEI(reqDetails))
|
||||
}
|
||||
dialAddr := reqDetails.LocalAddress
|
||||
dialNetAddr, _ := netaddr.FromStdIP(net.IP(dialAddr))
|
||||
isTailscaleIP := tsaddr.IsTailscaleIP(dialNetAddr)
|
||||
@@ -472,7 +470,10 @@ func (ns *Impl) forwardTCP(client *gonet.TCPConn, wq *waiter.Queue, dialAddr tcp
|
||||
}
|
||||
|
||||
func (ns *Impl) acceptUDP(r *udp.ForwarderRequest) {
|
||||
ns.logf("[v2] UDP ForwarderRequest: %v", r)
|
||||
reqDetails := r.ID()
|
||||
if debugNetstack {
|
||||
ns.logf("[v2] UDP ForwarderRequest: %v", stringifyTEI(reqDetails))
|
||||
}
|
||||
var wq waiter.Queue
|
||||
ep, err := r.CreateEndpoint(&wq)
|
||||
if err != nil {
|
||||
@@ -563,3 +564,9 @@ func startPacketCopy(ctx context.Context, cancel context.CancelFunc, dst net.Pac
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func stringifyTEI(tei stack.TransportEndpointID) string {
|
||||
localHostPort := net.JoinHostPort(tei.LocalAddress.String(), strconv.Itoa(int(tei.LocalPort)))
|
||||
remoteHostPort := net.JoinHostPort(tei.RemoteAddress.String(), strconv.Itoa(int(tei.RemotePort)))
|
||||
return fmt.Sprintf("%s -> %s", remoteHostPort, localHostPort)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user