Compare commits
68 Commits
bradfitz/n
...
onebinary
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1dc90404f3 | ||
|
|
25df067dd0 | ||
|
|
4f92f405ee | ||
|
|
0e9ea9f779 | ||
|
|
783f125003 | ||
|
|
01a359cec9 | ||
|
|
5b52b64094 | ||
|
|
6f62bbae79 | ||
|
|
6fd4e8d244 | ||
|
|
6307a9285d | ||
|
|
285d0e3b4d | ||
|
|
5a7c6f1678 | ||
|
|
d32667011d | ||
|
|
314d15b3fb | ||
|
|
ed9d825552 | ||
|
|
c0158bcd0b | ||
|
|
ebcd7ab890 | ||
|
|
aacb2107ae | ||
|
|
98cae48e70 | ||
|
|
9356912053 | ||
|
|
36a26e6a71 | ||
|
|
6ab2176dc7 | ||
|
|
712774a697 | ||
|
|
8368bac847 | ||
|
|
dfa0c90955 | ||
|
|
d4f805339e | ||
|
|
752f8c0f2f | ||
|
|
7891b34266 | ||
|
|
cb97062bac | ||
|
|
773fcfd007 | ||
|
|
68911f6778 | ||
|
|
d707e2f7e5 | ||
|
|
cfde997699 | ||
|
|
d82b28ba73 | ||
|
|
366b3d3f62 | ||
|
|
dc32b4695c | ||
|
|
c0a70f3a06 | ||
|
|
7027fa06c3 | ||
|
|
8d2a90529e | ||
|
|
a72fb7ac0b | ||
|
|
6618e82ba2 | ||
|
|
e9066ee625 | ||
|
|
7cd4766d5e | ||
|
|
3173c5a65c | ||
|
|
ceb568202b | ||
|
|
5190435d6e | ||
|
|
e72ed3fcc2 | ||
|
|
3c8e230ee1 | ||
|
|
a3b15bdf7e | ||
|
|
5bd38b10b4 | ||
|
|
7d16c8228b | ||
|
|
77e2375501 | ||
|
|
e78e26b6fb | ||
|
|
ddd85b9d91 | ||
|
|
e0bd3cc70c | ||
|
|
bc68e22c5b | ||
|
|
9bce1b7fc1 | ||
|
|
73ad1f804b | ||
|
|
05bed64772 | ||
|
|
a0dacba877 | ||
|
|
777c816b34 | ||
|
|
1f6c4ba7c3 | ||
|
|
462f7e38fc | ||
|
|
ed63a041bf | ||
|
|
4b14f72f1f | ||
|
|
b8fb8264a5 | ||
|
|
7f2eb1d87a | ||
|
|
2585edfaeb |
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
31
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -2,36 +2,7 @@
|
||||
name: Bug report
|
||||
about: Create a bug report
|
||||
title: ''
|
||||
labels: ''
|
||||
labels: 'needs-triage'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!-- Please note, this template is for definite bugs, not requests for
|
||||
support. If you need help with Tailscale, please email
|
||||
support@tailscale.com. We don't provide support via Github issues. -->
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Version information:**
|
||||
- Device: [e.g. iPhone X, laptop]
|
||||
- OS: [e.g. Windows, MacOS]
|
||||
- OS version: [e.g. Windows 10, Ubuntu 18.04]
|
||||
- Tailscale version: [e.g. 0.95-0]
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
|
||||
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
21
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,25 +2,6 @@
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: ''
|
||||
labels: 'needs-triage'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
|
||||
A clear and concise description of what the problem is. Ex. I'm always
|
||||
frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
|
||||
A clear and concise description of any alternative solutions or
|
||||
features you've considered.
|
||||
|
||||
**Additional context**
|
||||
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.7.0
|
||||
1.9.0
|
||||
|
||||
@@ -112,13 +112,13 @@ func tailscaleIP(who *apitype.WhoIsResponse) string {
|
||||
return ""
|
||||
}
|
||||
for _, nodeIP := range who.Node.Addresses {
|
||||
if nodeIP.IP.Is4() && nodeIP.IsSingleIP() {
|
||||
return nodeIP.IP.String()
|
||||
if nodeIP.IP().Is4() && nodeIP.IsSingleIP() {
|
||||
return nodeIP.IP().String()
|
||||
}
|
||||
}
|
||||
for _, nodeIP := range who.Node.Addresses {
|
||||
if nodeIP.IsSingleIP() {
|
||||
return nodeIP.IP.String()
|
||||
return nodeIP.IP().String()
|
||||
}
|
||||
}
|
||||
return ""
|
||||
|
||||
@@ -1,638 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"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
|
||||
// all flags. This will panic if a new flag creeps in that's unhandled.
|
||||
func TestUpdateMaskedPrefsFromUpFlag(t *testing.T) {
|
||||
mp := new(ipn.MaskedPrefs)
|
||||
upFlagSet.VisitAll(func(f *flag.Flag) {
|
||||
updateMaskedPrefsFromUpFlag(mp, f.Name)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
f := func(flags ...string) map[string]bool {
|
||||
m := make(map[string]bool)
|
||||
for _, f := range flags {
|
||||
m[f] = true
|
||||
}
|
||||
return m
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
flagSet map[string]bool
|
||||
curPrefs *ipn.Prefs
|
||||
curUser string // os.Getenv("USER") on the client side
|
||||
goos string // empty means "linux"
|
||||
mp *ipn.MaskedPrefs
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "bare_up_means_up",
|
||||
flagSet: f(),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
Hostname: "foo",
|
||||
},
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
WantRunning: true,
|
||||
},
|
||||
WantRunningSet: true,
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
want: accidentalUpPrefix + " --accept-dns --hostname=foo",
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
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, "linux"),
|
||||
WantRunningSet: true,
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "implicit_operator_change",
|
||||
flagSet: f("hostname"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
OperatorUser: "alice",
|
||||
},
|
||||
curUser: "eve",
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
},
|
||||
ControlURLSet: true,
|
||||
},
|
||||
want: accidentalUpPrefix + " --hostname= --operator=alice",
|
||||
},
|
||||
{
|
||||
name: "implicit_operator_matches_shell_user",
|
||||
flagSet: f("hostname"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
OperatorUser: "alice",
|
||||
},
|
||||
curUser: "alice",
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
},
|
||||
ControlURLSet: true,
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "error_advertised_routes_exit_node_removed",
|
||||
flagSet: f("advertise-routes"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.42.0/24"),
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
},
|
||||
},
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.42.0/24"),
|
||||
},
|
||||
},
|
||||
AdvertiseRoutesSet: true,
|
||||
},
|
||||
want: accidentalUpPrefix + " --advertise-routes=10.0.42.0/24 --advertise-exit-node",
|
||||
},
|
||||
{
|
||||
name: "advertised_routes_exit_node_removed",
|
||||
flagSet: f("advertise-routes", "advertise-exit-node"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.42.0/24"),
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
},
|
||||
},
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.42.0/24"),
|
||||
},
|
||||
},
|
||||
AdvertiseRoutesSet: true,
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "advertised_routes_includes_the_0_routes", // but no --advertise-exit-node
|
||||
flagSet: f("advertise-routes"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.42.0/24"),
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
},
|
||||
},
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("11.1.43.0/24"),
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
},
|
||||
},
|
||||
AdvertiseRoutesSet: true,
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "advertised_routes_includes_only_one_0_route", // and no --advertise-exit-node
|
||||
flagSet: f("advertise-routes"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.42.0/24"),
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
},
|
||||
},
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("11.1.43.0/24"),
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
},
|
||||
},
|
||||
AdvertiseRoutesSet: true,
|
||||
},
|
||||
want: accidentalUpPrefix + " --advertise-routes=11.1.43.0/24,0.0.0.0/0 --advertise-exit-node",
|
||||
},
|
||||
{
|
||||
name: "exit_node_clearing", // Issue 1777
|
||||
flagSet: f("exit-node"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
ExitNodeID: "fooID",
|
||||
},
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
ExitNodeIP: netaddr.IP{},
|
||||
},
|
||||
ExitNodeIPSet: true,
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "remove_all_implicit",
|
||||
flagSet: f("force-reauth"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
RouteAll: true,
|
||||
AllowSingleHosts: false,
|
||||
ExitNodeIP: netaddr.MustParseIP("100.64.5.6"),
|
||||
CorpDNS: true,
|
||||
ShieldsUp: true,
|
||||
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
||||
Hostname: "myhostname",
|
||||
ForceDaemon: true,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.0.0/16"),
|
||||
},
|
||||
NetfilterMode: preftype.NetfilterNoDivert,
|
||||
OperatorUser: "alice",
|
||||
},
|
||||
curUser: "eve",
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
},
|
||||
},
|
||||
want: accidentalUpPrefix + " --accept-routes --exit-node=100.64.5.6 --accept-dns --shields-up --advertise-tags=tag:foo,tag:bar --hostname=myhostname --unattended --advertise-routes=10.0.0.0/16 --netfilter-mode=nodivert --operator=alice",
|
||||
},
|
||||
{
|
||||
name: "remove_all_implicit_except_hostname",
|
||||
flagSet: f("hostname"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
RouteAll: true,
|
||||
AllowSingleHosts: false,
|
||||
ExitNodeIP: netaddr.MustParseIP("100.64.5.6"),
|
||||
CorpDNS: true,
|
||||
ShieldsUp: true,
|
||||
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
||||
Hostname: "myhostname",
|
||||
ForceDaemon: true,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.0.0/16"),
|
||||
},
|
||||
NetfilterMode: preftype.NetfilterNoDivert,
|
||||
OperatorUser: "alice",
|
||||
},
|
||||
curUser: "eve",
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
Hostname: "newhostname",
|
||||
},
|
||||
HostnameSet: true,
|
||||
},
|
||||
want: accidentalUpPrefix + " --hostname=newhostname --accept-routes --exit-node=100.64.5.6 --accept-dns --shields-up --advertise-tags=tag:foo,tag:bar --unattended --advertise-routes=10.0.0.0/16 --netfilter-mode=nodivert --operator=alice",
|
||||
},
|
||||
{
|
||||
name: "loggedout_is_implicit",
|
||||
flagSet: f("advertise-exit-node"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
LoggedOut: true,
|
||||
},
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
},
|
||||
},
|
||||
AdvertiseRoutesSet: true,
|
||||
},
|
||||
// not an error. LoggedOut is implicit.
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
// Test that a pre-1.8 version of Tailscale with bogus NoSNAT pref
|
||||
// values is able to enable exit nodes without warnings.
|
||||
name: "make_windows_exit_node",
|
||||
flagSet: f("advertise-exit-node"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
NoSNAT: true, // assume this no-op accidental pre-1.8 value
|
||||
},
|
||||
goos: "windows",
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("192.168.0.0/16"),
|
||||
},
|
||||
},
|
||||
AdvertiseRoutesSet: true,
|
||||
},
|
||||
want: "", // not an error
|
||||
},
|
||||
{
|
||||
name: "ignore_netfilter_change_non_linux",
|
||||
flagSet: f("accept-dns"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
NetfilterMode: preftype.NetfilterNoDivert, // we never had this bug, but pretend it got set non-zero on Windows somehow
|
||||
},
|
||||
goos: "windows",
|
||||
mp: &ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: false,
|
||||
},
|
||||
CorpDNSSet: true,
|
||||
},
|
||||
want: "", // not an error
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
goos := "linux"
|
||||
if tt.goos != "" {
|
||||
goos = tt.goos
|
||||
}
|
||||
var got string
|
||||
if err := checkForAccidentalSettingReverts(tt.flagSet, tt.curPrefs, tt.mp, goos, tt.curUser); err != nil {
|
||||
got = err.Error()
|
||||
}
|
||||
if strings.TrimSpace(got) != tt.want {
|
||||
t.Errorf("unexpected result\n got: %s\nwant: %s\n", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func defaultPrefsFromUpArgs(t testing.TB, goos string) *ipn.Prefs {
|
||||
upArgs := defaultUpArgsByGOOS(goos)
|
||||
prefs, err := prefsFromUpArgs(upArgs, logger.Discard, new(ipnstate.Status), "linux")
|
||||
if err != nil {
|
||||
t.Fatalf("defaultPrefsFromUpArgs: %v", err)
|
||||
}
|
||||
prefs.WantRunning = true
|
||||
return prefs
|
||||
}
|
||||
|
||||
func defaultUpArgsByGOOS(goos string) (args upArgsT) {
|
||||
fs := newUpFlagSet(goos, &args)
|
||||
fs.Parse(nil) // populates args
|
||||
return
|
||||
}
|
||||
|
||||
func TestPrefsFromUpArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args upArgsT
|
||||
goos string // runtime.GOOS; empty means linux
|
||||
st *ipnstate.Status // or nil
|
||||
want *ipn.Prefs
|
||||
wantErr string
|
||||
wantWarn string
|
||||
}{
|
||||
{
|
||||
name: "default_linux",
|
||||
goos: "linux",
|
||||
args: defaultUpArgsByGOOS("linux"),
|
||||
want: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
NoSNAT: false,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default_windows",
|
||||
goos: "windows",
|
||||
args: defaultUpArgsByGOOS("windows"),
|
||||
want: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error_advertise_route_invalid_ip",
|
||||
args: upArgsT{
|
||||
advertiseRoutes: "foo",
|
||||
},
|
||||
wantErr: `"foo" is not a valid IP address or CIDR prefix`,
|
||||
},
|
||||
{
|
||||
name: "error_advertise_route_unmasked_bits",
|
||||
args: upArgsT{
|
||||
advertiseRoutes: "1.2.3.4/16",
|
||||
},
|
||||
wantErr: `1.2.3.4/16 has non-address bits set; expected 1.2.0.0/16`,
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_bad_ip",
|
||||
args: upArgsT{
|
||||
exitNodeIP: "foo",
|
||||
},
|
||||
wantErr: `invalid IP address "foo" for --exit-node: unable to parse IP`,
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_allow_lan_without_exit_node",
|
||||
args: upArgsT{
|
||||
exitNodeAllowLANAccess: true,
|
||||
},
|
||||
wantErr: `--exit-node-allow-lan-access can only be used with --exit-node`,
|
||||
},
|
||||
{
|
||||
name: "error_tag_prefix",
|
||||
args: upArgsT{
|
||||
advertiseTags: "foo",
|
||||
},
|
||||
wantErr: `tag: "foo": tags must start with 'tag:'`,
|
||||
},
|
||||
{
|
||||
name: "error_long_hostname",
|
||||
args: upArgsT{
|
||||
hostname: strings.Repeat("a", 300),
|
||||
},
|
||||
wantErr: `hostname too long: 300 bytes (max 256)`,
|
||||
},
|
||||
{
|
||||
name: "error_linux_netfilter_empty",
|
||||
args: upArgsT{
|
||||
netfilterMode: "",
|
||||
},
|
||||
wantErr: `invalid value --netfilter-mode=""`,
|
||||
},
|
||||
{
|
||||
name: "error_linux_netfilter_bogus",
|
||||
args: upArgsT{
|
||||
netfilterMode: "bogus",
|
||||
},
|
||||
wantErr: `invalid value --netfilter-mode="bogus"`,
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_ip_is_self_ip",
|
||||
args: upArgsT{
|
||||
exitNodeIP: "100.105.106.107",
|
||||
},
|
||||
st: &ipnstate.Status{
|
||||
TailscaleIPs: []netaddr.IP{netaddr.MustParseIP("100.105.106.107")},
|
||||
},
|
||||
wantErr: `cannot use 100.105.106.107 as the exit node as it is a local IP address to this machine, did you mean --advertise-exit-node?`,
|
||||
},
|
||||
{
|
||||
name: "warn_linux_netfilter_nodivert",
|
||||
goos: "linux",
|
||||
args: upArgsT{
|
||||
netfilterMode: "nodivert",
|
||||
},
|
||||
wantWarn: "netfilter=nodivert; add iptables calls to ts-* chains manually.",
|
||||
want: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
NetfilterMode: preftype.NetfilterNoDivert,
|
||||
NoSNAT: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "warn_linux_netfilter_off",
|
||||
goos: "linux",
|
||||
args: upArgsT{
|
||||
netfilterMode: "off",
|
||||
},
|
||||
wantWarn: "netfilter=off; configure iptables yourself.",
|
||||
want: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
NetfilterMode: preftype.NetfilterOff,
|
||||
NoSNAT: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var warnBuf bytes.Buffer
|
||||
warnf := func(format string, a ...interface{}) {
|
||||
fmt.Fprintf(&warnBuf, format, a...)
|
||||
}
|
||||
goos := tt.goos
|
||||
if goos == "" {
|
||||
goos = "linux"
|
||||
}
|
||||
st := tt.st
|
||||
if st == nil {
|
||||
st = new(ipnstate.Status)
|
||||
}
|
||||
got, err := prefsFromUpArgs(tt.args, warnf, st, goos)
|
||||
gotErr := fmt.Sprint(err)
|
||||
if tt.wantErr != "" {
|
||||
if tt.wantErr != gotErr {
|
||||
t.Errorf("wrong error.\n got error: %v\nwant error: %v\n", gotErr, tt.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tt.want == nil {
|
||||
t.Fatal("tt.want is nil")
|
||||
}
|
||||
if !got.Equals(tt.want) {
|
||||
jgot, _ := json.MarshalIndent(got, "", "\t")
|
||||
jwant, _ := json.MarshalIndent(tt.want, "", "\t")
|
||||
if bytes.Equal(jgot, jwant) {
|
||||
t.Logf("prefs differ only in non-JSON-visible ways (nil/non-nil zero-length arrays)")
|
||||
}
|
||||
t.Errorf("wrong prefs\n got: %s\nwant: %s\n\ngot: %s\nwant: %s\n",
|
||||
got.Pretty(), tt.want.Pretty(),
|
||||
jgot, jwant,
|
||||
)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPrefFlagMapping(t *testing.T) {
|
||||
prefType := reflect.TypeOf(ipn.Prefs{})
|
||||
for i := 0; i < prefType.NumField(); i++ {
|
||||
prefName := prefType.Field(i).Name
|
||||
if _, ok := flagForPref[prefName]; ok {
|
||||
continue
|
||||
}
|
||||
switch prefName {
|
||||
case "WantRunning", "Persist", "LoggedOut":
|
||||
// All explicitly handled (ignored) by checkForAccidentalSettingReverts.
|
||||
continue
|
||||
case "OSVersion", "DeviceModel":
|
||||
// Only used by Android, which doesn't have a CLI mode anyway, so
|
||||
// fine to not map.
|
||||
continue
|
||||
case "NotepadURLs":
|
||||
// TODO(bradfitz): https://github.com/tailscale/tailscale/issues/1830
|
||||
continue
|
||||
}
|
||||
t.Errorf("unexpected new ipn.Pref field %q is not handled by up.go (see addPrefFlagMapping and checkForAccidentalSettingReverts)", prefName)
|
||||
}
|
||||
}
|
||||
668
cmd/tailscaled/cli/cli_test.go
Normal file
668
cmd/tailscaled/cli/cli_test.go
Normal file
@@ -0,0 +1,668 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/types/preftype"
|
||||
)
|
||||
|
||||
// geese is a collection of gooses. It need not be complete.
|
||||
// But it should include anything handled specially (e.g. linux, windows)
|
||||
// and at least one thing that's not (darwin, freebsd).
|
||||
var geese = []string{"linux", "darwin", "windows", "freebsd"}
|
||||
|
||||
// Test that checkForAccidentalSettingReverts's updateMaskedPrefsFromUpFlag can handle
|
||||
// all flags. This will panic if a new flag creeps in that's unhandled.
|
||||
//
|
||||
// Also, issue 1880: advertise-exit-node was being ignored. Verify that all flags cause an edit.
|
||||
func TestUpdateMaskedPrefsFromUpFlag(t *testing.T) {
|
||||
for _, goos := range geese {
|
||||
var upArgs upArgsT
|
||||
fs := newUpFlagSet(goos, &upArgs)
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
mp := new(ipn.MaskedPrefs)
|
||||
updateMaskedPrefsFromUpFlag(mp, f.Name)
|
||||
got := mp.Pretty()
|
||||
wantEmpty := preflessFlag(f.Name)
|
||||
isEmpty := got == "MaskedPrefs{}"
|
||||
if isEmpty != wantEmpty {
|
||||
t.Errorf("flag %q created MaskedPrefs %s; want empty=%v", f.Name, got, wantEmpty)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
flags []string // argv to be parsed by FlagSet
|
||||
curPrefs *ipn.Prefs
|
||||
|
||||
curExitNodeIP netaddr.IP
|
||||
curUser string // os.Getenv("USER") on the client side
|
||||
goos string // empty means "linux"
|
||||
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "bare_up_means_up",
|
||||
flags: []string{},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
Hostname: "foo",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "losing_hostname",
|
||||
flags: []string{"--accept-dns"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
Hostname: "foo",
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AllowSingleHosts: true,
|
||||
},
|
||||
want: accidentalUpPrefix + " --accept-dns --hostname=foo",
|
||||
},
|
||||
{
|
||||
name: "hostname_changing_explicitly",
|
||||
flags: []string{"--hostname=bar"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AllowSingleHosts: true,
|
||||
Hostname: "foo",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "hostname_changing_empty_explicitly",
|
||||
flags: []string{"--hostname="},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AllowSingleHosts: true,
|
||||
Hostname: "foo",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
// Issue 1725: "tailscale up --authkey=..." (or other non-empty flags) works from
|
||||
// a fresh server's initial prefs.
|
||||
name: "up_with_default_prefs",
|
||||
flags: []string{"--authkey=foosdlkfjskdljf"},
|
||||
curPrefs: ipn.NewPrefs(),
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "implicit_operator_change",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
OperatorUser: "alice",
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
curUser: "eve",
|
||||
want: accidentalUpPrefix + " --hostname=foo --operator=alice",
|
||||
},
|
||||
{
|
||||
name: "implicit_operator_matches_shell_user",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
OperatorUser: "alice",
|
||||
},
|
||||
curUser: "alice",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "error_advertised_routes_exit_node_removed",
|
||||
flags: []string{"--advertise-routes=10.0.42.0/24"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.42.0/24"),
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
},
|
||||
},
|
||||
want: accidentalUpPrefix + " --advertise-routes=10.0.42.0/24 --advertise-exit-node",
|
||||
},
|
||||
{
|
||||
name: "advertised_routes_exit_node_removed_explicit",
|
||||
flags: []string{"--advertise-routes=10.0.42.0/24", "--advertise-exit-node=false"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.42.0/24"),
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
},
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "advertised_routes_includes_the_0_routes", // but no --advertise-exit-node
|
||||
flags: []string{"--advertise-routes=11.1.43.0/24,0.0.0.0/0,::/0"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.42.0/24"),
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
},
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "advertise_exit_node", // Issue 1859
|
||||
flags: []string{"--advertise-exit-node"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "advertise_exit_node_over_existing_routes",
|
||||
flags: []string{"--advertise-exit-node"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("1.2.0.0/16"),
|
||||
},
|
||||
},
|
||||
want: accidentalUpPrefix + " --advertise-exit-node --advertise-routes=1.2.0.0/16",
|
||||
},
|
||||
{
|
||||
name: "advertise_exit_node_over_existing_routes_and_exit_node",
|
||||
flags: []string{"--advertise-exit-node"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
netaddr.MustParseIPPrefix("1.2.0.0/16"),
|
||||
},
|
||||
},
|
||||
want: accidentalUpPrefix + " --advertise-exit-node --advertise-routes=1.2.0.0/16",
|
||||
},
|
||||
{
|
||||
name: "exit_node_clearing", // Issue 1777
|
||||
flags: []string{"--exit-node="},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
|
||||
ExitNodeID: "fooID",
|
||||
},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "remove_all_implicit",
|
||||
flags: []string{"--force-reauth"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
RouteAll: true,
|
||||
AllowSingleHosts: false,
|
||||
ExitNodeIP: netaddr.MustParseIP("100.64.5.6"),
|
||||
CorpDNS: false,
|
||||
ShieldsUp: true,
|
||||
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
||||
Hostname: "myhostname",
|
||||
ForceDaemon: true,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.0.0/16"),
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
},
|
||||
NetfilterMode: preftype.NetfilterNoDivert,
|
||||
OperatorUser: "alice",
|
||||
},
|
||||
curUser: "eve",
|
||||
want: accidentalUpPrefix + " --force-reauth --accept-dns=false --accept-routes --advertise-exit-node --advertise-routes=10.0.0.0/16 --advertise-tags=tag:foo,tag:bar --exit-node=100.64.5.6 --host-routes=false --hostname=myhostname --netfilter-mode=nodivert --operator=alice --shields-up",
|
||||
},
|
||||
{
|
||||
name: "remove_all_implicit_except_hostname",
|
||||
flags: []string{"--hostname=newhostname"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
RouteAll: true,
|
||||
AllowSingleHosts: false,
|
||||
ExitNodeIP: netaddr.MustParseIP("100.64.5.6"),
|
||||
CorpDNS: false,
|
||||
ShieldsUp: true,
|
||||
AdvertiseTags: []string{"tag:foo", "tag:bar"},
|
||||
Hostname: "myhostname",
|
||||
ForceDaemon: true,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.0.0/16"),
|
||||
},
|
||||
NetfilterMode: preftype.NetfilterNoDivert,
|
||||
OperatorUser: "alice",
|
||||
},
|
||||
curUser: "eve",
|
||||
want: accidentalUpPrefix + " --hostname=newhostname --accept-dns=false --accept-routes --advertise-routes=10.0.0.0/16 --advertise-tags=tag:foo,tag:bar --exit-node=100.64.5.6 --host-routes=false --netfilter-mode=nodivert --operator=alice --shields-up",
|
||||
},
|
||||
{
|
||||
name: "loggedout_is_implicit",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
LoggedOut: true,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
want: "", // not an error. LoggedOut is implicit.
|
||||
},
|
||||
{
|
||||
// Test that a pre-1.8 version of Tailscale with bogus NoSNAT pref
|
||||
// values is able to enable exit nodes without warnings.
|
||||
name: "make_windows_exit_node",
|
||||
flags: []string{"--advertise-exit-node"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
|
||||
// And assume this no-op accidental pre-1.8 value:
|
||||
NoSNAT: true,
|
||||
},
|
||||
goos: "windows",
|
||||
want: "", // not an error
|
||||
},
|
||||
{
|
||||
name: "ignore_netfilter_change_non_linux",
|
||||
flags: []string{"--accept-dns"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
|
||||
NetfilterMode: preftype.NetfilterNoDivert, // we never had this bug, but pretend it got set non-zero on Windows somehow
|
||||
},
|
||||
goos: "windows",
|
||||
want: "", // not an error
|
||||
},
|
||||
{
|
||||
name: "operator_losing_routes_step1", // https://twitter.com/EXPbits/status/1390418145047887877
|
||||
flags: []string{"--operator=expbits"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
netaddr.MustParseIPPrefix("1.2.0.0/16"),
|
||||
},
|
||||
},
|
||||
want: accidentalUpPrefix + " --operator=expbits --advertise-exit-node --advertise-routes=1.2.0.0/16",
|
||||
},
|
||||
{
|
||||
name: "operator_losing_routes_step2", // https://twitter.com/EXPbits/status/1390418145047887877
|
||||
flags: []string{"--operator=expbits", "--advertise-routes=1.2.0.0/16"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
netaddr.MustParseIPPrefix("1.2.0.0/16"),
|
||||
},
|
||||
},
|
||||
want: accidentalUpPrefix + " --advertise-routes=1.2.0.0/16 --operator=expbits --advertise-exit-node",
|
||||
},
|
||||
{
|
||||
name: "errors_preserve_explicit_flags",
|
||||
flags: []string{"--reset", "--force-reauth=false", "--authkey=secretrand"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: false,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
AllowSingleHosts: true,
|
||||
|
||||
Hostname: "foo",
|
||||
},
|
||||
want: accidentalUpPrefix + " --authkey=secretrand --force-reauth=false --reset --hostname=foo",
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_omit_with_ip_pref",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
|
||||
ExitNodeIP: netaddr.MustParseIP("100.64.5.4"),
|
||||
},
|
||||
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.4",
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_omit_with_id_pref",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curExitNodeIP: netaddr.MustParseIP("100.64.5.7"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
|
||||
ExitNodeID: "some_stable_id",
|
||||
},
|
||||
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.7",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
goos := "linux"
|
||||
if tt.goos != "" {
|
||||
goos = tt.goos
|
||||
}
|
||||
var upArgs upArgsT
|
||||
flagSet := newUpFlagSet(goos, &upArgs)
|
||||
flagSet.Parse(tt.flags)
|
||||
newPrefs, err := prefsFromUpArgs(upArgs, t.Logf, new(ipnstate.Status), goos)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
applyImplicitPrefs(newPrefs, tt.curPrefs, tt.curUser)
|
||||
var got string
|
||||
if err := checkForAccidentalSettingReverts(flagSet, tt.curPrefs, newPrefs, upCheckEnv{
|
||||
goos: goos,
|
||||
curExitNodeIP: tt.curExitNodeIP,
|
||||
}); err != nil {
|
||||
got = err.Error()
|
||||
}
|
||||
if strings.TrimSpace(got) != tt.want {
|
||||
t.Errorf("unexpected result\n got: %s\nwant: %s\n", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func upArgsFromOSArgs(goos string, flagArgs ...string) (args upArgsT) {
|
||||
fs := newUpFlagSet(goos, &args)
|
||||
fs.Parse(flagArgs) // populates args
|
||||
return
|
||||
}
|
||||
|
||||
func TestPrefsFromUpArgs(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args upArgsT
|
||||
goos string // runtime.GOOS; empty means linux
|
||||
st *ipnstate.Status // or nil
|
||||
want *ipn.Prefs
|
||||
wantErr string
|
||||
wantWarn string
|
||||
}{
|
||||
{
|
||||
name: "default_linux",
|
||||
goos: "linux",
|
||||
args: upArgsFromOSArgs("linux"),
|
||||
want: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
NoSNAT: false,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "default_windows",
|
||||
goos: "windows",
|
||||
args: upArgsFromOSArgs("windows"),
|
||||
want: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "advertise_default_route",
|
||||
args: upArgsFromOSArgs("linux", "--advertise-exit-node"),
|
||||
want: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
WantRunning: true,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
AdvertiseRoutes: []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("0.0.0.0/0"),
|
||||
netaddr.MustParseIPPrefix("::/0"),
|
||||
},
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "error_advertise_route_invalid_ip",
|
||||
args: upArgsT{
|
||||
advertiseRoutes: "foo",
|
||||
},
|
||||
wantErr: `"foo" is not a valid IP address or CIDR prefix`,
|
||||
},
|
||||
{
|
||||
name: "error_advertise_route_unmasked_bits",
|
||||
args: upArgsT{
|
||||
advertiseRoutes: "1.2.3.4/16",
|
||||
},
|
||||
wantErr: `1.2.3.4/16 has non-address bits set; expected 1.2.0.0/16`,
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_bad_ip",
|
||||
args: upArgsT{
|
||||
exitNodeIP: "foo",
|
||||
},
|
||||
wantErr: `invalid IP address "foo" for --exit-node: ParseIP("foo"): unable to parse IP`,
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_allow_lan_without_exit_node",
|
||||
args: upArgsT{
|
||||
exitNodeAllowLANAccess: true,
|
||||
},
|
||||
wantErr: `--exit-node-allow-lan-access can only be used with --exit-node`,
|
||||
},
|
||||
{
|
||||
name: "error_tag_prefix",
|
||||
args: upArgsT{
|
||||
advertiseTags: "foo",
|
||||
},
|
||||
wantErr: `tag: "foo": tags must start with 'tag:'`,
|
||||
},
|
||||
{
|
||||
name: "error_long_hostname",
|
||||
args: upArgsT{
|
||||
hostname: strings.Repeat("a", 300),
|
||||
},
|
||||
wantErr: `hostname too long: 300 bytes (max 256)`,
|
||||
},
|
||||
{
|
||||
name: "error_linux_netfilter_empty",
|
||||
args: upArgsT{
|
||||
netfilterMode: "",
|
||||
},
|
||||
wantErr: `invalid value --netfilter-mode=""`,
|
||||
},
|
||||
{
|
||||
name: "error_linux_netfilter_bogus",
|
||||
args: upArgsT{
|
||||
netfilterMode: "bogus",
|
||||
},
|
||||
wantErr: `invalid value --netfilter-mode="bogus"`,
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_ip_is_self_ip",
|
||||
args: upArgsT{
|
||||
exitNodeIP: "100.105.106.107",
|
||||
},
|
||||
st: &ipnstate.Status{
|
||||
TailscaleIPs: []netaddr.IP{netaddr.MustParseIP("100.105.106.107")},
|
||||
},
|
||||
wantErr: `cannot use 100.105.106.107 as the exit node as it is a local IP address to this machine, did you mean --advertise-exit-node?`,
|
||||
},
|
||||
{
|
||||
name: "warn_linux_netfilter_nodivert",
|
||||
goos: "linux",
|
||||
args: upArgsT{
|
||||
netfilterMode: "nodivert",
|
||||
},
|
||||
wantWarn: "netfilter=nodivert; add iptables calls to ts-* chains manually.",
|
||||
want: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
NetfilterMode: preftype.NetfilterNoDivert,
|
||||
NoSNAT: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "warn_linux_netfilter_off",
|
||||
goos: "linux",
|
||||
args: upArgsT{
|
||||
netfilterMode: "off",
|
||||
},
|
||||
wantWarn: "netfilter=off; configure iptables yourself.",
|
||||
want: &ipn.Prefs{
|
||||
WantRunning: true,
|
||||
NetfilterMode: preftype.NetfilterOff,
|
||||
NoSNAT: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var warnBuf bytes.Buffer
|
||||
warnf := func(format string, a ...interface{}) {
|
||||
fmt.Fprintf(&warnBuf, format, a...)
|
||||
}
|
||||
goos := tt.goos
|
||||
if goos == "" {
|
||||
goos = "linux"
|
||||
}
|
||||
st := tt.st
|
||||
if st == nil {
|
||||
st = new(ipnstate.Status)
|
||||
}
|
||||
got, err := prefsFromUpArgs(tt.args, warnf, st, goos)
|
||||
gotErr := fmt.Sprint(err)
|
||||
if tt.wantErr != "" {
|
||||
if tt.wantErr != gotErr {
|
||||
t.Errorf("wrong error.\n got error: %v\nwant error: %v\n", gotErr, tt.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tt.want == nil {
|
||||
t.Fatal("tt.want is nil")
|
||||
}
|
||||
if !got.Equals(tt.want) {
|
||||
jgot, _ := json.MarshalIndent(got, "", "\t")
|
||||
jwant, _ := json.MarshalIndent(tt.want, "", "\t")
|
||||
if bytes.Equal(jgot, jwant) {
|
||||
t.Logf("prefs differ only in non-JSON-visible ways (nil/non-nil zero-length arrays)")
|
||||
}
|
||||
t.Errorf("wrong prefs\n got: %s\nwant: %s\n\ngot: %s\nwant: %s\n",
|
||||
got.Pretty(), tt.want.Pretty(),
|
||||
jgot, jwant,
|
||||
)
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPrefFlagMapping(t *testing.T) {
|
||||
prefHasFlag := map[string]bool{}
|
||||
for _, pv := range prefsOfFlag {
|
||||
for _, pref := range pv {
|
||||
prefHasFlag[pref] = true
|
||||
}
|
||||
}
|
||||
|
||||
prefType := reflect.TypeOf(ipn.Prefs{})
|
||||
for i := 0; i < prefType.NumField(); i++ {
|
||||
prefName := prefType.Field(i).Name
|
||||
if prefHasFlag[prefName] {
|
||||
continue
|
||||
}
|
||||
switch prefName {
|
||||
case "WantRunning", "Persist", "LoggedOut":
|
||||
// All explicitly handled (ignored) by checkForAccidentalSettingReverts.
|
||||
continue
|
||||
case "OSVersion", "DeviceModel":
|
||||
// Only used by Android, which doesn't have a CLI mode anyway, so
|
||||
// fine to not map.
|
||||
continue
|
||||
case "NotepadURLs":
|
||||
// TODO(bradfitz): https://github.com/tailscale/tailscale/issues/1830
|
||||
continue
|
||||
}
|
||||
t.Errorf("unexpected new ipn.Pref field %q is not handled by up.go (see addPrefFlagMapping and checkForAccidentalSettingReverts)", prefName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlagAppliesToOS(t *testing.T) {
|
||||
for _, goos := range geese {
|
||||
var upArgs upArgsT
|
||||
fs := newUpFlagSet(goos, &upArgs)
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
if !flagAppliesToOS(f.Name, goos) {
|
||||
t.Errorf("flagAppliesToOS(%q, %q) = false but found in %s set", f.Name, goos, goos)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -194,7 +194,7 @@ func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, lastSe
|
||||
for _, ft := range fts {
|
||||
n := ft.Node
|
||||
for _, a := range n.Addresses {
|
||||
if a.IP != ip {
|
||||
if a.IP() != ip {
|
||||
continue
|
||||
}
|
||||
if n.LastSeen != nil {
|
||||
@@ -301,7 +301,7 @@ func runCpTargets(ctx context.Context, args []string) error {
|
||||
if detail != "" {
|
||||
detail = "\t" + detail
|
||||
}
|
||||
fmt.Printf("%s\t%s%s\n", n.Addresses[0].IP, n.ComputedName, detail)
|
||||
fmt.Printf("%s\t%s%s\n", n.Addresses[0].IP(), n.ComputedName, detail)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -139,6 +139,9 @@ func runPing(ctx context.Context, args []string) error {
|
||||
if !anyPong {
|
||||
return errors.New("no reply")
|
||||
}
|
||||
if pingArgs.untilDirect {
|
||||
return errors.New("direct connection not established")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -165,10 +164,10 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
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
|
||||
if routes[i].Bits() != routes[j].Bits() {
|
||||
return routes[i].Bits() < routes[j].Bits()
|
||||
}
|
||||
return routes[i].IP.Less(routes[j].IP)
|
||||
return routes[i].IP().Less(routes[j].IP())
|
||||
})
|
||||
|
||||
var exitNodeIP netaddr.IP
|
||||
@@ -295,17 +294,13 @@ func runUp(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
flagSet := map[string]bool{}
|
||||
mp := new(ipn.MaskedPrefs)
|
||||
mp.WantRunningSet = true
|
||||
mp.Prefs = *prefs
|
||||
upFlagSet.Visit(func(f *flag.Flag) {
|
||||
updateMaskedPrefsFromUpFlag(mp, f.Name)
|
||||
flagSet[f.Name] = true
|
||||
})
|
||||
|
||||
if !upArgs.reset {
|
||||
if err := checkForAccidentalSettingReverts(flagSet, curPrefs, mp, runtime.GOOS, os.Getenv("USER")); err != nil {
|
||||
applyImplicitPrefs(prefs, curPrefs, os.Getenv("USER"))
|
||||
|
||||
if err := checkForAccidentalSettingReverts(upFlagSet, curPrefs, prefs, upCheckEnv{
|
||||
goos: runtime.GOOS,
|
||||
curExitNodeIP: exitNodeIP(prefs, st),
|
||||
}); err != nil {
|
||||
fatalf("%s", err)
|
||||
}
|
||||
}
|
||||
@@ -317,7 +312,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
|
||||
// If we're already running and none of the flags require a
|
||||
// restart, we can just do an EditPrefs call and change the
|
||||
// prefs at runtime (e.g. changing hostname, changinged
|
||||
// prefs at runtime (e.g. changing hostname, changing
|
||||
// advertised tags, routes, etc)
|
||||
justEdit := st.BackendState == ipn.Running.String() &&
|
||||
!upArgs.forceReauth &&
|
||||
@@ -325,6 +320,13 @@ func runUp(ctx context.Context, args []string) error {
|
||||
upArgs.authKey == "" &&
|
||||
!controlURLChanged
|
||||
if justEdit {
|
||||
mp := new(ipn.MaskedPrefs)
|
||||
mp.WantRunningSet = true
|
||||
mp.Prefs = *prefs
|
||||
upFlagSet.Visit(func(f *flag.Flag) {
|
||||
updateMaskedPrefsFromUpFlag(mp, f.Name)
|
||||
})
|
||||
|
||||
_, err := tailscale.EditPrefs(ctx, mp)
|
||||
return err
|
||||
}
|
||||
@@ -332,7 +334,7 @@ func runUp(ctx context.Context, args []string) error {
|
||||
// simpleUp is whether we're running a simple "tailscale up"
|
||||
// to transition to running from a previously-logged-in but
|
||||
// down state, without changing any settings.
|
||||
simpleUp := len(flagSet) == 0 &&
|
||||
simpleUp := upFlagSet.NFlag() == 0 &&
|
||||
curPrefs.Persist != nil &&
|
||||
curPrefs.Persist.LoginName != "" &&
|
||||
st.BackendState != ipn.NeedsLogin.String()
|
||||
@@ -464,14 +466,20 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
var (
|
||||
flagForPref = map[string]string{} // "ExitNodeIP" => "exit-node"
|
||||
prefsOfFlag = map[string][]string{}
|
||||
prefsOfFlag = map[string][]string{} // "exit-node" => ExitNodeIP, ExitNodeID
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Both these have the same ipn.Pref:
|
||||
addPrefFlagMapping("advertise-exit-node", "AdvertiseRoutes")
|
||||
addPrefFlagMapping("advertise-routes", "AdvertiseRoutes")
|
||||
|
||||
// And this flag has two ipn.Prefs:
|
||||
addPrefFlagMapping("exit-node", "ExitNodeIP", "ExitNodeID")
|
||||
|
||||
// The rest are 1:1:
|
||||
addPrefFlagMapping("accept-dns", "CorpDNS")
|
||||
addPrefFlagMapping("accept-routes", "RouteAll")
|
||||
addPrefFlagMapping("advertise-routes", "AdvertiseRoutes")
|
||||
addPrefFlagMapping("advertise-tags", "AdvertiseTags")
|
||||
addPrefFlagMapping("host-routes", "AllowSingleHosts")
|
||||
addPrefFlagMapping("hostname", "Hostname")
|
||||
@@ -479,7 +487,6 @@ func init() {
|
||||
addPrefFlagMapping("netfilter-mode", "NetfilterMode")
|
||||
addPrefFlagMapping("shields-up", "ShieldsUp")
|
||||
addPrefFlagMapping("snat-subnet-routes", "NoSNAT")
|
||||
addPrefFlagMapping("exit-node", "ExitNodeIP", "ExitNodeID")
|
||||
addPrefFlagMapping("exit-node-allow-lan-access", "ExitNodeAllowLANAccess")
|
||||
addPrefFlagMapping("unattended", "ForceDaemon")
|
||||
addPrefFlagMapping("operator", "OperatorUser")
|
||||
@@ -489,8 +496,6 @@ func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||
prefsOfFlag[flagName] = prefNames
|
||||
prefType := reflect.TypeOf(ipn.Prefs{})
|
||||
for _, pref := range prefNames {
|
||||
flagForPref[pref] = flagName
|
||||
|
||||
// Crash at runtime if there's a typo in the prefName.
|
||||
if _, ok := prefType.FieldByName(pref); !ok {
|
||||
panic(fmt.Sprintf("invalid ipn.Prefs field %q", pref))
|
||||
@@ -498,21 +503,27 @@ func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||
}
|
||||
}
|
||||
|
||||
// preflessFlag reports whether flagName is a flag that doesn't
|
||||
// correspond to an ipn.Pref.
|
||||
func preflessFlag(flagName string) bool {
|
||||
switch flagName {
|
||||
case "authkey", "force-reauth", "reset":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func updateMaskedPrefsFromUpFlag(mp *ipn.MaskedPrefs, flagName string) {
|
||||
if preflessFlag(flagName) {
|
||||
return
|
||||
}
|
||||
if prefs, ok := prefsOfFlag[flagName]; ok {
|
||||
for _, pref := range prefs {
|
||||
reflect.ValueOf(mp).Elem().FieldByName(pref + "Set").SetBool(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
switch flagName {
|
||||
case "authkey", "force-reauth", "reset":
|
||||
// Not pref-related flags.
|
||||
case "advertise-exit-node":
|
||||
// This pref is a shorthand for advertise-routes.
|
||||
default:
|
||||
panic(fmt.Sprintf("internal error: unhandled flag %q", flagName))
|
||||
}
|
||||
panic(fmt.Sprintf("internal error: unhandled flag %q", flagName))
|
||||
}
|
||||
|
||||
const accidentalUpPrefix = "Error: changing settings via 'tailscale up' requires mentioning all\n" +
|
||||
@@ -521,9 +532,16 @@ const accidentalUpPrefix = "Error: changing settings via 'tailscale up' requires
|
||||
"all non-default settings:\n\n" +
|
||||
"\ttailscale up"
|
||||
|
||||
// checkForAccidentalSettingReverts checks for people running
|
||||
// "tailscale up" with a subset of the flags they originally ran it
|
||||
// with.
|
||||
// upCheckEnv are extra parameters describing the environment as
|
||||
// needed by checkForAccidentalSettingReverts and friends.
|
||||
type upCheckEnv struct {
|
||||
goos string
|
||||
curExitNodeIP netaddr.IP
|
||||
}
|
||||
|
||||
// checkForAccidentalSettingReverts (the "up checker") checks for
|
||||
// people running "tailscale up" with a subset of the flags they
|
||||
// originally ran it with.
|
||||
//
|
||||
// For example, in Tailscale 1.6 and prior, a user might've advertised
|
||||
// a tag, but later tried to change just one other setting and forgot
|
||||
@@ -535,166 +553,223 @@ const accidentalUpPrefix = "Error: changing settings via 'tailscale up' requires
|
||||
//
|
||||
// mp is the mask of settings actually set, where mp.Prefs is the new
|
||||
// preferences to set, including any values set from implicit flags.
|
||||
func checkForAccidentalSettingReverts(flagSet map[string]bool, curPrefs *ipn.Prefs, mp *ipn.MaskedPrefs, goos, curUser string) error {
|
||||
if len(flagSet) == 0 {
|
||||
// A bare "tailscale up" is a special case to just
|
||||
// mean bringing the network up without any changes.
|
||||
return nil
|
||||
}
|
||||
func checkForAccidentalSettingReverts(flagSet *flag.FlagSet, curPrefs, newPrefs *ipn.Prefs, env upCheckEnv) error {
|
||||
if curPrefs.ControlURL == "" {
|
||||
// Don't validate things on initial "up" before a control URL has been set.
|
||||
return nil
|
||||
}
|
||||
curWithExplicitEdits := curPrefs.Clone()
|
||||
curWithExplicitEdits.ApplyEdits(mp)
|
||||
|
||||
prefType := reflect.TypeOf(ipn.Prefs{})
|
||||
flagIsSet := map[string]bool{}
|
||||
flagSet.Visit(func(f *flag.Flag) {
|
||||
flagIsSet[f.Name] = true
|
||||
})
|
||||
|
||||
// Explicit values (current + explicit edit):
|
||||
ev := reflect.ValueOf(curWithExplicitEdits).Elem()
|
||||
// Implicit values (what we'd get if we replaced everything with flag defaults):
|
||||
iv := reflect.ValueOf(&mp.Prefs).Elem()
|
||||
if len(flagIsSet) == 0 {
|
||||
// A bare "tailscale up" is a special case to just
|
||||
// mean bringing the network up without any changes.
|
||||
return nil
|
||||
}
|
||||
|
||||
// flagsCur is what flags we'd need to use to keep the exact
|
||||
// settings as-is.
|
||||
flagsCur := prefsToFlags(env, curPrefs)
|
||||
flagsNew := prefsToFlags(env, newPrefs)
|
||||
|
||||
var missing []string
|
||||
flagExplicitValue := map[string]interface{}{} // e.g. "accept-dns" => true (from flagSet)
|
||||
for i := 0; i < prefType.NumField(); i++ {
|
||||
prefName := prefType.Field(i).Name
|
||||
// Persist is a legacy field used for storing keys, which
|
||||
// probably should never have been part of Prefs. It's
|
||||
// likely to migrate elsewhere eventually.
|
||||
if prefName == "Persist" {
|
||||
for flagName := range flagsCur {
|
||||
valCur, valNew := flagsCur[flagName], flagsNew[flagName]
|
||||
if flagIsSet[flagName] {
|
||||
continue
|
||||
}
|
||||
// LoggedOut is a preference, but running the "up" command
|
||||
// always implies that the user now prefers LoggedOut->false.
|
||||
if prefName == "LoggedOut" {
|
||||
if reflect.DeepEqual(valCur, valNew) {
|
||||
continue
|
||||
}
|
||||
flagName, hasFlag := flagForPref[prefName]
|
||||
|
||||
// Special case for advertise-exit-node; which is a
|
||||
// flag but doesn't have a corresponding pref. The
|
||||
// flag augments advertise-routes, so we have to infer
|
||||
// the imaginary pref's current value from the routes.
|
||||
if prefName == "AdvertiseRoutes" &&
|
||||
hasExitNodeRoutes(curPrefs.AdvertiseRoutes) &&
|
||||
!hasExitNodeRoutes(curWithExplicitEdits.AdvertiseRoutes) &&
|
||||
!flagSet["advertise-exit-node"] {
|
||||
missing = append(missing, "--advertise-exit-node")
|
||||
}
|
||||
|
||||
if hasFlag && flagSet[flagName] {
|
||||
flagExplicitValue[flagName] = ev.Field(i).Interface()
|
||||
continue
|
||||
}
|
||||
// Get explicit value and implicit value
|
||||
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 {
|
||||
case "operator":
|
||||
if imi == "" && exi == curUser {
|
||||
// Don't require setting operator if the current user matches
|
||||
// the configured operator.
|
||||
continue
|
||||
}
|
||||
case "snat-subnet-routes", "netfilter-mode":
|
||||
if goos != "linux" {
|
||||
// Issue 1833: we used to accidentally set the NoSNAT
|
||||
// pref for non-Linux nodes. It only affects Linux, so
|
||||
// ignore it if it changes. Likewise, ignore
|
||||
// Linux-only netfilter-mode on non-Linux.
|
||||
continue
|
||||
}
|
||||
}
|
||||
switch flagName {
|
||||
case "":
|
||||
return fmt.Errorf("'tailscale up' without --reset requires all preferences with changing values to be explicitly mentioned; this command would change the value of flagless pref %q", prefName)
|
||||
case "exit-node":
|
||||
if prefName == "ExitNodeIP" {
|
||||
missing = append(missing, fmtFlagValueArg("exit-node", fmtSettingVal(exi)))
|
||||
}
|
||||
default:
|
||||
missing = append(missing, fmtFlagValueArg(flagName, fmtSettingVal(exi)))
|
||||
}
|
||||
missing = append(missing, fmtFlagValueArg(flagName, valCur))
|
||||
}
|
||||
if len(missing) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Strings(missing)
|
||||
|
||||
// Compute the stringification of the explicitly provided args in flagSet
|
||||
// to prepend to the command to run.
|
||||
var explicit []string
|
||||
flagSet.Visit(func(f *flag.Flag) {
|
||||
type isBool interface {
|
||||
IsBoolFlag() bool
|
||||
}
|
||||
if ib, ok := f.Value.(isBool); ok && ib.IsBoolFlag() {
|
||||
if f.Value.String() == "false" {
|
||||
explicit = append(explicit, "--"+f.Name+"=false")
|
||||
} else {
|
||||
explicit = append(explicit, "--"+f.Name)
|
||||
}
|
||||
} else {
|
||||
explicit = append(explicit, fmtFlagValueArg(f.Name, f.Value.String()))
|
||||
}
|
||||
})
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString(accidentalUpPrefix)
|
||||
|
||||
var flagSetSorted []string
|
||||
for f := range flagSet {
|
||||
flagSetSorted = append(flagSetSorted, f)
|
||||
}
|
||||
sort.Strings(flagSetSorted)
|
||||
for _, flagName := range flagSetSorted {
|
||||
if ev, ok := flagExplicitValue[flagName]; ok {
|
||||
fmt.Fprintf(&sb, " %s", fmtFlagValueArg(flagName, fmtSettingVal(ev)))
|
||||
}
|
||||
}
|
||||
for _, a := range missing {
|
||||
for _, a := range append(explicit, missing...) {
|
||||
fmt.Fprintf(&sb, " %s", a)
|
||||
}
|
||||
sb.WriteString("\n\n")
|
||||
return errors.New(sb.String())
|
||||
}
|
||||
|
||||
func fmtFlagValueArg(flagName, val string) string {
|
||||
if val == "true" {
|
||||
// TODO: check flagName's type to see if its Pref is of type bool
|
||||
// applyImplicitPrefs mutates prefs to add implicit preferences. Currently
|
||||
// this is just the operator user, which only needs to be set if it doesn't
|
||||
// match the current user.
|
||||
//
|
||||
// curUser is os.Getenv("USER"). It's pulled out for testability.
|
||||
func applyImplicitPrefs(prefs, oldPrefs *ipn.Prefs, curUser string) {
|
||||
if prefs.OperatorUser == "" && oldPrefs.OperatorUser == curUser {
|
||||
prefs.OperatorUser = oldPrefs.OperatorUser
|
||||
}
|
||||
}
|
||||
|
||||
func flagAppliesToOS(flag, goos string) bool {
|
||||
switch flag {
|
||||
case "netfilter-mode", "snat-subnet-routes":
|
||||
return goos == "linux"
|
||||
case "unattended":
|
||||
return goos == "windows"
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func prefsToFlags(env upCheckEnv, prefs *ipn.Prefs) (flagVal map[string]interface{}) {
|
||||
ret := make(map[string]interface{})
|
||||
|
||||
exitNodeIPStr := func() string {
|
||||
if !prefs.ExitNodeIP.IsZero() {
|
||||
return prefs.ExitNodeIP.String()
|
||||
}
|
||||
if prefs.ExitNodeID.IsZero() || env.curExitNodeIP.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return env.curExitNodeIP.String()
|
||||
}
|
||||
|
||||
fs := newUpFlagSet(env.goos, new(upArgsT) /* dummy */)
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
if preflessFlag(f.Name) {
|
||||
return
|
||||
}
|
||||
set := func(v interface{}) {
|
||||
if flagAppliesToOS(f.Name, env.goos) {
|
||||
ret[f.Name] = v
|
||||
} else {
|
||||
ret[f.Name] = nil
|
||||
}
|
||||
}
|
||||
switch f.Name {
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled flag %q", f.Name))
|
||||
case "login-server":
|
||||
set(prefs.ControlURL)
|
||||
case "accept-routes":
|
||||
set(prefs.RouteAll)
|
||||
case "host-routes":
|
||||
set(prefs.AllowSingleHosts)
|
||||
case "accept-dns":
|
||||
set(prefs.CorpDNS)
|
||||
case "shields-up":
|
||||
set(prefs.ShieldsUp)
|
||||
case "exit-node":
|
||||
set(exitNodeIPStr())
|
||||
case "exit-node-allow-lan-access":
|
||||
set(prefs.ExitNodeAllowLANAccess)
|
||||
case "advertise-tags":
|
||||
set(strings.Join(prefs.AdvertiseTags, ","))
|
||||
case "hostname":
|
||||
set(prefs.Hostname)
|
||||
case "operator":
|
||||
set(prefs.OperatorUser)
|
||||
case "advertise-routes":
|
||||
var sb strings.Builder
|
||||
for i, r := range withoutExitNodes(prefs.AdvertiseRoutes) {
|
||||
if i > 0 {
|
||||
sb.WriteByte(',')
|
||||
}
|
||||
sb.WriteString(r.String())
|
||||
}
|
||||
set(sb.String())
|
||||
case "advertise-exit-node":
|
||||
set(hasExitNodeRoutes(prefs.AdvertiseRoutes))
|
||||
case "snat-subnet-routes":
|
||||
set(!prefs.NoSNAT)
|
||||
case "netfilter-mode":
|
||||
set(prefs.NetfilterMode.String())
|
||||
case "unattended":
|
||||
set(prefs.ForceDaemon)
|
||||
}
|
||||
})
|
||||
return ret
|
||||
}
|
||||
|
||||
func fmtFlagValueArg(flagName string, val interface{}) string {
|
||||
if val == true {
|
||||
return "--" + flagName
|
||||
}
|
||||
if val == "" {
|
||||
return "--" + flagName + "="
|
||||
}
|
||||
return fmt.Sprintf("--%s=%v", flagName, shellquote.Join(val))
|
||||
}
|
||||
|
||||
func fmtSettingVal(v interface{}) string {
|
||||
switch v := v.(type) {
|
||||
case bool:
|
||||
return strconv.FormatBool(v)
|
||||
case string:
|
||||
return v
|
||||
case preftype.NetfilterMode:
|
||||
return v.String()
|
||||
case []string:
|
||||
return strings.Join(v, ",")
|
||||
case []netaddr.IPPrefix:
|
||||
var sb strings.Builder
|
||||
for i, r := range v {
|
||||
if i > 0 {
|
||||
sb.WriteByte(',')
|
||||
}
|
||||
sb.WriteString(r.String())
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
return fmt.Sprint(v)
|
||||
return fmt.Sprintf("--%s=%v", flagName, shellquote.Join(fmt.Sprint(val)))
|
||||
}
|
||||
|
||||
func hasExitNodeRoutes(rr []netaddr.IPPrefix) bool {
|
||||
var v4, v6 bool
|
||||
for _, r := range rr {
|
||||
if r.Bits == 0 {
|
||||
if r.IP.Is4() {
|
||||
if r.Bits() == 0 {
|
||||
if r.IP().Is4() {
|
||||
v4 = true
|
||||
} else if r.IP.Is6() {
|
||||
} else if r.IP().Is6() {
|
||||
v6 = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return v4 && v6
|
||||
}
|
||||
|
||||
// withoutExitNodes returns rr unchanged if it has only 1 or 0 /0
|
||||
// routes. If it has both IPv4 and IPv6 /0 routes, then it returns
|
||||
// a copy with all /0 routes removed.
|
||||
func withoutExitNodes(rr []netaddr.IPPrefix) []netaddr.IPPrefix {
|
||||
if !hasExitNodeRoutes(rr) {
|
||||
return rr
|
||||
}
|
||||
var out []netaddr.IPPrefix
|
||||
for _, r := range rr {
|
||||
if r.Bits() > 0 {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// exitNodeIP returns the exit node IP from p, using st to map
|
||||
// it from its ID form to an IP address if needed.
|
||||
func exitNodeIP(p *ipn.Prefs, st *ipnstate.Status) (ip netaddr.IP) {
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
if !p.ExitNodeIP.IsZero() {
|
||||
return p.ExitNodeIP
|
||||
}
|
||||
id := p.ExitNodeID
|
||||
if id.IsZero() {
|
||||
return
|
||||
}
|
||||
for _, p := range st.Peer {
|
||||
if p.ID == id {
|
||||
if len(p.TailscaleIPs) > 0 {
|
||||
return p.TailscaleIPs[0]
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -78,7 +78,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/derp/derpmap from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/disco from tailscale.com/derp+
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/internal/deepprint from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/internal/deephash from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/ipn from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/ipn/ipnlocal from tailscale.com/ipn/ipnserver+
|
||||
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
|
||||
|
||||
16
cmd/tailscaled/main.go
Normal file
16
cmd/tailscaled/main.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if strings.HasSuffix(os.Args[0], "tailscaled") {
|
||||
tailscaled_main()
|
||||
} else if strings.HasSuffix(os.Args[0], "tailscale") {
|
||||
tailscale_main()
|
||||
} else {
|
||||
panic(os.Args[0])
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
// The tailscale command is the Tailscale command-line client. It interacts
|
||||
// with the tailscaled node agent.
|
||||
package main // import "tailscale.com/cmd/tailscale"
|
||||
package main // import "tailscale.com/cmd/tailscaled"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -12,10 +12,10 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/cmd/tailscale/cli"
|
||||
"tailscale.com/cmd/tailscaled/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
func tailscale_main() {
|
||||
args := os.Args[1:]
|
||||
if name, _ := os.Executable(); strings.HasSuffix(filepath.Base(name), ".cgi") {
|
||||
args = []string{"web", "-cgi"}
|
||||
@@ -101,7 +101,7 @@ var subCommands = map[string]*func([]string) error{
|
||||
"debug": &debugModeFunc,
|
||||
}
|
||||
|
||||
func main() {
|
||||
func tailscaled_main() {
|
||||
// We aren't very performance sensitive, and the parts that are
|
||||
// performance sensitive (wireguard) try hard not to do any memory
|
||||
// allocations. So let's be aggressive about garbage collection,
|
||||
@@ -266,7 +266,7 @@ func run() error {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ns != nil && useNetstackForIP(ipp.IP) {
|
||||
if ns != nil && useNetstackForIP(ipp.IP()) {
|
||||
return ns.DialContextTCP(ctx, addr)
|
||||
}
|
||||
var d net.Dialer
|
||||
|
||||
@@ -460,10 +460,10 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
request.NodeKey.ShortString())
|
||||
return true, "", nil
|
||||
}
|
||||
if persist.Provider == "" {
|
||||
if resp.Login.Provider != "" {
|
||||
persist.Provider = resp.Login.Provider
|
||||
}
|
||||
if persist.LoginName == "" {
|
||||
if resp.Login.LoginName != "" {
|
||||
persist.LoginName = resp.Login.LoginName
|
||||
}
|
||||
|
||||
@@ -1091,7 +1091,7 @@ func ipForwardingBroken(routes []netaddr.IPPrefix, state *interfaces.State) bool
|
||||
localIPs := map[netaddr.IP]bool{}
|
||||
for _, addrs := range state.InterfaceIPs {
|
||||
for _, pfx := range addrs {
|
||||
localIPs[pfx.IP] = true
|
||||
localIPs[pfx.IP()] = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1100,10 +1100,10 @@ func ipForwardingBroken(routes []netaddr.IPPrefix, state *interfaces.State) bool
|
||||
// It's possible to advertise a route to one of the local
|
||||
// machine's local IPs. IP forwarding isn't required for this
|
||||
// to work, so we shouldn't warn for such exports.
|
||||
if r.IsSingleIP() && localIPs[r.IP] {
|
||||
if r.IsSingleIP() && localIPs[r.IP()] {
|
||||
continue
|
||||
}
|
||||
if r.IP.Is4() {
|
||||
if r.IP().Is4() {
|
||||
v4Routes = true
|
||||
} else {
|
||||
v6Routes = true
|
||||
|
||||
@@ -86,7 +86,7 @@ func TestNewDirect(t *testing.T) {
|
||||
func fakeEndpoints(ports ...uint16) (ret []tailcfg.Endpoint) {
|
||||
for _, port := range ports {
|
||||
ret = append(ret, tailcfg.Endpoint{
|
||||
Addr: netaddr.IPPort{Port: port},
|
||||
Addr: netaddr.IPPortFrom(netaddr.IP{}, port),
|
||||
})
|
||||
}
|
||||
return
|
||||
|
||||
@@ -147,9 +147,9 @@ const epLength = 16 + 2 // 16 byte IP address + 2 byte port
|
||||
func (m *CallMeMaybe) AppendMarshal(b []byte) []byte {
|
||||
ret, p := appendMsgHeader(b, TypeCallMeMaybe, v0, epLength*len(m.MyNumber))
|
||||
for _, ipp := range m.MyNumber {
|
||||
a := ipp.IP.As16()
|
||||
a := ipp.IP().As16()
|
||||
copy(p[:], a[:])
|
||||
binary.BigEndian.PutUint16(p[16:], ipp.Port)
|
||||
binary.BigEndian.PutUint16(p[16:], ipp.Port())
|
||||
p = p[epLength:]
|
||||
}
|
||||
return ret
|
||||
@@ -164,10 +164,9 @@ func parseCallMeMaybe(ver uint8, p []byte) (m *CallMeMaybe, err error) {
|
||||
for len(p) > 0 {
|
||||
var a [16]byte
|
||||
copy(a[:], p)
|
||||
m.MyNumber = append(m.MyNumber, netaddr.IPPort{
|
||||
IP: netaddr.IPFrom16(a),
|
||||
Port: binary.BigEndian.Uint16(p[16:18]),
|
||||
})
|
||||
m.MyNumber = append(m.MyNumber, netaddr.IPPortFrom(
|
||||
netaddr.IPFrom16(a),
|
||||
binary.BigEndian.Uint16(p[16:18])))
|
||||
p = p[epLength:]
|
||||
}
|
||||
return m, nil
|
||||
@@ -187,9 +186,9 @@ const pongLen = 12 + 16 + 2
|
||||
func (m *Pong) AppendMarshal(b []byte) []byte {
|
||||
ret, d := appendMsgHeader(b, TypePong, v0, pongLen)
|
||||
d = d[copy(d, m.TxID[:]):]
|
||||
ip16 := m.Src.IP.As16()
|
||||
ip16 := m.Src.IP().As16()
|
||||
d = d[copy(d, ip16[:]):]
|
||||
binary.BigEndian.PutUint16(d, m.Src.Port)
|
||||
binary.BigEndian.PutUint16(d, m.Src.Port())
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -201,10 +200,10 @@ func parsePong(ver uint8, p []byte) (m *Pong, err error) {
|
||||
copy(m.TxID[:], p)
|
||||
p = p[12:]
|
||||
|
||||
m.Src.IP, _ = netaddr.FromStdIP(net.IP(p[:16]))
|
||||
srcIP, _ := netaddr.FromStdIP(net.IP(p[:16]))
|
||||
p = p[16:]
|
||||
|
||||
m.Src.Port = binary.BigEndian.Uint16(p)
|
||||
port := binary.BigEndian.Uint16(p)
|
||||
m.Src = netaddr.IPPortFrom(srcIP, port)
|
||||
return m, nil
|
||||
}
|
||||
|
||||
|
||||
11
go.mod
11
go.mod
@@ -26,23 +26,24 @@ 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-20210429195722-6cd106ab1339
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210510192616-d1aa5623121d
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
|
||||
golang.org/x/crypto v0.0.0-20210317152858-513c2a44f670
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58
|
||||
golang.org/x/tools v0.1.0
|
||||
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8
|
||||
gopkg.in/yaml.v2 v2.2.8 // indirect
|
||||
honnef.co/go/tools v0.1.0
|
||||
inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44
|
||||
inet.af/netaddr v0.0.0-20210515010201-ad03edc7c841
|
||||
inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22
|
||||
inet.af/peercred v0.0.0-20210302202138-56e694897155
|
||||
inet.af/wf v0.0.0-20210516214145-a5343001b756
|
||||
rsc.io/goversion v1.2.0
|
||||
)
|
||||
|
||||
|
||||
36
go.sum
36
go.sum
@@ -23,6 +23,7 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20201127111758-49e582c6c23d/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BHsljHzVlRcyQhjrss6TZTdY2VfCqZPbv5k3iBFa2ZQ=
|
||||
github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc=
|
||||
github.com/frankban/quicktest v1.12.1 h1:P6vQcHwZYgVGIpUzKB5DXzkEeYJppJOStPLuh9aB89c=
|
||||
@@ -44,7 +45,6 @@ github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4 h1:L8R9j+yAqZuZjsqh/z+F1NCffTKKLShY6zXTItVIZ8M=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
@@ -70,7 +70,6 @@ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:C
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.10.10 h1:a/y8CglcM7gLGYmlbP/stPE5sR3hbhFRUjCBfd/0B3I=
|
||||
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
@@ -108,6 +107,7 @@ github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3/go.mod h1:85jBQOZwp
|
||||
github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys=
|
||||
github.com/peterbourgon/ff/v2 v2.0.0 h1:lx0oYI5qr/FU1xnpNhQ+EZM04gKgn46jyYvGEEqBBbY=
|
||||
github.com/peterbourgon/ff/v2 v2.0.0/go.mod h1:xjwr+t+SjWm4L46fcj/D+Ap+6ME7+HqFzaP22pP5Ggk=
|
||||
github.com/peterbourgon/ff/v3 v3.0.0/go.mod h1:UILIFjRH5a/ar8TjXYLTkIvSvekZqPm5Eb/qbGk6CT0=
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 h1:+/+DxvQaYifJ+grD4klzrS5y+KJXldn/2YTl5JG+vZ8=
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -127,6 +127,10 @@ github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 h1:lK99QQdH3yBW
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210429195722-6cd106ab1339 h1:OjLaZ57xeWJUUBAJN5KmsgjsaUABTZhcvgO/lKtZ8sQ=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210429195722-6cd106ab1339/go.mod h1:ys4yUmhKncXy1jWP34qUHKipRjl322VVhxoh1Rkfo7c=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210510175647-030c638da3df h1:ekBw6cxmDhXf9YxTmMZh7SPwUh9rnRRnaoX7HFiGobc=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210510175647-030c638da3df/go.mod h1:ys4yUmhKncXy1jWP34qUHKipRjl322VVhxoh1Rkfo7c=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210510192616-d1aa5623121d h1:qJSz1zlpuPLmfACtnj+tAH4g3iasJMBW8dpeFm5f4wg=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20210510192616-d1aa5623121d/go.mod h1:ys4yUmhKncXy1jWP34qUHKipRjl322VVhxoh1Rkfo7c=
|
||||
github.com/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=
|
||||
@@ -173,8 +177,9 @@ golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwY
|
||||
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed h1:p9UgmWI9wKpfYmgaV/IZKGdXc5qEK45tDwwwDyjS26I=
|
||||
golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -198,29 +203,35 @@ golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57 h1:F5Gozwx4I1xtr/sr/8CFbb57iKi3297KFs0QDbGN60A=
|
||||
golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007 h1:gG67DSER+11cZvqIMb8S8bt0vZtiN6xWYARwirrOSfE=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6 h1:EC6+IGYTjPpRfv9a2b/6Puw0W+hLtAhkV1tPsXhutqs=
|
||||
golang.org/x/term v0.0.0-20210317153231-de623e64d2a6/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba h1:O8mE0/t419eoIwhTFpKVkHiTs/Igowgfkj25AcZrtiE=
|
||||
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.0.0-20200609164405-eb789aa7ce50/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58 h1:1Bs6RVeBFtLZ8Yi1Hk07DiOqzvwLD/4hln4iahvFlag=
|
||||
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -242,11 +253,22 @@ gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
honnef.co/go/tools v0.1.0 h1:AWNL1W1i7f0wNZ8VwOKNJ0sliKvOF/adn0EHenfUh+c=
|
||||
honnef.co/go/tools v0.1.0/go.mod h1:XtegFAyX/PfluP4921rXU5IkjkqBCDnUq4W8VCIoKvM=
|
||||
inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44 h1:p7fX77zWzZMuNdJUhniBsmN1OvFOrW9SOtvgnzqUZX4=
|
||||
inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44/go.mod h1:I2i9ONCXRZDnG1+7O8fSuYzjcPxHQXrIfzD/IkR87x4=
|
||||
inet.af/netaddr v0.0.0-20210508014949-da1c2a70a83d h1:9tuJMxDV7THGfXWirKBD/v9rbsBC21bHd2eEYsYuIek=
|
||||
inet.af/netaddr v0.0.0-20210508014949-da1c2a70a83d/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20210511181906-37180328850c h1:rzDy/tC8LjEdN94+i0Bu22tTo/qE9cvhKyfD0HMU0NU=
|
||||
inet.af/netaddr v0.0.0-20210511181906-37180328850c/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20210515010201-ad03edc7c841 h1:2HpK+rC0Arcu98JukIlyVfEaE2OsvtmBFc8rs/2SJYs=
|
||||
inet.af/netaddr v0.0.0-20210515010201-ad03edc7c841/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22 h1:DNtszwGa6w76qlIr+PbPEnlBJdiRV8SaxeigOy0q1gg=
|
||||
inet.af/netstack v0.0.0-20210317161235-a1bf4e56ef22/go.mod h1:GVx+5OZtbG4TVOW5ilmyRZAZXr1cNwfqUEkTOtWK0PM=
|
||||
inet.af/peercred v0.0.0-20210302202138-56e694897155 h1:KojYNEYqDkZ2O3LdyTstR1l13L3ePKTIEM2h7ONkfkE=
|
||||
inet.af/peercred v0.0.0-20210302202138-56e694897155/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU=
|
||||
inet.af/wf v0.0.0-20210424212123-eaa011a774a4 h1:g1VVXY1xRKoO17aKY3g9KeJxDW0lGx1n2Y+WPSWkOL8=
|
||||
inet.af/wf v0.0.0-20210424212123-eaa011a774a4/go.mod h1:56/0QVlZ4NmPRh1QuU2OfrKqjSgt5P39R534gD2JMpQ=
|
||||
inet.af/wf v0.0.0-20210515021317-09f8efa8ac30 h1:TLxVVv7rmErJW7l81tbbR2BkOIYBI3YdxbJbEs/HJt8=
|
||||
inet.af/wf v0.0.0-20210515021317-09f8efa8ac30/go.mod h1:ViGMZRA6+RA318D7GCncrjv5gHUrPYrNDejjU12tikA=
|
||||
inet.af/wf v0.0.0-20210516214145-a5343001b756 h1:muIT3C1rH3/xpvIH8blKkMvhctV7F+OtZqs7kcwHDBQ=
|
||||
inet.af/wf v0.0.0-20210516214145-a5343001b756/go.mod h1:ViGMZRA6+RA318D7GCncrjv5gHUrPYrNDejjU12tikA=
|
||||
rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w=
|
||||
rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=
|
||||
|
||||
174
internal/deephash/deephash.go
Normal file
174
internal/deephash/deephash.go
Normal file
@@ -0,0 +1,174 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package deephash hashes a Go value recursively, in a predictable
|
||||
// order, without looping.
|
||||
package deephash
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
func Hash(v ...interface{}) string {
|
||||
h := sha256.New()
|
||||
// 64 matches the chunk size in crypto/sha256/sha256.go
|
||||
b := bufio.NewWriterSize(h, 64)
|
||||
Print(b, v)
|
||||
b.Flush()
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// UpdateHash sets last to the hash of v and reports whether its value changed.
|
||||
func UpdateHash(last *string, v ...interface{}) (changed bool) {
|
||||
sig := Hash(v)
|
||||
if *last != sig {
|
||||
*last = sig
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func Print(w *bufio.Writer, v ...interface{}) {
|
||||
print(w, reflect.ValueOf(v), make(map[uintptr]bool))
|
||||
}
|
||||
|
||||
var (
|
||||
netaddrIPType = reflect.TypeOf(netaddr.IP{})
|
||||
netaddrIPPrefix = reflect.TypeOf(netaddr.IPPrefix{})
|
||||
wgkeyKeyType = reflect.TypeOf(wgkey.Key{})
|
||||
wgkeyPrivateType = reflect.TypeOf(wgkey.Private{})
|
||||
tailcfgDiscoKeyType = reflect.TypeOf(tailcfg.DiscoKey{})
|
||||
)
|
||||
|
||||
func print(w *bufio.Writer, v reflect.Value, visited map[uintptr]bool) {
|
||||
if !v.IsValid() {
|
||||
return
|
||||
}
|
||||
|
||||
// Special case some common types.
|
||||
if v.CanInterface() {
|
||||
switch v.Type() {
|
||||
case netaddrIPType:
|
||||
var b []byte
|
||||
var err error
|
||||
if v.CanAddr() {
|
||||
x := v.Addr().Interface().(*netaddr.IP)
|
||||
b, err = x.MarshalText()
|
||||
} else {
|
||||
x := v.Interface().(netaddr.IP)
|
||||
b, err = x.MarshalText()
|
||||
}
|
||||
if err == nil {
|
||||
w.Write(b)
|
||||
return
|
||||
}
|
||||
case netaddrIPPrefix:
|
||||
var b []byte
|
||||
var err error
|
||||
if v.CanAddr() {
|
||||
x := v.Addr().Interface().(*netaddr.IPPrefix)
|
||||
b, err = x.MarshalText()
|
||||
} else {
|
||||
x := v.Interface().(netaddr.IPPrefix)
|
||||
b, err = x.MarshalText()
|
||||
}
|
||||
if err == nil {
|
||||
w.Write(b)
|
||||
return
|
||||
}
|
||||
case wgkeyKeyType:
|
||||
if v.CanAddr() {
|
||||
x := v.Addr().Interface().(*wgkey.Key)
|
||||
w.Write(x[:])
|
||||
} else {
|
||||
x := v.Interface().(wgkey.Key)
|
||||
w.Write(x[:])
|
||||
}
|
||||
return
|
||||
case wgkeyPrivateType:
|
||||
if v.CanAddr() {
|
||||
x := v.Addr().Interface().(*wgkey.Private)
|
||||
w.Write(x[:])
|
||||
} else {
|
||||
x := v.Interface().(wgkey.Private)
|
||||
w.Write(x[:])
|
||||
}
|
||||
return
|
||||
case tailcfgDiscoKeyType:
|
||||
if v.CanAddr() {
|
||||
x := v.Addr().Interface().(*tailcfg.DiscoKey)
|
||||
w.Write(x[:])
|
||||
} else {
|
||||
x := v.Interface().(tailcfg.DiscoKey)
|
||||
w.Write(x[:])
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Generic handling.
|
||||
switch v.Kind() {
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled kind %v for type %v", v.Kind(), v.Type()))
|
||||
case reflect.Ptr:
|
||||
ptr := v.Pointer()
|
||||
if visited[ptr] {
|
||||
return
|
||||
}
|
||||
visited[ptr] = true
|
||||
print(w, v.Elem(), visited)
|
||||
return
|
||||
case reflect.Struct:
|
||||
w.WriteString("struct{\n")
|
||||
for i, n := 0, v.NumField(); i < n; i++ {
|
||||
fmt.Fprintf(w, " [%d]: ", i)
|
||||
print(w, v.Field(i), visited)
|
||||
w.WriteString("\n")
|
||||
}
|
||||
w.WriteString("}\n")
|
||||
case reflect.Slice, reflect.Array:
|
||||
if v.Type().Elem().Kind() == reflect.Uint8 && v.CanInterface() {
|
||||
fmt.Fprintf(w, "%q", v.Interface())
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "[%d]{\n", v.Len())
|
||||
for i, ln := 0, v.Len(); i < ln; i++ {
|
||||
fmt.Fprintf(w, " [%d]: ", i)
|
||||
print(w, v.Index(i), visited)
|
||||
w.WriteString("\n")
|
||||
}
|
||||
w.WriteString("}\n")
|
||||
case reflect.Interface:
|
||||
print(w, v.Elem(), visited)
|
||||
case reflect.Map:
|
||||
sm := newSortedMap(v)
|
||||
fmt.Fprintf(w, "map[%d]{\n", len(sm.Key))
|
||||
for i, k := range sm.Key {
|
||||
print(w, k, visited)
|
||||
w.WriteString(": ")
|
||||
print(w, sm.Value[i], visited)
|
||||
w.WriteString("\n")
|
||||
}
|
||||
w.WriteString("}\n")
|
||||
case reflect.String:
|
||||
w.WriteString(v.String())
|
||||
case reflect.Bool:
|
||||
fmt.Fprintf(w, "%v", v.Bool())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
fmt.Fprintf(w, "%v", v.Int())
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
fmt.Fprintf(w, "%v", v.Uint())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
fmt.Fprintf(w, "%v", v.Float())
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
fmt.Fprintf(w, "%v", v.Complex())
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,14 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package deepprint
|
||||
package deephash
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
@@ -18,10 +19,6 @@ func TestDeepPrint(t *testing.T) {
|
||||
// Mostly we're just testing that we don't panic on handled types.
|
||||
v := getVal()
|
||||
|
||||
var buf bytes.Buffer
|
||||
Print(&buf, v)
|
||||
t.Logf("Got: %s", buf.Bytes())
|
||||
|
||||
hash1 := Hash(v)
|
||||
t.Logf("hash: %v", hash1)
|
||||
for i := 0; i < 20; i++ {
|
||||
@@ -36,10 +33,12 @@ func getVal() []interface{} {
|
||||
return []interface{}{
|
||||
&wgcfg.Config{
|
||||
Name: "foo",
|
||||
Addresses: []netaddr.IPPrefix{{Bits: 5, IP: netaddr.IPFrom16([16]byte{3: 3})}},
|
||||
Addresses: []netaddr.IPPrefix{netaddr.IPPrefixFrom(netaddr.IPFrom16([16]byte{3: 3}), 5)},
|
||||
Peers: []wgcfg.Peer{
|
||||
{
|
||||
Endpoints: "foo:5",
|
||||
Endpoints: wgcfg.Endpoints{
|
||||
IPPorts: wgcfg.NewIPPortSet(netaddr.MustParseIPPort("42.42.42.42:5")),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -49,16 +48,25 @@ func getVal() []interface{} {
|
||||
netaddr.MustParseIPPrefix("1234::/64"),
|
||||
},
|
||||
},
|
||||
map[string]string{
|
||||
"key1": "val1",
|
||||
"key2": "val2",
|
||||
"key3": "val3",
|
||||
"key4": "val4",
|
||||
"key5": "val5",
|
||||
"key6": "val6",
|
||||
"key7": "val7",
|
||||
"key8": "val8",
|
||||
"key9": "val9",
|
||||
map[dnsname.FQDN][]netaddr.IP{
|
||||
dnsname.FQDN("a."): {netaddr.MustParseIP("1.2.3.4"), netaddr.MustParseIP("4.3.2.1")},
|
||||
dnsname.FQDN("b."): {netaddr.MustParseIP("8.8.8.8"), netaddr.MustParseIP("9.9.9.9")},
|
||||
},
|
||||
map[dnsname.FQDN][]netaddr.IPPort{
|
||||
dnsname.FQDN("a."): {netaddr.MustParseIPPort("1.2.3.4:11"), netaddr.MustParseIPPort("4.3.2.1:22")},
|
||||
dnsname.FQDN("b."): {netaddr.MustParseIPPort("8.8.8.8:11"), netaddr.MustParseIPPort("9.9.9.9:22")},
|
||||
},
|
||||
map[tailcfg.DiscoKey]bool{
|
||||
{1: 1}: true,
|
||||
{1: 2}: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkHash(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
v := getVal()
|
||||
for i := 0; i < b.N; i++ {
|
||||
Hash(v)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
// This is a slightly modified fork of Go's src/internal/fmtsort/sort.go
|
||||
|
||||
package deepprint
|
||||
package deephash
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
@@ -1,103 +0,0 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package deepprint walks a Go value recursively, in a predictable
|
||||
// order, without looping, and prints each value out to a given
|
||||
// Writer, which is assumed to be a hash.Hash, as this package doesn't
|
||||
// format things nicely.
|
||||
//
|
||||
// This is intended as a lighter version of go-spew, etc. We don't need its
|
||||
// features when our writer is just a hash.
|
||||
package deepprint
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func Hash(v ...interface{}) string {
|
||||
h := sha256.New()
|
||||
Print(h, v)
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// UpdateHash sets last to the hash of v and reports whether its value changed.
|
||||
func UpdateHash(last *string, v ...interface{}) (changed bool) {
|
||||
sig := Hash(v)
|
||||
if *last != sig {
|
||||
*last = sig
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func Print(w io.Writer, v ...interface{}) {
|
||||
print(w, reflect.ValueOf(v), make(map[uintptr]bool))
|
||||
}
|
||||
|
||||
func print(w io.Writer, v reflect.Value, visited map[uintptr]bool) {
|
||||
if !v.IsValid() {
|
||||
return
|
||||
}
|
||||
switch v.Kind() {
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled kind %v for type %v", v.Kind(), v.Type()))
|
||||
case reflect.Ptr:
|
||||
ptr := v.Pointer()
|
||||
if visited[ptr] {
|
||||
return
|
||||
}
|
||||
visited[ptr] = true
|
||||
print(w, v.Elem(), visited)
|
||||
return
|
||||
case reflect.Struct:
|
||||
fmt.Fprintf(w, "struct{\n")
|
||||
t := v.Type()
|
||||
for i, n := 0, v.NumField(); i < n; i++ {
|
||||
sf := t.Field(i)
|
||||
fmt.Fprintf(w, "%s: ", sf.Name)
|
||||
print(w, v.Field(i), visited)
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
case reflect.Slice, reflect.Array:
|
||||
if v.Type().Elem().Kind() == reflect.Uint8 && v.CanInterface() {
|
||||
fmt.Fprintf(w, "%q", v.Interface())
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "[%d]{\n", v.Len())
|
||||
for i, ln := 0, v.Len(); i < ln; i++ {
|
||||
fmt.Fprintf(w, " [%d]: ", i)
|
||||
print(w, v.Index(i), visited)
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
fmt.Fprintf(w, "}\n")
|
||||
case reflect.Interface:
|
||||
print(w, v.Elem(), visited)
|
||||
case reflect.Map:
|
||||
sm := newSortedMap(v)
|
||||
fmt.Fprintf(w, "map[%d]{\n", len(sm.Key))
|
||||
for i, k := range sm.Key {
|
||||
print(w, k, visited)
|
||||
fmt.Fprintf(w, ": ")
|
||||
print(w, sm.Value[i], visited)
|
||||
fmt.Fprintf(w, "\n")
|
||||
}
|
||||
fmt.Fprintf(w, "}\n")
|
||||
|
||||
case reflect.String:
|
||||
fmt.Fprintf(w, "%s", v.String())
|
||||
case reflect.Bool:
|
||||
fmt.Fprintf(w, "%v", v.Bool())
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
fmt.Fprintf(w, "%v", v.Int())
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
fmt.Fprintf(w, "%v", v.Uint())
|
||||
case reflect.Float32, reflect.Float64:
|
||||
fmt.Fprintf(w, "%v", v.Float())
|
||||
case reflect.Complex64, reflect.Complex128:
|
||||
fmt.Fprintf(w, "%v", v.Complex())
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ import (
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/internal/deepprint"
|
||||
"tailscale.com/internal/deephash"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn/policy"
|
||||
@@ -326,6 +326,9 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
|
||||
}
|
||||
})
|
||||
sb.MutateSelfStatus(func(ss *ipnstate.PeerStatus) {
|
||||
if b.netMap != nil && b.netMap.SelfNode != nil {
|
||||
ss.ID = b.netMap.SelfNode.StableID
|
||||
}
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
ss.PeerAPIURL = append(ss.PeerAPIURL, pln.urlStr)
|
||||
}
|
||||
@@ -353,18 +356,19 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
|
||||
var tailAddr4 string
|
||||
var tailscaleIPs = make([]netaddr.IP, 0, len(p.Addresses))
|
||||
for _, addr := range p.Addresses {
|
||||
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.IP) {
|
||||
if addr.IP.Is4() && tailAddr4 == "" {
|
||||
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.IP()) {
|
||||
if addr.IP().Is4() && tailAddr4 == "" {
|
||||
// The peer struct previously only allowed a single
|
||||
// Tailscale IP address. For compatibility for a few releases starting
|
||||
// with 1.8, keep it pulled out as IPv4-only for a bit.
|
||||
tailAddr4 = addr.IP.String()
|
||||
tailAddr4 = addr.IP().String()
|
||||
}
|
||||
tailscaleIPs = append(tailscaleIPs, addr.IP)
|
||||
tailscaleIPs = append(tailscaleIPs, addr.IP())
|
||||
}
|
||||
}
|
||||
sb.AddPeer(key.Public(p.Key), &ipnstate.PeerStatus{
|
||||
InNetworkMap: true,
|
||||
ID: p.StableID,
|
||||
UserID: p.User,
|
||||
TailAddrDeprecated: tailAddr4,
|
||||
TailscaleIPs: tailscaleIPs,
|
||||
@@ -386,10 +390,10 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
|
||||
func (b *LocalBackend) WhoIs(ipp netaddr.IPPort) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
n, ok = b.nodeByAddr[ipp.IP]
|
||||
n, ok = b.nodeByAddr[ipp.IP()]
|
||||
if !ok {
|
||||
var ip netaddr.IP
|
||||
if ipp.Port != 0 {
|
||||
if ipp.Port() != 0 {
|
||||
ip, ok = b.e.WhoIsIPPort(ipp)
|
||||
}
|
||||
if !ok {
|
||||
@@ -430,14 +434,15 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
}
|
||||
return
|
||||
}
|
||||
if st.LoginFinished != nil {
|
||||
|
||||
b.mu.Lock()
|
||||
wasBlocked := b.blocked
|
||||
b.mu.Unlock()
|
||||
|
||||
if st.LoginFinished != nil && wasBlocked {
|
||||
// Auth completed, unblock the engine
|
||||
b.blockEngineUpdates(false)
|
||||
b.authReconfig()
|
||||
b.EditPrefs(&ipn.MaskedPrefs{
|
||||
LoggedOutSet: true,
|
||||
Prefs: ipn.Prefs{LoggedOut: false},
|
||||
})
|
||||
b.send(ipn.Notify{LoginFinished: &empty.Message{}})
|
||||
}
|
||||
|
||||
@@ -476,11 +481,15 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
b.authURL = st.URL
|
||||
b.authURLSticky = st.URL
|
||||
}
|
||||
if b.state == ipn.NeedsLogin {
|
||||
if !b.prefs.WantRunning {
|
||||
if wasBlocked && st.LoginFinished != nil {
|
||||
// Interactive login finished successfully (URL visited).
|
||||
// After an interactive login, the user always wants
|
||||
// WantRunning.
|
||||
if !b.prefs.WantRunning || b.prefs.LoggedOut {
|
||||
prefsChanged = true
|
||||
}
|
||||
b.prefs.WantRunning = true
|
||||
b.prefs.LoggedOut = false
|
||||
}
|
||||
// Prefs will be written out; this is not safe unless locked or cloned.
|
||||
if prefsChanged {
|
||||
@@ -543,7 +552,7 @@ func (b *LocalBackend) findExitNodeIDLocked(nm *netmap.NetworkMap) (prefsChanged
|
||||
|
||||
for _, peer := range nm.Peers {
|
||||
for _, addr := range peer.Addresses {
|
||||
if !addr.IsSingleIP() || addr.IP != b.prefs.ExitNodeIP {
|
||||
if !addr.IsSingleIP() || addr.IP() != b.prefs.ExitNodeIP {
|
||||
continue
|
||||
}
|
||||
// Found the node being referenced, upgrade prefs to
|
||||
@@ -562,10 +571,18 @@ func (b *LocalBackend) findExitNodeIDLocked(nm *netmap.NetworkMap) (prefsChanged
|
||||
func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) {
|
||||
if err != nil {
|
||||
b.logf("wgengine status error: %v", err)
|
||||
|
||||
b.statusLock.Lock()
|
||||
b.statusChanged.Broadcast()
|
||||
b.statusLock.Unlock()
|
||||
return
|
||||
}
|
||||
if s == nil {
|
||||
b.logf("[unexpected] non-error wgengine update with status=nil: %v", s)
|
||||
|
||||
b.statusLock.Lock()
|
||||
b.statusChanged.Broadcast()
|
||||
b.statusLock.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -874,7 +891,7 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
|
||||
}
|
||||
if prefs != nil {
|
||||
for _, r := range prefs.AdvertiseRoutes {
|
||||
if r.Bits == 0 {
|
||||
if r.Bits() == 0 {
|
||||
// When offering a default route to the world, we
|
||||
// filter out locally reachable LANs, so that the
|
||||
// default route effectively appears to be a "guest
|
||||
@@ -899,7 +916,7 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
|
||||
localNets := localNetsB.IPSet()
|
||||
logNets := logNetsB.IPSet()
|
||||
|
||||
changed := deepprint.UpdateHash(&b.filterHash, haveNetmap, addrs, packetFilter, localNets.Ranges(), logNets.Ranges(), shieldsUp)
|
||||
changed := deephash.UpdateHash(&b.filterHash, haveNetmap, addrs, packetFilter, localNets.Ranges(), logNets.Ranges(), shieldsUp)
|
||||
if !changed {
|
||||
return
|
||||
}
|
||||
@@ -942,13 +959,13 @@ var removeFromDefaultRoute = []netaddr.IPPrefix{
|
||||
func interfaceRoutes() (ips *netaddr.IPSet, hostIPs []netaddr.IP, err error) {
|
||||
var b netaddr.IPSetBuilder
|
||||
if err := interfaces.ForeachInterfaceAddress(func(_ interfaces.Interface, pfx netaddr.IPPrefix) {
|
||||
if tsaddr.IsTailscaleIP(pfx.IP) {
|
||||
if tsaddr.IsTailscaleIP(pfx.IP()) {
|
||||
return
|
||||
}
|
||||
if pfx.IsSingleIP() {
|
||||
return
|
||||
}
|
||||
hostIPs = append(hostIPs, pfx.IP)
|
||||
hostIPs = append(hostIPs, pfx.IP())
|
||||
b.AddPrefix(pfx)
|
||||
}); err != nil {
|
||||
return nil, nil, err
|
||||
@@ -1734,10 +1751,10 @@ func (b *LocalBackend) authReconfig() {
|
||||
// https://github.com/tailscale/tailscale/issues/1152
|
||||
// tracks adding the right capability reporting to
|
||||
// enable AAAA in MagicDNS.
|
||||
if addr.IP.Is6() {
|
||||
if addr.IP().Is6() {
|
||||
continue
|
||||
}
|
||||
ips = append(ips, addr.IP)
|
||||
ips = append(ips, addr.IP())
|
||||
}
|
||||
dcfg.Hosts[fqdn] = ips
|
||||
}
|
||||
@@ -1792,10 +1809,7 @@ func parseResolver(cfg tailcfg.DNSResolver) (netaddr.IPPort, error) {
|
||||
if err != nil {
|
||||
return netaddr.IPPort{}, fmt.Errorf("[unexpected] non-IP resolver %q", cfg.Addr)
|
||||
}
|
||||
return netaddr.IPPort{
|
||||
IP: ip,
|
||||
Port: 53,
|
||||
}, nil
|
||||
return netaddr.IPPortFrom(ip, 53), nil
|
||||
}
|
||||
|
||||
// tailscaleVarRoot returns the root directory of Tailscale's writable
|
||||
@@ -1853,7 +1867,7 @@ func (b *LocalBackend) initPeerAPIListener() {
|
||||
if len(b.netMap.Addresses) == len(b.peerAPIListeners) {
|
||||
allSame := true
|
||||
for i, pln := range b.peerAPIListeners {
|
||||
if pln.ip != b.netMap.Addresses[i].IP {
|
||||
if pln.ip != b.netMap.Addresses[i].IP() {
|
||||
allSame = false
|
||||
break
|
||||
}
|
||||
@@ -1898,7 +1912,7 @@ func (b *LocalBackend) initPeerAPIListener() {
|
||||
var err error
|
||||
skipListen := i > 0 && isNetstack
|
||||
if !skipListen {
|
||||
ln, err = ps.listen(a.IP, b.prevIfState)
|
||||
ln, err = ps.listen(a.IP(), b.prevIfState)
|
||||
if err != nil {
|
||||
if runtime.GOOS == "windows" {
|
||||
// Expected for now. See Issue 1620.
|
||||
@@ -1912,7 +1926,7 @@ func (b *LocalBackend) initPeerAPIListener() {
|
||||
}
|
||||
pln := &peerAPIListener{
|
||||
ps: ps,
|
||||
ip: a.IP,
|
||||
ip: a.IP(),
|
||||
ln: ln, // nil for 2nd+ on netstack
|
||||
lb: b,
|
||||
}
|
||||
@@ -1921,7 +1935,7 @@ func (b *LocalBackend) initPeerAPIListener() {
|
||||
} else {
|
||||
pln.port = ln.Addr().(*net.TCPAddr).Port
|
||||
}
|
||||
pln.urlStr = "http://" + net.JoinHostPort(a.IP.String(), strconv.Itoa(pln.port))
|
||||
pln.urlStr = "http://" + net.JoinHostPort(a.IP().String(), strconv.Itoa(pln.port))
|
||||
b.logf("peerapi: serving on %s", pln.urlStr)
|
||||
go pln.serve()
|
||||
b.peerAPIListeners = append(b.peerAPIListeners, pln)
|
||||
@@ -1972,14 +1986,14 @@ func peerRoutes(peers []wgcfg.Peer, cgnatThreshold int) (routes []netaddr.IPPref
|
||||
for _, aip := range peer.AllowedIPs {
|
||||
aip = unmapIPPrefix(aip)
|
||||
// Only add the Tailscale IPv6 ULA once, if we see anybody using part of it.
|
||||
if aip.IP.Is6() && aip.IsSingleIP() && tsULA.Contains(aip.IP) {
|
||||
if aip.IP().Is6() && aip.IsSingleIP() && tsULA.Contains(aip.IP()) {
|
||||
if !didULA {
|
||||
didULA = true
|
||||
routes = append(routes, tsULA)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if aip.IsSingleIP() && cgNAT.Contains(aip.IP) {
|
||||
if aip.IsSingleIP() && cgNAT.Contains(aip.IP()) {
|
||||
cgNATIPs = append(cgNATIPs, aip)
|
||||
} else {
|
||||
routes = append(routes, aip)
|
||||
@@ -2046,16 +2060,13 @@ func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router
|
||||
}
|
||||
}
|
||||
|
||||
rs.Routes = append(rs.Routes, netaddr.IPPrefix{
|
||||
IP: tsaddr.TailscaleServiceIP(),
|
||||
Bits: 32,
|
||||
})
|
||||
rs.Routes = append(rs.Routes, netaddr.IPPrefixFrom(tsaddr.TailscaleServiceIP(), 32))
|
||||
|
||||
return rs
|
||||
}
|
||||
|
||||
func unmapIPPrefix(ipp netaddr.IPPrefix) netaddr.IPPrefix {
|
||||
return netaddr.IPPrefix{IP: ipp.IP.Unmap(), Bits: ipp.Bits}
|
||||
return netaddr.IPPrefixFrom(ipp.IP().Unmap(), ipp.Bits())
|
||||
}
|
||||
|
||||
func unmapIPPrefixes(ippsList ...[]netaddr.IPPrefix) (ret []netaddr.IPPrefix) {
|
||||
@@ -2109,8 +2120,8 @@ func (b *LocalBackend) enterState(newState ipn.State) {
|
||||
if oldState == newState {
|
||||
return
|
||||
}
|
||||
b.logf("Switching ipn state %v -> %v (WantRunning=%v)",
|
||||
oldState, newState, prefs.WantRunning)
|
||||
b.logf("Switching ipn state %v -> %v (WantRunning=%v, nm=%v)",
|
||||
oldState, newState, prefs.WantRunning, netMap != nil)
|
||||
health.SetIPNState(newState.String(), prefs.WantRunning)
|
||||
b.send(ipn.Notify{State: &newState})
|
||||
|
||||
@@ -2139,7 +2150,7 @@ func (b *LocalBackend) enterState(newState ipn.State) {
|
||||
case ipn.Running:
|
||||
var addrs []string
|
||||
for _, addr := range b.netMap.Addresses {
|
||||
addrs = append(addrs, addr.IP.String())
|
||||
addrs = append(addrs, addr.IP().String())
|
||||
}
|
||||
systemd.Status("Connected; %s; %s", activeLogin, strings.Join(addrs, " "))
|
||||
default:
|
||||
@@ -2166,13 +2177,14 @@ func (b *LocalBackend) nextState() ipn.State {
|
||||
cc = b.cc
|
||||
netMap = b.netMap
|
||||
state = b.state
|
||||
blocked = b.blocked
|
||||
wantRunning = b.prefs.WantRunning
|
||||
loggedOut = b.prefs.LoggedOut
|
||||
)
|
||||
b.mu.Unlock()
|
||||
|
||||
switch {
|
||||
case !wantRunning && !loggedOut && b.hasNodeKey():
|
||||
case !wantRunning && !loggedOut && !blocked && b.hasNodeKey():
|
||||
return ipn.Stopped
|
||||
case netMap == nil:
|
||||
if cc.AuthCantContinue() || loggedOut {
|
||||
@@ -2406,7 +2418,7 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
addNode := func(n *tailcfg.Node) {
|
||||
for _, ipp := range n.Addresses {
|
||||
if ipp.IsSingleIP() {
|
||||
b.nodeByAddr[ipp.IP] = n
|
||||
b.nodeByAddr[ipp.IP()] = n
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2558,9 +2570,9 @@ func peerAPIBase(nm *netmap.NetworkMap, peer *tailcfg.Node) string {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case a.IP.Is4():
|
||||
case a.IP().Is4():
|
||||
have4 = true
|
||||
case a.IP.Is6():
|
||||
case a.IP().Is6():
|
||||
have6 = true
|
||||
}
|
||||
}
|
||||
@@ -2576,11 +2588,11 @@ func peerAPIBase(nm *netmap.NetworkMap, peer *tailcfg.Node) string {
|
||||
var ipp netaddr.IPPort
|
||||
switch {
|
||||
case have4 && p4 != 0:
|
||||
ipp = netaddr.IPPort{IP: nodeIP(peer, netaddr.IP.Is4), Port: p4}
|
||||
ipp = netaddr.IPPortFrom(nodeIP(peer, netaddr.IP.Is4), p4)
|
||||
case have6 && p6 != 0:
|
||||
ipp = netaddr.IPPort{IP: nodeIP(peer, netaddr.IP.Is6), Port: p6}
|
||||
ipp = netaddr.IPPortFrom(nodeIP(peer, netaddr.IP.Is6), p6)
|
||||
}
|
||||
if ipp.IP.IsZero() {
|
||||
if ipp.IP().IsZero() {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("http://%v", ipp)
|
||||
@@ -2588,8 +2600,8 @@ func peerAPIBase(nm *netmap.NetworkMap, peer *tailcfg.Node) string {
|
||||
|
||||
func nodeIP(n *tailcfg.Node, pred func(netaddr.IP) bool) netaddr.IP {
|
||||
for _, a := range n.Addresses {
|
||||
if a.IsSingleIP() && pred(a.IP) {
|
||||
return a.IP
|
||||
if a.IsSingleIP() && pred(a.IP()) {
|
||||
return a.IP()
|
||||
}
|
||||
}
|
||||
return netaddr.IP{}
|
||||
|
||||
@@ -171,7 +171,7 @@ func TestShrinkDefaultRoute(t *testing.T) {
|
||||
out: []string{
|
||||
"fe80::1",
|
||||
"ff00::1",
|
||||
tsaddr.TailscaleULARange().IP.String(),
|
||||
tsaddr.TailscaleULARange().IP().String(),
|
||||
},
|
||||
localIPFn: func(ip netaddr.IP) bool { return !inRemove(ip) && ip.Is6() },
|
||||
},
|
||||
|
||||
@@ -510,7 +510,7 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
<body>
|
||||
<h1>Hello, %s (%v)</h1>
|
||||
This is my Tailscale device. Your device is %v.
|
||||
`, html.EscapeString(who), h.remoteAddr.IP, html.EscapeString(h.peerNode.ComputedName))
|
||||
`, html.EscapeString(who), h.remoteAddr.IP(), html.EscapeString(h.peerNode.ComputedName))
|
||||
|
||||
if h.isSelf {
|
||||
fmt.Fprintf(w, "<p>You are the owner of this node.\n")
|
||||
|
||||
@@ -368,13 +368,11 @@ func TestStateMachine(t *testing.T) {
|
||||
{
|
||||
c.Assert(cc.getCalls(), qt.DeepEquals, []string{"Login"})
|
||||
notifies.drain(0)
|
||||
// BUG: this should immediately set WantRunning to true.
|
||||
// Users don't log in if they don't want to also connect.
|
||||
// (Generally, we're inconsistent about who is supposed to
|
||||
// update Prefs at what time. But the overall philosophy is:
|
||||
// update it when the user's intent changes. This is clearly
|
||||
// at the time the user *requests* Login, not at the time
|
||||
// the login finishes.)
|
||||
// Note: WantRunning isn't true yet. It'll switch to true
|
||||
// after a successful login finishes.
|
||||
// (This behaviour is needed so that b.Login() won't
|
||||
// start connecting to an old account right away, if one
|
||||
// exists when you launch another login.)
|
||||
}
|
||||
|
||||
// Attempted non-interactive login with no key; indicate that
|
||||
@@ -384,18 +382,16 @@ func TestStateMachine(t *testing.T) {
|
||||
url1 := "http://localhost:1/1"
|
||||
cc.send(nil, url1, false, nil)
|
||||
{
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(cc.getCalls(), qt.DeepEquals, []string{})
|
||||
|
||||
// ...but backend eats that notification, because the user
|
||||
// didn't explicitly request interactive login yet, and
|
||||
// we're already in NeedsLogin state.
|
||||
nn := notifies.drain(1)
|
||||
|
||||
// Trying to log in automatically sets WantRunning.
|
||||
// BUG: that should have happened right after Login().
|
||||
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsTrue)
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
|
||||
}
|
||||
|
||||
// Now we'll try an interactive login.
|
||||
@@ -451,11 +447,12 @@ func TestStateMachine(t *testing.T) {
|
||||
// same time.
|
||||
// The backend should propagate this upward for the UI.
|
||||
t.Logf("\n\nLoginFinished")
|
||||
notifies.expect(2)
|
||||
notifies.expect(3)
|
||||
cc.setAuthBlocked(false)
|
||||
cc.persist.LoginName = "user1"
|
||||
cc.send(nil, "", true, &netmap.NetworkMap{})
|
||||
{
|
||||
nn := notifies.drain(2)
|
||||
nn := notifies.drain(3)
|
||||
// BUG: still too soon for UpdateEndpoints.
|
||||
//
|
||||
// Arguably it makes sense to unpause now, since the machine
|
||||
@@ -468,15 +465,12 @@ func TestStateMachine(t *testing.T) {
|
||||
// it's visible in the logs)
|
||||
c.Assert([]string{"unpause", "UpdateEndpoints"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].LoginFinished, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].State, qt.Not(qt.IsNil))
|
||||
c.Assert(ipn.NeedsMachineAuth, qt.Equals, *nn[1].State)
|
||||
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[2].State, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].Prefs.Persist.LoginName, qt.Equals, "user1")
|
||||
c.Assert(ipn.NeedsMachineAuth, qt.Equals, *nn[2].State)
|
||||
}
|
||||
|
||||
// TODO: check that the logged-in username propagates from control
|
||||
// through to the UI notifications. I think it's used as a hint
|
||||
// for future logins, to pre-fill the username box? Not really sure
|
||||
// how it works.
|
||||
|
||||
// Pretend that the administrator has authorized our machine.
|
||||
t.Logf("\n\nMachineAuthorized")
|
||||
notifies.expect(1)
|
||||
@@ -581,77 +575,72 @@ func TestStateMachine(t *testing.T) {
|
||||
|
||||
// Let's make the logout succeed.
|
||||
t.Logf("\n\nLogout (async) - succeed")
|
||||
notifies.expect(1)
|
||||
notifies.expect(0)
|
||||
cc.setAuthBlocked(true)
|
||||
cc.send(nil, "", false, nil)
|
||||
{
|
||||
nn := notifies.drain(1)
|
||||
notifies.drain(0)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsTrue)
|
||||
// BUG: WantRunning should be false after manual logout.
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsTrue)
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
// A second logout should do nothing, since the prefs haven't changed.
|
||||
t.Logf("\n\nLogout2 (async)")
|
||||
notifies.expect(1)
|
||||
notifies.expect(0)
|
||||
b.Logout()
|
||||
{
|
||||
nn := notifies.drain(1)
|
||||
notifies.drain(0)
|
||||
// BUG: the backend has already called StartLogout, and we're
|
||||
// still logged out. So it shouldn't call it again.
|
||||
c.Assert([]string{"StartLogout"}, qt.DeepEquals, cc.getCalls())
|
||||
// BUG: Prefs should not change here. Already logged out.
|
||||
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsTrue)
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
// Let's acknowledge the second logout too.
|
||||
t.Logf("\n\nLogout2 (async) - succeed")
|
||||
notifies.expect(1)
|
||||
notifies.expect(0)
|
||||
cc.setAuthBlocked(true)
|
||||
cc.send(nil, "", false, nil)
|
||||
{
|
||||
nn := notifies.drain(1)
|
||||
notifies.drain(0)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsTrue)
|
||||
// BUG: second logout shouldn't cause WantRunning->true !!
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsTrue)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
// Try the synchronous logout feature.
|
||||
t.Logf("\n\nLogout3 (sync)")
|
||||
notifies.expect(1)
|
||||
notifies.expect(0)
|
||||
b.LogoutSync(context.Background())
|
||||
// NOTE: This returns as soon as cc.Logout() returns, which is okay
|
||||
// I guess, since that's supposed to be synchronous.
|
||||
{
|
||||
nn := notifies.drain(1)
|
||||
notifies.drain(0)
|
||||
c.Assert([]string{"Logout"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsTrue)
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
// Generate the third logout event.
|
||||
t.Logf("\n\nLogout3 (sync) - succeed")
|
||||
notifies.expect(1)
|
||||
notifies.expect(0)
|
||||
cc.setAuthBlocked(true)
|
||||
cc.send(nil, "", false, nil)
|
||||
{
|
||||
nn := notifies.drain(1)
|
||||
notifies.drain(0)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsTrue)
|
||||
// BUG: third logout shouldn't cause WantRunning->true !!
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsTrue)
|
||||
c.Assert(cc.getCalls(), qt.HasLen, 0)
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
@@ -669,10 +658,6 @@ func TestStateMachine(t *testing.T) {
|
||||
// happens if the user exits and restarts while logged out.
|
||||
// Note that it's explicitly okay to call b.Start() over and over
|
||||
// again, every time the frontend reconnects.
|
||||
//
|
||||
// BUG: WantRunning is true here (because of the bug above).
|
||||
// We'll have to adjust the following test's expectations if we
|
||||
// fix that.
|
||||
|
||||
// TODO: test user switching between statekeys.
|
||||
|
||||
@@ -691,7 +676,7 @@ func TestStateMachine(t *testing.T) {
|
||||
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].State, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsTrue)
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsTrue)
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[1].State)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
@@ -703,16 +688,20 @@ func TestStateMachine(t *testing.T) {
|
||||
t.Logf("\n\nLoginFinished3")
|
||||
notifies.expect(3)
|
||||
cc.setAuthBlocked(false)
|
||||
cc.persist.LoginName = "user2"
|
||||
cc.send(nil, "", true, &netmap.NetworkMap{
|
||||
MachineStatus: tailcfg.MachineAuthorized,
|
||||
})
|
||||
{
|
||||
nn := notifies.drain(3)
|
||||
c.Assert([]string{"unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].LoginFinished, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[0].LoginFinished, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[2].State, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
|
||||
// Prefs after finishing the login, so LoginName updated.
|
||||
c.Assert(nn[1].Prefs.Persist.LoginName, qt.Equals, "user2")
|
||||
c.Assert(nn[1].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[1].Prefs.WantRunning, qt.IsTrue)
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[2].State)
|
||||
}
|
||||
|
||||
@@ -773,6 +762,63 @@ func TestStateMachine(t *testing.T) {
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[0].State)
|
||||
}
|
||||
|
||||
// Disconnect.
|
||||
t.Logf("\n\nStop")
|
||||
notifies.expect(2)
|
||||
b.EditPrefs(&ipn.MaskedPrefs{
|
||||
WantRunningSet: true,
|
||||
Prefs: ipn.Prefs{WantRunning: false},
|
||||
})
|
||||
{
|
||||
nn := notifies.drain(2)
|
||||
c.Assert([]string{"unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
// BUG: I would expect Prefs to change first, and state after.
|
||||
c.Assert(nn[0].State, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(ipn.Stopped, qt.Equals, *nn[0].State)
|
||||
}
|
||||
|
||||
// We want to try logging in as a different user, while Stopped.
|
||||
// First, start the login process (without logging out first).
|
||||
t.Logf("\n\nLoginDifferent")
|
||||
notifies.expect(2)
|
||||
b.StartLoginInteractive()
|
||||
url3 := "http://localhost:1/3"
|
||||
cc.send(nil, url3, false, nil)
|
||||
{
|
||||
nn := notifies.drain(2)
|
||||
// It might seem like WantRunning should switch to true here,
|
||||
// but that would be risky since we already have a valid
|
||||
// user account. It might try to reconnect to the old account
|
||||
// before the new one is ready. So no change yet.
|
||||
c.Assert([]string{"Login", "unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].BrowseToURL, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].State, qt.Not(qt.IsNil))
|
||||
c.Assert(*nn[0].BrowseToURL, qt.Equals, url3)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[1].State)
|
||||
}
|
||||
|
||||
// Now, let's say the interactive login completed, using a different
|
||||
// user account than before.
|
||||
t.Logf("\n\nLoginDifferent URL visited")
|
||||
notifies.expect(3)
|
||||
cc.persist.LoginName = "user3"
|
||||
cc.send(nil, "", true, &netmap.NetworkMap{
|
||||
MachineStatus: tailcfg.MachineAuthorized,
|
||||
})
|
||||
{
|
||||
nn := notifies.drain(3)
|
||||
c.Assert([]string{"unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].LoginFinished, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].Prefs, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[2].State, qt.Not(qt.IsNil))
|
||||
// Prefs after finishing the login, so LoginName updated.
|
||||
c.Assert(nn[1].Prefs.Persist.LoginName, qt.Equals, "user3")
|
||||
c.Assert(nn[1].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[1].Prefs.WantRunning, qt.IsTrue)
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[2].State)
|
||||
}
|
||||
|
||||
// The last test case is the most common one: restarting when both
|
||||
// logged in and WantRunning.
|
||||
t.Logf("\n\nStart5")
|
||||
@@ -793,17 +839,18 @@ func TestStateMachine(t *testing.T) {
|
||||
|
||||
// Control server accepts our valid key from before.
|
||||
t.Logf("\n\nLoginFinished5")
|
||||
notifies.expect(2)
|
||||
notifies.expect(1)
|
||||
cc.setAuthBlocked(false)
|
||||
cc.send(nil, "", true, &netmap.NetworkMap{
|
||||
MachineStatus: tailcfg.MachineAuthorized,
|
||||
})
|
||||
{
|
||||
nn := notifies.drain(2)
|
||||
nn := notifies.drain(1)
|
||||
c.Assert([]string{"unpause"}, qt.DeepEquals, cc.getCalls())
|
||||
c.Assert(nn[0].LoginFinished, qt.Not(qt.IsNil))
|
||||
c.Assert(nn[1].State, qt.Not(qt.IsNil))
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[1].State)
|
||||
// NOTE: No LoginFinished message since no interactive
|
||||
// login was needed.
|
||||
c.Assert(nn[0].State, qt.Not(qt.IsNil))
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[0].State)
|
||||
// NOTE: No prefs change this time. WantRunning stays true.
|
||||
// We were in Starting in the first place, so that doesn't
|
||||
// change either.
|
||||
|
||||
@@ -6,7 +6,9 @@ package ipnserver
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -145,7 +147,7 @@ func (s *server) getConnIdentity(c net.Conn) (ci connIdentity, err error) {
|
||||
if err != nil {
|
||||
return ci, fmt.Errorf("parsing local remote: %w", err)
|
||||
}
|
||||
if !la.IP.IsLoopback() || !ra.IP.IsLoopback() {
|
||||
if !la.IP().IsLoopback() || !ra.IP().IsLoopback() {
|
||||
return ci, errors.New("non-loopback connection")
|
||||
}
|
||||
tab, err := netstat.Get()
|
||||
@@ -251,8 +253,7 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
return
|
||||
}
|
||||
defer c.Close()
|
||||
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
|
||||
bs := ipn.NewBackendServer(logf, nil, serverToClient)
|
||||
bs := ipn.NewBackendServer(logf, nil, jsonNotifier(c, s.logf))
|
||||
_, occupied := err.(inUseOtherUserError)
|
||||
if occupied {
|
||||
bs.SendInUseOtherUserErrorMessage(err.Error())
|
||||
@@ -567,7 +568,9 @@ func (s *server) setServerModeUserLocked() {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) writeToClients(b []byte) {
|
||||
var jsonEscapedZero = []byte(`\u0000`)
|
||||
|
||||
func (s *server) writeToClients(n ipn.Notify) {
|
||||
inServerMode := s.b.InServerMode()
|
||||
|
||||
s.mu.Lock()
|
||||
@@ -584,8 +587,17 @@ func (s *server) writeToClients(b []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
for c := range s.clients {
|
||||
ipn.WriteMsg(c, b)
|
||||
if len(s.clients) == 0 {
|
||||
// Common case (at least on busy servers): nobody
|
||||
// connected (no GUI, etc), so return before
|
||||
// serializing JSON.
|
||||
return
|
||||
}
|
||||
|
||||
if b, ok := marshalNotify(n, s.logf); ok {
|
||||
for c := range s.clients {
|
||||
ipn.WriteMsg(c, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -671,8 +683,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
|
||||
errMsg := err.Error()
|
||||
go func() {
|
||||
defer c.Close()
|
||||
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
|
||||
bs := ipn.NewBackendServer(logf, nil, serverToClient)
|
||||
bs := ipn.NewBackendServer(logf, nil, jsonNotifier(c, logf))
|
||||
bs.SendErrorMessage(errMsg)
|
||||
time.Sleep(time.Second)
|
||||
}()
|
||||
@@ -962,3 +973,25 @@ func peerPid(entries []netstat.Entry, la, ra netaddr.IPPort) int {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// jsonNotifier returns a notify-writer func that writes ipn.Notify
|
||||
// messages to w.
|
||||
func jsonNotifier(w io.Writer, logf logger.Logf) func(ipn.Notify) {
|
||||
return func(n ipn.Notify) {
|
||||
if b, ok := marshalNotify(n, logf); ok {
|
||||
ipn.WriteMsg(w, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func marshalNotify(n ipn.Notify, logf logger.Logf) (b []byte, ok bool) {
|
||||
b, err := json.Marshal(n)
|
||||
if err != nil {
|
||||
logf("ipnserver: [unexpected] error serializing JSON: %v", err)
|
||||
return nil, false
|
||||
}
|
||||
if bytes.Contains(b, jsonEscapedZero) {
|
||||
logf("[unexpected] zero byte in BackendServer.send notify message: %q", b)
|
||||
}
|
||||
return b, true
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ type PeerStatusLite struct {
|
||||
}
|
||||
|
||||
type PeerStatus struct {
|
||||
ID tailcfg.StableNodeID
|
||||
PublicKey key.Public
|
||||
HostName string // HostInfo's Hostname (not a DNS name or necessarily unique)
|
||||
DNSName string
|
||||
@@ -203,6 +204,9 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
|
||||
return
|
||||
}
|
||||
|
||||
if v := st.ID; v != "" {
|
||||
e.ID = v
|
||||
}
|
||||
if v := st.HostName; v != "" {
|
||||
e.HostName = v
|
||||
}
|
||||
|
||||
@@ -88,9 +88,9 @@ type Command struct {
|
||||
|
||||
type BackendServer struct {
|
||||
logf logger.Logf
|
||||
b Backend // the Backend we are serving up
|
||||
sendNotifyMsg func(jsonMsg []byte) // send a notification message
|
||||
GotQuit bool // a Quit command was received
|
||||
b Backend // the Backend we are serving up
|
||||
sendNotifyMsg func(Notify) // send a notification message
|
||||
GotQuit bool // a Quit command was received
|
||||
}
|
||||
|
||||
// NewBackendServer creates a new BackendServer using b.
|
||||
@@ -98,7 +98,7 @@ type BackendServer struct {
|
||||
// If sendNotifyMsg is non-nil, it additionally sets the Backend's
|
||||
// notification callback to call the func with ipn.Notify messages in
|
||||
// JSON form. If nil, it does not change the notification callback.
|
||||
func NewBackendServer(logf logger.Logf, b Backend, sendNotifyMsg func(b []byte)) *BackendServer {
|
||||
func NewBackendServer(logf logger.Logf, b Backend, sendNotifyMsg func(Notify)) *BackendServer {
|
||||
bs := &BackendServer{
|
||||
logf: logf,
|
||||
b: b,
|
||||
@@ -115,14 +115,7 @@ func (bs *BackendServer) send(n Notify) {
|
||||
return
|
||||
}
|
||||
n.Version = version.Long
|
||||
b, err := json.Marshal(n)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed json.Marshal(notify): %v\n%#v", err, n)
|
||||
}
|
||||
if bytes.Contains(b, jsonEscapedZero) {
|
||||
log.Printf("[unexpected] zero byte in BackendServer.send notify message: %q", b)
|
||||
}
|
||||
bs.sendNotifyMsg(b)
|
||||
bs.sendNotifyMsg(n)
|
||||
}
|
||||
|
||||
func (bs *BackendServer) SendErrorMessage(msg string) {
|
||||
|
||||
@@ -7,6 +7,7 @@ package ipn
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -74,7 +75,11 @@ func TestClientServer(t *testing.T) {
|
||||
bc.GotNotifyMsg(b)
|
||||
}
|
||||
}()
|
||||
serverToClient := func(b []byte) {
|
||||
serverToClient := func(n Notify) {
|
||||
b, err := json.Marshal(n)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
serverToClientCh <- append([]byte{}, b...)
|
||||
}
|
||||
clientToServer := func(b []byte) {
|
||||
|
||||
@@ -211,7 +211,7 @@ func (m *Manager) compileConfig(cfg Config) (resolver.Config, OSConfig, error) {
|
||||
func toIPsOnly(ipps []netaddr.IPPort) (ret []netaddr.IP) {
|
||||
ret = make([]netaddr.IP, 0, len(ipps))
|
||||
for _, ipp := range ipps {
|
||||
ret = append(ret, ipp.IP)
|
||||
ret = append(ret, ipp.IP())
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -219,7 +219,7 @@ func toIPsOnly(ipps []netaddr.IPPort) (ret []netaddr.IP) {
|
||||
func toIPPorts(ips []netaddr.IP) (ret []netaddr.IPPort) {
|
||||
ret = make([]netaddr.IPPort, 0, len(ips))
|
||||
for _, ip := range ips {
|
||||
ret = append(ret, netaddr.IPPort{IP: ip, Port: 53})
|
||||
ret = append(ret, netaddr.IPPortFrom(ip, 53))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -57,12 +57,12 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurat
|
||||
}
|
||||
if err := dbusPing("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/DnsManager"); err != nil {
|
||||
dbg("nm", "no")
|
||||
return newResolvedManager(logf)
|
||||
return newResolvedManager(logf, interfaceName)
|
||||
}
|
||||
dbg("nm", "yes")
|
||||
if err := nmIsUsingResolved(); err != nil {
|
||||
dbg("nm-resolved", "no")
|
||||
return newResolvedManager(logf)
|
||||
return newResolvedManager(logf, interfaceName)
|
||||
}
|
||||
dbg("nm-resolved", "yes")
|
||||
|
||||
@@ -90,7 +90,7 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurat
|
||||
return newNMManager(interfaceName)
|
||||
}
|
||||
dbg("nm-old", "no")
|
||||
return newResolvedManager(logf)
|
||||
return newResolvedManager(logf, interfaceName)
|
||||
case "resolvconf":
|
||||
dbg("rc", "resolvconf")
|
||||
if err := resolvconfSourceIsNM(bs); err == nil {
|
||||
|
||||
@@ -368,11 +368,12 @@ func TestManager(t *testing.T) {
|
||||
if err := m.Set(test.in); err != nil {
|
||||
t.Fatalf("m.Set: %v", err)
|
||||
}
|
||||
tr := cmp.Transformer("ipStr", func(ip netaddr.IP) string { return ip.String() })
|
||||
if diff := cmp.Diff(f.OSConfig, test.os, tr, cmpopts.EquateEmpty()); diff != "" {
|
||||
trIP := cmp.Transformer("ipStr", func(ip netaddr.IP) string { return ip.String() })
|
||||
trIPPort := cmp.Transformer("ippStr", func(ipp netaddr.IPPort) string { return ipp.String() })
|
||||
if diff := cmp.Diff(f.OSConfig, test.os, trIP, trIPPort, cmpopts.EquateEmpty()); diff != "" {
|
||||
t.Errorf("wrong OSConfig (-got+want)\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(f.ResolverConfig, test.rs, tr, cmpopts.EquateEmpty()); diff != "" {
|
||||
if diff := cmp.Diff(f.ResolverConfig, test.rs, trIP, trIPPort, cmpopts.EquateEmpty()); diff != "" {
|
||||
t.Errorf("wrong resolver.Config (-got+want)\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -175,9 +175,12 @@ func (m *nmManager) trySet(ctx context.Context, config OSConfig) error {
|
||||
search = append(search, "~.")
|
||||
}
|
||||
|
||||
general := settings["connection"]
|
||||
general["llmnr"] = dbus.MakeVariant(0)
|
||||
general["mdns"] = dbus.MakeVariant(0)
|
||||
// Ideally we would like to disable LLMNR and mdns on the
|
||||
// interface here, but older NetworkManagers don't understand
|
||||
// those settings and choke on them, so we don't. Both LLMNR and
|
||||
// mdns will fail since tailscale0 doesn't do multicast, so it's
|
||||
// effectively fine. We used to try and enforce LLMNR and mdns
|
||||
// settings here, but that led to #1870.
|
||||
|
||||
ipv4Map := settings["ipv4"]
|
||||
ipv4Map["dns"] = dbus.MakeVariant(dnsv4)
|
||||
@@ -247,7 +250,7 @@ func (m *nmManager) trySet(ctx context.Context, config OSConfig) error {
|
||||
}
|
||||
|
||||
if call := device.CallWithContext(ctx, "org.freedesktop.NetworkManager.Device.Reapply", 0, settings, version, uint32(0)); call.Err != nil {
|
||||
return fmt.Errorf("reapply: %w", err)
|
||||
return fmt.Errorf("reapply: %w", call.Err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -12,11 +12,11 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"golang.org/x/sys/unix"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
@@ -85,17 +85,24 @@ func isResolvedActive() bool {
|
||||
// resolvedManager uses the systemd-resolved DBus API.
|
||||
type resolvedManager struct {
|
||||
logf logger.Logf
|
||||
ifidx int
|
||||
resolved dbus.BusObject
|
||||
}
|
||||
|
||||
func newResolvedManager(logf logger.Logf) (*resolvedManager, error) {
|
||||
func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManager, error) {
|
||||
conn, err := dbus.SystemBus()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
iface, err := net.InterfaceByName(interfaceName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &resolvedManager{
|
||||
logf: logf,
|
||||
ifidx: iface.Index,
|
||||
resolved: conn.Object("org.freedesktop.resolve1", dbus.ObjectPath("/org/freedesktop/resolve1")),
|
||||
}, nil
|
||||
}
|
||||
@@ -105,16 +112,6 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
|
||||
defer cancel()
|
||||
|
||||
// In principle, we could persist this in the manager struct
|
||||
// if we knew that interface indices are persistent. This does not seem to be the case.
|
||||
_, iface, err := interfaces.Tailscale()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting interface index: %w", err)
|
||||
}
|
||||
if iface == nil {
|
||||
return errNotReady
|
||||
}
|
||||
|
||||
var linkNameservers = make([]resolvedLinkNameserver, len(config.Nameservers))
|
||||
for i, server := range config.Nameservers {
|
||||
ip := server.As16()
|
||||
@@ -131,9 +128,9 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
|
||||
}
|
||||
}
|
||||
|
||||
err = m.resolved.CallWithContext(
|
||||
err := m.resolved.CallWithContext(
|
||||
ctx, "org.freedesktop.resolve1.Manager.SetLinkDNS", 0,
|
||||
iface.Index, linkNameservers,
|
||||
m.ifidx, linkNameservers,
|
||||
).Store()
|
||||
if err != nil {
|
||||
return fmt.Errorf("setLinkDNS: %w", err)
|
||||
@@ -174,13 +171,13 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
|
||||
|
||||
err = m.resolved.CallWithContext(
|
||||
ctx, "org.freedesktop.resolve1.Manager.SetLinkDomains", 0,
|
||||
iface.Index, linkDomains,
|
||||
m.ifidx, linkDomains,
|
||||
).Store()
|
||||
if err != nil {
|
||||
return fmt.Errorf("setLinkDomains: %w", err)
|
||||
}
|
||||
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDefaultRoute", 0, iface.Index, len(config.MatchDomains) == 0); call.Err != nil {
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDefaultRoute", 0, m.ifidx, len(config.MatchDomains) == 0); call.Err != nil {
|
||||
return fmt.Errorf("setLinkDefaultRoute: %w", err)
|
||||
}
|
||||
|
||||
@@ -189,22 +186,22 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
|
||||
// or something).
|
||||
|
||||
// Disable LLMNR, we don't do multicast.
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkLLMNR", 0, iface.Index, "no"); call.Err != nil {
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkLLMNR", 0, m.ifidx, "no"); call.Err != nil {
|
||||
m.logf("[v1] failed to disable LLMNR: %v", call.Err)
|
||||
}
|
||||
|
||||
// Disable mdns.
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkMulticastDNS", 0, iface.Index, "no"); call.Err != nil {
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkMulticastDNS", 0, m.ifidx, "no"); call.Err != nil {
|
||||
m.logf("[v1] failed to disable mdns: %v", call.Err)
|
||||
}
|
||||
|
||||
// We don't support dnssec consistently right now, force it off to
|
||||
// avoid partial failures when we split DNS internally.
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDNSSEC", 0, iface.Index, "no"); call.Err != nil {
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDNSSEC", 0, m.ifidx, "no"); call.Err != nil {
|
||||
m.logf("[v1] failed to disable DNSSEC: %v", call.Err)
|
||||
}
|
||||
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDNSOverTLS", 0, iface.Index, "no"); call.Err != nil {
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDNSOverTLS", 0, m.ifidx, "no"); call.Err != nil {
|
||||
m.logf("[v1] failed to disable DoT: %v", call.Err)
|
||||
}
|
||||
|
||||
@@ -227,15 +224,7 @@ func (m *resolvedManager) Close() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
|
||||
defer cancel()
|
||||
|
||||
_, iface, err := interfaces.Tailscale()
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting interface index: %w", err)
|
||||
}
|
||||
if iface == nil {
|
||||
return errNotReady
|
||||
}
|
||||
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.RevertLink", 0, iface.Index); call.Err != nil {
|
||||
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.RevertLink", 0, m.ifidx); call.Err != nil {
|
||||
return fmt.Errorf("RevertLink: %w", call.Err)
|
||||
}
|
||||
|
||||
|
||||
@@ -433,8 +433,8 @@ func TestDelegateCollision(t *testing.T) {
|
||||
qtype dns.Type
|
||||
addr netaddr.IPPort
|
||||
}{
|
||||
{"test.site.", dns.TypeA, netaddr.IPPort{IP: netaddr.IPv4(1, 1, 1, 1), Port: 1001}},
|
||||
{"test.site.", dns.TypeAAAA, netaddr.IPPort{IP: netaddr.IPv4(1, 1, 1, 1), Port: 1002}},
|
||||
{"test.site.", dns.TypeA, netaddr.IPPortFrom(netaddr.IPv4(1, 1, 1, 1), 1001)},
|
||||
{"test.site.", dns.TypeAAAA, netaddr.IPPortFrom(netaddr.IPv4(1, 1, 1, 1), 1002)},
|
||||
}
|
||||
|
||||
// packets will have the same dns txid.
|
||||
|
||||
@@ -195,7 +195,7 @@ func ForeachInterface(fn func(Interface, []netaddr.IPPrefix)) error {
|
||||
}
|
||||
}
|
||||
sort.Slice(pfxs, func(i, j int) bool {
|
||||
return pfxs[i].IP.Less(pfxs[j].IP)
|
||||
return pfxs[i].IP().Less(pfxs[j].IP())
|
||||
})
|
||||
fn(Interface{iface}, pfxs)
|
||||
}
|
||||
@@ -264,7 +264,7 @@ func (s *State) String() string {
|
||||
fmt.Fprintf(&sb, "%s:[", ifName)
|
||||
needSpace := false
|
||||
for _, pfx := range s.InterfaceIPs[ifName] {
|
||||
if !isInterestingIP(pfx.IP) {
|
||||
if !isInterestingIP(pfx.IP()) {
|
||||
continue
|
||||
}
|
||||
if needSpace {
|
||||
@@ -367,7 +367,7 @@ func (s *State) AnyInterfaceUp() bool {
|
||||
|
||||
func hasTailscaleIP(pfxs []netaddr.IPPrefix) bool {
|
||||
for _, pfx := range pfxs {
|
||||
if tsaddr.IsTailscaleIP(pfx.IP) {
|
||||
if tsaddr.IsTailscaleIP(pfx.IP()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -407,11 +407,11 @@ func GetState() (*State, error) {
|
||||
return
|
||||
}
|
||||
for _, pfx := range pfxs {
|
||||
if pfx.IP.IsLoopback() || pfx.IP.IsLinkLocalUnicast() {
|
||||
if pfx.IP().IsLoopback() || pfx.IP().IsLinkLocalUnicast() {
|
||||
continue
|
||||
}
|
||||
s.HaveV6Global = s.HaveV6Global || isGlobalV6(pfx.IP)
|
||||
s.HaveV4 = s.HaveV4 || pfx.IP.Is4()
|
||||
s.HaveV6Global = s.HaveV6Global || isGlobalV6(pfx.IP())
|
||||
s.HaveV4 = s.HaveV4 || pfx.IP().Is4()
|
||||
}
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
@@ -447,7 +447,7 @@ func HTTPOfListener(ln net.Listener) string {
|
||||
var goodIP string
|
||||
var privateIP string
|
||||
ForeachInterfaceAddress(func(i Interface, pfx netaddr.IPPrefix) {
|
||||
ip := pfx.IP
|
||||
ip := pfx.IP()
|
||||
if isPrivateIP(ip) {
|
||||
if privateIP == "" {
|
||||
privateIP = ip.String()
|
||||
@@ -484,7 +484,7 @@ func LikelyHomeRouterIP() (gateway, myIP netaddr.IP, ok bool) {
|
||||
return
|
||||
}
|
||||
ForeachInterfaceAddress(func(i Interface, pfx netaddr.IPPrefix) {
|
||||
ip := pfx.IP
|
||||
ip := pfx.IP()
|
||||
if !i.IsUp() || ip.IsZero() || !myIP.IsZero() {
|
||||
return
|
||||
}
|
||||
@@ -528,7 +528,7 @@ var (
|
||||
// isInterestingIP.
|
||||
func anyInterestingIP(pfxs []netaddr.IPPrefix) bool {
|
||||
for _, pfx := range pfxs {
|
||||
if isInterestingIP(pfx.IP) {
|
||||
if isInterestingIP(pfx.IP()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
@@ -29,32 +28,9 @@ func DefaultRouteInterface() (string, error) {
|
||||
return iface.Name, nil
|
||||
}
|
||||
|
||||
// fetchRoutingTable is a retry loop around route.FetchRIB, fetching NET_RT_DUMP2.
|
||||
//
|
||||
// The retry loop is due to a bug in the BSDs (or Go?). See
|
||||
// https://github.com/tailscale/tailscale/issues/1345
|
||||
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP2.
|
||||
func fetchRoutingTable() (rib []byte, err error) {
|
||||
fails := 0
|
||||
for {
|
||||
rib, err := route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0)
|
||||
if err == nil {
|
||||
return rib, nil
|
||||
}
|
||||
fails++
|
||||
if fails < 10 {
|
||||
// Empirically, 1 retry is enough. In a long
|
||||
// stress test while toggling wifi on & off, I
|
||||
// only saw a few occurrences of 2 and one 3.
|
||||
// So 10 should be more plenty.
|
||||
if fails > 5 {
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("route.FetchRIB: %w", err)
|
||||
}
|
||||
}
|
||||
return route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0)
|
||||
}
|
||||
|
||||
func DefaultRouteInterfaceIndex() (int, error) {
|
||||
|
||||
@@ -625,7 +625,7 @@ func (rs *reportState) stopTimers() {
|
||||
func (rs *reportState) addNodeLatency(node *tailcfg.DERPNode, ipp netaddr.IPPort, d time.Duration) {
|
||||
var ipPortStr string
|
||||
if ipp != (netaddr.IPPort{}) {
|
||||
ipPortStr = net.JoinHostPort(ipp.IP.String(), fmt.Sprint(ipp.Port))
|
||||
ipPortStr = net.JoinHostPort(ipp.IP().String(), fmt.Sprint(ipp.Port()))
|
||||
}
|
||||
|
||||
rs.mu.Lock()
|
||||
@@ -650,13 +650,13 @@ func (rs *reportState) addNodeLatency(node *tailcfg.DERPNode, ipp netaddr.IPPort
|
||||
}
|
||||
|
||||
switch {
|
||||
case ipp.IP.Is6():
|
||||
case ipp.IP().Is6():
|
||||
updateLatency(ret.RegionV6Latency, node.RegionID, d)
|
||||
ret.IPv6 = true
|
||||
ret.GlobalV6 = ipPortStr
|
||||
// TODO: track MappingVariesByDestIP for IPv6
|
||||
// too? Would be sad if so, but who knows.
|
||||
case ipp.IP.Is4():
|
||||
case ipp.IP().Is4():
|
||||
updateLatency(ret.RegionV4Latency, node.RegionID, d)
|
||||
ret.IPv4 = true
|
||||
if rs.gotEP4 == "" {
|
||||
@@ -1172,7 +1172,7 @@ func (c *Client) nodeAddr(ctx context.Context, n *tailcfg.DERPNode, proto probeP
|
||||
if proto == probeIPv6 && ip.Is4() {
|
||||
return nil
|
||||
}
|
||||
return netaddr.IPPort{IP: ip, Port: uint16(port)}.UDPAddr()
|
||||
return netaddr.IPPortFrom(ip, uint16(port)).UDPAddr()
|
||||
}
|
||||
|
||||
switch proto {
|
||||
@@ -1182,7 +1182,7 @@ func (c *Client) nodeAddr(ctx context.Context, n *tailcfg.DERPNode, proto probeP
|
||||
if !ip.Is4() {
|
||||
return nil
|
||||
}
|
||||
return netaddr.IPPort{IP: ip, Port: uint16(port)}.UDPAddr()
|
||||
return netaddr.IPPortFrom(ip, uint16(port)).UDPAddr()
|
||||
}
|
||||
case probeIPv6:
|
||||
if n.IPv6 != "" {
|
||||
@@ -1190,7 +1190,7 @@ func (c *Client) nodeAddr(ctx context.Context, n *tailcfg.DERPNode, proto probeP
|
||||
if !ip.Is6() {
|
||||
return nil
|
||||
}
|
||||
return netaddr.IPPort{IP: ip, Port: uint16(port)}.UDPAddr()
|
||||
return netaddr.IPPortFrom(ip, uint16(port)).UDPAddr()
|
||||
}
|
||||
default:
|
||||
return nil
|
||||
|
||||
@@ -157,10 +157,9 @@ func ipport4(addr uint32, port uint16) netaddr.IPPort {
|
||||
if !endian.Big {
|
||||
addr = bits.ReverseBytes32(addr)
|
||||
}
|
||||
return netaddr.IPPort{
|
||||
IP: netaddr.IPv4(byte(addr>>24), byte(addr>>16), byte(addr>>8), byte(addr)),
|
||||
Port: port,
|
||||
}
|
||||
return netaddr.IPPortFrom(
|
||||
netaddr.IPv4(byte(addr>>24), byte(addr>>16), byte(addr>>8), byte(addr)),
|
||||
port)
|
||||
}
|
||||
|
||||
func ipport6(addr [16]byte, scope uint32, port uint16) netaddr.IPPort {
|
||||
@@ -169,10 +168,7 @@ func ipport6(addr [16]byte, scope uint32, port uint16) netaddr.IPPort {
|
||||
// TODO: something better here?
|
||||
ip = ip.WithZone(fmt.Sprint(scope))
|
||||
}
|
||||
return netaddr.IPPort{
|
||||
IP: ip,
|
||||
Port: port,
|
||||
}
|
||||
return netaddr.IPPortFrom(ip, port)
|
||||
}
|
||||
|
||||
func port(v *uint32) uint16 {
|
||||
|
||||
@@ -76,8 +76,8 @@ func (p *Parsed) String() string {
|
||||
//
|
||||
// TODO: make netaddr more efficient in this area, and retire this func.
|
||||
func writeIPPort(sb *strbuilder.Builder, ipp netaddr.IPPort) {
|
||||
if ipp.IP.Is4() {
|
||||
raw := ipp.IP.As4()
|
||||
if ipp.IP().Is4() {
|
||||
raw := ipp.IP().As4()
|
||||
sb.WriteUint(uint64(raw[0]))
|
||||
sb.WriteByte('.')
|
||||
sb.WriteUint(uint64(raw[1]))
|
||||
@@ -88,10 +88,10 @@ func writeIPPort(sb *strbuilder.Builder, ipp netaddr.IPPort) {
|
||||
sb.WriteByte(':')
|
||||
} else {
|
||||
sb.WriteByte('[')
|
||||
sb.WriteString(ipp.IP.String()) // TODO: faster?
|
||||
sb.WriteString(ipp.IP().String()) // TODO: faster?
|
||||
sb.WriteString("]:")
|
||||
}
|
||||
sb.WriteUint(uint64(ipp.Port))
|
||||
sb.WriteUint(uint64(ipp.Port()))
|
||||
}
|
||||
|
||||
// Decode extracts data from the packet in b into q.
|
||||
@@ -142,8 +142,8 @@ func (q *Parsed) decode4(b []byte) {
|
||||
}
|
||||
|
||||
// If it's valid IPv4, then the IP addresses are valid
|
||||
q.Src.IP = netaddr.IPv4(b[12], b[13], b[14], b[15])
|
||||
q.Dst.IP = netaddr.IPv4(b[16], b[17], b[18], b[19])
|
||||
q.Src = q.Src.WithIP(netaddr.IPv4(b[12], b[13], b[14], b[15]))
|
||||
q.Dst = q.Dst.WithIP(netaddr.IPv4(b[16], b[17], b[18], b[19]))
|
||||
|
||||
q.subofs = int((b[0] & 0x0F) << 2)
|
||||
if q.subofs > q.length {
|
||||
@@ -185,8 +185,8 @@ func (q *Parsed) decode4(b []byte) {
|
||||
q.IPProto = unknown
|
||||
return
|
||||
}
|
||||
q.Src.Port = 0
|
||||
q.Dst.Port = 0
|
||||
q.Src = q.Src.WithPort(0)
|
||||
q.Dst = q.Dst.WithPort(0)
|
||||
q.dataofs = q.subofs + icmp4HeaderLength
|
||||
return
|
||||
case ipproto.IGMP:
|
||||
@@ -198,8 +198,8 @@ func (q *Parsed) decode4(b []byte) {
|
||||
q.IPProto = unknown
|
||||
return
|
||||
}
|
||||
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.Src = q.Src.WithPort(binary.BigEndian.Uint16(sub[0:2]))
|
||||
q.Dst = q.Dst.WithPort(binary.BigEndian.Uint16(sub[2:4]))
|
||||
q.TCPFlags = TCPFlag(sub[13]) & 0x3F
|
||||
headerLength := (sub[12] & 0xF0) >> 2
|
||||
q.dataofs = q.subofs + int(headerLength)
|
||||
@@ -209,8 +209,8 @@ func (q *Parsed) decode4(b []byte) {
|
||||
q.IPProto = unknown
|
||||
return
|
||||
}
|
||||
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.Src = q.Src.WithPort(binary.BigEndian.Uint16(sub[0:2]))
|
||||
q.Dst = q.Dst.WithPort(binary.BigEndian.Uint16(sub[2:4]))
|
||||
q.dataofs = q.subofs + udpHeaderLength
|
||||
return
|
||||
case ipproto.SCTP:
|
||||
@@ -218,8 +218,8 @@ func (q *Parsed) decode4(b []byte) {
|
||||
q.IPProto = unknown
|
||||
return
|
||||
}
|
||||
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.Src = q.Src.WithPort(binary.BigEndian.Uint16(sub[0:2]))
|
||||
q.Dst = q.Dst.WithPort(binary.BigEndian.Uint16(sub[2:4]))
|
||||
return
|
||||
case ipproto.TSMP:
|
||||
// Inter-tailscale messages.
|
||||
@@ -265,8 +265,10 @@ func (q *Parsed) decode6(b []byte) {
|
||||
|
||||
// okay to ignore `ok` here, because IPs pulled from packets are
|
||||
// always well-formed stdlib IPs.
|
||||
q.Src.IP, _ = netaddr.FromStdIP(net.IP(b[8:24]))
|
||||
q.Dst.IP, _ = netaddr.FromStdIP(net.IP(b[24:40]))
|
||||
srcIP, _ := netaddr.FromStdIP(net.IP(b[8:24]))
|
||||
dstIP, _ := netaddr.FromStdIP(net.IP(b[24:40]))
|
||||
q.Src = q.Src.WithIP(srcIP)
|
||||
q.Dst = q.Dst.WithIP(dstIP)
|
||||
|
||||
// We don't support any IPv6 extension headers. Don't try to
|
||||
// be clever. Therefore, the IP subprotocol always starts at
|
||||
@@ -290,16 +292,16 @@ func (q *Parsed) decode6(b []byte) {
|
||||
q.IPProto = unknown
|
||||
return
|
||||
}
|
||||
q.Src.Port = 0
|
||||
q.Dst.Port = 0
|
||||
q.Src = q.Src.WithPort(0)
|
||||
q.Dst = q.Dst.WithPort(0)
|
||||
q.dataofs = q.subofs + icmp6HeaderLength
|
||||
case ipproto.TCP:
|
||||
if len(sub) < tcpHeaderLength {
|
||||
q.IPProto = unknown
|
||||
return
|
||||
}
|
||||
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.Src = q.Src.WithPort(binary.BigEndian.Uint16(sub[0:2]))
|
||||
q.Dst = q.Dst.WithPort(binary.BigEndian.Uint16(sub[2:4]))
|
||||
q.TCPFlags = TCPFlag(sub[13]) & 0x3F
|
||||
headerLength := (sub[12] & 0xF0) >> 2
|
||||
q.dataofs = q.subofs + int(headerLength)
|
||||
@@ -309,16 +311,16 @@ func (q *Parsed) decode6(b []byte) {
|
||||
q.IPProto = unknown
|
||||
return
|
||||
}
|
||||
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.Src = q.Src.WithPort(binary.BigEndian.Uint16(sub[0:2]))
|
||||
q.Dst = q.Dst.WithPort(binary.BigEndian.Uint16(sub[2:4]))
|
||||
q.dataofs = q.subofs + udpHeaderLength
|
||||
case ipproto.SCTP:
|
||||
if len(sub) < sctpHeaderLength {
|
||||
q.IPProto = unknown
|
||||
return
|
||||
}
|
||||
q.Src.Port = binary.BigEndian.Uint16(sub[0:2])
|
||||
q.Dst.Port = binary.BigEndian.Uint16(sub[2:4])
|
||||
q.Src = q.Src.WithPort(binary.BigEndian.Uint16(sub[0:2]))
|
||||
q.Dst = q.Dst.WithPort(binary.BigEndian.Uint16(sub[2:4]))
|
||||
return
|
||||
case ipproto.TSMP:
|
||||
// Inter-tailscale messages.
|
||||
@@ -338,8 +340,8 @@ func (q *Parsed) IP4Header() IP4Header {
|
||||
return IP4Header{
|
||||
IPID: ipid,
|
||||
IPProto: q.IPProto,
|
||||
Src: q.Src.IP,
|
||||
Dst: q.Dst.IP,
|
||||
Src: q.Src.IP(),
|
||||
Dst: q.Dst.IP(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,8 +353,8 @@ func (q *Parsed) IP6Header() IP6Header {
|
||||
return IP6Header{
|
||||
IPID: ipid,
|
||||
IPProto: q.IPProto,
|
||||
Src: q.Src.IP,
|
||||
Dst: q.Dst.IP,
|
||||
Src: q.Src.IP(),
|
||||
Dst: q.Dst.IP(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,8 +375,8 @@ func (q *Parsed) UDP4Header() UDP4Header {
|
||||
}
|
||||
return UDP4Header{
|
||||
IP4Header: q.IP4Header(),
|
||||
SrcPort: q.Src.Port,
|
||||
DstPort: q.Dst.Port,
|
||||
SrcPort: q.Src.Port(),
|
||||
DstPort: q.Dst.Port(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -143,7 +143,7 @@ func (h TailscaleRejectedHeader) Marshal(buf []byte) error {
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
if h.Src.IP.Is4() {
|
||||
if h.Src.IP().Is4() {
|
||||
iph := IP4Header{
|
||||
IPProto: ipproto.TSMP,
|
||||
Src: h.IPSrc,
|
||||
@@ -151,7 +151,7 @@ func (h TailscaleRejectedHeader) Marshal(buf []byte) error {
|
||||
}
|
||||
iph.Marshal(buf)
|
||||
buf = buf[ip4HeaderLength:]
|
||||
} else if h.Src.IP.Is6() {
|
||||
} else if h.Src.IP().Is6() {
|
||||
iph := IP6Header{
|
||||
IPProto: ipproto.TSMP,
|
||||
Src: h.IPSrc,
|
||||
@@ -165,8 +165,8 @@ func (h TailscaleRejectedHeader) Marshal(buf []byte) error {
|
||||
buf[0] = byte(TSMPTypeRejectedConn)
|
||||
buf[1] = byte(h.Proto)
|
||||
buf[2] = byte(h.Reason)
|
||||
binary.BigEndian.PutUint16(buf[3:5], h.Src.Port)
|
||||
binary.BigEndian.PutUint16(buf[5:7], h.Dst.Port)
|
||||
binary.BigEndian.PutUint16(buf[3:5], h.Src.Port())
|
||||
binary.BigEndian.PutUint16(buf[5:7], h.Dst.Port())
|
||||
|
||||
if h.hasFlags() {
|
||||
var flags byte
|
||||
@@ -190,10 +190,10 @@ func (pp *Parsed) AsTailscaleRejectedHeader() (h TailscaleRejectedHeader, ok boo
|
||||
h = TailscaleRejectedHeader{
|
||||
Proto: ipproto.Proto(p[1]),
|
||||
Reason: TailscaleRejectReason(p[2]),
|
||||
IPSrc: pp.Src.IP,
|
||||
IPDst: pp.Dst.IP,
|
||||
Src: netaddr.IPPort{IP: pp.Dst.IP, Port: binary.BigEndian.Uint16(p[3:5])},
|
||||
Dst: netaddr.IPPort{IP: pp.Src.IP, Port: binary.BigEndian.Uint16(p[5:7])},
|
||||
IPSrc: pp.Src.IP(),
|
||||
IPDst: pp.Dst.IP(),
|
||||
Src: netaddr.IPPortFrom(pp.Dst.IP(), binary.BigEndian.Uint16(p[3:5])),
|
||||
Dst: netaddr.IPPortFrom(pp.Src.IP(), binary.BigEndian.Uint16(p[5:7])),
|
||||
}
|
||||
if len(p) > 7 {
|
||||
flags := p[7]
|
||||
|
||||
@@ -84,7 +84,7 @@ type pmpMapping struct {
|
||||
|
||||
// externalValid reports whether m.external is valid, with both its IP and Port populated.
|
||||
func (m *pmpMapping) externalValid() bool {
|
||||
return !m.external.IP.IsZero() && m.external.Port != 0
|
||||
return !m.external.IP().IsZero() && m.external.Port() != 0
|
||||
}
|
||||
|
||||
// release does a best effort fire-and-forget release of the PMP mapping m.
|
||||
@@ -94,8 +94,8 @@ func (m *pmpMapping) release() {
|
||||
return
|
||||
}
|
||||
defer uc.Close()
|
||||
pkt := buildPMPRequestMappingPacket(m.internal.Port, m.external.Port, pmpMapLifetimeDelete)
|
||||
uc.WriteTo(pkt, netaddr.IPPort{IP: m.gw, Port: pmpPort}.UDPAddr())
|
||||
pkt := buildPMPRequestMappingPacket(m.internal.Port(), m.external.Port(), pmpMapLifetimeDelete)
|
||||
uc.WriteTo(pkt, netaddr.IPPortFrom(m.gw, pmpPort).UDPAddr())
|
||||
}
|
||||
|
||||
// NewClient returns a new portmapping client.
|
||||
@@ -256,7 +256,7 @@ func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
localPort := c.localPort
|
||||
m := &pmpMapping{
|
||||
gw: gw,
|
||||
internal: netaddr.IPPort{IP: myIP, Port: localPort},
|
||||
internal: netaddr.IPPortFrom(myIP, localPort),
|
||||
}
|
||||
|
||||
// prevPort is the port we had most previously, if any. We try
|
||||
@@ -271,7 +271,7 @@ func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
return m.external, nil
|
||||
}
|
||||
// The mapping might still be valid, so just try to renew it.
|
||||
prevPort = m.external.Port
|
||||
prevPort = m.external.Port()
|
||||
}
|
||||
|
||||
// If we just did a Probe (e.g. via netchecker) but didn't
|
||||
@@ -279,7 +279,7 @@ func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
// again. Cuts down latency for most clients.
|
||||
haveRecentPMP := c.sawPMPRecentlyLocked()
|
||||
if haveRecentPMP {
|
||||
m.external.IP = c.pmpPubIP
|
||||
m.external = m.external.WithIP(c.pmpPubIP)
|
||||
}
|
||||
if c.lastProbe.After(now.Add(-5*time.Second)) && !haveRecentPMP {
|
||||
c.mu.Unlock()
|
||||
@@ -297,11 +297,11 @@ func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
uc.SetReadDeadline(time.Now().Add(portMapServiceTimeout))
|
||||
defer closeCloserOnContextDone(ctx, uc)()
|
||||
|
||||
pmpAddr := netaddr.IPPort{IP: gw, Port: pmpPort}
|
||||
pmpAddr := netaddr.IPPortFrom(gw, pmpPort)
|
||||
pmpAddru := pmpAddr.UDPAddr()
|
||||
|
||||
// Ask for our external address if needed.
|
||||
if m.external.IP.IsZero() {
|
||||
if m.external.IP().IsZero() {
|
||||
if _, err := uc.WriteTo(pmpReqExternalAddrPacket, pmpAddru); err != nil {
|
||||
return netaddr.IPPort{}, err
|
||||
}
|
||||
@@ -337,10 +337,10 @@ func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
return netaddr.IPPort{}, NoMappingError{fmt.Errorf("PMP response Op=0x%x,Res=0x%x", pres.OpCode, pres.ResultCode)}
|
||||
}
|
||||
if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr {
|
||||
m.external.IP = pres.PublicAddr
|
||||
m.external = m.external.WithIP(pres.PublicAddr)
|
||||
}
|
||||
if pres.OpCode == pmpOpReply|pmpOpMapUDP {
|
||||
m.external.Port = pres.ExternalPort
|
||||
m.external = m.external.WithPort(pres.ExternalPort)
|
||||
d := time.Duration(pres.MappingValidSeconds) * time.Second
|
||||
d /= 2 // renew in half the time
|
||||
m.useUntil = time.Now().Add(d)
|
||||
@@ -468,9 +468,9 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
defer cancel()
|
||||
defer closeCloserOnContextDone(ctx, uc)()
|
||||
|
||||
pcpAddr := netaddr.IPPort{IP: gw, Port: pcpPort}.UDPAddr()
|
||||
pmpAddr := netaddr.IPPort{IP: gw, Port: pmpPort}.UDPAddr()
|
||||
upnpAddr := netaddr.IPPort{IP: gw, Port: upnpPort}.UDPAddr()
|
||||
pcpAddr := netaddr.IPPortFrom(gw, pcpPort).UDPAddr()
|
||||
pmpAddr := netaddr.IPPortFrom(gw, pmpPort).UDPAddr()
|
||||
upnpAddr := netaddr.IPPortFrom(gw, upnpPort).UDPAddr()
|
||||
|
||||
// Don't send probes to services that we recently learned (for
|
||||
// the same gw/myIP) are available. See
|
||||
|
||||
@@ -92,7 +92,7 @@ func TailscaleEphemeral6Range() netaddr.IPPrefix {
|
||||
// Currently used to work around a Windows limitation when programming
|
||||
// IPv6 routes in corner cases.
|
||||
func Tailscale4To6Placeholder() netaddr.IP {
|
||||
return Tailscale4To6Range().IP
|
||||
return Tailscale4To6Range().IP()
|
||||
}
|
||||
|
||||
// Tailscale4To6 returns a Tailscale IPv6 address that maps 1:1 to the
|
||||
@@ -102,7 +102,7 @@ func Tailscale4To6(ipv4 netaddr.IP) netaddr.IP {
|
||||
if !ipv4.Is4() || !IsTailscaleIP(ipv4) {
|
||||
return netaddr.IP{}
|
||||
}
|
||||
ret := Tailscale4To6Range().IP.As16()
|
||||
ret := Tailscale4To6Range().IP().As16()
|
||||
v4 := ipv4.As4()
|
||||
copy(ret[13:], v4[1:])
|
||||
return netaddr.IPFrom16(ret)
|
||||
@@ -172,16 +172,16 @@ func NewContainsIPFunc(addrs []netaddr.IPPrefix) func(ip netaddr.IP) bool {
|
||||
// Fast paths for 1 and 2 IPs:
|
||||
if len(addrs) == 1 {
|
||||
a := addrs[0]
|
||||
return func(ip netaddr.IP) bool { return ip == a.IP }
|
||||
return func(ip netaddr.IP) bool { return ip == a.IP() }
|
||||
}
|
||||
if len(addrs) == 2 {
|
||||
a, b := addrs[0], addrs[1]
|
||||
return func(ip netaddr.IP) bool { return ip == a.IP || ip == b.IP }
|
||||
return func(ip netaddr.IP) bool { return ip == a.IP() || ip == b.IP() }
|
||||
}
|
||||
// General case:
|
||||
m := map[netaddr.IP]bool{}
|
||||
for _, a := range addrs {
|
||||
m[a.IP] = true
|
||||
m[a.IP()] = true
|
||||
}
|
||||
return func(ip netaddr.IP) bool { return m[ip] }
|
||||
}
|
||||
|
||||
@@ -352,7 +352,7 @@ func (t *Wrapper) Read(buf []byte, offset int) (int, error) {
|
||||
p.Decode(buf[offset : offset+n])
|
||||
|
||||
if m, ok := t.destIPActivity.Load().(map[netaddr.IP]func()); ok {
|
||||
if fn := m[p.Dst.IP]; fn != nil {
|
||||
if fn := m[p.Dst.IP()]; fn != nil {
|
||||
fn()
|
||||
}
|
||||
}
|
||||
@@ -412,7 +412,7 @@ func (t *Wrapper) filterIn(buf []byte) filter.Response {
|
||||
p.IPProto == ipproto.TCP &&
|
||||
p.TCPFlags&packet.TCPSyn != 0 &&
|
||||
t.PeerAPIPort != nil {
|
||||
if port, ok := t.PeerAPIPort(p.Dst.IP); ok && port == p.Dst.Port {
|
||||
if port, ok := t.PeerAPIPort(p.Dst.IP()); ok && port == p.Dst.Port() {
|
||||
outcome = filter.Accept
|
||||
}
|
||||
}
|
||||
@@ -425,8 +425,8 @@ func (t *Wrapper) filterIn(buf []byte) filter.Response {
|
||||
// can show them a rejection history with reasons.
|
||||
if p.IPVersion == 4 && p.IPProto == ipproto.TCP && p.TCPFlags&packet.TCPSyn != 0 && !t.disableTSMPRejected {
|
||||
rj := packet.TailscaleRejectedHeader{
|
||||
IPSrc: p.Dst.IP,
|
||||
IPDst: p.Src.IP,
|
||||
IPSrc: p.Dst.IP(),
|
||||
IPDst: p.Src.IP(),
|
||||
Src: p.Src,
|
||||
Dst: p.Dst,
|
||||
Proto: p.IPProto,
|
||||
@@ -457,13 +457,22 @@ func (t *Wrapper) filterIn(buf []byte) filter.Response {
|
||||
// like wireguard-go/tun.Device.Write.
|
||||
func (t *Wrapper) Write(buf []byte, offset int) (int, error) {
|
||||
if !t.disableFilter {
|
||||
res := t.filterIn(buf[offset:])
|
||||
if res == filter.DropSilently {
|
||||
if t.filterIn(buf[offset:]) != filter.Accept {
|
||||
// If we're not accepting the packet, lie to wireguard-go and pretend
|
||||
// that everything is okay with a nil error, so wireguard-go
|
||||
// doesn't log about this Write "failure".
|
||||
//
|
||||
// We return len(buf), but the ill-defined wireguard-go/tun.Device.Write
|
||||
// method doesn't specify how the offset affects the return value.
|
||||
// In fact, the Linux implementation does one of two different things depending
|
||||
// on how the /dev/net/tun was created. But fortunately the wireguard-go
|
||||
// code ignores the int return and only looks at the error:
|
||||
//
|
||||
// device/receive.go: _, err = device.tun.device.Write(....)
|
||||
//
|
||||
// TODO(bradfitz): fix upstream interface docs, implementation.
|
||||
return len(buf), nil
|
||||
}
|
||||
if res != filter.Accept {
|
||||
return 0, ErrFiltered
|
||||
}
|
||||
}
|
||||
|
||||
t.noteActivity()
|
||||
@@ -527,7 +536,7 @@ func (t *Wrapper) injectOutboundPong(pp *packet.Parsed, req packet.TSMPPingReque
|
||||
Data: req.Data,
|
||||
}
|
||||
if t.PeerAPIPort != nil {
|
||||
pong.PeerAPIPort, _ = t.PeerAPIPort(pp.Dst.IP)
|
||||
pong.PeerAPIPort, _ = t.PeerAPIPort(pp.Dst.IP())
|
||||
}
|
||||
switch pp.IPVersion {
|
||||
case 4:
|
||||
|
||||
@@ -82,7 +82,7 @@ func nets(nets ...string) (ret []netaddr.IPPrefix) {
|
||||
if ip.Is6() {
|
||||
bits = 128
|
||||
}
|
||||
ret = append(ret, netaddr.IPPrefix{IP: ip, Bits: bits})
|
||||
ret = append(ret, netaddr.IPPrefixFrom(ip, bits))
|
||||
} else {
|
||||
pfx, err := netaddr.ParseIPPrefix(s)
|
||||
if err != nil {
|
||||
@@ -329,11 +329,14 @@ func TestFilter(t *testing.T) {
|
||||
var filtered bool
|
||||
|
||||
if tt.dir == in {
|
||||
// Use the side effect of updating the last
|
||||
// activity atomic to determine whether the
|
||||
// data was actually filtered.
|
||||
// If it stays zero, nothing made it through
|
||||
// to the wrapped TUN.
|
||||
atomic.StoreInt64(&tun.lastActivityAtomic, 0)
|
||||
_, err = tun.Write(tt.data, 0)
|
||||
if err == ErrFiltered {
|
||||
filtered = true
|
||||
err = nil
|
||||
}
|
||||
filtered = atomic.LoadInt64(&tun.lastActivityAtomic) == 0
|
||||
} else {
|
||||
chtun.Outbound <- tt.data
|
||||
n, err = tun.Read(buf[:], 0)
|
||||
|
||||
399
scripts/installer.sh
Executable file
399
scripts/installer.sh
Executable file
@@ -0,0 +1,399 @@
|
||||
#!/bin/sh
|
||||
# Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
# Use of this source code is governed by a BSD-style
|
||||
# license that can be found in the LICENSE file.
|
||||
#
|
||||
# This script detects the current operating system, and installs
|
||||
# Tailscale according to that OS's conventions.
|
||||
|
||||
set -eu
|
||||
|
||||
# All the code is wrapped in a main function that gets called at the
|
||||
# bottom of the file, so that a truncated partial download doesn't end
|
||||
# up executing half a script.
|
||||
main() {
|
||||
# Step 1: detect the current linux distro, version, and packaging system.
|
||||
#
|
||||
# We rely on a combination of 'uname' and /etc/os-release to find
|
||||
# an OS name and version, and from there work out what
|
||||
# installation method we should be using.
|
||||
#
|
||||
# The end result of this step is that the following three
|
||||
# variables are populated, if detection was successful.
|
||||
OS=""
|
||||
VERSION=""
|
||||
PACKAGETYPE=""
|
||||
|
||||
if [ -f /etc/os-release ]; then
|
||||
# /etc/os-release populates a number of shell variables. We care about the following:
|
||||
# - ID: the short name of the OS (e.g. "debian", "freebsd")
|
||||
# - VERSION_ID: the numeric release version for the OS, if any (e.g. "18.04")
|
||||
# - VERSION_CODENAME: the codename of the OS release, if any (e.g. "buster")
|
||||
. /etc/os-release
|
||||
case "$ID" in
|
||||
ubuntu)
|
||||
OS="$ID"
|
||||
VERSION="$VERSION_CODENAME"
|
||||
PACKAGETYPE="apt"
|
||||
;;
|
||||
debian)
|
||||
OS="$ID"
|
||||
VERSION="$VERSION_CODENAME"
|
||||
PACKAGETYPE="apt"
|
||||
;;
|
||||
raspbian)
|
||||
OS="$ID"
|
||||
VERSION="$VERSION_CODENAME"
|
||||
PACKAGETYPE="apt"
|
||||
;;
|
||||
centos)
|
||||
OS="$ID"
|
||||
VERSION="$VERSION_ID"
|
||||
PACKAGETYPE="dnf"
|
||||
if [ "$VERSION" = "7" ]; then
|
||||
PACKAGETYPE="yum"
|
||||
fi
|
||||
;;
|
||||
rhel)
|
||||
OS="$ID"
|
||||
VERSION="$(echo "$VERSION_ID" | cut -f1 -d.)"
|
||||
PACKAGETYPE="dnf"
|
||||
;;
|
||||
fedora)
|
||||
OS="$ID"
|
||||
VERSION=""
|
||||
PACKAGETYPE="dnf"
|
||||
;;
|
||||
amzn)
|
||||
OS="amazon-linux"
|
||||
VERSION="$VERSION_ID"
|
||||
PACKAGETYPE="yum"
|
||||
;;
|
||||
opensuse-leap)
|
||||
OS="opensuse"
|
||||
VERSION="leap/$VERSION_ID"
|
||||
PACKAGETYPE="zypper"
|
||||
;;
|
||||
opensuse-tumbleweed)
|
||||
OS="opensuse"
|
||||
VERSION="tumbleweed"
|
||||
PACKAGETYPE="zypper"
|
||||
;;
|
||||
arch)
|
||||
OS="$ID"
|
||||
VERSION="" # rolling release
|
||||
PACKAGETYPE="pacman"
|
||||
;;
|
||||
manjaro)
|
||||
OS="$ID"
|
||||
VERSION="" # rolling release
|
||||
PACKAGETYPE="pacman"
|
||||
;;
|
||||
alpine)
|
||||
OS="$ID"
|
||||
VERSION="$VERSION_ID"
|
||||
PACKAGETYPE="apk"
|
||||
;;
|
||||
nixos)
|
||||
echo "Please add Tailscale to your NixOS configuration directly:"
|
||||
echo
|
||||
echo "services.tailscale.enable = true;"
|
||||
exit 1
|
||||
;;
|
||||
void)
|
||||
OS="$ID"
|
||||
VERSION="" # rolling release
|
||||
PACKAGETYPE="xbps"
|
||||
;;
|
||||
gentoo)
|
||||
OS="$ID"
|
||||
VERSION="" # rolling release
|
||||
PACKAGETYPE="emerge"
|
||||
;;
|
||||
freebsd)
|
||||
OS="$ID"
|
||||
VERSION="$(echo "$VERSION_ID" | cut -f1 -d.)"
|
||||
PACKAGETYPE="pkg"
|
||||
;;
|
||||
# TODO: wsl?
|
||||
# TODO: synology? qnap?
|
||||
esac
|
||||
fi
|
||||
|
||||
# If we failed to detect something through os-release, consult
|
||||
# uname and try to infer things from that.
|
||||
if [ -z "$OS" ]; then
|
||||
if type uname >/dev/null 2>&1; then
|
||||
case "$(uname)" in
|
||||
FreeBSD)
|
||||
# FreeBSD before 12.2 doesn't have
|
||||
# /etc/os-release, so we wouldn't have found it in
|
||||
# the os-release probing above.
|
||||
OS="freebsd"
|
||||
VERSION="$(freebsd-version | cut -f1 -d.)"
|
||||
PACKAGETYPE="pkg"
|
||||
;;
|
||||
OpenBSD)
|
||||
OS="openbsd"
|
||||
VERSION="$(uname -r)"
|
||||
PACKAGETYPE=""
|
||||
;;
|
||||
Darwin)
|
||||
OS="macos"
|
||||
VERSION="$(sw_vers -productVersion | cut -f1-2 -d.)"
|
||||
PACKAGETYPE="appstore"
|
||||
;;
|
||||
Linux)
|
||||
OS="other-linux"
|
||||
VERSION=""
|
||||
PACKAGETYPE=""
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 2: having detected an OS we support, is it one of the
|
||||
# versions we support?
|
||||
OS_UNSUPPORTED=
|
||||
case "$OS" in
|
||||
ubuntu)
|
||||
if [ "$VERSION" != "xenial" ] && \
|
||||
[ "$VERSION" != "bionic" ] && \
|
||||
[ "$VERSION" != "eoan" ] && \
|
||||
[ "$VERSION" != "focal" ] && \
|
||||
[ "$VERSION" != "groovy" ] && \
|
||||
[ "$VERSION" != "hirsute" ]
|
||||
then
|
||||
OS_UNSUPPORTED=1
|
||||
fi
|
||||
;;
|
||||
debian)
|
||||
if [ "$VERSION" != "stretch" ] && \
|
||||
[ "$VERSION" != "buster" ] && \
|
||||
[ "$VERSION" != "bullseye" ] && \
|
||||
[ "$VERSION" != "sid" ]
|
||||
then
|
||||
OS_UNSUPPORTED=1
|
||||
fi
|
||||
;;
|
||||
raspbian)
|
||||
if [ "$VERSION" != "buster" ]
|
||||
then
|
||||
OS_UNSUPPORTED=1
|
||||
fi
|
||||
;;
|
||||
centos)
|
||||
if [ "$VERSION" != "7" ] && \
|
||||
[ "$VERSION" != "8" ]
|
||||
then
|
||||
OS_UNSUPPORTED=1
|
||||
fi
|
||||
;;
|
||||
rhel)
|
||||
if [ "$VERSION" != "8" ]
|
||||
then
|
||||
OS_UNSUPPORTED=1
|
||||
fi
|
||||
;;
|
||||
amazon-linux)
|
||||
if [ "$VERSION" != "2" ]
|
||||
then
|
||||
OS_UNSUPPORTED=1
|
||||
fi
|
||||
;;
|
||||
opensuse)
|
||||
if [ "$VERSION" != "leap/15.1" ] && \
|
||||
[ "$VERSION" != "leap/15.2" ] && \
|
||||
[ "$VERSION" != "tumbleweed" ]
|
||||
then
|
||||
OS_UNSUPPORTED=1
|
||||
fi
|
||||
;;
|
||||
arch)
|
||||
# Rolling release, no version checking needed.
|
||||
;;
|
||||
manjaro)
|
||||
# Rolling release, no version checking needed.
|
||||
;;
|
||||
alpine)
|
||||
# All versions supported, no version checking needed.
|
||||
# TODO: is that true? When was tailscale packaged?
|
||||
;;
|
||||
void)
|
||||
# Rolling release, no version checking needed.
|
||||
;;
|
||||
gentoo)
|
||||
# Rolling release, no version checking needed.
|
||||
;;
|
||||
freebsd)
|
||||
if [ "$VERSION" != "12" ] && \
|
||||
[ "$VERSION" != "13" ]
|
||||
then
|
||||
OS_UNSUPPORTED=1
|
||||
fi
|
||||
;;
|
||||
openbsd)
|
||||
OS_UNSUPPORTED=1
|
||||
;;
|
||||
macos)
|
||||
# We delegate macOS installation to the app store, it will
|
||||
# perform version checks for us.
|
||||
;;
|
||||
other-linux)
|
||||
OS_UNSUPPORTED=1
|
||||
;;
|
||||
*)
|
||||
OS_UNSUPPORTED=1
|
||||
;;
|
||||
esac
|
||||
if [ "$OS_UNSUPPORTED" = "1" ]; then
|
||||
case "$OS" in
|
||||
other-linux)
|
||||
echo "Couldn't determine what kind of Linux is running."
|
||||
echo "You could try the static binaries at:"
|
||||
echo "https://pkgs.tailscale.com/stable/#static"
|
||||
;;
|
||||
"")
|
||||
echo "Couldn't determine what operating system you're running."
|
||||
;;
|
||||
*)
|
||||
echo "$OS $VERSION isn't supported by this script yet."
|
||||
;;
|
||||
esac
|
||||
echo
|
||||
echo "If you'd like us to support your system better, please email support@tailscale.com"
|
||||
echo "and tell us what OS you're running."
|
||||
echo
|
||||
echo "Please include the following information we gathered from your system:"
|
||||
echo
|
||||
echo "OS=$OS"
|
||||
echo "VERSION=$VERSION"
|
||||
echo "PACKAGETYPE=$PACKAGETYPE"
|
||||
if type uname >/dev/null 2>&1; then
|
||||
echo "UNAME=$(uname -a)"
|
||||
else
|
||||
echo "UNAME="
|
||||
fi
|
||||
echo
|
||||
if [ -f /etc/os-release ]; then
|
||||
cat /etc/os-release
|
||||
else
|
||||
echo "No /etc/os-release"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 3: work out if we can run privileged commands, and if so,
|
||||
# how.
|
||||
CAN_ROOT=
|
||||
SUDO=
|
||||
if [ "$(id -u)" = 0 ]; then
|
||||
CAN_ROOT=1
|
||||
SUDO=""
|
||||
elif type sudo >/dev/null; then
|
||||
CAN_ROOT=1
|
||||
SUDO="sudo"
|
||||
elif type doas >/dev/null; then
|
||||
CAN_ROOT=1
|
||||
SUDO="doas"
|
||||
fi
|
||||
if [ "$CAN_ROOT" != "1" ]; then
|
||||
echo "This installer needs to run commands as root."
|
||||
echo "We tried looking for 'sudo' and 'doas', but couldn't find them."
|
||||
echo "Either re-run this script as root, or set up sudo/doas."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# Step 4: run the installation.
|
||||
echo "Installing Tailscale for $OS $VERSION, using method $PACKAGETYPE"
|
||||
case "$PACKAGETYPE" in
|
||||
apt)
|
||||
# Ideally we want to use curl, but on some installs we
|
||||
# only have wget. Detect and use what's available.
|
||||
CURL=
|
||||
if type curl >/dev/null; then
|
||||
CURL="curl -fsSL"
|
||||
elif type wget >/dev/null; then
|
||||
CURL="wget -q -O-"
|
||||
fi
|
||||
if [ -z "$CURL" ]; then
|
||||
echo "The installer needs either curl or wget to download files."
|
||||
echo "Please install either curl or wget to proceed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# TODO: use newfangled per-repo signature scheme
|
||||
set -x
|
||||
$CURL "https://pkgs.tailscale.com/stable/$OS/$VERSION.gpg" | $SUDO apt-key add -
|
||||
$CURL "https://pkgs.tailscale.com/stable/$OS/$VERSION.list" | $SUDO tee /etc/apt/sources.list.d/tailscale.list
|
||||
$SUDO apt-get update
|
||||
$SUDO apt-get install tailscale
|
||||
set +x
|
||||
;;
|
||||
yum)
|
||||
set -x
|
||||
$SUDO yum install yum-utils
|
||||
$SUDO yum-config-manager --add-repo "https://pkgs.tailscale.com/stable/$OS/$VERSION/tailscale.repo"
|
||||
$SUDO yum install tailscale
|
||||
$SUDO systemctl enable --now tailscaled
|
||||
set +x
|
||||
;;
|
||||
dnf)
|
||||
set -x
|
||||
$SUDO dnf config-manager --add-repo "https://pkgs.tailscale.com/stable/$OS/$VERSION/tailscale.repo"
|
||||
$SUDO dnf install tailscale
|
||||
$SUDO systemctl enable --now tailscaled
|
||||
set +x
|
||||
;;
|
||||
zypper)
|
||||
set -x
|
||||
$SUDO zypper ar -g -r "https://pkgs.tailscale.com/stable/$OS/$VERSION/tailscale.repo"
|
||||
$SUDO zypper ref
|
||||
$SUDO zypper in tailscale
|
||||
$SUDO systemctl enable --now tailscaled
|
||||
set +x
|
||||
;;
|
||||
pacman)
|
||||
set -x
|
||||
$SUDO pacman -S tailscale
|
||||
$SUDO systemctl enable --now tailscaled
|
||||
set +x
|
||||
;;
|
||||
apk)
|
||||
set -x
|
||||
$SUDO apk add tailscale
|
||||
$SUDO rc-update add tailscale
|
||||
set +x
|
||||
;;
|
||||
xbps)
|
||||
set -x
|
||||
$SUDO xbps-install tailscale
|
||||
set +x
|
||||
;;
|
||||
emerge)
|
||||
set -x
|
||||
$SUDO emerge net-vpn/tailscale
|
||||
set +x
|
||||
;;
|
||||
appstore)
|
||||
set -x
|
||||
open "https://apps.apple.com/us/app/tailscale/id1475387142"
|
||||
set +x
|
||||
;;
|
||||
*)
|
||||
echo "unexpected: unknown package type $PACKAGETYPE"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Installation complete! Log in to start using Tailscale by running:"
|
||||
echo
|
||||
if [ -z "$SUDO" ]; then
|
||||
echo "tailscale up"
|
||||
else
|
||||
echo "$SUDO tailscale up"
|
||||
fi
|
||||
}
|
||||
|
||||
main
|
||||
@@ -7,7 +7,7 @@ package tailcfg
|
||||
//go:generate go run tailscale.com/cmd/cloner --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse --clonefunc=true --output=tailcfg_clone.go
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
@@ -679,7 +679,7 @@ func (et EndpointType) String() string {
|
||||
|
||||
// Endpoint is an endpoint IPPort and an associated type.
|
||||
// It doesn't currently go over the wire as is but is instead
|
||||
// broken up into two parallel slices in MapReqeust, for compatibility
|
||||
// broken up into two parallel slices in MapRequest, for compatibility
|
||||
// reasons. But this type is used in the codebase.
|
||||
type Endpoint struct {
|
||||
Addr netaddr.IPPort
|
||||
@@ -1027,9 +1027,10 @@ func (k MachineKey) HexString() string { return fmt.Sprintf("%x",
|
||||
func (k *MachineKey) UnmarshalText(text []byte) error { return keyUnmarshalText(k[:], "mkey:", text) }
|
||||
|
||||
func keyMarshalText(prefix string, k [32]byte) []byte {
|
||||
buf := bytes.NewBuffer(make([]byte, 0, len(prefix)+64))
|
||||
fmt.Fprintf(buf, "%s%x", prefix, k[:])
|
||||
return buf.Bytes()
|
||||
buf := make([]byte, len(prefix)+64)
|
||||
copy(buf, prefix)
|
||||
hex.Encode(buf[len(prefix):], k[:])
|
||||
return buf
|
||||
}
|
||||
|
||||
func keyUnmarshalText(dst []byte, prefix string, text []byte) error {
|
||||
|
||||
@@ -518,3 +518,13 @@ func TestEndpointTypeMarshal(t *testing.T) {
|
||||
t.Errorf("got %s; want %s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
var sinkBytes []byte
|
||||
|
||||
func BenchmarkKeyMarshalText(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
var k [32]byte
|
||||
for i := 0; i < b.N; i++ {
|
||||
sinkBytes = keyMarshalText("prefix", k)
|
||||
}
|
||||
}
|
||||
|
||||
43
tsnet/example/tshello/tshello.go
Normal file
43
tsnet/example/tshello/tshello.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The tshello server demonstrates how to use Tailscale as a library.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"tailscale.com/tsnet"
|
||||
)
|
||||
|
||||
func main() {
|
||||
s := new(tsnet.Server)
|
||||
ln, err := s.Listen("tcp", ":80")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Fatal(http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
who, ok := s.WhoIs(r.RemoteAddr)
|
||||
if !ok {
|
||||
http.Error(w, "WhoIs failed", 500)
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, "<html><body><h1>Hello, world!</h1>\n")
|
||||
fmt.Fprintf(w, "<p>You are <b>%s</b> from <b>%s</b> (%s)</p>",
|
||||
html.EscapeString(who.UserProfile.LoginName),
|
||||
html.EscapeString(firstLabel(who.Node.ComputedName)),
|
||||
r.RemoteAddr)
|
||||
})))
|
||||
}
|
||||
|
||||
func firstLabel(s string) string {
|
||||
if i := strings.Index(s, "."); i != -1 {
|
||||
return s[:i]
|
||||
}
|
||||
return s
|
||||
}
|
||||
274
tsnet/tsnet.go
Normal file
274
tsnet/tsnet.go
Normal file
@@ -0,0 +1,274 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package tsnet provides Tailscale as a library.
|
||||
//
|
||||
// It is an experimental work in progress.
|
||||
package tsnet
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
)
|
||||
|
||||
// Server is an embedded Tailscale server.
|
||||
//
|
||||
// Its exported fields may be changed until the first call to Listen.
|
||||
type Server struct {
|
||||
// Dir specifies the name of the directory to use for
|
||||
// state. If empty, a directory is selected automatically
|
||||
// under os.UserConfigDir (https://golang.org/pkg/os/#UserConfigDir).
|
||||
// based on the name of the binary.
|
||||
Dir string
|
||||
|
||||
// Hostname is the hostname to present to the control server.
|
||||
// If empty, the binary name is used.l
|
||||
Hostname string
|
||||
|
||||
// Logf, if non-nil, specifies the logger to use. By default,
|
||||
// log.Printf is used.
|
||||
Logf logger.Logf
|
||||
|
||||
initOnce sync.Once
|
||||
initErr error
|
||||
lb *ipnlocal.LocalBackend
|
||||
// the state directory
|
||||
dir string
|
||||
hostname string
|
||||
|
||||
mu sync.Mutex
|
||||
listeners map[listenKey]*listener
|
||||
}
|
||||
|
||||
// WhoIs reports the node and user who owns the node with the given
|
||||
// address. The addr may be an ip:port (as from an
|
||||
// http.Request.RemoteAddr) or just an IP address.
|
||||
func (s *Server) WhoIs(addr string) (w *apitype.WhoIsResponse, ok bool) {
|
||||
ipp, err := netaddr.ParseIPPort(addr)
|
||||
if err != nil {
|
||||
ip, err := netaddr.ParseIP(addr)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
ipp = ipp.WithIP(ip)
|
||||
}
|
||||
n, up, ok := s.lb.WhoIs(ipp)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
return &apitype.WhoIsResponse{
|
||||
Node: n,
|
||||
UserProfile: &up,
|
||||
}, true
|
||||
}
|
||||
|
||||
func (s *Server) doInit() {
|
||||
if err := s.start(); err != nil {
|
||||
s.initErr = fmt.Errorf("tsnet: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) start() error {
|
||||
if v, _ := strconv.ParseBool(os.Getenv("TAILSCALE_USE_WIP_CODE")); !v {
|
||||
return errors.New("code disabled without environment variable TAILSCALE_USE_WIP_CODE set true")
|
||||
}
|
||||
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
prog := strings.TrimSuffix(strings.ToLower(filepath.Base(exe)), ".exe")
|
||||
|
||||
s.hostname = s.Hostname
|
||||
if s.hostname == "" {
|
||||
s.hostname = prog
|
||||
}
|
||||
|
||||
s.dir = s.Dir
|
||||
if s.dir == "" {
|
||||
confDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.dir = filepath.Join(confDir, "tslib-"+prog)
|
||||
if err := os.MkdirAll(s.dir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if fi, err := os.Stat(s.dir); err != nil {
|
||||
return err
|
||||
} else if !fi.IsDir() {
|
||||
return fmt.Errorf("%v is not a directory", s.dir)
|
||||
}
|
||||
|
||||
logf := s.Logf
|
||||
if logf == nil {
|
||||
logf = log.Printf
|
||||
}
|
||||
|
||||
// TODO(bradfitz): start logtail? don't use filch, perhaps?
|
||||
// only upload plumbed Logf?
|
||||
|
||||
linkMon, err := monitor.New(logf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
ListenPort: 0,
|
||||
LinkMonitor: linkMon,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tunDev, magicConn, ok := eng.(wgengine.InternalsGetter).GetInternals()
|
||||
if !ok {
|
||||
return fmt.Errorf("%T is not a wgengine.InternalsGetter", eng)
|
||||
}
|
||||
|
||||
ns, err := netstack.Create(logf, tunDev, eng, magicConn, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf("netstack.Create: %w", err)
|
||||
}
|
||||
ns.ForwardTCPIn = s.forwardTCP
|
||||
if err := ns.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start netstack: %w", err)
|
||||
}
|
||||
|
||||
statePath := filepath.Join(s.dir, "tailscaled.state")
|
||||
store, err := ipn.NewFileStore(statePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logid := "tslib-TODO"
|
||||
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, eng)
|
||||
if err != nil {
|
||||
return fmt.Errorf("NewLocalBackend: %v", err)
|
||||
}
|
||||
s.lb = lb
|
||||
lb.SetDecompressor(func() (controlclient.Decompressor, error) {
|
||||
return smallzstd.NewDecoder(nil)
|
||||
})
|
||||
prefs := ipn.NewPrefs()
|
||||
prefs.Hostname = s.hostname
|
||||
prefs.WantRunning = true
|
||||
err = lb.Start(ipn.Options{
|
||||
StateKey: ipn.GlobalDaemonStateKey,
|
||||
UpdatePrefs: prefs,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("starting backend: %w", err)
|
||||
}
|
||||
if os.Getenv("TS_LOGIN") == "1" {
|
||||
s.lb.StartLoginInteractive()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) forwardTCP(c net.Conn, port uint16) {
|
||||
s.mu.Lock()
|
||||
ln, ok := s.listeners[listenKey{"tcp", "", fmt.Sprint(port)}]
|
||||
s.mu.Unlock()
|
||||
if !ok {
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
t := time.NewTimer(time.Second)
|
||||
defer t.Stop()
|
||||
select {
|
||||
case ln.conn <- c:
|
||||
case <-t.C:
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Listen(network, addr string) (net.Listener, error) {
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tsnet: %w", err)
|
||||
}
|
||||
|
||||
s.initOnce.Do(s.doInit)
|
||||
if s.initErr != nil {
|
||||
return nil, s.initErr
|
||||
}
|
||||
|
||||
key := listenKey{network, host, port}
|
||||
ln := &listener{
|
||||
s: s,
|
||||
key: key,
|
||||
addr: addr,
|
||||
|
||||
conn: make(chan net.Conn),
|
||||
}
|
||||
s.mu.Lock()
|
||||
if s.listeners == nil {
|
||||
s.listeners = map[listenKey]*listener{}
|
||||
}
|
||||
if _, ok := s.listeners[key]; ok {
|
||||
s.mu.Unlock()
|
||||
return nil, fmt.Errorf("tsnet: listener already open for %s, %s", network, addr)
|
||||
}
|
||||
s.listeners[key] = ln
|
||||
s.mu.Unlock()
|
||||
return ln, nil
|
||||
}
|
||||
|
||||
type listenKey struct {
|
||||
network string
|
||||
host string
|
||||
port string
|
||||
}
|
||||
|
||||
type listener struct {
|
||||
s *Server
|
||||
key listenKey
|
||||
addr string
|
||||
conn chan net.Conn
|
||||
}
|
||||
|
||||
func (ln *listener) Accept() (net.Conn, error) {
|
||||
c, ok := <-ln.conn
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("tsnet: %w", net.ErrClosed)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (ln *listener) Addr() net.Addr { return addr{ln} }
|
||||
func (ln *listener) Close() error {
|
||||
ln.s.mu.Lock()
|
||||
defer ln.s.mu.Unlock()
|
||||
if v, ok := ln.s.listeners[ln.key]; ok && v == ln {
|
||||
delete(ln.s.listeners, ln.key)
|
||||
close(ln.conn)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type addr struct{ ln *listener }
|
||||
|
||||
func (a addr) Network() string { return a.ln.key.network }
|
||||
func (a addr) String() string { return a.ln.addr }
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
crand "crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -21,6 +23,7 @@ import (
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -41,8 +44,11 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/nettype"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var verbose = flag.Bool("verbose", false, "verbose debug logs")
|
||||
|
||||
var mainError atomic.Value // of error
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
@@ -57,11 +63,8 @@ func TestMain(m *testing.M) {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func TestIntegration(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("not tested/working on Windows yet")
|
||||
}
|
||||
|
||||
func TestOneNodeUp_NoAuth(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := buildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
@@ -69,8 +72,8 @@ func TestIntegration(t *testing.T) {
|
||||
|
||||
n1 := newTestNode(t, env)
|
||||
|
||||
dcmd := n1.StartDaemon(t)
|
||||
defer dcmd.Process.Kill()
|
||||
d1 := n1.StartDaemon(t)
|
||||
defer d1.Kill()
|
||||
|
||||
n1.AwaitListening(t)
|
||||
|
||||
@@ -87,44 +90,110 @@ func TestIntegration(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
t.Logf("Running up --login-server=%s ...", env.ControlServer.URL)
|
||||
if err := n1.Tailscale("up", "--login-server="+env.ControlServer.URL).Run(); err != nil {
|
||||
t.Fatalf("up: %v", err)
|
||||
}
|
||||
n1.MustUp()
|
||||
|
||||
if d, _ := time.ParseDuration(os.Getenv("TS_POST_UP_SLEEP")); d > 0 {
|
||||
t.Logf("Sleeping for %v to give 'up' time to misbehave (https://github.com/tailscale/tailscale/issues/1840) ...", d)
|
||||
time.Sleep(d)
|
||||
}
|
||||
|
||||
var ip string
|
||||
if err := tstest.WaitFor(20*time.Second, func() error {
|
||||
out, err := n1.Tailscale("ip").Output()
|
||||
if err != nil {
|
||||
return err
|
||||
t.Logf("Got IP: %v", n1.AwaitIP(t))
|
||||
n1.AwaitRunning(t)
|
||||
|
||||
d1.MustCleanShutdown(t)
|
||||
|
||||
t.Logf("number of HTTP logcatcher requests: %v", env.LogCatcher.numRequests())
|
||||
}
|
||||
|
||||
func TestOneNodeUp_Auth(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := buildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
defer env.Close()
|
||||
env.Control.RequireAuth = true
|
||||
|
||||
n1 := newTestNode(t, env)
|
||||
d1 := n1.StartDaemon(t)
|
||||
defer d1.Kill()
|
||||
|
||||
n1.AwaitListening(t)
|
||||
|
||||
st := n1.MustStatus(t)
|
||||
t.Logf("Status: %s", st.BackendState)
|
||||
|
||||
t.Logf("Running up --login-server=%s ...", env.ControlServer.URL)
|
||||
|
||||
cmd := n1.Tailscale("up", "--login-server="+env.ControlServer.URL)
|
||||
var authCountAtomic int32
|
||||
cmd.Stdout = &authURLParserWriter{fn: func(urlStr string) error {
|
||||
if env.Control.CompleteAuth(urlStr) {
|
||||
atomic.AddInt32(&authCountAtomic, 1)
|
||||
t.Logf("completed auth path %s", urlStr)
|
||||
return nil
|
||||
}
|
||||
err := fmt.Errorf("Failed to complete auth path to %q", urlStr)
|
||||
t.Log(err)
|
||||
return err
|
||||
}}
|
||||
cmd.Stderr = cmd.Stdout
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("up: %v", err)
|
||||
}
|
||||
t.Logf("Got IP: %v", n1.AwaitIP(t))
|
||||
|
||||
n1.AwaitRunning(t)
|
||||
|
||||
if n := atomic.LoadInt32(&authCountAtomic); n != 1 {
|
||||
t.Errorf("Auth URLs completed = %d; want 1", n)
|
||||
}
|
||||
|
||||
d1.MustCleanShutdown(t)
|
||||
|
||||
}
|
||||
|
||||
func TestTwoNodes(t *testing.T) {
|
||||
t.Parallel()
|
||||
bins := buildTestBinaries(t)
|
||||
|
||||
env := newTestEnv(t, bins)
|
||||
defer env.Close()
|
||||
|
||||
// Create two nodes:
|
||||
n1 := newTestNode(t, env)
|
||||
d1 := n1.StartDaemon(t)
|
||||
defer d1.Kill()
|
||||
|
||||
n2 := newTestNode(t, env)
|
||||
d2 := n2.StartDaemon(t)
|
||||
defer d2.Kill()
|
||||
|
||||
n1.AwaitListening(t)
|
||||
n2.AwaitListening(t)
|
||||
n1.MustUp()
|
||||
n2.MustUp()
|
||||
n1.AwaitRunning(t)
|
||||
n2.AwaitRunning(t)
|
||||
|
||||
if err := tstest.WaitFor(2*time.Second, func() error {
|
||||
st := n1.MustStatus(t)
|
||||
if len(st.Peer) == 0 {
|
||||
return errors.New("no peers")
|
||||
}
|
||||
if len(st.Peer) > 1 {
|
||||
return fmt.Errorf("got %d peers; want 1", len(st.Peer))
|
||||
}
|
||||
peer := st.Peer[st.Peers()[0]]
|
||||
if peer.ID == st.Self.ID {
|
||||
return errors.New("peer is self")
|
||||
}
|
||||
ip = string(out)
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
t.Logf("Got IP: %v", ip)
|
||||
|
||||
dcmd.Process.Signal(os.Interrupt)
|
||||
|
||||
ps, err := dcmd.Process.Wait()
|
||||
if err != nil {
|
||||
t.Fatalf("tailscaled Wait: %v", err)
|
||||
}
|
||||
if ps.ExitCode() != 0 {
|
||||
t.Errorf("tailscaled ExitCode = %d; want 0", ps.ExitCode())
|
||||
}
|
||||
|
||||
t.Logf("number of HTTP logcatcher requests: %v", env.LogCatcher.numRequests())
|
||||
if err := env.TrafficTrap.Err(); err != nil {
|
||||
t.Errorf("traffic trap: %v", err)
|
||||
t.Logf("logs: %s", env.LogCatcher.logsString())
|
||||
}
|
||||
d1.MustCleanShutdown(t)
|
||||
d2.MustCleanShutdown(t)
|
||||
}
|
||||
|
||||
// testBinaries are the paths to a tailscaled and tailscale binary.
|
||||
@@ -139,16 +208,18 @@ type testBinaries struct {
|
||||
// if they fail to compile.
|
||||
func buildTestBinaries(t testing.TB) *testBinaries {
|
||||
td := t.TempDir()
|
||||
build(t, td, "tailscale.com/cmd/tailscaled", "tailscale.com/cmd/tailscale")
|
||||
return &testBinaries{
|
||||
dir: td,
|
||||
daemon: build(t, td, "tailscale.com/cmd/tailscaled"),
|
||||
cli: build(t, td, "tailscale.com/cmd/tailscale"),
|
||||
daemon: filepath.Join(td, "tailscaled"+exe()),
|
||||
cli: filepath.Join(td, "tailscale"+exe()),
|
||||
}
|
||||
}
|
||||
|
||||
// testEnv contains the test environment (set of servers) used by one
|
||||
// or more nodes.
|
||||
type testEnv struct {
|
||||
t testing.TB
|
||||
Binaries *testBinaries
|
||||
|
||||
LogCatcher *logCatcher
|
||||
@@ -168,6 +239,9 @@ type testEnv struct {
|
||||
//
|
||||
// Call Close to shut everything down.
|
||||
func newTestEnv(t testing.TB, bins *testBinaries) *testEnv {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("not tested/working on Windows yet")
|
||||
}
|
||||
derpMap, derpShutdown := runDERPAndStun(t, logger.Discard)
|
||||
logc := new(logCatcher)
|
||||
control := &testcontrol.Server{
|
||||
@@ -175,6 +249,7 @@ func newTestEnv(t testing.TB, bins *testBinaries) *testEnv {
|
||||
}
|
||||
trafficTrap := new(trafficTrap)
|
||||
e := &testEnv{
|
||||
t: t,
|
||||
Binaries: bins,
|
||||
LogCatcher: logc,
|
||||
LogCatcherServer: httptest.NewServer(logc),
|
||||
@@ -184,10 +259,16 @@ func newTestEnv(t testing.TB, bins *testBinaries) *testEnv {
|
||||
TrafficTrapServer: httptest.NewServer(trafficTrap),
|
||||
derpShutdown: derpShutdown,
|
||||
}
|
||||
e.Control.BaseURL = e.ControlServer.URL
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *testEnv) Close() error {
|
||||
if err := e.TrafficTrap.Err(); err != nil {
|
||||
e.t.Errorf("traffic trap: %v", err)
|
||||
e.t.Logf("logs: %s", e.LogCatcher.logsString())
|
||||
}
|
||||
|
||||
e.LogCatcherServer.Close()
|
||||
e.TrafficTrapServer.Close()
|
||||
e.ControlServer.Close()
|
||||
@@ -218,9 +299,28 @@ func newTestNode(t *testing.T, env *testEnv) *testNode {
|
||||
}
|
||||
}
|
||||
|
||||
type Daemon struct {
|
||||
Process *os.Process
|
||||
}
|
||||
|
||||
func (d *Daemon) Kill() {
|
||||
d.Process.Kill()
|
||||
}
|
||||
|
||||
func (d *Daemon) MustCleanShutdown(t testing.TB) {
|
||||
d.Process.Signal(os.Interrupt)
|
||||
ps, err := d.Process.Wait()
|
||||
if err != nil {
|
||||
t.Fatalf("tailscaled Wait: %v", err)
|
||||
}
|
||||
if ps.ExitCode() != 0 {
|
||||
t.Errorf("tailscaled ExitCode = %d; want 0", ps.ExitCode())
|
||||
}
|
||||
}
|
||||
|
||||
// StartDaemon starts the node's tailscaled, failing if it fails to
|
||||
// start.
|
||||
func (n *testNode) StartDaemon(t testing.TB) *exec.Cmd {
|
||||
func (n *testNode) StartDaemon(t testing.TB) *Daemon {
|
||||
cmd := exec.Command(n.env.Binaries.daemon,
|
||||
"--tun=userspace-networking",
|
||||
"--state="+n.stateFile,
|
||||
@@ -234,7 +334,17 @@ func (n *testNode) StartDaemon(t testing.TB) *exec.Cmd {
|
||||
if err := cmd.Start(); err != nil {
|
||||
t.Fatalf("starting tailscaled: %v", err)
|
||||
}
|
||||
return cmd
|
||||
return &Daemon{
|
||||
Process: cmd.Process,
|
||||
}
|
||||
}
|
||||
|
||||
func (n *testNode) MustUp() {
|
||||
t := n.env.t
|
||||
t.Logf("Running up --login-server=%s ...", n.env.ControlServer.URL)
|
||||
if err := n.Tailscale("up", "--login-server="+n.env.ControlServer.URL).Run(); err != nil {
|
||||
t.Fatalf("up: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// AwaitListening waits for the tailscaled to be serving local clients
|
||||
@@ -252,6 +362,40 @@ func (n *testNode) AwaitListening(t testing.TB) {
|
||||
}
|
||||
}
|
||||
|
||||
func (n *testNode) AwaitIP(t testing.TB) (ips string) {
|
||||
t.Helper()
|
||||
if err := tstest.WaitFor(20*time.Second, func() error {
|
||||
out, err := n.Tailscale("ip").Output()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ips = string(out)
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatalf("awaiting an IP address: %v", err)
|
||||
}
|
||||
if ips == "" {
|
||||
t.Fatalf("returned IP address was blank")
|
||||
}
|
||||
return ips
|
||||
}
|
||||
|
||||
func (n *testNode) AwaitRunning(t testing.TB) {
|
||||
t.Helper()
|
||||
if err := tstest.WaitFor(20*time.Second, func() error {
|
||||
st, err := n.Status()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if st.BackendState != "Running" {
|
||||
return fmt.Errorf("in state %q", st.BackendState)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatalf("failure/timeout waiting for transition to Running status: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tailscale returns a command that runs the tailscale CLI with the provided arguments.
|
||||
// It does not start the process.
|
||||
func (n *testNode) Tailscale(arg ...string) *exec.Cmd {
|
||||
@@ -261,15 +405,23 @@ func (n *testNode) Tailscale(arg ...string) *exec.Cmd {
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (n *testNode) MustStatus(tb testing.TB) *ipnstate.Status {
|
||||
tb.Helper()
|
||||
func (n *testNode) Status() (*ipnstate.Status, error) {
|
||||
out, err := n.Tailscale("status", "--json").CombinedOutput()
|
||||
if err != nil {
|
||||
tb.Fatalf("getting status: %v, %s", err, out)
|
||||
return nil, fmt.Errorf("running tailscale status: %v, %s", err, out)
|
||||
}
|
||||
st := new(ipnstate.Status)
|
||||
if err := json.Unmarshal(out, st); err != nil {
|
||||
tb.Fatalf("parsing status json: %v, from: %s", err, out)
|
||||
return nil, fmt.Errorf("decoding tailscale status JSON: %w", err)
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
|
||||
func (n *testNode) MustStatus(tb testing.TB) *ipnstate.Status {
|
||||
tb.Helper()
|
||||
st, err := n.Status()
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
return st
|
||||
}
|
||||
@@ -291,21 +443,44 @@ func findGo(t testing.TB) string {
|
||||
} else if !fi.Mode().IsRegular() {
|
||||
t.Fatalf("%v is unexpected %v", goBin, fi.Mode())
|
||||
}
|
||||
t.Logf("using go binary %v", goBin)
|
||||
return goBin
|
||||
}
|
||||
|
||||
func build(t testing.TB, outDir, target string) string {
|
||||
exe := ""
|
||||
if runtime.GOOS == "windows" {
|
||||
exe = ".exe"
|
||||
// buildMu limits our use of "go build" to one at a time, so we don't
|
||||
// fight Go's built-in caching trying to do the same build concurrently.
|
||||
var buildMu sync.Mutex
|
||||
|
||||
func build(t testing.TB, outDir string, targets ...string) {
|
||||
buildMu.Lock()
|
||||
defer buildMu.Unlock()
|
||||
|
||||
t0 := time.Now()
|
||||
defer func() { t.Logf("built %s in %v", targets, time.Since(t0).Round(time.Millisecond)) }()
|
||||
|
||||
goBin := findGo(t)
|
||||
cmd := exec.Command(goBin, "install")
|
||||
if version.IsRace() {
|
||||
cmd.Args = append(cmd.Args, "-race")
|
||||
}
|
||||
bin := filepath.Join(outDir, path.Base(target)) + exe
|
||||
errOut, err := exec.Command(findGo(t), "build", "-o", bin, target).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to build %v: %v, %s", target, err, errOut)
|
||||
cmd.Args = append(cmd.Args, targets...)
|
||||
cmd.Env = append(os.Environ(), "GOARCH="+runtime.GOARCH, "GOBIN="+outDir)
|
||||
errOut, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
return bin
|
||||
if strings.Contains(string(errOut), "when GOBIN is set") {
|
||||
// Fallback slow path for cross-compiled binaries.
|
||||
for _, target := range targets {
|
||||
outFile := filepath.Join(outDir, path.Base(target)+exe())
|
||||
cmd := exec.Command(goBin, "build", "-o", outFile, target)
|
||||
cmd.Env = append(os.Environ(), "GOARCH="+runtime.GOARCH)
|
||||
if errOut, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("failed to build %v with %v: %v, %s", target, goBin, err, errOut)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
t.Fatalf("failed to build %v with %v: %v, %s", targets, goBin, err, errOut)
|
||||
}
|
||||
|
||||
// logCatcher is a minimal logcatcher for the logtail upload client.
|
||||
@@ -378,6 +553,9 @@ func (lc *logCatcher) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
} else {
|
||||
for _, ent := range jreq {
|
||||
fmt.Fprintf(&lc.buf, "%s\n", strings.TrimSpace(ent.Text))
|
||||
if *verbose {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", strings.TrimSpace(ent.Text))
|
||||
}
|
||||
}
|
||||
}
|
||||
w.WriteHeader(200) // must have no content, but not a 204
|
||||
@@ -454,3 +632,23 @@ func runDERPAndStun(t testing.TB, logf logger.Logf) (derpMap *tailcfg.DERPMap, c
|
||||
|
||||
return m, cleanup
|
||||
}
|
||||
|
||||
type authURLParserWriter struct {
|
||||
buf bytes.Buffer
|
||||
fn func(urlStr string) error
|
||||
}
|
||||
|
||||
var authURLRx = regexp.MustCompile(`(https?://\S+/auth/\S+)`)
|
||||
|
||||
func (w *authURLParserWriter) Write(p []byte) (n int, err error) {
|
||||
n, err = w.buf.Write(p)
|
||||
m := authURLRx.FindSubmatch(w.buf.Bytes())
|
||||
if m != nil {
|
||||
urlStr := string(m[1])
|
||||
w.buf.Reset() // so it's not matched again
|
||||
if err := w.fn(urlStr); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -34,19 +36,43 @@ import (
|
||||
// Server is a control plane server. Its zero value is ready for use.
|
||||
// Everything is stored in-memory in one tailnet.
|
||||
type Server struct {
|
||||
Logf logger.Logf // nil means to use the log package
|
||||
DERPMap *tailcfg.DERPMap // nil means to use prod DERP map
|
||||
Logf logger.Logf // nil means to use the log package
|
||||
DERPMap *tailcfg.DERPMap // nil means to use prod DERP map
|
||||
RequireAuth bool
|
||||
BaseURL string // must be set to e.g. "http://127.0.0.1:1234" with no trailing URL
|
||||
Verbose bool
|
||||
|
||||
initMuxOnce sync.Once
|
||||
mux *http.ServeMux
|
||||
|
||||
mu sync.Mutex
|
||||
pubKey wgkey.Key
|
||||
privKey wgkey.Private
|
||||
nodes map[tailcfg.NodeKey]*tailcfg.Node
|
||||
users map[tailcfg.NodeKey]*tailcfg.User
|
||||
logins map[tailcfg.NodeKey]*tailcfg.Login
|
||||
updates map[tailcfg.NodeID]chan updateType
|
||||
mu sync.Mutex
|
||||
pubKey wgkey.Key
|
||||
privKey wgkey.Private
|
||||
nodes map[tailcfg.NodeKey]*tailcfg.Node
|
||||
users map[tailcfg.NodeKey]*tailcfg.User
|
||||
logins map[tailcfg.NodeKey]*tailcfg.Login
|
||||
updates map[tailcfg.NodeID]chan updateType
|
||||
authPath map[string]*AuthPath
|
||||
nodeKeyAuthed map[tailcfg.NodeKey]bool // key => true once authenticated
|
||||
}
|
||||
|
||||
type AuthPath struct {
|
||||
nodeKey tailcfg.NodeKey
|
||||
|
||||
closeOnce sync.Once
|
||||
ch chan struct{}
|
||||
success bool
|
||||
}
|
||||
|
||||
func (ap *AuthPath) completeSuccessfully() {
|
||||
ap.success = true
|
||||
close(ap.ch)
|
||||
}
|
||||
|
||||
// CompleteSuccessfully completes the login path successfully, as if
|
||||
// the user did the whole auth dance.
|
||||
func (ap *AuthPath) CompleteSuccessfully() {
|
||||
ap.closeOnce.Do(ap.completeSuccessfully)
|
||||
}
|
||||
|
||||
func (s *Server) logf(format string, a ...interface{}) {
|
||||
@@ -142,6 +168,18 @@ func (s *Server) Node(nodeKey tailcfg.NodeKey) *tailcfg.Node {
|
||||
return s.nodes[nodeKey].Clone()
|
||||
}
|
||||
|
||||
func (s *Server) AllNodes() (nodes []*tailcfg.Node) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for _, n := range s.nodes {
|
||||
nodes = append(nodes, n.Clone())
|
||||
}
|
||||
sort.Slice(nodes, func(i, j int) bool {
|
||||
return nodes[i].StableID < nodes[j].StableID
|
||||
})
|
||||
return nodes
|
||||
}
|
||||
|
||||
func (s *Server) getUser(nodeKey tailcfg.NodeKey) (*tailcfg.User, *tailcfg.Login) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -178,6 +216,56 @@ func (s *Server) getUser(nodeKey tailcfg.NodeKey) (*tailcfg.User, *tailcfg.Login
|
||||
return user, login
|
||||
}
|
||||
|
||||
// authPathDone returns a close-only struct that's closed when the
|
||||
// authPath ("/auth/XXXXXX") has authenticated.
|
||||
func (s *Server) authPathDone(authPath string) <-chan struct{} {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if a, ok := s.authPath[authPath]; ok {
|
||||
return a.ch
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) addAuthPath(authPath string, nodeKey tailcfg.NodeKey) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.authPath == nil {
|
||||
s.authPath = map[string]*AuthPath{}
|
||||
}
|
||||
s.authPath[authPath] = &AuthPath{
|
||||
ch: make(chan struct{}),
|
||||
nodeKey: nodeKey,
|
||||
}
|
||||
}
|
||||
|
||||
// CompleteAuth marks the provided path or URL (containing
|
||||
// "/auth/...") as successfully authenticated, unblocking any
|
||||
// requests blocked on that in serveRegister.
|
||||
func (s *Server) CompleteAuth(authPathOrURL string) bool {
|
||||
i := strings.Index(authPathOrURL, "/auth/")
|
||||
if i == -1 {
|
||||
return false
|
||||
}
|
||||
authPath := authPathOrURL[i:]
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
ap, ok := s.authPath[authPath]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if ap.nodeKey.IsZero() {
|
||||
panic("zero AuthPath.NodeKey")
|
||||
}
|
||||
if s.nodeKeyAuthed == nil {
|
||||
s.nodeKeyAuthed = map[tailcfg.NodeKey]bool{}
|
||||
}
|
||||
s.nodeKeyAuthed[ap.nodeKey] = true
|
||||
ap.CompleteSuccessfully()
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey tailcfg.MachineKey) {
|
||||
var req tailcfg.RegisterRequest
|
||||
if err := s.decode(mkey, r.Body, &req); err != nil {
|
||||
@@ -189,28 +277,65 @@ func (s *Server) serveRegister(w http.ResponseWriter, r *http.Request, mkey tail
|
||||
if req.NodeKey.IsZero() {
|
||||
panic("serveRegister: request has zero node key")
|
||||
}
|
||||
if s.Verbose {
|
||||
j, _ := json.MarshalIndent(req, "", "\t")
|
||||
log.Printf("Got %T: %s", req, j)
|
||||
}
|
||||
|
||||
// If this is a followup request, wait until interactive followup URL visit complete.
|
||||
if req.Followup != "" {
|
||||
followupURL, err := url.Parse(req.Followup)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
doneCh := s.authPathDone(followupURL.Path)
|
||||
select {
|
||||
case <-r.Context().Done():
|
||||
return
|
||||
case <-doneCh:
|
||||
}
|
||||
// TODO(bradfitz): support a side test API to mark an
|
||||
// auth as failued so we can send an error response in
|
||||
// some follow-ups? For now all are successes.
|
||||
}
|
||||
|
||||
user, login := s.getUser(req.NodeKey)
|
||||
s.mu.Lock()
|
||||
if s.nodes == nil {
|
||||
s.nodes = map[tailcfg.NodeKey]*tailcfg.Node{}
|
||||
}
|
||||
|
||||
machineAuthorized := true // TODO: add Server.RequireMachineAuth
|
||||
|
||||
s.nodes[req.NodeKey] = &tailcfg.Node{
|
||||
ID: tailcfg.NodeID(user.ID),
|
||||
StableID: tailcfg.StableNodeID(fmt.Sprintf("TESTCTRL%08x", int(user.ID))),
|
||||
User: user.ID,
|
||||
Machine: mkey,
|
||||
Key: req.NodeKey,
|
||||
MachineAuthorized: true,
|
||||
MachineAuthorized: machineAuthorized,
|
||||
}
|
||||
requireAuth := s.RequireAuth
|
||||
if requireAuth && s.nodeKeyAuthed[req.NodeKey] {
|
||||
requireAuth = false
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
authURL := ""
|
||||
if requireAuth {
|
||||
randHex := make([]byte, 10)
|
||||
crand.Read(randHex)
|
||||
authPath := fmt.Sprintf("/auth/%x", randHex)
|
||||
s.addAuthPath(authPath, req.NodeKey)
|
||||
authURL = s.BaseURL + authPath
|
||||
}
|
||||
|
||||
res, err := s.encode(mkey, false, tailcfg.RegisterResponse{
|
||||
User: *user,
|
||||
Login: *login,
|
||||
NodeKeyExpired: false,
|
||||
MachineAuthorized: true,
|
||||
AuthURL: "", // all good; TODO(bradfitz): add ways to not start all good.
|
||||
MachineAuthorized: machineAuthorized,
|
||||
AuthURL: authURL,
|
||||
})
|
||||
if err != nil {
|
||||
go panic(fmt.Sprintf("serveRegister: encode: %v", err))
|
||||
@@ -254,6 +379,21 @@ func sendUpdate(dst chan<- updateType, updateType updateType) {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) UpdateNode(n *tailcfg.Node) (peersToUpdate []tailcfg.NodeID) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if n.Key.IsZero() {
|
||||
panic("zero nodekey")
|
||||
}
|
||||
s.nodes[n.Key] = n.Clone()
|
||||
for _, n2 := range s.nodes {
|
||||
if n.ID != n2.ID {
|
||||
peersToUpdate = append(peersToUpdate, n2.ID)
|
||||
}
|
||||
}
|
||||
return peersToUpdate
|
||||
}
|
||||
|
||||
func (s *Server) serveMap(w http.ResponseWriter, r *http.Request, mkey tailcfg.MachineKey) {
|
||||
ctx := r.Context()
|
||||
|
||||
@@ -279,10 +419,8 @@ func (s *Server) serveMap(w http.ResponseWriter, r *http.Request, mkey tailcfg.M
|
||||
if !req.ReadOnly {
|
||||
endpoints := filterInvalidIPv6Endpoints(req.Endpoints)
|
||||
node.Endpoints = endpoints
|
||||
// TODO: more
|
||||
// TODO: register node,
|
||||
//s.UpdateEndpoint(mkey, req.NodeKey,
|
||||
// XXX
|
||||
node.DiscoKey = req.DiscoKey
|
||||
peersToUpdate = s.UpdateNode(node)
|
||||
}
|
||||
|
||||
nodeID := node.ID
|
||||
@@ -389,6 +527,12 @@ func (s *Server) MapResponse(req *tailcfg.MapRequest) (res *tailcfg.MapResponse,
|
||||
CollectServices: "true",
|
||||
PacketFilter: tailcfg.FilterAllowAll,
|
||||
}
|
||||
for _, p := range s.AllNodes() {
|
||||
if p.StableID != node.StableID {
|
||||
res.Peers = append(res.Peers, p)
|
||||
}
|
||||
}
|
||||
|
||||
res.Node.Addresses = []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix(fmt.Sprintf("100.64.%d.%d/32", uint8(node.ID>>8), uint8(node.ID))),
|
||||
}
|
||||
@@ -506,7 +650,7 @@ func keepClientEndpoint(ep string) bool {
|
||||
// the incoming JSON response.
|
||||
return false
|
||||
}
|
||||
ip := ipp.IP
|
||||
ip := ipp.IP()
|
||||
if ip.Zone() != "" {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func (s FirewallType) key(src, dst netaddr.IPPort) fwKey {
|
||||
switch s {
|
||||
case EndpointIndependentFirewall:
|
||||
case AddressDependentFirewall:
|
||||
k.dst.IP = dst.IP
|
||||
k.dst = k.dst.WithIP(dst.IP())
|
||||
case AddressAndPortDependentFirewall:
|
||||
k.dst = dst
|
||||
default:
|
||||
|
||||
@@ -62,7 +62,7 @@ func (t NATType) key(src, dst netaddr.IPPort) natKey {
|
||||
switch t {
|
||||
case EndpointIndependentNAT:
|
||||
case AddressDependentNAT:
|
||||
k.dst.IP = dst.IP
|
||||
k.dst = k.dst.WithIP(dst.IP())
|
||||
case AddressAndPortDependentNAT:
|
||||
k.dst = dst
|
||||
default:
|
||||
@@ -171,7 +171,7 @@ func (n *SNAT44) HandleIn(p *Packet, iif *Interface) *Packet {
|
||||
func (n *SNAT44) HandleForward(p *Packet, iif, oif *Interface) *Packet {
|
||||
switch {
|
||||
case oif == n.ExternalInterface:
|
||||
if p.Src.IP == oif.V4() {
|
||||
if p.Src.IP() == oif.V4() {
|
||||
// Packet already NATed and is just retraversing Forward,
|
||||
// don't touch it again.
|
||||
return p
|
||||
@@ -237,10 +237,7 @@ func (n *SNAT44) allocateMappedPort() (net.PacketConn, netaddr.IPPort) {
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("ran out of NAT ports: %v", err))
|
||||
}
|
||||
addr := netaddr.IPPort{
|
||||
IP: ip,
|
||||
Port: uint16(pc.LocalAddr().(*net.UDPAddr).Port),
|
||||
}
|
||||
addr := netaddr.IPPortFrom(ip, uint16(pc.LocalAddr().(*net.UDPAddr).Port))
|
||||
return pc, addr
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ func (n *Network) allocIPv4(iface *Interface) netaddr.IP {
|
||||
return netaddr.IP{}
|
||||
}
|
||||
if n.lastV4.IsZero() {
|
||||
n.lastV4 = n.Prefix4.IP
|
||||
n.lastV4 = n.Prefix4.IP()
|
||||
}
|
||||
a := n.lastV4.As16()
|
||||
addOne(&a, 15)
|
||||
@@ -157,7 +157,7 @@ func (n *Network) allocIPv6(iface *Interface) netaddr.IP {
|
||||
return netaddr.IP{}
|
||||
}
|
||||
if n.lastV6.IsZero() {
|
||||
n.lastV6 = n.Prefix6.IP
|
||||
n.lastV6 = n.Prefix6.IP()
|
||||
}
|
||||
a := n.lastV6.As16()
|
||||
addOne(&a, 15)
|
||||
@@ -183,15 +183,15 @@ func (n *Network) write(p *Packet) (num int, err error) {
|
||||
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
iface, ok := n.machine[p.Dst.IP]
|
||||
iface, ok := n.machine[p.Dst.IP()]
|
||||
if !ok {
|
||||
// If the destination is within the network's authoritative
|
||||
// range, no route to host.
|
||||
if p.Dst.IP.Is4() && n.Prefix4.Contains(p.Dst.IP) {
|
||||
if p.Dst.IP().Is4() && n.Prefix4.Contains(p.Dst.IP()) {
|
||||
p.Trace("no route to %v", p.Dst.IP)
|
||||
return len(p.Payload), nil
|
||||
}
|
||||
if p.Dst.IP.Is6() && n.Prefix6.Contains(p.Dst.IP) {
|
||||
if p.Dst.IP().Is6() && n.Prefix6.Contains(p.Dst.IP()) {
|
||||
p.Trace("no route to %v", p.Dst.IP)
|
||||
return len(p.Payload), nil
|
||||
}
|
||||
@@ -363,7 +363,7 @@ func (m *Machine) isLocalIP(ip netaddr.IP) bool {
|
||||
func (m *Machine) deliverIncomingPacket(p *Packet, iface *Interface) {
|
||||
p.setLocator("mach=%s if=%s", m.Name, iface.name)
|
||||
|
||||
if m.isLocalIP(p.Dst.IP) {
|
||||
if m.isLocalIP(p.Dst.IP()) {
|
||||
m.deliverLocalPacket(p, iface)
|
||||
} else {
|
||||
m.forwardPacket(p, iface)
|
||||
@@ -391,13 +391,13 @@ func (m *Machine) deliverLocalPacket(p *Packet, iface *Interface) {
|
||||
defer m.mu.Unlock()
|
||||
|
||||
conns := m.conns4
|
||||
if p.Dst.IP.Is6() {
|
||||
if p.Dst.IP().Is6() {
|
||||
conns = m.conns6
|
||||
}
|
||||
possibleDsts := []netaddr.IPPort{
|
||||
p.Dst,
|
||||
netaddr.IPPort{IP: v6unspec, Port: p.Dst.Port},
|
||||
netaddr.IPPort{IP: v4unspec, Port: p.Dst.Port},
|
||||
netaddr.IPPortFrom(v6unspec, p.Dst.Port()),
|
||||
netaddr.IPPortFrom(v4unspec, p.Dst.Port()),
|
||||
}
|
||||
for _, dest := range possibleDsts {
|
||||
c, ok := conns[dest]
|
||||
@@ -417,7 +417,7 @@ func (m *Machine) deliverLocalPacket(p *Packet, iface *Interface) {
|
||||
}
|
||||
|
||||
func (m *Machine) forwardPacket(p *Packet, iif *Interface) {
|
||||
oif, err := m.interfaceForIP(p.Dst.IP)
|
||||
oif, err := m.interfaceForIP(p.Dst.IP())
|
||||
if err != nil {
|
||||
p.Trace("%v", err)
|
||||
return
|
||||
@@ -501,7 +501,7 @@ func (m *Machine) Attach(interfaceName string, n *Network) *Interface {
|
||||
}
|
||||
}
|
||||
sort.Slice(m.routes, func(i, j int) bool {
|
||||
return m.routes[i].prefix.Bits > m.routes[j].prefix.Bits
|
||||
return m.routes[i].prefix.Bits() > m.routes[j].prefix.Bits()
|
||||
})
|
||||
|
||||
return f
|
||||
@@ -515,33 +515,33 @@ var (
|
||||
func (m *Machine) writePacket(p *Packet) (n int, err error) {
|
||||
p.setLocator("mach=%s", m.Name)
|
||||
|
||||
iface, err := m.interfaceForIP(p.Dst.IP)
|
||||
iface, err := m.interfaceForIP(p.Dst.IP())
|
||||
if err != nil {
|
||||
p.Trace("%v", err)
|
||||
return 0, err
|
||||
}
|
||||
origSrcIP := p.Src.IP
|
||||
origSrcIP := p.Src.IP()
|
||||
switch {
|
||||
case p.Src.IP == v4unspec:
|
||||
case p.Src.IP() == v4unspec:
|
||||
p.Trace("assigning srcIP=%s", iface.V4())
|
||||
p.Src.IP = iface.V4()
|
||||
case p.Src.IP == v6unspec:
|
||||
p.Src = p.Src.WithIP(iface.V4())
|
||||
case p.Src.IP() == v6unspec:
|
||||
// v6unspec in Go means "any src, but match address families"
|
||||
if p.Dst.IP.Is6() {
|
||||
if p.Dst.IP().Is6() {
|
||||
p.Trace("assigning srcIP=%s", iface.V6())
|
||||
p.Src.IP = iface.V6()
|
||||
} else if p.Dst.IP.Is4() {
|
||||
p.Src = p.Src.WithIP(iface.V6())
|
||||
} else if p.Dst.IP().Is4() {
|
||||
p.Trace("assigning srcIP=%s", iface.V4())
|
||||
p.Src.IP = iface.V4()
|
||||
p.Src = p.Src.WithIP(iface.V4())
|
||||
}
|
||||
default:
|
||||
if !iface.Contains(p.Src.IP) {
|
||||
err := fmt.Errorf("can't send to %v with src %v on interface %v", p.Dst.IP, p.Src.IP, iface)
|
||||
if !iface.Contains(p.Src.IP()) {
|
||||
err := fmt.Errorf("can't send to %v with src %v on interface %v", p.Dst.IP(), p.Src.IP(), iface)
|
||||
p.Trace("%v", err)
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
if p.Src.IP.IsZero() {
|
||||
if p.Src.IP().IsZero() {
|
||||
err := fmt.Errorf("no matching address for address family for %v", origSrcIP)
|
||||
p.Trace("%v", err)
|
||||
return 0, err
|
||||
@@ -602,12 +602,12 @@ func (m *Machine) pickEphemPort() (port uint16, err error) {
|
||||
|
||||
func (m *Machine) portInUseLocked(port uint16) bool {
|
||||
for ipp := range m.conns4 {
|
||||
if ipp.Port == port {
|
||||
if ipp.Port() == port {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for ipp := range m.conns6 {
|
||||
if ipp.Port == port {
|
||||
if ipp.Port() == port {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -617,7 +617,7 @@ func (m *Machine) portInUseLocked(port uint16) bool {
|
||||
func (m *Machine) registerConn4(c *conn) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if c.ipp.IP.Is6() && c.ipp.IP != v6unspec {
|
||||
if c.ipp.IP().Is6() && c.ipp.IP() != v6unspec {
|
||||
return fmt.Errorf("registerConn4 got IPv6 %s", c.ipp)
|
||||
}
|
||||
return registerConn(&m.conns4, c)
|
||||
@@ -632,7 +632,7 @@ func (m *Machine) unregisterConn4(c *conn) {
|
||||
func (m *Machine) registerConn6(c *conn) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if c.ipp.IP.Is4() {
|
||||
if c.ipp.IP().Is4() {
|
||||
return fmt.Errorf("registerConn6 got IPv4 %s", c.ipp)
|
||||
}
|
||||
return registerConn(&m.conns6, c)
|
||||
@@ -707,7 +707,7 @@ func (m *Machine) ListenPacket(ctx context.Context, network, address string) (ne
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
ipp := netaddr.IPPort{IP: ip, Port: port}
|
||||
ipp := netaddr.IPPortFrom(ip, port)
|
||||
|
||||
c := &conn{
|
||||
m: m,
|
||||
|
||||
@@ -49,8 +49,8 @@ func TestSendPacket(t *testing.T) {
|
||||
ifFoo := foo.Attach("eth0", internet)
|
||||
ifBar := bar.Attach("enp0s1", internet)
|
||||
|
||||
fooAddr := netaddr.IPPort{IP: ifFoo.V4(), Port: 123}
|
||||
barAddr := netaddr.IPPort{IP: ifBar.V4(), Port: 456}
|
||||
fooAddr := netaddr.IPPortFrom(ifFoo.V4(), 123)
|
||||
barAddr := netaddr.IPPortFrom(ifBar.V4(), 456)
|
||||
|
||||
ctx := context.Background()
|
||||
fooPC, err := foo.ListenPacket(ctx, "udp4", fooAddr.String())
|
||||
@@ -111,10 +111,10 @@ func TestMultiNetwork(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
clientAddr := netaddr.IPPort{IP: ifClient.V4(), Port: 123}
|
||||
natLANAddr := netaddr.IPPort{IP: ifNATLAN.V4(), Port: 456}
|
||||
natWANAddr := netaddr.IPPort{IP: ifNATWAN.V4(), Port: 456}
|
||||
serverAddr := netaddr.IPPort{IP: ifServer.V4(), Port: 789}
|
||||
clientAddr := netaddr.IPPortFrom(ifClient.V4(), 123)
|
||||
natLANAddr := netaddr.IPPortFrom(ifNATLAN.V4(), 456)
|
||||
natWANAddr := netaddr.IPPortFrom(ifNATWAN.V4(), 456)
|
||||
serverAddr := netaddr.IPPortFrom(ifServer.V4(), 789)
|
||||
|
||||
const msg1, msg2 = "hello", "world"
|
||||
if _, err := natPC.WriteTo([]byte(msg1), clientAddr.UDPAddr()); err != nil {
|
||||
@@ -154,8 +154,8 @@ type trivialNAT struct {
|
||||
}
|
||||
|
||||
func (n *trivialNAT) HandleIn(p *Packet, iface *Interface) *Packet {
|
||||
if iface == n.wanIf && p.Dst.IP == n.wanIf.V4() {
|
||||
p.Dst.IP = n.clientIP
|
||||
if iface == n.wanIf && p.Dst.IP() == n.wanIf.V4() {
|
||||
p.Dst = p.Dst.WithIP(n.clientIP)
|
||||
}
|
||||
return p
|
||||
}
|
||||
@@ -167,13 +167,13 @@ func (n trivialNAT) HandleOut(p *Packet, iface *Interface) *Packet {
|
||||
func (n *trivialNAT) HandleForward(p *Packet, iif, oif *Interface) *Packet {
|
||||
// Outbound from LAN -> apply NAT, continue
|
||||
if iif == n.lanIf && oif == n.wanIf {
|
||||
if p.Src.IP == n.clientIP {
|
||||
p.Src.IP = n.wanIf.V4()
|
||||
if p.Src.IP() == n.clientIP {
|
||||
p.Src = p.Src.WithIP(n.wanIf.V4())
|
||||
}
|
||||
return p
|
||||
}
|
||||
// Return traffic to LAN, allow if right dst.
|
||||
if iif == n.wanIf && oif == n.lanIf && p.Dst.IP == n.clientIP {
|
||||
if iif == n.wanIf && oif == n.lanIf && p.Dst.IP() == n.clientIP {
|
||||
return p
|
||||
}
|
||||
// Else drop.
|
||||
@@ -216,7 +216,7 @@ func TestPacketHandler(t *testing.T) {
|
||||
}
|
||||
|
||||
const msg = "some message"
|
||||
serverAddr := netaddr.IPPort{IP: ifServer.V4(), Port: 456}
|
||||
serverAddr := netaddr.IPPortFrom(ifServer.V4(), 456)
|
||||
if _, err := clientPC.WriteTo([]byte(msg), serverAddr.UDPAddr()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -230,7 +230,7 @@ func TestPacketHandler(t *testing.T) {
|
||||
if string(buf) != msg {
|
||||
t.Errorf("read %q; want %q", buf, msg)
|
||||
}
|
||||
mappedAddr := netaddr.IPPort{IP: ifNATWAN.V4(), Port: 123}
|
||||
mappedAddr := netaddr.IPPortFrom(ifNATWAN.V4(), 123)
|
||||
if addr.String() != mappedAddr.String() {
|
||||
t.Errorf("addr = %q; want %q", addr, mappedAddr)
|
||||
}
|
||||
|
||||
@@ -250,7 +250,7 @@ func TestConciseDiffFrom(t *testing.T) {
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:41641", "1.1.1.1:41641"},
|
||||
DiscoKey: testDiscoKey("f00f00f00f"),
|
||||
AllowedIPs: []netaddr.IPPrefix{{IP: netaddr.IPv4(100, 102, 103, 104), Bits: 32}},
|
||||
AllowedIPs: []netaddr.IPPrefix{netaddr.IPPrefixFrom(netaddr.IPv4(100, 102, 103, 104), 32)},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -263,7 +263,7 @@ func TestConciseDiffFrom(t *testing.T) {
|
||||
DERP: "127.3.3.40:2",
|
||||
Endpoints: []string{"192.168.0.100:41641", "1.1.1.1:41641"},
|
||||
DiscoKey: testDiscoKey("ba4ba4ba4b"),
|
||||
AllowedIPs: []netaddr.IPPrefix{{IP: netaddr.IPv4(100, 102, 103, 104), Bits: 32}},
|
||||
AllowedIPs: []netaddr.IPPrefix{netaddr.IPPrefixFrom(netaddr.IPv4(100, 102, 103, 104), 32)},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -78,8 +78,16 @@ func (k Key) HexString() string { return hex.EncodeToString(k[:]) }
|
||||
func (k Key) Equal(k2 Key) bool { return subtle.ConstantTimeCompare(k[:], k2[:]) == 1 }
|
||||
|
||||
func (k *Key) ShortString() string {
|
||||
long := k.Base64()
|
||||
return "[" + long[0:5] + "]"
|
||||
// The goal here is to generate "[" + base64.StdEncoding.EncodeToString(k[:])[:5] + "]".
|
||||
// Since we only care about the first 5 characters, it suffices to encode the first 4 bytes of k.
|
||||
// Encoding those 4 bytes requires 8 bytes.
|
||||
// Make dst have size 9, to fit the leading '[' plus those 8 bytes.
|
||||
// We slice the unused ones away at the end.
|
||||
dst := make([]byte, 9)
|
||||
dst[0] = '['
|
||||
base64.StdEncoding.Encode(dst[1:], k[:4])
|
||||
dst[6] = ']'
|
||||
return string(dst[:7])
|
||||
}
|
||||
|
||||
func (k *Key) IsZero() bool {
|
||||
@@ -106,11 +114,10 @@ func (k *Key) UnmarshalJSON(b []byte) error {
|
||||
return errors.New("wgkey.Key: UnmarshalJSON not given a string")
|
||||
}
|
||||
b = b[1 : len(b)-1]
|
||||
key, err := ParseHex(string(b))
|
||||
if err != nil {
|
||||
return fmt.Errorf("wgkey.Key: UnmarshalJSON: %v", err)
|
||||
if len(b) != 2*Size {
|
||||
return fmt.Errorf("wgkey.Key: UnmarshalJSON input wrong size: %d", len(b))
|
||||
}
|
||||
copy(k[:], key[:])
|
||||
hex.Decode(k[:], b)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -156,3 +156,28 @@ func BenchmarkMarshalJSON(b *testing.B) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnmarshalJSON(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
var k Key
|
||||
buf, err := k.MarshalJSON()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
for i := 0; i < b.N; i++ {
|
||||
err := k.UnmarshalJSON(buf)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var sinkString string
|
||||
|
||||
func BenchmarkShortString(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
var k Key
|
||||
for i := 0; i < b.N; i++ {
|
||||
sinkString = k.ShortString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,13 +24,16 @@ func ToFQDN(s string) (FQDN, error) {
|
||||
if isValidFQDN(s) {
|
||||
return FQDN(s), nil
|
||||
}
|
||||
if len(s) == 0 {
|
||||
if len(s) == 0 || s == "." {
|
||||
return FQDN("."), nil
|
||||
}
|
||||
|
||||
if s[len(s)-1] == '.' {
|
||||
s = s[:len(s)-1]
|
||||
}
|
||||
if s[0] == '.' {
|
||||
s = s[1:]
|
||||
}
|
||||
if len(s) > maxNameLength {
|
||||
return "", fmt.Errorf("%q is too long to be a DNS name", s)
|
||||
}
|
||||
|
||||
@@ -20,11 +20,12 @@ func TestFQDN(t *testing.T) {
|
||||
{".", ".", false, 0},
|
||||
{"foo.com", "foo.com.", false, 2},
|
||||
{"foo.com.", "foo.com.", false, 2},
|
||||
{".foo.com.", "foo.com.", false, 2},
|
||||
{".foo.com", "foo.com.", false, 2},
|
||||
{"com", "com.", false, 1},
|
||||
{"www.tailscale.com", "www.tailscale.com.", false, 3},
|
||||
{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com", "", true, 0},
|
||||
{strings.Repeat("aaaaa.", 60) + "com", "", true, 0},
|
||||
{".com", "", true, 0},
|
||||
{"foo..com", "", true, 0},
|
||||
}
|
||||
|
||||
|
||||
11
version/race.go
Normal file
11
version/race.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build race
|
||||
|
||||
package version
|
||||
|
||||
// IsRace reports whether the current binary was built with the Go
|
||||
// race detector enabled.
|
||||
func IsRace() bool { return true }
|
||||
11
version/race_off.go
Normal file
11
version/race_off.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !race
|
||||
|
||||
package version
|
||||
|
||||
// IsRace reports whether the current binary was built with the Go
|
||||
// race detector enabled.
|
||||
func IsRace() bool { return false }
|
||||
510
wf/firewall.go
Normal file
510
wf/firewall.go
Normal file
@@ -0,0 +1,510 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build windows
|
||||
|
||||
package wf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"inet.af/netaddr"
|
||||
"inet.af/wf"
|
||||
)
|
||||
|
||||
// Known addresses.
|
||||
var (
|
||||
linkLocalRange = netaddr.MustParseIPPrefix("ff80::/10")
|
||||
linkLocalDHCPMulticast = netaddr.MustParseIP("ff02::1:2")
|
||||
siteLocalDHCPMulticast = netaddr.MustParseIP("ff05::1:3")
|
||||
linkLocalRouterMulticast = netaddr.MustParseIP("ff02::2")
|
||||
)
|
||||
|
||||
type direction int
|
||||
|
||||
const (
|
||||
directionInbound direction = iota
|
||||
directionOutbound
|
||||
directionBoth
|
||||
)
|
||||
|
||||
type protocol int
|
||||
|
||||
const (
|
||||
protocolV4 protocol = iota
|
||||
protocolV6
|
||||
protocolAll
|
||||
)
|
||||
|
||||
// getLayers returns the wf.LayerIDs where the rules should be added based
|
||||
// on the protocol and direction.
|
||||
func (p protocol) getLayers(d direction) []wf.LayerID {
|
||||
var layers []wf.LayerID
|
||||
if p == protocolAll || p == protocolV4 {
|
||||
if d == directionBoth || d == directionInbound {
|
||||
layers = append(layers, wf.LayerALEAuthRecvAcceptV4)
|
||||
}
|
||||
if d == directionBoth || d == directionOutbound {
|
||||
layers = append(layers, wf.LayerALEAuthConnectV4)
|
||||
}
|
||||
}
|
||||
if p == protocolAll || p == protocolV6 {
|
||||
if d == directionBoth || d == directionInbound {
|
||||
layers = append(layers, wf.LayerALEAuthRecvAcceptV6)
|
||||
}
|
||||
if d == directionBoth || d == directionOutbound {
|
||||
layers = append(layers, wf.LayerALEAuthConnectV6)
|
||||
}
|
||||
}
|
||||
return layers
|
||||
}
|
||||
|
||||
func ruleName(action wf.Action, l wf.LayerID, name string) string {
|
||||
switch l {
|
||||
case wf.LayerALEAuthConnectV4:
|
||||
return fmt.Sprintf("%s outbound %s (IPv4)", action, name)
|
||||
case wf.LayerALEAuthConnectV6:
|
||||
return fmt.Sprintf("%s outbound %s (IPv6)", action, name)
|
||||
case wf.LayerALEAuthRecvAcceptV4:
|
||||
return fmt.Sprintf("%s inbound %s (IPv4)", action, name)
|
||||
case wf.LayerALEAuthRecvAcceptV6:
|
||||
return fmt.Sprintf("%s inbound %s (IPv6)", action, name)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Firewall uses the Windows Filtering Platform to implement a network firewall.
|
||||
type Firewall struct {
|
||||
luid uint64
|
||||
providerID wf.ProviderID
|
||||
sublayerID wf.SublayerID
|
||||
session *wf.Session
|
||||
|
||||
permittedRoutes map[netaddr.IPPrefix][]*wf.Rule
|
||||
}
|
||||
|
||||
// New returns a new Firewall for the provdied interface ID.
|
||||
func New(luid uint64) (*Firewall, error) {
|
||||
session, err := wf.New(&wf.Options{
|
||||
Name: "Tailscale firewall",
|
||||
Dynamic: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wguid, err := windows.GenerateGUID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
providerID := wf.ProviderID(wguid)
|
||||
if err := session.AddProvider(&wf.Provider{
|
||||
ID: providerID,
|
||||
Name: "Tailscale provider",
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wguid, err = windows.GenerateGUID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
sublayerID := wf.SublayerID(wguid)
|
||||
if err := session.AddSublayer(&wf.Sublayer{
|
||||
ID: sublayerID,
|
||||
Name: "Tailscale permissive and blocking filters",
|
||||
Weight: 0,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f := &Firewall{
|
||||
luid: luid,
|
||||
session: session,
|
||||
providerID: providerID,
|
||||
sublayerID: sublayerID,
|
||||
permittedRoutes: make(map[netaddr.IPPrefix][]*wf.Rule),
|
||||
}
|
||||
if err := f.enable(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
type weight uint64
|
||||
|
||||
const (
|
||||
weightTailscaleTraffic weight = 15
|
||||
weightKnownTraffic weight = 12
|
||||
weightCatchAll weight = 0
|
||||
)
|
||||
|
||||
func (f *Firewall) enable() error {
|
||||
if err := f.permitTailscaleService(weightTailscaleTraffic); err != nil {
|
||||
return fmt.Errorf("permitTailscaleService failed: %w", err)
|
||||
}
|
||||
|
||||
if err := f.permitTunInterface(weightTailscaleTraffic); err != nil {
|
||||
return fmt.Errorf("permitTunInterface failed: %w", err)
|
||||
}
|
||||
|
||||
if err := f.permitDNS(weightTailscaleTraffic); err != nil {
|
||||
return fmt.Errorf("permitDNS failed: %w", err)
|
||||
}
|
||||
|
||||
if err := f.permitLoopback(weightKnownTraffic); err != nil {
|
||||
return fmt.Errorf("permitLoopback failed: %w", err)
|
||||
}
|
||||
|
||||
if err := f.permitDHCPv4(weightKnownTraffic); err != nil {
|
||||
return fmt.Errorf("permitDHCPv4 failed: %w", err)
|
||||
}
|
||||
|
||||
if err := f.permitDHCPv6(weightKnownTraffic); err != nil {
|
||||
return fmt.Errorf("permitDHCPv6 failed: %w", err)
|
||||
}
|
||||
|
||||
if err := f.permitNDP(weightKnownTraffic); err != nil {
|
||||
return fmt.Errorf("permitNDP failed: %w", err)
|
||||
}
|
||||
|
||||
/* TODO: actually evaluate if this does anything and if we need this. It's layer 2; our other rules are layer 3.
|
||||
* In other words, if somebody complains, try enabling it. For now, keep it off.
|
||||
* TODO(maisem): implement this.
|
||||
err = permitHyperV(session, baseObjects, weightKnownTraffic)
|
||||
if err != nil {
|
||||
return wrapErr(err)
|
||||
}
|
||||
*/
|
||||
|
||||
if err := f.blockAll(weightCatchAll); err != nil {
|
||||
return fmt.Errorf("blockAll failed: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatedPermittedRoutes adds rules to allow incoming and outgoing connections
|
||||
// from the provided prefixes. It will also remove rules for routes that were
|
||||
// previously added but have been removed.
|
||||
func (f *Firewall) UpdatePermittedRoutes(newRoutes []netaddr.IPPrefix) error {
|
||||
var routesToAdd []netaddr.IPPrefix
|
||||
routeMap := make(map[netaddr.IPPrefix]bool)
|
||||
for _, r := range newRoutes {
|
||||
routeMap[r] = true
|
||||
if _, ok := f.permittedRoutes[r]; !ok {
|
||||
routesToAdd = append(routesToAdd, r)
|
||||
}
|
||||
}
|
||||
var routesToRemove []netaddr.IPPrefix
|
||||
for r := range f.permittedRoutes {
|
||||
if !routeMap[r] {
|
||||
routesToRemove = append(routesToRemove, r)
|
||||
}
|
||||
}
|
||||
for _, r := range routesToRemove {
|
||||
for _, rule := range f.permittedRoutes[r] {
|
||||
if err := f.session.DeleteRule(rule.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
delete(f.permittedRoutes, r)
|
||||
}
|
||||
for _, r := range routesToAdd {
|
||||
conditions := []*wf.Match{
|
||||
{
|
||||
Field: wf.FieldIPRemoteAddress,
|
||||
Op: wf.MatchTypeEqual,
|
||||
Value: r,
|
||||
},
|
||||
}
|
||||
var p protocol
|
||||
if r.IP().Is4() {
|
||||
p = protocolV4
|
||||
} else {
|
||||
p = protocolV6
|
||||
}
|
||||
rules, err := f.addRules("local route", weightKnownTraffic, conditions, wf.ActionPermit, p, directionBoth)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.permittedRoutes[r] = rules
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firewall) newRule(name string, w weight, layer wf.LayerID, conditions []*wf.Match, action wf.Action) (*wf.Rule, error) {
|
||||
id, err := windows.GenerateGUID()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &wf.Rule{
|
||||
Name: ruleName(action, layer, name),
|
||||
ID: wf.RuleID(id),
|
||||
Provider: f.providerID,
|
||||
Sublayer: f.sublayerID,
|
||||
Layer: layer,
|
||||
Weight: uint64(w),
|
||||
Conditions: conditions,
|
||||
Action: action,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *Firewall) addRules(name string, w weight, conditions []*wf.Match, action wf.Action, p protocol, d direction) ([]*wf.Rule, error) {
|
||||
var rules []*wf.Rule
|
||||
for _, l := range p.getLayers(d) {
|
||||
r, err := f.newRule(name, w, l, conditions, action)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := f.session.AddRule(r); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rules = append(rules, r)
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func (f *Firewall) blockAll(w weight) error {
|
||||
_, err := f.addRules("all", w, nil, wf.ActionBlock, protocolAll, directionBoth)
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *Firewall) permitNDP(w weight) error {
|
||||
// These are aliased according to:
|
||||
// https://social.msdn.microsoft.com/Forums/azure/en-US/eb2aa3cd-5f1c-4461-af86-61e7d43ccc23/filtering-icmp-by-type-code?forum=wfp
|
||||
fieldICMPType := wf.FieldIPLocalPort
|
||||
fieldICMPCode := wf.FieldIPRemotePort
|
||||
|
||||
var icmpConditions = func(t, c uint16, remoteAddress interface{}) []*wf.Match {
|
||||
conditions := []*wf.Match{
|
||||
{
|
||||
Field: wf.FieldIPProtocol,
|
||||
Op: wf.MatchTypeEqual,
|
||||
Value: wf.IPProtoICMPV6,
|
||||
},
|
||||
{
|
||||
Field: fieldICMPType,
|
||||
Op: wf.MatchTypeEqual,
|
||||
Value: t,
|
||||
},
|
||||
{
|
||||
Field: fieldICMPCode,
|
||||
Op: wf.MatchTypeEqual,
|
||||
Value: c,
|
||||
},
|
||||
}
|
||||
if remoteAddress != nil {
|
||||
conditions = append(conditions, &wf.Match{
|
||||
Field: wf.FieldIPRemoteAddress,
|
||||
Op: wf.MatchTypeEqual,
|
||||
Value: linkLocalRouterMulticast,
|
||||
})
|
||||
}
|
||||
return conditions
|
||||
}
|
||||
/* TODO: actually handle the hop limit somehow! The rules should vaguely be:
|
||||
* - icmpv6 133: must be outgoing, dst must be FF02::2/128, hop limit must be 255
|
||||
* - icmpv6 134: must be incoming, src must be FE80::/10, hop limit must be 255
|
||||
* - icmpv6 135: either incoming or outgoing, hop limit must be 255
|
||||
* - icmpv6 136: either incoming or outgoing, hop limit must be 255
|
||||
* - icmpv6 137: must be incoming, src must be FE80::/10, hop limit must be 255
|
||||
*/
|
||||
|
||||
//
|
||||
// Router Solicitation Message
|
||||
// ICMP type 133, code 0. Outgoing.
|
||||
//
|
||||
conditions := icmpConditions(133, 0, linkLocalRouterMulticast)
|
||||
if _, err := f.addRules("NDP type 133", w, conditions, wf.ActionPermit, protocolV6, directionOutbound); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//
|
||||
// Router Advertisement Message
|
||||
// ICMP type 134, code 0. Incoming.
|
||||
//
|
||||
conditions = icmpConditions(134, 0, linkLocalRange)
|
||||
if _, err := f.addRules("NDP type 134", w, conditions, wf.ActionPermit, protocolV6, directionInbound); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//
|
||||
// Neighbor Solicitation Message
|
||||
// ICMP type 135, code 0. Bi-directional.
|
||||
//
|
||||
conditions = icmpConditions(135, 0, nil)
|
||||
if _, err := f.addRules("NDP type 135", w, conditions, wf.ActionPermit, protocolV6, directionBoth); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//
|
||||
// Neighbor Advertisement Message
|
||||
// ICMP type 136, code 0. Bi-directional.
|
||||
//
|
||||
conditions = icmpConditions(136, 0, nil)
|
||||
if _, err := f.addRules("NDP type 136", w, conditions, wf.ActionPermit, protocolV6, directionBoth); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//
|
||||
// Redirect Message
|
||||
// ICMP type 137, code 0. Incoming.
|
||||
//
|
||||
conditions = icmpConditions(137, 0, linkLocalRange)
|
||||
if _, err := f.addRules("NDP type 137", w, conditions, wf.ActionPermit, protocolV6, directionInbound); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firewall) permitDHCPv6(w weight) error {
|
||||
var dhcpConditions = func(remoteAddrs ...interface{}) []*wf.Match {
|
||||
conditions := []*wf.Match{
|
||||
{
|
||||
Field: wf.FieldIPProtocol,
|
||||
Op: wf.MatchTypeEqual,
|
||||
Value: wf.IPProtoUDP,
|
||||
},
|
||||
{
|
||||
Field: wf.FieldIPLocalAddress,
|
||||
Op: wf.MatchTypeEqual,
|
||||
Value: linkLocalRange,
|
||||
},
|
||||
{
|
||||
Field: wf.FieldIPLocalPort,
|
||||
Op: wf.MatchTypeEqual,
|
||||
Value: uint16(546),
|
||||
},
|
||||
{
|
||||
Field: wf.FieldIPRemotePort,
|
||||
Op: wf.MatchTypeEqual,
|
||||
Value: uint16(547),
|
||||
},
|
||||
}
|
||||
for _, a := range remoteAddrs {
|
||||
conditions = append(conditions, &wf.Match{
|
||||
Field: wf.FieldIPRemoteAddress,
|
||||
Op: wf.MatchTypeEqual,
|
||||
Value: a,
|
||||
})
|
||||
}
|
||||
return conditions
|
||||
}
|
||||
conditions := dhcpConditions(linkLocalDHCPMulticast, siteLocalDHCPMulticast)
|
||||
if _, err := f.addRules("DHCP request", w, conditions, wf.ActionPermit, protocolV6, directionOutbound); err != nil {
|
||||
return err
|
||||
}
|
||||
conditions = dhcpConditions(linkLocalRange)
|
||||
if _, err := f.addRules("DHCP response", w, conditions, wf.ActionPermit, protocolV6, directionInbound); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firewall) permitDHCPv4(w weight) error {
|
||||
var dhcpConditions = func(remoteAddrs ...interface{}) []*wf.Match {
|
||||
conditions := []*wf.Match{
|
||||
{
|
||||
Field: wf.FieldIPProtocol,
|
||||
Op: wf.MatchTypeEqual,
|
||||
Value: wf.IPProtoUDP,
|
||||
},
|
||||
{
|
||||
Field: wf.FieldIPLocalPort,
|
||||
Op: wf.MatchTypeEqual,
|
||||
Value: uint16(68),
|
||||
},
|
||||
{
|
||||
Field: wf.FieldIPRemotePort,
|
||||
Op: wf.MatchTypeEqual,
|
||||
Value: uint16(67),
|
||||
},
|
||||
}
|
||||
for _, a := range remoteAddrs {
|
||||
conditions = append(conditions, &wf.Match{
|
||||
Field: wf.FieldIPRemoteAddress,
|
||||
Op: wf.MatchTypeEqual,
|
||||
Value: a,
|
||||
})
|
||||
}
|
||||
return conditions
|
||||
}
|
||||
conditions := dhcpConditions(netaddr.IPv4(255, 255, 255, 255))
|
||||
if _, err := f.addRules("DHCP request", w, conditions, wf.ActionPermit, protocolV4, directionOutbound); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
conditions = dhcpConditions()
|
||||
if _, err := f.addRules("DHCP response", w, conditions, wf.ActionPermit, protocolV4, directionInbound); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *Firewall) permitTunInterface(w weight) error {
|
||||
condition := []*wf.Match{
|
||||
{
|
||||
Field: wf.FieldIPLocalInterface,
|
||||
Op: wf.MatchTypeEqual,
|
||||
Value: f.luid,
|
||||
},
|
||||
}
|
||||
_, err := f.addRules("on TUN", w, condition, wf.ActionPermit, protocolAll, directionBoth)
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *Firewall) permitLoopback(w weight) error {
|
||||
condition := []*wf.Match{
|
||||
{
|
||||
Field: wf.FieldFlags,
|
||||
Op: wf.MatchTypeEqual,
|
||||
Value: wf.ConditionFlagIsLoopback,
|
||||
},
|
||||
}
|
||||
_, err := f.addRules("on loopback", w, condition, wf.ActionPermit, protocolAll, directionBoth)
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *Firewall) permitDNS(w weight) error {
|
||||
conditions := []*wf.Match{
|
||||
{
|
||||
Field: wf.FieldIPRemotePort,
|
||||
Op: wf.MatchTypeEqual,
|
||||
Value: uint16(53),
|
||||
},
|
||||
// Repeat the condition type for logical OR.
|
||||
{
|
||||
Field: wf.FieldIPProtocol,
|
||||
Op: wf.MatchTypeEqual,
|
||||
Value: wf.IPProtoUDP,
|
||||
},
|
||||
{
|
||||
Field: wf.FieldIPProtocol,
|
||||
Op: wf.MatchTypeEqual,
|
||||
Value: wf.IPProtoTCP,
|
||||
},
|
||||
}
|
||||
_, err := f.addRules("DNS", w, conditions, wf.ActionPermit, protocolAll, directionBoth)
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *Firewall) permitTailscaleService(w weight) error {
|
||||
currentFile, err := os.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
appID, err := wf.AppID(currentFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not get app id for %q: %w", currentFile, err)
|
||||
}
|
||||
conditions := []*wf.Match{
|
||||
{
|
||||
Field: wf.FieldALEAppID,
|
||||
Op: wf.MatchTypeEqual,
|
||||
Value: appID,
|
||||
},
|
||||
}
|
||||
_, err = f.addRules("unrestricted traffic for Tailscale service", w, conditions, wf.ActionPermit, protocolAll, directionBoth)
|
||||
return err
|
||||
}
|
||||
@@ -80,14 +80,14 @@ func main() {
|
||||
|
||||
// tx=134236 rx=133166 (1070 = 0.80% loss) (1088.9 Mbits/sec)
|
||||
case 101:
|
||||
setupWGTest(logf, traf, Addr1, Addr2)
|
||||
setupWGTest(nil, logf, traf, Addr1, Addr2)
|
||||
|
||||
default:
|
||||
log.Fatalf("provide a valid test number (0..n)")
|
||||
}
|
||||
|
||||
logf("initialized ok.")
|
||||
traf.Start(Addr1.IP, Addr2.IP, PayloadSize+ICMPMinSize, 0)
|
||||
traf.Start(Addr1.IP(), Addr2.IP(), PayloadSize+ICMPMinSize, 0)
|
||||
|
||||
var cur, prev Snapshot
|
||||
var pps int64
|
||||
|
||||
@@ -43,7 +43,7 @@ func BenchmarkBatchTCP(b *testing.B) {
|
||||
|
||||
func BenchmarkWireGuardTest(b *testing.B) {
|
||||
run(b, func(logf logger.Logf, traf *TrafficGen) {
|
||||
setupWGTest(logf, traf, Addr1, Addr2)
|
||||
setupWGTest(b, logf, traf, Addr1, Addr2)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ func runOnce(b *testing.B, setup SetupFunc, payload int) {
|
||||
logf("initialized. (n=%v)", b.N)
|
||||
b.SetBytes(int64(payload))
|
||||
|
||||
traf.Start(Addr1.IP, Addr2.IP, payload, int64(b.N))
|
||||
traf.Start(Addr1.IP(), Addr2.IP(), payload, int64(b.N))
|
||||
|
||||
var cur, prev Snapshot
|
||||
var pps int64
|
||||
|
||||
@@ -180,6 +180,7 @@ func (t *TrafficGen) Generate(b []byte, ofs int) int {
|
||||
// GotPacket processes a packet that came back on the receive side.
|
||||
func (t *TrafficGen) GotPacket(b []byte, ofs int) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
|
||||
s := &t.cur
|
||||
seq := int64(binary.BigEndian.Uint64(
|
||||
@@ -203,9 +204,6 @@ func (t *TrafficGen) GotPacket(b []byte, ofs int) {
|
||||
|
||||
f := t.onFirstPacket
|
||||
t.onFirstPacket = nil
|
||||
|
||||
t.mu.Unlock()
|
||||
|
||||
if f != nil {
|
||||
f()
|
||||
}
|
||||
|
||||
@@ -5,11 +5,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/tailscale/wireguard-go/tun"
|
||||
"inet.af/netaddr"
|
||||
@@ -25,7 +26,7 @@ import (
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
func setupWGTest(logf logger.Logf, traf *TrafficGen, a1, a2 netaddr.IPPrefix) {
|
||||
func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netaddr.IPPrefix) {
|
||||
l1 := logger.WithPrefix(logf, "e1: ")
|
||||
k1, err := wgkey.NewPrivate()
|
||||
if err != nil {
|
||||
@@ -49,6 +50,9 @@ func setupWGTest(logf logger.Logf, traf *TrafficGen, a1, a2 netaddr.IPPrefix) {
|
||||
if err != nil {
|
||||
log.Fatalf("e1 init: %v", err)
|
||||
}
|
||||
if b != nil {
|
||||
b.Cleanup(e1.Close)
|
||||
}
|
||||
|
||||
l2 := logger.WithPrefix(logf, "e2: ")
|
||||
k2, err := wgkey.NewPrivate()
|
||||
@@ -73,6 +77,9 @@ func setupWGTest(logf logger.Logf, traf *TrafficGen, a1, a2 netaddr.IPPrefix) {
|
||||
if err != nil {
|
||||
log.Fatalf("e2 init: %v", err)
|
||||
}
|
||||
if b != nil {
|
||||
b.Cleanup(e2.Close)
|
||||
}
|
||||
|
||||
e1.SetFilter(filter.NewAllowAllForTest(l1))
|
||||
e2.SetFilter(filter.NewAllowAllForTest(l2))
|
||||
@@ -80,15 +87,25 @@ func setupWGTest(logf logger.Logf, traf *TrafficGen, a1, a2 netaddr.IPPrefix) {
|
||||
var wait sync.WaitGroup
|
||||
wait.Add(2)
|
||||
|
||||
var e1waitDoneOnce sync.Once
|
||||
e1.SetStatusCallback(func(st *wgengine.Status, err error) {
|
||||
if errors.Is(err, wgengine.ErrEngineClosing) {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("e1 status err: %v", err)
|
||||
}
|
||||
logf("e1 status: %v", *st)
|
||||
|
||||
var eps []string
|
||||
var ipps []netaddr.IPPort
|
||||
for _, ep := range st.LocalAddrs {
|
||||
eps = append(eps, ep.Addr.String())
|
||||
ipps = append(ipps, ep.Addr)
|
||||
}
|
||||
endpoint := wgcfg.Endpoints{
|
||||
PublicKey: c1.PrivateKey.Public(),
|
||||
IPPorts: wgcfg.NewIPPortSet(ipps...),
|
||||
}
|
||||
|
||||
n := tailcfg.Node{
|
||||
@@ -107,22 +124,32 @@ func setupWGTest(logf logger.Logf, traf *TrafficGen, a1, a2 netaddr.IPPrefix) {
|
||||
p := wgcfg.Peer{
|
||||
PublicKey: c1.PrivateKey.Public(),
|
||||
AllowedIPs: []netaddr.IPPrefix{a1},
|
||||
Endpoints: strings.Join(eps, ","),
|
||||
Endpoints: endpoint,
|
||||
}
|
||||
c2.Peers = []wgcfg.Peer{p}
|
||||
e2.Reconfig(&c2, &router.Config{}, new(dns.Config))
|
||||
wait.Done()
|
||||
e1waitDoneOnce.Do(wait.Done)
|
||||
})
|
||||
|
||||
var e2waitDoneOnce sync.Once
|
||||
e2.SetStatusCallback(func(st *wgengine.Status, err error) {
|
||||
if errors.Is(err, wgengine.ErrEngineClosing) {
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("e2 status err: %v", err)
|
||||
}
|
||||
logf("e2 status: %v", *st)
|
||||
|
||||
var eps []string
|
||||
var ipps []netaddr.IPPort
|
||||
for _, ep := range st.LocalAddrs {
|
||||
eps = append(eps, ep.Addr.String())
|
||||
ipps = append(ipps, ep.Addr)
|
||||
}
|
||||
endpoint := wgcfg.Endpoints{
|
||||
PublicKey: c2.PrivateKey.Public(),
|
||||
IPPorts: wgcfg.NewIPPortSet(ipps...),
|
||||
}
|
||||
|
||||
n := tailcfg.Node{
|
||||
@@ -141,11 +168,11 @@ func setupWGTest(logf logger.Logf, traf *TrafficGen, a1, a2 netaddr.IPPrefix) {
|
||||
p := wgcfg.Peer{
|
||||
PublicKey: c2.PrivateKey.Public(),
|
||||
AllowedIPs: []netaddr.IPPrefix{a2},
|
||||
Endpoints: strings.Join(eps, ","),
|
||||
Endpoints: endpoint,
|
||||
}
|
||||
c1.Peers = []wgcfg.Peer{p}
|
||||
e1.Reconfig(&c1, &router.Config{}, new(dns.Config))
|
||||
wait.Done()
|
||||
e2waitDoneOnce.Do(wait.Done)
|
||||
})
|
||||
|
||||
// Not using DERP in this test (for now?).
|
||||
|
||||
@@ -98,8 +98,8 @@ const (
|
||||
// everything. Use in tests only, as it permits some kinds of spoofing
|
||||
// attacks to reach the OS network stack.
|
||||
func NewAllowAllForTest(logf logger.Logf) *Filter {
|
||||
any4 := netaddr.IPPrefix{IP: netaddr.IPv4(0, 0, 0, 0), Bits: 0}
|
||||
any6 := netaddr.IPPrefix{IP: netaddr.IPFrom16([16]byte{}), Bits: 0}
|
||||
any4 := netaddr.IPPrefixFrom(netaddr.IPv4(0, 0, 0, 0), 0)
|
||||
any6 := netaddr.IPPrefixFrom(netaddr.IPFrom16([16]byte{}), 0)
|
||||
ms := []Match{
|
||||
{
|
||||
Srcs: []netaddr.IPPrefix{any4},
|
||||
@@ -185,12 +185,12 @@ func matchesFamily(ms matches, keep func(netaddr.IP) bool) matches {
|
||||
var retm Match
|
||||
retm.IPProto = m.IPProto
|
||||
for _, src := range m.Srcs {
|
||||
if keep(src.IP) {
|
||||
if keep(src.IP()) {
|
||||
retm.Srcs = append(retm.Srcs, src)
|
||||
}
|
||||
}
|
||||
for _, dst := range m.Dsts {
|
||||
if keep(dst.Net.IP) {
|
||||
if keep(dst.Net.IP()) {
|
||||
retm.Dsts = append(retm.Dsts, dst)
|
||||
}
|
||||
}
|
||||
@@ -266,12 +266,10 @@ func (f *Filter) CheckTCP(srcIP, dstIP netaddr.IP, dstPort uint16) Response {
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
pkt.Src.IP = srcIP
|
||||
pkt.Dst.IP = dstIP
|
||||
pkt.Src = netaddr.IPPortFrom(srcIP, 0)
|
||||
pkt.Dst = netaddr.IPPortFrom(dstIP, dstPort)
|
||||
pkt.IPProto = ipproto.TCP
|
||||
pkt.TCPFlags = packet.TCPSyn
|
||||
pkt.Src.Port = 0
|
||||
pkt.Dst.Port = dstPort
|
||||
|
||||
return f.RunIn(pkt, 0)
|
||||
}
|
||||
@@ -321,7 +319,7 @@ func (f *Filter) runIn4(q *packet.Parsed) (r Response, why string) {
|
||||
// A compromised peer could try to send us packets for
|
||||
// destinations we didn't explicitly advertise. This check is to
|
||||
// prevent that.
|
||||
if !f.local.Contains(q.Dst.IP) {
|
||||
if !f.local.Contains(q.Dst.IP()) {
|
||||
return Drop, "destination not allowed"
|
||||
}
|
||||
|
||||
@@ -378,7 +376,7 @@ func (f *Filter) runIn6(q *packet.Parsed) (r Response, why string) {
|
||||
// A compromised peer could try to send us packets for
|
||||
// destinations we didn't explicitly advertise. This check is to
|
||||
// prevent that.
|
||||
if !f.local.Contains(q.Dst.IP) {
|
||||
if !f.local.Contains(q.Dst.IP()) {
|
||||
return Drop, "destination not allowed"
|
||||
}
|
||||
|
||||
@@ -480,11 +478,11 @@ func (f *Filter) pre(q *packet.Parsed, rf RunFlags, dir direction) Response {
|
||||
return Drop
|
||||
}
|
||||
|
||||
if q.Dst.IP.IsMulticast() {
|
||||
if q.Dst.IP().IsMulticast() {
|
||||
f.logRateLimit(rf, q, dir, Drop, "multicast")
|
||||
return Drop
|
||||
}
|
||||
if q.Dst.IP.IsLinkLocalUnicast() && q.Dst.IP != gcpDNSAddr {
|
||||
if q.Dst.IP().IsLinkLocalUnicast() && q.Dst.IP() != gcpDNSAddr {
|
||||
f.logRateLimit(rf, q, dir, Drop, "link-local-unicast")
|
||||
return Drop
|
||||
}
|
||||
@@ -506,7 +504,7 @@ func (f *Filter) pre(q *packet.Parsed, rf RunFlags, dir direction) Response {
|
||||
|
||||
// loggingAllowed reports whether p can appear in logs at all.
|
||||
func (f *Filter) loggingAllowed(p *packet.Parsed) bool {
|
||||
return f.logIPs.Contains(p.Src.IP) && f.logIPs.Contains(p.Dst.IP)
|
||||
return f.logIPs.Contains(p.Src.IP()) && f.logIPs.Contains(p.Dst.IP())
|
||||
}
|
||||
|
||||
// omitDropLogging reports whether packet p, which has already been
|
||||
@@ -518,5 +516,5 @@ func omitDropLogging(p *packet.Parsed, dir direction) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
return p.Dst.IP.IsMulticast() || (p.Dst.IP.IsLinkLocalUnicast() && p.Dst.IP != gcpDNSAddr) || p.IPProto == ipproto.IGMP
|
||||
return p.Dst.IP().IsMulticast() || (p.Dst.IP().IsLinkLocalUnicast() && p.Dst.IP() != gcpDNSAddr) || p.IPProto == ipproto.IGMP
|
||||
}
|
||||
|
||||
@@ -120,9 +120,9 @@ func TestFilter(t *testing.T) {
|
||||
if test.p.IPProto == ipproto.TCP {
|
||||
var got Response
|
||||
if test.p.IPVersion == 4 {
|
||||
got = acl.CheckTCP(test.p.Src.IP, test.p.Dst.IP, test.p.Dst.Port)
|
||||
got = acl.CheckTCP(test.p.Src.IP(), test.p.Dst.IP(), test.p.Dst.Port())
|
||||
} else {
|
||||
got = acl.CheckTCP(test.p.Src.IP, test.p.Dst.IP, test.p.Dst.Port)
|
||||
got = acl.CheckTCP(test.p.Src.IP(), test.p.Dst.IP(), test.p.Dst.Port())
|
||||
}
|
||||
if test.want != got {
|
||||
t.Errorf("#%d CheckTCP got=%v want=%v packet:%v", i, got, test.want, test.p)
|
||||
@@ -254,7 +254,9 @@ func TestParseIPSet(t *testing.T) {
|
||||
}
|
||||
t.Errorf("parseIPSet(%q, %v) error: %v; want error %q", tt.host, tt.bits, err, tt.wantErr)
|
||||
}
|
||||
if diff := cmp.Diff(got, tt.want, cmp.Comparer(func(a, b netaddr.IP) bool { return a == b })); diff != "" {
|
||||
compareIP := cmp.Comparer(func(a, b netaddr.IP) bool { return a == b })
|
||||
compareIPPrefix := cmp.Comparer(func(a, b netaddr.IPPrefix) bool { return a == b })
|
||||
if diff := cmp.Diff(got, tt.want, compareIP, compareIPPrefix); diff != "" {
|
||||
t.Errorf("parseIPSet(%q, %v) = %s; want %s", tt.host, tt.bits, got, tt.want)
|
||||
continue
|
||||
}
|
||||
@@ -425,10 +427,10 @@ func TestLoggingPrivacy(t *testing.T) {
|
||||
f.logIPs = logB.IPSet()
|
||||
|
||||
var (
|
||||
ts4 = netaddr.IPPort{IP: tsaddr.CGNATRange().IP.Next(), Port: 1234}
|
||||
internet4 = netaddr.IPPort{IP: netaddr.MustParseIP("8.8.8.8"), Port: 1234}
|
||||
ts6 = netaddr.IPPort{IP: tsaddr.TailscaleULARange().IP.Next(), Port: 1234}
|
||||
internet6 = netaddr.IPPort{IP: netaddr.MustParseIP("2001::1"), Port: 1234}
|
||||
ts4 = netaddr.IPPortFrom(tsaddr.CGNATRange().IP().Next(), 1234)
|
||||
internet4 = netaddr.IPPortFrom(netaddr.MustParseIP("8.8.8.8"), 1234)
|
||||
ts6 = netaddr.IPPortFrom(tsaddr.TailscaleULARange().IP().Next(), 1234)
|
||||
internet6 = netaddr.IPPortFrom(netaddr.MustParseIP("2001::1"), 1234)
|
||||
)
|
||||
|
||||
tests := []struct {
|
||||
@@ -545,10 +547,8 @@ func parsed(proto ipproto.Proto, src, dst string, sport, dport uint16) packet.Pa
|
||||
var ret packet.Parsed
|
||||
ret.Decode(dummyPacket)
|
||||
ret.IPProto = proto
|
||||
ret.Src.IP = sip
|
||||
ret.Src.Port = sport
|
||||
ret.Dst.IP = dip
|
||||
ret.Dst.Port = dport
|
||||
ret.Src = netaddr.IPPortFrom(sip, sport)
|
||||
ret.Dst = netaddr.IPPortFrom(dip, dport)
|
||||
ret.TCPFlags = packet.TCPSyn
|
||||
|
||||
if sip.Is4() {
|
||||
@@ -674,7 +674,7 @@ func nets(nets ...string) (ret []netaddr.IPPrefix) {
|
||||
if ip.Is6() {
|
||||
bits = 128
|
||||
}
|
||||
ret = append(ret, netaddr.IPPrefix{IP: ip, Bits: bits})
|
||||
ret = append(ret, netaddr.IPPrefixFrom(ip, bits))
|
||||
} else {
|
||||
pfx, err := netaddr.ParseIPPrefix(s)
|
||||
if err != nil {
|
||||
|
||||
@@ -85,14 +85,14 @@ func (ms matches) match(q *packet.Parsed) bool {
|
||||
if !protoInList(q.IPProto, m.IPProto) {
|
||||
continue
|
||||
}
|
||||
if !ipInList(q.Src.IP, m.Srcs) {
|
||||
if !ipInList(q.Src.IP(), m.Srcs) {
|
||||
continue
|
||||
}
|
||||
for _, dst := range m.Dsts {
|
||||
if !dst.Net.Contains(q.Dst.IP) {
|
||||
if !dst.Net.Contains(q.Dst.IP()) {
|
||||
continue
|
||||
}
|
||||
if !dst.Ports.contains(q.Dst.Port) {
|
||||
if !dst.Ports.contains(q.Dst.Port()) {
|
||||
continue
|
||||
}
|
||||
return true
|
||||
@@ -103,11 +103,11 @@ func (ms matches) match(q *packet.Parsed) bool {
|
||||
|
||||
func (ms matches) matchIPsOnly(q *packet.Parsed) bool {
|
||||
for _, m := range ms {
|
||||
if !ipInList(q.Src.IP, m.Srcs) {
|
||||
if !ipInList(q.Src.IP(), m.Srcs) {
|
||||
continue
|
||||
}
|
||||
for _, dst := range m.Dsts {
|
||||
if dst.Net.Contains(q.Dst.IP) {
|
||||
if dst.Net.Contains(q.Dst.IP()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,8 +99,8 @@ func parseIPSet(arg string, bits *int) ([]netaddr.IPPrefix, error) {
|
||||
if arg == "*" {
|
||||
// User explicitly requested wildcard.
|
||||
return []netaddr.IPPrefix{
|
||||
{IP: zeroIP4, Bits: 0},
|
||||
{IP: zeroIP6, Bits: 0},
|
||||
netaddr.IPPrefixFrom(zeroIP4, 0),
|
||||
netaddr.IPPrefixFrom(zeroIP6, 0),
|
||||
}, nil
|
||||
}
|
||||
if strings.Contains(arg, "/") {
|
||||
@@ -124,7 +124,7 @@ func parseIPSet(arg string, bits *int) ([]netaddr.IPPrefix, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r := netaddr.IPRange{From: ip1, To: ip2}
|
||||
r := netaddr.IPRangeFrom(ip1, ip2)
|
||||
if !r.Valid() {
|
||||
return nil, fmt.Errorf("invalid IP range %q", arg)
|
||||
}
|
||||
@@ -141,5 +141,5 @@ func parseIPSet(arg string, bits *int) ([]netaddr.IPPrefix, error) {
|
||||
}
|
||||
bits8 = uint8(*bits)
|
||||
}
|
||||
return []netaddr.IPPrefix{{IP: ip, Bits: bits8}}, nil
|
||||
return []netaddr.IPPrefix{netaddr.IPPrefixFrom(ip, bits8)}, nil
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"crypto/subtle"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"net"
|
||||
"strings"
|
||||
@@ -27,6 +26,7 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/wgkey"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -34,7 +34,11 @@ var (
|
||||
errDisabled = errors.New("magicsock: legacy networking disabled")
|
||||
)
|
||||
|
||||
func (c *Conn) createLegacyEndpointLocked(pk key.Public, addrs string) (conn.Endpoint, error) {
|
||||
// createLegacyEndpointLocked creates a new wireguard-go endpoint for a legacy connection.
|
||||
// pk is the public key of the remote peer. addrs is the ordered set of addresses for the remote peer.
|
||||
// rawDest is the encoded wireguard-go endpoint string. It should be treated as a black box.
|
||||
// It is provided so that addrSet.DstToString can return it when requested by wireguard-go.
|
||||
func (c *Conn) createLegacyEndpointLocked(pk key.Public, addrs wgcfg.IPPortSet, rawDest string) (conn.Endpoint, error) {
|
||||
if c.disableLegacy {
|
||||
return nil, errDisabled
|
||||
}
|
||||
@@ -43,17 +47,9 @@ func (c *Conn) createLegacyEndpointLocked(pk key.Public, addrs string) (conn.End
|
||||
Logf: c.logf,
|
||||
publicKey: pk,
|
||||
curAddr: -1,
|
||||
rawdst: rawDest,
|
||||
}
|
||||
|
||||
if addrs != "" {
|
||||
for _, ep := range strings.Split(addrs, ",") {
|
||||
ipp, err := netaddr.ParseIPPort(ep)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bogus address %q", ep)
|
||||
}
|
||||
a.ipPorts = append(a.ipPorts, ipp)
|
||||
}
|
||||
}
|
||||
a.ipPorts = append(a.ipPorts, addrs.IPPorts()...)
|
||||
|
||||
// If this endpoint is being updated, remember its old set of
|
||||
// endpoints so we can remove any (from c.addrsByUDP) that are
|
||||
@@ -66,7 +62,7 @@ func (c *Conn) createLegacyEndpointLocked(pk key.Public, addrs string) (conn.End
|
||||
|
||||
// Add entries to c.addrsByUDP.
|
||||
for _, ipp := range a.ipPorts {
|
||||
if ipp.IP == derpMagicIPAddr {
|
||||
if ipp.IP() == derpMagicIPAddr {
|
||||
continue
|
||||
}
|
||||
c.addrsByUDP[ipp] = a
|
||||
@@ -74,7 +70,7 @@ func (c *Conn) createLegacyEndpointLocked(pk key.Public, addrs string) (conn.End
|
||||
|
||||
// Remove previous c.addrsByUDP entries that are no longer in the new set.
|
||||
for _, ipp := range oldIPP {
|
||||
if ipp.IP != derpMagicIPAddr && c.addrsByUDP[ipp] != a {
|
||||
if ipp.IP() != derpMagicIPAddr && c.addrsByUDP[ipp] != a {
|
||||
delete(c.addrsByUDP, ipp)
|
||||
}
|
||||
}
|
||||
@@ -384,13 +380,16 @@ type addrSet struct {
|
||||
// set to a better one. This is only to suppress some
|
||||
// redundant logs.
|
||||
loggedLogPriMask uint32
|
||||
|
||||
// rawdst is the destination string from/for wireguard-go.
|
||||
rawdst string
|
||||
}
|
||||
|
||||
// derpID returns this addrSet's home DERP node, or 0 if none is found.
|
||||
func (as *addrSet) derpID() int {
|
||||
for _, ua := range as.ipPorts {
|
||||
if ua.IP == derpMagicIPAddr {
|
||||
return int(ua.Port)
|
||||
if ua.IP() == derpMagicIPAddr {
|
||||
return int(ua.Port())
|
||||
}
|
||||
}
|
||||
return 0
|
||||
@@ -426,20 +425,10 @@ func (a *addrSet) DstToBytes() []byte {
|
||||
return packIPPort(a.dst())
|
||||
}
|
||||
func (a *addrSet) DstToString() string {
|
||||
var addrs []string
|
||||
for _, addr := range a.ipPorts {
|
||||
addrs = append(addrs, addr.String())
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.roamAddr != nil {
|
||||
addrs = append(addrs, a.roamAddr.String())
|
||||
}
|
||||
return strings.Join(addrs, ",")
|
||||
return a.rawdst
|
||||
}
|
||||
func (a *addrSet) DstIP() net.IP {
|
||||
return a.dst().IP.IPAddr().IP // TODO: add netaddr accessor to cut an alloc here?
|
||||
return a.dst().IP().IPAddr().IP // TODO: add netaddr accessor to cut an alloc here?
|
||||
}
|
||||
func (a *addrSet) SrcIP() net.IP { return nil }
|
||||
func (a *addrSet) SrcToString() string { return "" }
|
||||
@@ -448,7 +437,7 @@ func (a *addrSet) ClearSrc() {}
|
||||
// updateDst records receipt of a packet from new. This is used to
|
||||
// potentially update the transmit address used for this addrSet.
|
||||
func (a *addrSet) updateDst(new netaddr.IPPort) error {
|
||||
if new.IP == derpMagicIPAddr {
|
||||
if new.IP() == derpMagicIPAddr {
|
||||
// Never consider DERP addresses as a viable candidate for
|
||||
// either curAddr or roamAddr. It's only ever a last resort
|
||||
// choice, never a preferred choice.
|
||||
@@ -550,7 +539,7 @@ func (as *addrSet) populatePeerStatus(ps *ipnstate.PeerStatus) {
|
||||
|
||||
ps.LastWrite = as.lastSend
|
||||
for i, ua := range as.ipPorts {
|
||||
if ua.IP == derpMagicIPAddr {
|
||||
if ua.IP() == derpMagicIPAddr {
|
||||
continue
|
||||
}
|
||||
uaStr := ua.String()
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
@@ -27,7 +28,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/wireguard-go/conn"
|
||||
"go4.org/mem"
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
"golang.org/x/time/rate"
|
||||
"inet.af/netaddr"
|
||||
@@ -401,7 +401,7 @@ type Options struct {
|
||||
// and 10 seconds seems like a good trade-off between often
|
||||
// enough and not too often.) The provided func is called
|
||||
// while holding userspaceEngine.wgLock and likely calls
|
||||
// Conn.CreateEndpoint, which acquires Conn.mu. As such, you
|
||||
// Conn.ParseEndpoint, which acquires Conn.mu. As such, you
|
||||
// should not hold Conn.mu while calling it.
|
||||
NoteRecvActivity func(tailcfg.DiscoKey)
|
||||
|
||||
@@ -832,7 +832,7 @@ func (c *Conn) Ping(peer *tailcfg.Node, res *ipnstate.PingResult, cb func(*ipnst
|
||||
return
|
||||
}
|
||||
if len(peer.Addresses) > 0 {
|
||||
res.NodeIP = peer.Addresses[0].IP.String()
|
||||
res.NodeIP = peer.Addresses[0].IP().String()
|
||||
}
|
||||
res.NodeName = peer.Name // prefer DNS name
|
||||
if res.NodeName == "" {
|
||||
@@ -878,11 +878,11 @@ func (c *Conn) Ping(peer *tailcfg.Node, res *ipnstate.PingResult, cb func(*ipnst
|
||||
// c.mu must be held
|
||||
func (c *Conn) populateCLIPingResponseLocked(res *ipnstate.PingResult, latency time.Duration, ep netaddr.IPPort) {
|
||||
res.LatencySeconds = latency.Seconds()
|
||||
if ep.IP != derpMagicIPAddr {
|
||||
if ep.IP() != derpMagicIPAddr {
|
||||
res.Endpoint = ep.String()
|
||||
return
|
||||
}
|
||||
regionID := int(ep.Port)
|
||||
regionID := int(ep.Port())
|
||||
res.DERPRegionID = regionID
|
||||
if c.derpMap != nil {
|
||||
if dr, ok := c.derpMap.Regions[regionID]; ok {
|
||||
@@ -965,7 +965,7 @@ func (c *Conn) goDerpConnect(node int) {
|
||||
if node == 0 {
|
||||
return
|
||||
}
|
||||
go c.derpWriteChanOfAddr(netaddr.IPPort{IP: derpMagicIPAddr, Port: uint16(node)}, key.Public{})
|
||||
go c.derpWriteChanOfAddr(netaddr.IPPortFrom(derpMagicIPAddr, uint16(node)), key.Public{})
|
||||
}
|
||||
|
||||
// determineEndpoints returns the machine's endpoint addresses. It
|
||||
@@ -1037,7 +1037,7 @@ func (c *Conn) determineEndpoints(ctx context.Context) ([]tailcfg.Endpoint, erro
|
||||
ips = loopback
|
||||
}
|
||||
for _, ip := range ips {
|
||||
addAddr(netaddr.IPPort{IP: ip, Port: uint16(localAddr.Port)}, tailcfg.EndpointLocal)
|
||||
addAddr(netaddr.IPPortFrom(ip, uint16(localAddr.Port)), tailcfg.EndpointLocal)
|
||||
}
|
||||
} else {
|
||||
// Our local endpoint is bound to a particular address.
|
||||
@@ -1169,7 +1169,7 @@ func (c *Conn) sendUDPStd(addr *net.UDPAddr, b []byte) (sent bool, err error) {
|
||||
// IPv6 address when the local machine doesn't have IPv6 support
|
||||
// returns (false, nil); it's not an error, but nothing was sent.
|
||||
func (c *Conn) sendAddr(addr netaddr.IPPort, pubKey key.Public, b []byte) (sent bool, err error) {
|
||||
if addr.IP != derpMagicIPAddr {
|
||||
if addr.IP() != derpMagicIPAddr {
|
||||
return c.sendUDP(addr, b)
|
||||
}
|
||||
|
||||
@@ -1211,10 +1211,10 @@ const bufferedDerpWritesBeforeDrop = 32
|
||||
// If peer is non-zero, it can be used to find an active reverse
|
||||
// path, without using addr.
|
||||
func (c *Conn) derpWriteChanOfAddr(addr netaddr.IPPort, peer key.Public) chan<- derpWriteRequest {
|
||||
if addr.IP != derpMagicIPAddr {
|
||||
if addr.IP() != derpMagicIPAddr {
|
||||
return nil
|
||||
}
|
||||
regionID := int(addr.Port)
|
||||
regionID := int(addr.Port())
|
||||
|
||||
if c.networkDown() {
|
||||
return nil
|
||||
@@ -1402,7 +1402,7 @@ func (c *Conn) runDerpReader(ctx context.Context, derpFakeAddr netaddr.IPPort, d
|
||||
}
|
||||
|
||||
didCopy := make(chan struct{}, 1)
|
||||
regionID := int(derpFakeAddr.Port)
|
||||
regionID := int(derpFakeAddr.Port())
|
||||
res := derpReadResult{regionID: regionID}
|
||||
var pkt derp.ReceivedPacket
|
||||
res.copyBuf = func(dst []byte) int {
|
||||
@@ -1676,7 +1676,7 @@ func (c *Conn) processDERPReadResult(dm derpReadResult, b []byte) (n int, ep con
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
ipp := netaddr.IPPort{IP: derpMagicIPAddr, Port: uint16(regionID)}
|
||||
ipp := netaddr.IPPortFrom(derpMagicIPAddr, uint16(regionID))
|
||||
if c.handleDiscoMessage(b[:n], ipp) {
|
||||
return 0, nil
|
||||
}
|
||||
@@ -1696,7 +1696,7 @@ func (c *Conn) processDERPReadResult(dm derpReadResult, b []byte) (n int, ep con
|
||||
if discoEp == nil && c.noteRecvActivity != nil {
|
||||
didNoteRecvActivity = true
|
||||
c.mu.Unlock() // release lock before calling noteRecvActivity
|
||||
c.noteRecvActivity(dk) // (calls back into CreateEndpoint)
|
||||
c.noteRecvActivity(dk) // (calls back into ParseEndpoint)
|
||||
// Now require the lock. No invariants need to be rechecked; just
|
||||
// 1-2 map lookups follow that are harmless if, say, the peer has
|
||||
// been deleted during this time.
|
||||
@@ -1837,7 +1837,7 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) (isDiscoMsg bo
|
||||
// We don't have an active endpoint for this sender but we knew about the node, so
|
||||
// it's an idle endpoint that doesn't yet exist in the wireguard config. We now have
|
||||
// to notify the userspace engine (via noteRecvActivity) so wireguard-go can create
|
||||
// an Endpoint (ultimately calling our CreateEndpoint).
|
||||
// an Endpoint (ultimately calling our ParseEndpoint).
|
||||
c.logf("magicsock: got disco message from idle peer, starting lazy conf for %v, %v", peerNode.Key.ShortString(), sender.ShortString())
|
||||
if c.noteRecvActivity == nil {
|
||||
c.logf("magicsock: [unexpected] have node without endpoint, without c.noteRecvActivity hook")
|
||||
@@ -1851,7 +1851,7 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) (isDiscoMsg bo
|
||||
// We can't hold Conn.mu while calling noteRecvActivity.
|
||||
// noteRecvActivity acquires userspaceEngine.wgLock (and per our
|
||||
// lock ordering rules: wgLock must come first), and also calls
|
||||
// back into our Conn.CreateEndpoint, which would double-acquire
|
||||
// back into our Conn.ParseEndpoint, which would double-acquire
|
||||
// Conn.mu.
|
||||
c.mu.Unlock()
|
||||
c.noteRecvActivity(sender)
|
||||
@@ -1922,7 +1922,7 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) (isDiscoMsg bo
|
||||
}
|
||||
de.handlePongConnLocked(dm, src)
|
||||
case *disco.CallMeMaybe:
|
||||
if src.IP != derpMagicIPAddr {
|
||||
if src.IP() != derpMagicIPAddr {
|
||||
// CallMeMaybe messages should only come via DERP.
|
||||
c.logf("[unexpected] CallMeMaybe packets should only come via DERP")
|
||||
return
|
||||
@@ -2722,7 +2722,7 @@ func (c *Conn) resetEndpointStates() {
|
||||
|
||||
// packIPPort packs an IPPort into the form wanted by WireGuard.
|
||||
func packIPPort(ua netaddr.IPPort) []byte {
|
||||
ip := ua.IP.Unmap()
|
||||
ip := ua.IP().Unmap()
|
||||
a := ip.As16()
|
||||
ipb := a[:]
|
||||
if ip.Is4() {
|
||||
@@ -2730,50 +2730,38 @@ func packIPPort(ua netaddr.IPPort) []byte {
|
||||
}
|
||||
b := make([]byte, 0, len(ipb)+2)
|
||||
b = append(b, ipb...)
|
||||
b = append(b, byte(ua.Port))
|
||||
b = append(b, byte(ua.Port>>8))
|
||||
b = append(b, byte(ua.Port()))
|
||||
b = append(b, byte(ua.Port()>>8))
|
||||
return b
|
||||
}
|
||||
|
||||
// ParseEndpoint is called by WireGuard to connect to an endpoint.
|
||||
//
|
||||
// keyAddrs is the 32 byte public key of the peer followed by addrs.
|
||||
// Addrs is either:
|
||||
//
|
||||
// 1) a comma-separated list of UDP ip:ports (the peer doesn't have a discovery key)
|
||||
// 2) "<hex-discovery-key>.disco.tailscale:12345", a magic value that means the peer
|
||||
// is running code that supports active discovery, so CreateEndpoint returns
|
||||
// a discoEndpoint.
|
||||
func (c *Conn) ParseEndpoint(keyAddrs string) (conn.Endpoint, error) {
|
||||
if len(keyAddrs) < 32 {
|
||||
c.logf("[unexpected] ParseEndpoint keyAddrs too short: %q", keyAddrs)
|
||||
return nil, errors.New("endpoint string too short")
|
||||
// endpointStr is a json-serialized wgcfg.Endpoints struct.
|
||||
// If those Endpoints contain an active discovery key, ParseEndpoint returns a discoEndpoint.
|
||||
// Otherwise it returns a legacy endpoint.
|
||||
func (c *Conn) ParseEndpoint(endpointStr string) (conn.Endpoint, error) {
|
||||
var endpoints wgcfg.Endpoints
|
||||
err := json.Unmarshal([]byte(endpointStr), &endpoints)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("magicsock: ParseEndpoint: json.Unmarshal failed on %q: %w", endpointStr, err)
|
||||
}
|
||||
var pk key.Public
|
||||
copy(pk[:], keyAddrs)
|
||||
addrs := keyAddrs[len(pk):]
|
||||
pk := key.Public(endpoints.PublicKey)
|
||||
discoKey := endpoints.DiscoKey
|
||||
c.logf("magicsock: ParseEndpoint: key=%s: disco=%s ipps=%s", pk.ShortString(), discoKey.ShortString(), derpStr(endpoints.IPPorts.String()))
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.logf("magicsock: ParseEndpoint: key=%s: %s", pk.ShortString(), derpStr(addrs))
|
||||
|
||||
if !strings.HasSuffix(addrs, wgcfg.EndpointDiscoSuffix) {
|
||||
return c.createLegacyEndpointLocked(pk, addrs)
|
||||
}
|
||||
|
||||
discoHex := strings.TrimSuffix(addrs, wgcfg.EndpointDiscoSuffix)
|
||||
discoKey, err := key.NewPublicFromHexMem(mem.S(discoHex))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("magicsock: invalid discokey endpoint %q for %v: %w", addrs, pk.ShortString(), err)
|
||||
if discoKey.IsZero() {
|
||||
return c.createLegacyEndpointLocked(pk, endpoints.IPPorts, endpointStr)
|
||||
}
|
||||
de := &discoEndpoint{
|
||||
c: c,
|
||||
publicKey: tailcfg.NodeKey(pk), // peer public key (for WireGuard + DERP)
|
||||
discoKey: tailcfg.DiscoKey(discoKey), // for discovery mesages
|
||||
discoShort: tailcfg.DiscoKey(discoKey).ShortString(),
|
||||
wgEndpointHostPort: addrs,
|
||||
sentPing: map[stun.TxID]sentPing{},
|
||||
endpointState: map[netaddr.IPPort]*endpointState{},
|
||||
c: c,
|
||||
publicKey: tailcfg.NodeKey(pk), // peer public key (for WireGuard + DERP)
|
||||
discoKey: tailcfg.DiscoKey(discoKey), // for discovery mesages
|
||||
discoShort: tailcfg.DiscoKey(discoKey).ShortString(),
|
||||
wgEndpoint: endpointStr,
|
||||
sentPing: map[stun.TxID]sentPing{},
|
||||
endpointState: map[netaddr.IPPort]*endpointState{},
|
||||
}
|
||||
de.initFakeUDPAddr()
|
||||
de.updateFromNode(c.nodeOfDisco[de.discoKey])
|
||||
@@ -2984,15 +2972,15 @@ func peerShort(k key.Public) string {
|
||||
}
|
||||
|
||||
func sbPrintAddr(sb *strings.Builder, a netaddr.IPPort) {
|
||||
is6 := a.IP.Is6()
|
||||
is6 := a.IP().Is6()
|
||||
if is6 {
|
||||
sb.WriteByte('[')
|
||||
}
|
||||
fmt.Fprintf(sb, "%s", a.IP)
|
||||
fmt.Fprintf(sb, "%s", a.IP())
|
||||
if is6 {
|
||||
sb.WriteByte(']')
|
||||
}
|
||||
fmt.Fprintf(sb, ":%d", a.Port)
|
||||
fmt.Fprintf(sb, ":%d", a.Port())
|
||||
}
|
||||
|
||||
func (c *Conn) derpRegionCodeOfAddrLocked(ipPort string) string {
|
||||
@@ -3029,15 +3017,15 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
if !addr.IsSingleIP() {
|
||||
continue
|
||||
}
|
||||
sb.AddTailscaleIP(addr.IP)
|
||||
sb.AddTailscaleIP(addr.IP())
|
||||
// TailAddr previously only allowed for a
|
||||
// single Tailscale IP. For compatibility for
|
||||
// a couple releases starting with 1.8, keep
|
||||
// that field pulled out separately.
|
||||
if addr.IP.Is4() {
|
||||
tailAddr4 = addr.IP.String()
|
||||
if addr.IP().Is4() {
|
||||
tailAddr4 = addr.IP().String()
|
||||
}
|
||||
tailscaleIPs = append(tailscaleIPs, addr.IP)
|
||||
tailscaleIPs = append(tailscaleIPs, addr.IP())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3096,8 +3084,8 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) {
|
||||
}
|
||||
|
||||
func ippDebugString(ua netaddr.IPPort) string {
|
||||
if ua.IP == derpMagicIPAddr {
|
||||
return fmt.Sprintf("derp-%d", ua.Port)
|
||||
if ua.IP() == derpMagicIPAddr {
|
||||
return fmt.Sprintf("derp-%d", ua.Port())
|
||||
}
|
||||
return ua.String()
|
||||
}
|
||||
@@ -3110,12 +3098,12 @@ type discoEndpoint struct {
|
||||
numStopAndResetAtomic int64
|
||||
|
||||
// These fields are initialized once and never modified.
|
||||
c *Conn
|
||||
publicKey tailcfg.NodeKey // peer public key (for WireGuard + DERP)
|
||||
discoKey tailcfg.DiscoKey // for discovery mesages
|
||||
discoShort string // ShortString of discoKey
|
||||
fakeWGAddr netaddr.IPPort // the UDP address we tell wireguard-go we're using
|
||||
wgEndpointHostPort string // string from CreateEndpoint: "<hex-discovery-key>.disco.tailscale:12345"
|
||||
c *Conn
|
||||
publicKey tailcfg.NodeKey // peer public key (for WireGuard + DERP)
|
||||
discoKey tailcfg.DiscoKey // for discovery mesages
|
||||
discoShort string // ShortString of discoKey
|
||||
fakeWGAddr netaddr.IPPort // the UDP address we tell wireguard-go we're using
|
||||
wgEndpoint string // string from ParseEndpoint, holds a JSON-serialized wgcfg.Endpoints
|
||||
|
||||
// Owned by Conn.mu:
|
||||
lastPingFrom netaddr.IPPort
|
||||
@@ -3266,10 +3254,7 @@ func (de *discoEndpoint) initFakeUDPAddr() {
|
||||
addr[0] = 0xfd
|
||||
addr[1] = 0x00
|
||||
binary.BigEndian.PutUint64(addr[2:], uint64(reflect.ValueOf(de).Pointer()))
|
||||
de.fakeWGAddr = netaddr.IPPort{
|
||||
IP: netaddr.IPFrom16(addr),
|
||||
Port: 12345,
|
||||
}
|
||||
de.fakeWGAddr = netaddr.IPPortFrom(netaddr.IPFrom16(addr), 12345)
|
||||
}
|
||||
|
||||
// isFirstRecvActivityInAwhile notes that receive activity has occured for this
|
||||
@@ -3295,7 +3280,7 @@ func (de *discoEndpoint) String() string {
|
||||
func (de *discoEndpoint) ClearSrc() {}
|
||||
func (de *discoEndpoint) SrcToString() string { panic("unused") } // unused by wireguard-go
|
||||
func (de *discoEndpoint) SrcIP() net.IP { panic("unused") } // unused by wireguard-go
|
||||
func (de *discoEndpoint) DstToString() string { return de.wgEndpointHostPort }
|
||||
func (de *discoEndpoint) DstToString() string { return de.wgEndpoint }
|
||||
func (de *discoEndpoint) DstIP() net.IP { panic("unused") }
|
||||
func (de *discoEndpoint) DstToBytes() []byte { return packIPPort(de.fakeWGAddr) }
|
||||
|
||||
@@ -3644,7 +3629,7 @@ func (de *discoEndpoint) handlePongConnLocked(m *disco.Pong, src netaddr.IPPort)
|
||||
de.mu.Lock()
|
||||
defer de.mu.Unlock()
|
||||
|
||||
isDerp := src.IP == derpMagicIPAddr
|
||||
isDerp := src.IP() == derpMagicIPAddr
|
||||
|
||||
sp, ok := de.sentPing[m.TxID]
|
||||
if !ok {
|
||||
@@ -3720,13 +3705,13 @@ func betterAddr(a, b addrLatency) bool {
|
||||
if a.IsZero() {
|
||||
return false
|
||||
}
|
||||
if a.IP.Is6() && b.IP.Is4() {
|
||||
if a.IP().Is6() && b.IP().Is4() {
|
||||
// Prefer IPv6 for being a bit more robust, as long as
|
||||
// the latencies are roughly equivalent.
|
||||
if a.latency/10*9 < b.latency {
|
||||
return true
|
||||
}
|
||||
} else if a.IP.Is4() && b.IP.Is6() {
|
||||
} else if a.IP().Is4() && b.IP().Is6() {
|
||||
if betterAddr(b, a) {
|
||||
return false
|
||||
}
|
||||
@@ -3766,7 +3751,7 @@ func (de *discoEndpoint) handleCallMeMaybe(m *disco.CallMeMaybe) {
|
||||
}
|
||||
var newEPs []netaddr.IPPort
|
||||
for _, ep := range m.MyNumber {
|
||||
if ep.IP.Is6() && ep.IP.IsLinkLocalUnicast() {
|
||||
if ep.IP().Is6() && ep.IP().IsLinkLocalUnicast() {
|
||||
// We send these out, but ignore them for now.
|
||||
// TODO: teach the ping code to ping on all interfaces
|
||||
// for these.
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
crand "crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@@ -167,7 +168,7 @@ func newMagicStack(t testing.TB, logf logger.Logf, l nettype.PacketListener, der
|
||||
tsTun.SetFilter(filter.NewAllowAllForTest(logf))
|
||||
|
||||
wgLogger := wglog.NewLogger(logf)
|
||||
dev := device.NewDevice(tsTun, conn.Bind(), wgLogger.DeviceLogger, new(device.DeviceOptions))
|
||||
dev := device.NewDevice(tsTun, conn.Bind(), wgLogger.DeviceLogger)
|
||||
dev.Up()
|
||||
|
||||
// Wait for magicsock to connect up to DERP.
|
||||
@@ -251,13 +252,13 @@ func meshStacks(logf logger.Logf, ms []*magicStack) (cleanup func()) {
|
||||
nm := &netmap.NetworkMap{
|
||||
PrivateKey: me.privateKey,
|
||||
NodeKey: tailcfg.NodeKey(me.privateKey.Public()),
|
||||
Addresses: []netaddr.IPPrefix{{IP: netaddr.IPv4(1, 0, 0, byte(myIdx+1)), Bits: 32}},
|
||||
Addresses: []netaddr.IPPrefix{netaddr.IPPrefixFrom(netaddr.IPv4(1, 0, 0, byte(myIdx+1)), 32)},
|
||||
}
|
||||
for i, peer := range ms {
|
||||
if i == myIdx {
|
||||
continue
|
||||
}
|
||||
addrs := []netaddr.IPPrefix{{IP: netaddr.IPv4(1, 0, 0, byte(i+1)), Bits: 32}}
|
||||
addrs := []netaddr.IPPrefix{netaddr.IPPrefixFrom(netaddr.IPv4(1, 0, 0, byte(i+1)), 32)}
|
||||
peer := &tailcfg.Node{
|
||||
ID: tailcfg.NodeID(i + 1),
|
||||
Name: fmt.Sprintf("node%d", i+1),
|
||||
@@ -432,7 +433,7 @@ func TestPickDERPFallback(t *testing.T) {
|
||||
// But move if peers are elsewhere.
|
||||
const otherNode = 789
|
||||
c.addrsByKey = map[key.Public]*addrSet{
|
||||
{1}: {ipPorts: []netaddr.IPPort{{IP: derpMagicIPAddr, Port: otherNode}}},
|
||||
{1}: {ipPorts: []netaddr.IPPort{netaddr.IPPortFrom(derpMagicIPAddr, otherNode)}},
|
||||
}
|
||||
if got := c.pickDERPFallback(); got != otherNode {
|
||||
t.Errorf("didn't join peers: got %v; want %v", got, someNode)
|
||||
@@ -453,7 +454,7 @@ func makeConfigs(t *testing.T, addrs []netaddr.IPPort) []wgcfg.Config {
|
||||
privKeys = append(privKeys, wgkey.Private(privKey))
|
||||
|
||||
addresses = append(addresses, []netaddr.IPPrefix{
|
||||
parseCIDR(t, fmt.Sprintf("1.0.0.%d/32", i+1)),
|
||||
netaddr.MustParseIPPrefix(fmt.Sprintf("1.0.0.%d/32", i+1)),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -468,10 +469,14 @@ func makeConfigs(t *testing.T, addrs []netaddr.IPPort) []wgcfg.Config {
|
||||
if peerNum == i {
|
||||
continue
|
||||
}
|
||||
publicKey := privKeys[peerNum].Public()
|
||||
peer := wgcfg.Peer{
|
||||
PublicKey: privKeys[peerNum].Public(),
|
||||
AllowedIPs: addresses[peerNum],
|
||||
Endpoints: addr.String(),
|
||||
PublicKey: publicKey,
|
||||
AllowedIPs: addresses[peerNum],
|
||||
Endpoints: wgcfg.Endpoints{
|
||||
PublicKey: publicKey,
|
||||
IPPorts: wgcfg.NewIPPortSet(addr),
|
||||
},
|
||||
PersistentKeepalive: 25,
|
||||
}
|
||||
cfg.Peers = append(cfg.Peers, peer)
|
||||
@@ -481,15 +486,6 @@ func makeConfigs(t *testing.T, addrs []netaddr.IPPort) []wgcfg.Config {
|
||||
return cfgs
|
||||
}
|
||||
|
||||
func parseCIDR(t *testing.T, addr string) netaddr.IPPrefix {
|
||||
t.Helper()
|
||||
cidr, err := netaddr.ParseIPPrefix(addr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return cidr
|
||||
}
|
||||
|
||||
// TestDeviceStartStop exercises the startup and shutdown logic of
|
||||
// wireguard-go, which is intimately intertwined with magicsock's own
|
||||
// lifecycle. We seem to be good at generating deadlocks here, so if
|
||||
@@ -513,7 +509,7 @@ func TestDeviceStartStop(t *testing.T) {
|
||||
|
||||
tun := tuntest.NewChannelTUN()
|
||||
wgLogger := wglog.NewLogger(t.Logf)
|
||||
dev := device.NewDevice(tun.TUN(), conn.Bind(), wgLogger.DeviceLogger, new(device.DeviceOptions))
|
||||
dev := device.NewDevice(tun.TUN(), conn.Bind(), wgLogger.DeviceLogger)
|
||||
dev.Up()
|
||||
dev.Close()
|
||||
}
|
||||
@@ -891,8 +887,8 @@ func testTwoDevicePing(t *testing.T, d *devices) {
|
||||
defer m2.Close()
|
||||
|
||||
addrs := []netaddr.IPPort{
|
||||
{IP: d.m1IP, Port: m1.conn.LocalPort()},
|
||||
{IP: d.m2IP, Port: m2.conn.LocalPort()},
|
||||
netaddr.IPPortFrom(d.m1IP, m1.conn.LocalPort()),
|
||||
netaddr.IPPortFrom(d.m2IP, m2.conn.LocalPort()),
|
||||
}
|
||||
cfgs := makeConfigs(t, addrs)
|
||||
|
||||
@@ -1251,6 +1247,19 @@ func newNonLegacyTestConn(t testing.TB) *Conn {
|
||||
return conn
|
||||
}
|
||||
|
||||
func makeEndpoint(tb testing.TB, public tailcfg.NodeKey, disco tailcfg.DiscoKey) string {
|
||||
tb.Helper()
|
||||
ep := wgcfg.Endpoints{
|
||||
PublicKey: wgkey.Key(public),
|
||||
DiscoKey: disco,
|
||||
}
|
||||
buf, err := json.Marshal(ep)
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
// addTestEndpoint sets conn's network map to a single peer expected
|
||||
// to receive packets from sendConn (or DERP), and returns that peer's
|
||||
// nodekey and discokey.
|
||||
@@ -1270,7 +1279,7 @@ func addTestEndpoint(tb testing.TB, conn *Conn, sendConn net.PacketConn) (tailcf
|
||||
},
|
||||
})
|
||||
conn.SetPrivateKey(wgkey.Private{0: 1})
|
||||
_, err := conn.ParseEndpoint(string(nodeKey[:]) + "0000000000000000000000000000000000000000000000000000000000000001.disco.tailscale:12345")
|
||||
_, err := conn.ParseEndpoint(makeEndpoint(tb, nodeKey, discoKey))
|
||||
if err != nil {
|
||||
tb.Fatal(err)
|
||||
}
|
||||
@@ -1444,7 +1453,7 @@ func TestSetNetworkMapChangingNodeKey(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
_, err := conn.ParseEndpoint(string(nodeKey1[:]) + "0000000000000000000000000000000000000000000000000000000000000001.disco.tailscale:12345")
|
||||
_, err := conn.ParseEndpoint(makeEndpoint(t, nodeKey1, discoKey))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -1546,7 +1555,7 @@ func TestEndpointSetsEqual(t *testing.T) {
|
||||
s := func(ports ...uint16) (ret []tailcfg.Endpoint) {
|
||||
for _, port := range ports {
|
||||
ret = append(ret, tailcfg.Endpoint{
|
||||
Addr: netaddr.IPPort{Port: port},
|
||||
Addr: netaddr.IPPortFrom(netaddr.IP{}, port),
|
||||
})
|
||||
}
|
||||
return
|
||||
|
||||
@@ -130,11 +130,11 @@ func netaddrIP(std net.IP) netaddr.IP {
|
||||
|
||||
func netaddrIPPrefix(std net.IP, bits uint8) netaddr.IPPrefix {
|
||||
ip, _ := netaddr.FromStdIP(std)
|
||||
return netaddr.IPPrefix{IP: ip, Bits: bits}
|
||||
return netaddr.IPPrefixFrom(ip, bits)
|
||||
}
|
||||
|
||||
func condNetAddrPrefix(ipp netaddr.IPPrefix) string {
|
||||
if ipp.IP.IsZero() {
|
||||
if ipp.IP().IsZero() {
|
||||
return ""
|
||||
}
|
||||
return ipp.String()
|
||||
@@ -157,7 +157,7 @@ type newRouteMessage struct {
|
||||
const tsTable = 52
|
||||
|
||||
func (m *newRouteMessage) ignore() bool {
|
||||
return m.Table == tsTable || tsaddr.IsTailscaleIP(m.Dst.IP)
|
||||
return m.Table == tsTable || tsaddr.IsTailscaleIP(m.Dst.IP())
|
||||
}
|
||||
|
||||
// newAddrMessage is a message for a new address being added.
|
||||
|
||||
@@ -48,6 +48,12 @@ const debugNetstack = false
|
||||
// and implements wgengine.FakeImpl to act as a userspace network
|
||||
// stack when Tailscale is running in fake mode.
|
||||
type Impl struct {
|
||||
// ForwardTCPIn, if non-nil, handles forwarding an inbound TCP
|
||||
// connection.
|
||||
// TODO(bradfitz): provide mechanism for tsnet to reject a
|
||||
// port other than accepting it and closing it.
|
||||
ForwardTCPIn func(c net.Conn, port uint16)
|
||||
|
||||
ipstack *stack.Stack
|
||||
linkEP *channel.Endpoint
|
||||
tundev *tstun.Wrapper
|
||||
@@ -173,7 +179,7 @@ func DNSMapFromNetworkMap(nm *netmap.NetworkMap) DNSMap {
|
||||
suffix := nm.MagicDNSSuffix()
|
||||
|
||||
if nm.Name != "" && len(nm.Addresses) > 0 {
|
||||
ip := nm.Addresses[0].IP
|
||||
ip := nm.Addresses[0].IP()
|
||||
ret[strings.TrimRight(nm.Name, ".")] = ip
|
||||
if dnsname.HasSuffix(nm.Name, suffix) {
|
||||
ret[dnsname.TrimSuffix(nm.Name, suffix)] = ip
|
||||
@@ -181,7 +187,7 @@ func DNSMapFromNetworkMap(nm *netmap.NetworkMap) DNSMap {
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
if p.Name != "" && len(p.Addresses) > 0 {
|
||||
ip := p.Addresses[0].IP
|
||||
ip := p.Addresses[0].IP()
|
||||
ret[strings.TrimRight(p.Name, ".")] = ip
|
||||
if dnsname.HasSuffix(p.Name, suffix) {
|
||||
ret[dnsname.TrimSuffix(p.Name, suffix)] = ip
|
||||
@@ -221,8 +227,8 @@ func (ns *Impl) removeSubnetAddress(ip netaddr.IP) {
|
||||
|
||||
func ipPrefixToAddressWithPrefix(ipp netaddr.IPPrefix) tcpip.AddressWithPrefix {
|
||||
return tcpip.AddressWithPrefix{
|
||||
Address: tcpip.Address(ipp.IP.IPAddr().IP),
|
||||
PrefixLen: int(ipp.Bits),
|
||||
Address: tcpip.Address(ipp.IP().IPAddr().IP),
|
||||
PrefixLen: int(ipp.Bits()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,7 +322,7 @@ func (m DNSMap) Resolve(ctx context.Context, addr string) (netaddr.IPPort, error
|
||||
// Try MagicDNS first, else otherwise a real DNS lookup.
|
||||
ip := m[host]
|
||||
if !ip.IsZero() {
|
||||
return netaddr.IPPort{IP: ip, Port: uint16(port16)}, nil
|
||||
return netaddr.IPPortFrom(ip, uint16(port16)), nil
|
||||
}
|
||||
|
||||
// No MagicDNS name so try real DNS.
|
||||
@@ -329,7 +335,7 @@ func (m DNSMap) Resolve(ctx context.Context, addr string) (netaddr.IPPort, error
|
||||
return netaddr.IPPort{}, fmt.Errorf("DNS lookup returned no results for %q", host)
|
||||
}
|
||||
ip, _ = netaddr.FromStdIP(ips[0])
|
||||
return netaddr.IPPort{IP: ip, Port: uint16(port16)}, nil
|
||||
return netaddr.IPPortFrom(ip, uint16(port16)), nil
|
||||
}
|
||||
|
||||
func (ns *Impl) DialContextTCP(ctx context.Context, addr string) (*gonet.TCPConn, error) {
|
||||
@@ -343,11 +349,11 @@ func (ns *Impl) DialContextTCP(ctx context.Context, addr string) (*gonet.TCPConn
|
||||
}
|
||||
remoteAddress := tcpip.FullAddress{
|
||||
NIC: nicID,
|
||||
Addr: tcpip.Address(remoteIPPort.IP.IPAddr().IP),
|
||||
Port: remoteIPPort.Port,
|
||||
Addr: tcpip.Address(remoteIPPort.IP().IPAddr().IP),
|
||||
Port: remoteIPPort.Port(),
|
||||
}
|
||||
var ipType tcpip.NetworkProtocolNumber
|
||||
if remoteIPPort.IP.Is4() {
|
||||
if remoteIPPort.IP().Is4() {
|
||||
ipType = ipv4.ProtocolNumber
|
||||
} else {
|
||||
ipType = ipv6.ProtocolNumber
|
||||
@@ -389,7 +395,7 @@ func (ns *Impl) isLocalIP(ip netaddr.IP) bool {
|
||||
}
|
||||
|
||||
func (ns *Impl) injectInbound(p *packet.Parsed, t *tstun.Wrapper) filter.Response {
|
||||
if ns.onlySubnets && ns.isLocalIP(p.Dst.IP) {
|
||||
if ns.onlySubnets && ns.isLocalIP(p.Dst.IP()) {
|
||||
// In hybrid ("only subnets") mode, bail out early if
|
||||
// the traffic is destined for an actual Tailscale
|
||||
// address. The real host OS interface will handle it.
|
||||
@@ -410,7 +416,14 @@ func (ns *Impl) injectInbound(p *packet.Parsed, t *tstun.Wrapper) filter.Respons
|
||||
Data: vv,
|
||||
})
|
||||
ns.linkEP.InjectInbound(pn, packetBuf)
|
||||
return filter.Accept
|
||||
|
||||
// We've now delivered this to netstack, so we're done.
|
||||
// Instead of returning a filter.Accept here (which would also
|
||||
// potentially deliver it to the host OS), and instead of
|
||||
// filter.Drop (which would log about rejected traffic),
|
||||
// instead return filter.DropSilently which just quietly stops
|
||||
// processing it in the tstun TUN wrapper.
|
||||
return filter.DropSilently
|
||||
}
|
||||
|
||||
func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
|
||||
@@ -434,11 +447,15 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
|
||||
r.Complete(true)
|
||||
return
|
||||
}
|
||||
r.Complete(false)
|
||||
c := gonet.NewTCPConn(&wq, ep)
|
||||
if ns.ForwardTCPIn != nil {
|
||||
ns.ForwardTCPIn(c, reqDetails.LocalPort)
|
||||
return
|
||||
}
|
||||
if isTailscaleIP {
|
||||
dialAddr = tcpip.Address(net.ParseIP("127.0.0.1")).To4()
|
||||
}
|
||||
r.Complete(false)
|
||||
c := gonet.NewTCPConn(&wq, ep)
|
||||
ns.forwardTCP(c, &wq, dialAddr, reqDetails.LocalPort)
|
||||
}
|
||||
|
||||
|
||||
@@ -115,8 +115,8 @@ func (e *userspaceEngine) trackOpenPostFilterOut(pp *packet.Parsed, t *tstun.Wra
|
||||
// Don't start timers tracking those. They won't succeed anyway. Avoids log spam
|
||||
// like:
|
||||
// open-conn-track: timeout opening (100.115.73.60:52501 => 17.125.252.5:443); no associated peer node
|
||||
if runtime.GOOS == "ios" && flow.Dst.Port == 443 && !tsaddr.IsTailscaleIP(flow.Dst.IP) {
|
||||
if _, err := e.peerForIP(flow.Dst.IP); err != nil {
|
||||
if runtime.GOOS == "ios" && flow.Dst.Port() == 443 && !tsaddr.IsTailscaleIP(flow.Dst.IP()) {
|
||||
if _, err := e.peerForIP(flow.Dst.IP()); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -156,7 +156,7 @@ func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) {
|
||||
}
|
||||
|
||||
// Diagnose why it might've timed out.
|
||||
n, err := e.peerForIP(flow.Dst.IP)
|
||||
n, err := e.peerForIP(flow.Dst.IP())
|
||||
if err != nil {
|
||||
e.logf("open-conn-track: timeout opening %v; peerForIP: %v", flow, err)
|
||||
return
|
||||
@@ -193,7 +193,7 @@ func (e *userspaceEngine) onOpenTimeout(flow flowtrack.Tuple) {
|
||||
if ps == nil {
|
||||
onlyZeroRoute := true // whether peerForIP returned n only because its /0 route matched
|
||||
for _, r := range n.AllowedIPs {
|
||||
if r.Bits != 0 && r.Contains(flow.Dst.IP) {
|
||||
if r.Bits() != 0 && r.Contains(flow.Dst.IP()) {
|
||||
onlyZeroRoute = false
|
||||
break
|
||||
}
|
||||
|
||||
@@ -324,16 +324,16 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) {
|
||||
var firstGateway6 *net.IP
|
||||
addresses := make([]*net.IPNet, 0, len(cfg.LocalAddrs))
|
||||
for _, addr := range cfg.LocalAddrs {
|
||||
if (addr.IP.Is4() && ipif4 == nil) || (addr.IP.Is6() && ipif6 == nil) {
|
||||
if (addr.IP().Is4() && ipif4 == nil) || (addr.IP().Is6() && ipif6 == nil) {
|
||||
// Can't program addresses for disabled protocol.
|
||||
continue
|
||||
}
|
||||
ipnet := addr.IPNet()
|
||||
addresses = append(addresses, ipnet)
|
||||
gateway := ipnet.IP
|
||||
if addr.IP.Is4() && firstGateway4 == nil {
|
||||
if addr.IP().Is4() && firstGateway4 == nil {
|
||||
firstGateway4 = &gateway
|
||||
} else if addr.IP.Is6() && firstGateway6 == nil {
|
||||
} else if addr.IP().Is6() && firstGateway6 == nil {
|
||||
firstGateway6 = &gateway
|
||||
}
|
||||
}
|
||||
@@ -342,12 +342,12 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) {
|
||||
foundDefault4 := false
|
||||
foundDefault6 := false
|
||||
for _, route := range cfg.Routes {
|
||||
if (route.IP.Is4() && ipif4 == nil) || (route.IP.Is6() && ipif6 == nil) {
|
||||
if (route.IP().Is4() && ipif4 == nil) || (route.IP().Is6() && ipif6 == nil) {
|
||||
// Can't program routes for disabled protocol.
|
||||
continue
|
||||
}
|
||||
|
||||
if route.IP.Is6() && firstGateway6 == nil {
|
||||
if route.IP().Is6() && firstGateway6 == nil {
|
||||
// Windows won't let us set IPv6 routes without having an
|
||||
// IPv6 local address set. However, when we've configured
|
||||
// a default route, we want to forcibly grab IPv6 traffic
|
||||
@@ -357,16 +357,16 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) {
|
||||
ipnet := &net.IPNet{tsaddr.Tailscale4To6Placeholder().IPAddr().IP, net.CIDRMask(128, 128)}
|
||||
addresses = append(addresses, ipnet)
|
||||
firstGateway6 = &ipnet.IP
|
||||
} else if route.IP.Is4() && firstGateway4 == nil {
|
||||
} else if route.IP().Is4() && firstGateway4 == nil {
|
||||
// TODO: do same dummy behavior as v6?
|
||||
return errors.New("due to a Windows limitation, one cannot have interface routes without an interface address")
|
||||
}
|
||||
|
||||
ipn := route.IPNet()
|
||||
var gateway net.IP
|
||||
if route.IP.Is4() {
|
||||
if route.IP().Is4() {
|
||||
gateway = *firstGateway4
|
||||
} else if route.IP.Is6() {
|
||||
} else if route.IP().Is6() {
|
||||
gateway = *firstGateway6
|
||||
}
|
||||
r := winipcfg.RouteData{
|
||||
@@ -385,13 +385,13 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) {
|
||||
// then the interface's IP won't be pingable.
|
||||
continue
|
||||
}
|
||||
if route.IP.Is4() {
|
||||
if route.Bits == 0 {
|
||||
if route.IP().Is4() {
|
||||
if route.Bits() == 0 {
|
||||
foundDefault4 = true
|
||||
}
|
||||
r.NextHop = *firstGateway4
|
||||
} else if route.IP.Is6() {
|
||||
if route.Bits == 0 {
|
||||
} else if route.IP().Is6() {
|
||||
if route.Bits() == 0 {
|
||||
foundDefault6 = true
|
||||
}
|
||||
r.NextHop = *firstGateway6
|
||||
@@ -760,11 +760,8 @@ func filterRoutes(routes []*winipcfg.RouteData, dontDelete []netaddr.IPPrefix) [
|
||||
if nr.IsSingleIP() {
|
||||
continue
|
||||
}
|
||||
lastIP := nr.Range().To
|
||||
ddm[netaddr.IPPrefix{
|
||||
IP: lastIP,
|
||||
Bits: lastIP.BitLen(),
|
||||
}] = true
|
||||
lastIP := nr.Range().To()
|
||||
ddm[netaddr.IPPrefixFrom(lastIP, lastIP.BitLen())] = true
|
||||
}
|
||||
filtered := make([]*winipcfg.RouteData, 0, len(routes))
|
||||
for _, r := range routes {
|
||||
|
||||
@@ -360,7 +360,7 @@ func (r *linuxRouter) setNetfilterMode(mode preftype.NetfilterMode) error {
|
||||
}
|
||||
|
||||
for cidr := range r.addrs {
|
||||
if err := r.addLoopbackRule(cidr.IP); err != nil {
|
||||
if err := r.addLoopbackRule(cidr.IP()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -372,13 +372,13 @@ func (r *linuxRouter) setNetfilterMode(mode preftype.NetfilterMode) error {
|
||||
// address is already assigned to the interface, or if the addition
|
||||
// fails.
|
||||
func (r *linuxRouter) addAddress(addr netaddr.IPPrefix) error {
|
||||
if !r.v6Available && addr.IP.Is6() {
|
||||
if !r.v6Available && addr.IP().Is6() {
|
||||
return nil
|
||||
}
|
||||
if err := r.cmd.run("ip", "addr", "add", addr.String(), "dev", r.tunname); err != nil {
|
||||
return fmt.Errorf("adding address %q to tunnel interface: %w", addr, err)
|
||||
}
|
||||
if err := r.addLoopbackRule(addr.IP); err != nil {
|
||||
if err := r.addLoopbackRule(addr.IP()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -388,10 +388,10 @@ func (r *linuxRouter) addAddress(addr netaddr.IPPrefix) error {
|
||||
// the address is not assigned to the interface, or if the removal
|
||||
// fails.
|
||||
func (r *linuxRouter) delAddress(addr netaddr.IPPrefix) error {
|
||||
if !r.v6Available && addr.IP.Is6() {
|
||||
if !r.v6Available && addr.IP().Is6() {
|
||||
return nil
|
||||
}
|
||||
if err := r.delLoopbackRule(addr.IP); err != nil {
|
||||
if err := r.delLoopbackRule(addr.IP()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := r.cmd.run("ip", "addr", "del", addr.String(), "dev", r.tunname); err != nil {
|
||||
@@ -463,7 +463,7 @@ func (r *linuxRouter) addThrowRoute(cidr netaddr.IPPrefix) error {
|
||||
}
|
||||
|
||||
func (r *linuxRouter) addRouteDef(routeDef []string, cidr netaddr.IPPrefix) error {
|
||||
if !r.v6Available && cidr.IP.Is6() {
|
||||
if !r.v6Available && cidr.IP().Is6() {
|
||||
return nil
|
||||
}
|
||||
args := append([]string{"ip", "route", "add"}, routeDef...)
|
||||
@@ -490,7 +490,7 @@ func (r *linuxRouter) delThrowRoute(cidr netaddr.IPPrefix) error {
|
||||
}
|
||||
|
||||
func (r *linuxRouter) delRouteDef(routeDef []string, cidr netaddr.IPPrefix) error {
|
||||
if !r.v6Available && cidr.IP.Is6() {
|
||||
if !r.v6Available && cidr.IP().Is6() {
|
||||
return nil
|
||||
}
|
||||
args := append([]string{"ip", "route", "del"}, routeDef...)
|
||||
@@ -520,7 +520,7 @@ func dashFam(ip netaddr.IP) string {
|
||||
}
|
||||
|
||||
func (r *linuxRouter) hasRoute(routeDef []string, cidr netaddr.IPPrefix) (bool, error) {
|
||||
args := append([]string{"ip", dashFam(cidr.IP), "route", "show"}, routeDef...)
|
||||
args := append([]string{"ip", dashFam(cidr.IP()), "route", "show"}, routeDef...)
|
||||
if r.ipRuleAvailable {
|
||||
args = append(args, "table", tailscaleRouteTable)
|
||||
}
|
||||
|
||||
@@ -69,11 +69,11 @@ func (r *openbsdRouter) Set(cfg *Config) error {
|
||||
localAddr4 := netaddr.IPPrefix{}
|
||||
localAddr6 := netaddr.IPPrefix{}
|
||||
for _, addr := range cfg.LocalAddrs {
|
||||
if addr.IP.Is4() {
|
||||
if addr.IP().Is4() {
|
||||
numIPv4++
|
||||
localAddr4 = addr
|
||||
}
|
||||
if addr.IP.Is6() {
|
||||
if addr.IP().Is6() {
|
||||
numIPv6++
|
||||
localAddr6 = addr
|
||||
}
|
||||
@@ -98,7 +98,7 @@ func (r *openbsdRouter) Set(cfg *Config) error {
|
||||
|
||||
routedel := []string{"route", "-q", "-n",
|
||||
"del", "-inet", r.local4.String(),
|
||||
"-iface", r.local4.IP.String()}
|
||||
"-iface", r.local4.IP().String()}
|
||||
if out, err := cmd(routedel...).CombinedOutput(); err != nil {
|
||||
r.logf("route del failed: %v: %v\n%s", routedel, err, out)
|
||||
if errq == nil {
|
||||
@@ -120,7 +120,7 @@ func (r *openbsdRouter) Set(cfg *Config) error {
|
||||
|
||||
routeadd := []string{"route", "-q", "-n",
|
||||
"add", "-inet", localAddr4.String(),
|
||||
"-iface", localAddr4.IP.String()}
|
||||
"-iface", localAddr4.IP().String()}
|
||||
if out, err := cmd(routeadd...).CombinedOutput(); err != nil {
|
||||
r.logf("route add failed: %v: %v\n%s", routeadd, err, out)
|
||||
if errq == nil {
|
||||
@@ -134,7 +134,7 @@ func (r *openbsdRouter) Set(cfg *Config) error {
|
||||
// in https://github.com/tailscale/tailscale/issues/1307 we made
|
||||
// FreeBSD use a /48 for IPv6 addresses, which is nice because we
|
||||
// don't need to additionally add routing entries. Do that here too.
|
||||
localAddr6 = netaddr.IPPrefix{localAddr6.IP, 48}
|
||||
localAddr6 = netaddr.IPPrefixFrom(localAddr6.IP(), 48)
|
||||
}
|
||||
|
||||
if localAddr6 != r.local6 {
|
||||
@@ -171,10 +171,10 @@ func (r *openbsdRouter) Set(cfg *Config) error {
|
||||
if _, keep := newRoutes[route]; !keep {
|
||||
net := route.IPNet()
|
||||
nip := net.IP.Mask(net.Mask)
|
||||
nstr := fmt.Sprintf("%v/%d", nip, route.Bits)
|
||||
nstr := fmt.Sprintf("%v/%d", nip, route.Bits())
|
||||
routedel := []string{"route", "-q", "-n",
|
||||
"del", "-inet", nstr,
|
||||
"-iface", localAddr4.IP.String()}
|
||||
"-iface", localAddr4.IP().String()}
|
||||
out, err := cmd(routedel...).CombinedOutput()
|
||||
if err != nil {
|
||||
r.logf("route del failed: %v: %v\n%s", routedel, err, out)
|
||||
@@ -188,10 +188,10 @@ func (r *openbsdRouter) Set(cfg *Config) error {
|
||||
if _, exists := r.routes[route]; !exists {
|
||||
net := route.IPNet()
|
||||
nip := net.IP.Mask(net.Mask)
|
||||
nstr := fmt.Sprintf("%v/%d", nip, route.Bits)
|
||||
nstr := fmt.Sprintf("%v/%d", nip, route.Bits())
|
||||
routeadd := []string{"route", "-q", "-n",
|
||||
"add", "-inet", nstr,
|
||||
"-iface", localAddr4.IP.String()}
|
||||
"-iface", localAddr4.IP().String()}
|
||||
out, err := cmd(routeadd...).CombinedOutput()
|
||||
if err != nil {
|
||||
r.logf("addr add failed: %v: %v\n%s", routeadd, err, out)
|
||||
|
||||
@@ -87,7 +87,7 @@ func (r *userspaceBSDRouter) Up() error {
|
||||
}
|
||||
|
||||
func inet(p netaddr.IPPrefix) string {
|
||||
if p.IP.Is6() {
|
||||
if p.IP().Is6() {
|
||||
return "inet6"
|
||||
}
|
||||
return "inet"
|
||||
@@ -116,15 +116,15 @@ func (r *userspaceBSDRouter) Set(cfg *Config) (reterr error) {
|
||||
}
|
||||
for _, addr := range r.addrsToAdd(cfg.LocalAddrs) {
|
||||
var arg []string
|
||||
if runtime.GOOS == "freebsd" && addr.IP.Is6() && addr.Bits == 128 {
|
||||
if runtime.GOOS == "freebsd" && addr.IP().Is6() && addr.Bits() == 128 {
|
||||
// FreeBSD rejects tun addresses of the form fc00::1/128 -> fc00::1,
|
||||
// https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=218508
|
||||
// Instead add our whole /48, which works because we use a /48 route.
|
||||
// Full history: https://github.com/tailscale/tailscale/issues/1307
|
||||
tmp := netaddr.IPPrefix{IP: addr.IP, Bits: 48}
|
||||
tmp := netaddr.IPPrefixFrom(addr.IP(), 48)
|
||||
arg = []string{"ifconfig", r.tunname, inet(tmp), tmp.String()}
|
||||
} else {
|
||||
arg = []string{"ifconfig", r.tunname, inet(addr), addr.String(), addr.IP.String()}
|
||||
arg = []string{"ifconfig", r.tunname, inet(addr), addr.String(), addr.IP().String()}
|
||||
}
|
||||
out, err := cmd(arg...).CombinedOutput()
|
||||
if err != nil {
|
||||
@@ -148,7 +148,7 @@ func (r *userspaceBSDRouter) Set(cfg *Config) (reterr error) {
|
||||
if _, keep := newRoutes[route]; !keep {
|
||||
net := route.IPNet()
|
||||
nip := net.IP.Mask(net.Mask)
|
||||
nstr := fmt.Sprintf("%v/%d", nip, route.Bits)
|
||||
nstr := fmt.Sprintf("%v/%d", nip, route.Bits())
|
||||
del := "del"
|
||||
if version.OS() == "macOS" {
|
||||
del = "delete"
|
||||
@@ -168,7 +168,7 @@ func (r *userspaceBSDRouter) Set(cfg *Config) (reterr error) {
|
||||
if _, exists := r.routes[route]; !exists {
|
||||
net := route.IPNet()
|
||||
nip := net.IP.Mask(net.Mask)
|
||||
nstr := fmt.Sprintf("%v/%d", nip, route.Bits)
|
||||
nstr := fmt.Sprintf("%v/%d", nip, route.Bits())
|
||||
routeadd := []string{"route", "-q", "-n",
|
||||
"add", "-" + inet(route), nstr,
|
||||
"-iface", r.tunname}
|
||||
|
||||
@@ -91,7 +91,7 @@ func (r *winRouter) Set(cfg *Config) error {
|
||||
|
||||
func hasDefaultRoute(routes []netaddr.IPPrefix) bool {
|
||||
for _, route := range routes {
|
||||
if route.Bits == 0 {
|
||||
if route.Bits() == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,10 @@ package wgengine
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
crand "crypto/rand"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"reflect"
|
||||
"runtime"
|
||||
@@ -28,7 +26,7 @@ import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/internal/deepprint"
|
||||
"tailscale.com/internal/deephash"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/dns/resolver"
|
||||
@@ -120,7 +118,6 @@ type userspaceEngine struct {
|
||||
statusCallback StatusCallback
|
||||
peerSequence []wgkey.Key
|
||||
endpoints []tailcfg.Endpoint
|
||||
pingers map[wgkey.Key]*pinger // legacy pingers for pre-discovery peers
|
||||
pendOpen map[flowtrack.Tuple]*pendingOpenFlow // see pendopen.go
|
||||
networkMapCallbacks map[*someHandle]NetworkMapCallback
|
||||
tsIPByIPPort map[netaddr.IPPort]netaddr.IP // allows registration of IP:ports as belonging to a certain Tailscale IP for whois lookups
|
||||
@@ -241,7 +238,6 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
|
||||
waitCh: make(chan struct{}),
|
||||
tundev: tsTUNDev,
|
||||
router: conf.Router,
|
||||
pingers: make(map[wgkey.Key]*pinger),
|
||||
}
|
||||
e.isLocalAddr.Store(tsaddr.NewContainsIPFunc(nil))
|
||||
|
||||
@@ -310,52 +306,6 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
|
||||
}
|
||||
|
||||
e.wgLogger = wglog.NewLogger(logf)
|
||||
opts := &device.DeviceOptions{
|
||||
HandshakeDone: func(peerKey device.NoisePublicKey, peer *device.Peer, deviceAllowedIPs *device.AllowedIPs) {
|
||||
// Send an unsolicited status event every time a
|
||||
// handshake completes. This makes sure our UI can
|
||||
// update quickly as soon as it connects to a peer.
|
||||
//
|
||||
// We use a goroutine here to avoid deadlocking
|
||||
// wireguard, since RequestStatus() will call back
|
||||
// into it, and wireguard is what called us to get
|
||||
// here.
|
||||
go e.RequestStatus()
|
||||
|
||||
peerWGKey := wgkey.Key(peerKey)
|
||||
if e.magicConn.PeerHasDiscoKey(tailcfg.NodeKey(peerKey)) {
|
||||
e.logf("wireguard handshake complete for %v", peerWGKey.ShortString())
|
||||
// This is a modern peer with discovery support. No need to send pings.
|
||||
return
|
||||
}
|
||||
|
||||
e.logf("wireguard handshake complete for %v; sending legacy pings", peerWGKey.ShortString())
|
||||
|
||||
// Ping every single-IP that peer routes.
|
||||
// These synthetic packets are used to traverse NATs.
|
||||
var ips []netaddr.IP
|
||||
var allowedIPs []netaddr.IPPrefix
|
||||
deviceAllowedIPs.EntriesForPeer(peer, func(stdIP net.IP, cidr uint) bool {
|
||||
ip, ok := netaddr.FromStdIP(stdIP)
|
||||
if !ok {
|
||||
logf("[unexpected] bad IP from deviceAllowedIPs.EntriesForPeer: %v", stdIP)
|
||||
return true
|
||||
}
|
||||
ipp := netaddr.IPPrefix{IP: ip, Bits: uint8(cidr)}
|
||||
allowedIPs = append(allowedIPs, ipp)
|
||||
if ipp.IsSingleIP() {
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
return true
|
||||
})
|
||||
if len(ips) > 0 {
|
||||
go e.pinger(peerWGKey, ips)
|
||||
} else {
|
||||
logf("[unexpected] peer %s has no single-IP routes: %v", peerWGKey.ShortString(), allowedIPs)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
e.tundev.OnTSMPPongReceived = func(pong packet.TSMPPongReply) {
|
||||
e.mu.Lock()
|
||||
defer e.mu.Unlock()
|
||||
@@ -368,7 +318,7 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
|
||||
|
||||
// wgdev takes ownership of tundev, will close it when closed.
|
||||
e.logf("Creating wireguard device...")
|
||||
e.wgdev = device.NewDevice(e.tundev, e.magicConn.Bind(), e.wgLogger.DeviceLogger, opts)
|
||||
e.wgdev = device.NewDevice(e.tundev, e.magicConn.Bind(), e.wgLogger.DeviceLogger)
|
||||
closePool.addFunc(e.wgdev.Close)
|
||||
closePool.addFunc(func() {
|
||||
if err := e.magicConn.Close(); err != nil {
|
||||
@@ -449,7 +399,7 @@ func (e *userspaceEngine) handleLocalPackets(p *packet.Parsed, t *tstun.Wrapper)
|
||||
isLocalAddr, ok := e.isLocalAddr.Load().(func(netaddr.IP) bool)
|
||||
if !ok {
|
||||
e.logf("[unexpected] e.isLocalAddr was nil, can't check for loopback packet")
|
||||
} else if isLocalAddr(p.Dst.IP) {
|
||||
} else if isLocalAddr(p.Dst.IP()) {
|
||||
// macOS NetworkExtension directs packets destined to the
|
||||
// tunnel's local IP address into the tunnel, instead of
|
||||
// looping back within the kernel network stack. We have to
|
||||
@@ -465,7 +415,7 @@ func (e *userspaceEngine) handleLocalPackets(p *packet.Parsed, t *tstun.Wrapper)
|
||||
|
||||
// handleDNS is an outbound pre-filter resolving Tailscale domains.
|
||||
func (e *userspaceEngine) handleDNS(p *packet.Parsed, t *tstun.Wrapper) filter.Response {
|
||||
if p.Dst.IP == magicDNSIP && p.Dst.Port == magicDNSPort && p.IPProto == ipproto.UDP {
|
||||
if p.Dst.IP() == magicDNSIP && p.Dst.Port() == magicDNSPort && p.IPProto == ipproto.UDP {
|
||||
err := e.dns.EnqueueRequest(append([]byte(nil), p.Payload()...), p.Src)
|
||||
if err != nil {
|
||||
e.logf("dns: enqueue: %v", err)
|
||||
@@ -490,10 +440,10 @@ func (e *userspaceEngine) pollResolver() {
|
||||
h := packet.UDP4Header{
|
||||
IP4Header: packet.IP4Header{
|
||||
Src: magicDNSIP,
|
||||
Dst: to.IP,
|
||||
Dst: to.IP(),
|
||||
},
|
||||
SrcPort: magicDNSPort,
|
||||
DstPort: to.Port,
|
||||
DstPort: to.Port(),
|
||||
}
|
||||
hlen := h.Len()
|
||||
|
||||
@@ -507,132 +457,6 @@ func (e *userspaceEngine) pollResolver() {
|
||||
}
|
||||
}
|
||||
|
||||
// pinger sends ping packets for a few seconds.
|
||||
//
|
||||
// These generated packets are used to ensure we trigger the spray logic in
|
||||
// the magicsock package for NAT traversal.
|
||||
//
|
||||
// These are only used with legacy peers (before 0.100.0) that don't
|
||||
// have advertised discovery keys.
|
||||
type pinger struct {
|
||||
e *userspaceEngine
|
||||
done chan struct{} // closed after shutdown (not the ctx.Done() chan)
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// close cleans up pinger and removes it from the userspaceEngine.pingers map.
|
||||
// It cannot be called while p.e.mu is held.
|
||||
func (p *pinger) close() {
|
||||
p.cancel()
|
||||
<-p.done
|
||||
}
|
||||
|
||||
func (p *pinger) run(ctx context.Context, peerKey wgkey.Key, ips []netaddr.IP, srcIP netaddr.IP) {
|
||||
defer func() {
|
||||
p.e.mu.Lock()
|
||||
if p.e.pingers[peerKey] == p {
|
||||
delete(p.e.pingers, peerKey)
|
||||
}
|
||||
p.e.mu.Unlock()
|
||||
|
||||
close(p.done)
|
||||
}()
|
||||
|
||||
header := packet.ICMP4Header{
|
||||
IP4Header: packet.IP4Header{
|
||||
Src: srcIP,
|
||||
},
|
||||
Type: packet.ICMP4EchoRequest,
|
||||
Code: packet.ICMP4NoCode,
|
||||
}
|
||||
|
||||
// sendFreq is slightly longer than sprayFreq in magicsock to ensure
|
||||
// that if these ping packets are the only source of early packets
|
||||
// sent to the peer, that each one will be sprayed.
|
||||
const sendFreq = 300 * time.Millisecond
|
||||
const stopAfter = 3 * time.Second
|
||||
|
||||
start := time.Now()
|
||||
var dstIPs []netaddr.IP
|
||||
for _, ip := range ips {
|
||||
if ip.Is6() {
|
||||
// This code is only used for legacy (pre-discovery)
|
||||
// peers. They're not going to work right with IPv6 on the
|
||||
// overlay anyway, so don't bother trying to make ping
|
||||
// work.
|
||||
continue
|
||||
}
|
||||
dstIPs = append(dstIPs, ip)
|
||||
}
|
||||
|
||||
payload := []byte("magicsock_spray") // no meaning
|
||||
|
||||
header.IPID = 1
|
||||
t := time.NewTicker(sendFreq)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
}
|
||||
if time.Since(start) > stopAfter {
|
||||
return
|
||||
}
|
||||
for _, dstIP := range dstIPs {
|
||||
header.Dst = dstIP
|
||||
// InjectOutbound take ownership of the packet, so we allocate.
|
||||
b := packet.Generate(&header, payload)
|
||||
p.e.tundev.InjectOutbound(b)
|
||||
}
|
||||
header.IPID++
|
||||
}
|
||||
}
|
||||
|
||||
// pinger sends ping packets for a few seconds.
|
||||
//
|
||||
// These generated packets are used to ensure we trigger the spray logic in
|
||||
// the magicsock package for NAT traversal.
|
||||
//
|
||||
// This is only used with legacy peers (before 0.100.0) that don't
|
||||
// have advertised discovery keys.
|
||||
func (e *userspaceEngine) pinger(peerKey wgkey.Key, ips []netaddr.IP) {
|
||||
e.logf("[v1] generating initial ping traffic to %s (%v)", peerKey.ShortString(), ips)
|
||||
var srcIP netaddr.IP
|
||||
|
||||
e.wgLock.Lock()
|
||||
if len(e.lastCfgFull.Addresses) > 0 {
|
||||
srcIP = e.lastCfgFull.Addresses[0].IP
|
||||
}
|
||||
e.wgLock.Unlock()
|
||||
|
||||
if srcIP.IsZero() {
|
||||
e.logf("generating initial ping traffic: no source IP")
|
||||
return
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
p := &pinger{
|
||||
e: e,
|
||||
done: make(chan struct{}),
|
||||
cancel: cancel,
|
||||
}
|
||||
|
||||
e.mu.Lock()
|
||||
if e.closing {
|
||||
e.mu.Unlock()
|
||||
return
|
||||
}
|
||||
oldPinger := e.pingers[peerKey]
|
||||
e.pingers[peerKey] = p
|
||||
e.mu.Unlock()
|
||||
|
||||
if oldPinger != nil {
|
||||
oldPinger.close()
|
||||
}
|
||||
p.run(ctx, peerKey, ips, srcIP)
|
||||
}
|
||||
|
||||
var (
|
||||
debugTrimWireguardEnv = os.Getenv("TS_DEBUG_TRIM_WIREGUARD")
|
||||
debugTrimWireguard, _ = strconv.ParseBool(debugTrimWireguardEnv)
|
||||
@@ -675,15 +499,7 @@ func isTrimmablePeer(p *wgcfg.Peer, numPeers int) bool {
|
||||
if forceFullWireguardConfig(numPeers) {
|
||||
return false
|
||||
}
|
||||
if !isSingleEndpoint(p.Endpoints) {
|
||||
return false
|
||||
}
|
||||
|
||||
host, _, err := net.SplitHostPort(p.Endpoints)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if !strings.HasSuffix(host, ".disco.tailscale") {
|
||||
if p.Endpoints.DiscoKey.IsZero() {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -753,26 +569,6 @@ func (e *userspaceEngine) isActiveSince(dk tailcfg.DiscoKey, ip netaddr.IP, t ti
|
||||
return unixTime >= t.Unix()
|
||||
}
|
||||
|
||||
// discoKeyFromPeer returns the DiscoKey for a wireguard config's Peer.
|
||||
//
|
||||
// Invariant: isTrimmablePeer(p) == true, so it should have 1 endpoint with
|
||||
// Host of form "<64-hex-digits>.disco.tailscale". If invariant is violated,
|
||||
// we return the zero value.
|
||||
func discoKeyFromPeer(p *wgcfg.Peer) tailcfg.DiscoKey {
|
||||
if len(p.Endpoints) < 64 {
|
||||
return tailcfg.DiscoKey{}
|
||||
}
|
||||
host, rest := p.Endpoints[:64], p.Endpoints[64:]
|
||||
if !strings.HasPrefix(rest, ".disco.tailscale") {
|
||||
return tailcfg.DiscoKey{}
|
||||
}
|
||||
k, err := key.NewPublicFromHexMem(mem.S(host))
|
||||
if err != nil {
|
||||
return tailcfg.DiscoKey{}
|
||||
}
|
||||
return tailcfg.DiscoKey(k)
|
||||
}
|
||||
|
||||
// discoChanged are the set of peers whose disco keys have changed, implying they've restarted.
|
||||
// If a peer is in this set and was previously in the live wireguard config,
|
||||
// it needs to be first removed and then re-added to flush out its wireguard session key.
|
||||
@@ -820,12 +616,12 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked(discoChanged map[key.Publ
|
||||
}
|
||||
continue
|
||||
}
|
||||
dk := discoKeyFromPeer(p)
|
||||
dk := p.Endpoints.DiscoKey
|
||||
trackDisco = append(trackDisco, dk)
|
||||
recentlyActive := false
|
||||
for _, cidr := range p.AllowedIPs {
|
||||
trackIPs = append(trackIPs, cidr.IP)
|
||||
recentlyActive = recentlyActive || e.isActiveSince(dk, cidr.IP, activeCutoff)
|
||||
trackIPs = append(trackIPs, cidr.IP())
|
||||
recentlyActive = recentlyActive || e.isActiveSince(dk, cidr.IP(), activeCutoff)
|
||||
}
|
||||
if recentlyActive {
|
||||
min.Peers = append(min.Peers, *p)
|
||||
@@ -837,7 +633,7 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked(discoChanged map[key.Publ
|
||||
}
|
||||
}
|
||||
|
||||
if !deepprint.UpdateHash(&e.lastEngineSigTrim, min, trimmedDisco, trackDisco, trackIPs) {
|
||||
if !deephash.UpdateHash(&e.lastEngineSigTrim, min, trimmedDisco, trackDisco, trackIPs) {
|
||||
// No changes
|
||||
return nil
|
||||
}
|
||||
@@ -958,8 +754,8 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config,
|
||||
}
|
||||
e.mu.Unlock()
|
||||
|
||||
engineChanged := deepprint.UpdateHash(&e.lastEngineSigFull, cfg)
|
||||
routerChanged := deepprint.UpdateHash(&e.lastRouterSig, routerCfg, dnsCfg)
|
||||
engineChanged := deephash.UpdateHash(&e.lastEngineSigFull, cfg)
|
||||
routerChanged := deephash.UpdateHash(&e.lastRouterSig, routerCfg, dnsCfg)
|
||||
if !engineChanged && !routerChanged {
|
||||
return ErrNoChanges
|
||||
}
|
||||
@@ -970,26 +766,26 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config,
|
||||
// and a second time with it.
|
||||
discoChanged := make(map[key.Public]bool)
|
||||
{
|
||||
prevEP := make(map[key.Public]string)
|
||||
prevEP := make(map[key.Public]tailcfg.DiscoKey)
|
||||
for i := range e.lastCfgFull.Peers {
|
||||
if p := &e.lastCfgFull.Peers[i]; isSingleEndpoint(p.Endpoints) {
|
||||
prevEP[key.Public(p.PublicKey)] = p.Endpoints
|
||||
if p := &e.lastCfgFull.Peers[i]; !p.Endpoints.DiscoKey.IsZero() {
|
||||
prevEP[key.Public(p.PublicKey)] = p.Endpoints.DiscoKey
|
||||
}
|
||||
}
|
||||
for i := range cfg.Peers {
|
||||
p := &cfg.Peers[i]
|
||||
if !isSingleEndpoint(p.Endpoints) {
|
||||
if p.Endpoints.DiscoKey.IsZero() {
|
||||
continue
|
||||
}
|
||||
pub := key.Public(p.PublicKey)
|
||||
if old, ok := prevEP[pub]; ok && old != p.Endpoints {
|
||||
if old, ok := prevEP[pub]; ok && old != p.Endpoints.DiscoKey {
|
||||
discoChanged[pub] = true
|
||||
e.logf("wgengine: Reconfig: %s changed from %q to %q", pub.ShortString(), old, p.Endpoints)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
e.lastCfgFull = cfg.Copy()
|
||||
e.lastCfgFull = *cfg.Clone()
|
||||
|
||||
// Tell magicsock about the new (or initial) private key
|
||||
// (which is needed by DERP) before wgdev gets it, as wgdev
|
||||
@@ -1026,11 +822,6 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config,
|
||||
return nil
|
||||
}
|
||||
|
||||
// isSingleEndpoint reports whether endpoints contains exactly one host:port pair.
|
||||
func isSingleEndpoint(s string) bool {
|
||||
return s != "" && !strings.Contains(s, ",")
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) GetFilter() *filter.Filter {
|
||||
return e.tundev.GetFilter()
|
||||
}
|
||||
@@ -1053,6 +844,8 @@ func (e *userspaceEngine) getStatusCallback() StatusCallback {
|
||||
|
||||
var singleNewline = []byte{'\n'}
|
||||
|
||||
var ErrEngineClosing = errors.New("engine closing; no status")
|
||||
|
||||
func (e *userspaceEngine) getStatus() (*Status, error) {
|
||||
// Grab derpConns before acquiring wgLock to not violate lock ordering;
|
||||
// the DERPs method acquires magicsock.Conn.mu.
|
||||
@@ -1066,7 +859,7 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
|
||||
closing := e.closing
|
||||
e.mu.Unlock()
|
||||
if closing {
|
||||
return nil, errors.New("engine closing; no status")
|
||||
return nil, ErrEngineClosing
|
||||
}
|
||||
|
||||
if e.wgdev == nil {
|
||||
@@ -1213,17 +1006,12 @@ func (e *userspaceEngine) RequestStatus() {
|
||||
}
|
||||
|
||||
func (e *userspaceEngine) Close() {
|
||||
var pingers []*pinger
|
||||
|
||||
e.mu.Lock()
|
||||
if e.closing {
|
||||
e.mu.Unlock()
|
||||
return
|
||||
}
|
||||
e.closing = true
|
||||
for _, pinger := range e.pingers {
|
||||
pingers = append(pingers, pinger)
|
||||
}
|
||||
e.mu.Unlock()
|
||||
|
||||
r := bufio.NewReader(strings.NewReader(""))
|
||||
@@ -1237,13 +1025,6 @@ func (e *userspaceEngine) Close() {
|
||||
e.router.Close()
|
||||
e.wgdev.Close()
|
||||
e.tundev.Close()
|
||||
|
||||
// Shut down pingers after tundev is closed (by e.wgdev.Close) so the
|
||||
// synchronous close does not get stuck on InjectOutbound.
|
||||
for _, pinger := range pingers {
|
||||
pinger.close()
|
||||
}
|
||||
|
||||
close(e.waitCh)
|
||||
}
|
||||
|
||||
@@ -1375,8 +1156,8 @@ func (e *userspaceEngine) mySelfIPMatchingFamily(dst netaddr.IP) (src netaddr.IP
|
||||
return netaddr.IP{}, errors.New("no netmap")
|
||||
}
|
||||
for _, a := range e.netMap.Addresses {
|
||||
if a.IsSingleIP() && a.IP.BitLen() == dst.BitLen() {
|
||||
return a.IP, nil
|
||||
if a.IsSingleIP() && a.IP().BitLen() == dst.BitLen() {
|
||||
return a.IP(), nil
|
||||
}
|
||||
}
|
||||
if len(e.netMap.Addresses) == 0 {
|
||||
@@ -1512,7 +1293,7 @@ func (e *userspaceEngine) peerForIP(ip netaddr.IP) (n *tailcfg.Node, err error)
|
||||
var bestInNM *tailcfg.Node
|
||||
for _, p := range nm.Peers {
|
||||
for _, a := range p.Addresses {
|
||||
if a.IP == ip && a.IsSingleIP() && tsaddr.IsTailscaleIP(ip) {
|
||||
if a.IP() == ip && a.IsSingleIP() && tsaddr.IsTailscaleIP(ip) {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
@@ -1520,7 +1301,7 @@ func (e *userspaceEngine) peerForIP(ip netaddr.IP) (n *tailcfg.Node, err error)
|
||||
if !cidr.Contains(ip) {
|
||||
continue
|
||||
}
|
||||
if bestInNMPrefix.IsZero() || cidr.Bits > bestInNMPrefix.Bits {
|
||||
if bestInNMPrefix.IsZero() || cidr.Bits() > bestInNMPrefix.Bits() {
|
||||
bestInNMPrefix = cidr
|
||||
bestInNM = p
|
||||
}
|
||||
@@ -1538,7 +1319,7 @@ func (e *userspaceEngine) peerForIP(ip netaddr.IP) (n *tailcfg.Node, err error)
|
||||
if !cidr.Contains(ip) {
|
||||
continue
|
||||
}
|
||||
if best.IsZero() || cidr.Bits > best.Bits {
|
||||
if best.IsZero() || cidr.Bits() > best.Bits() {
|
||||
best = cidr
|
||||
bestKey = tailcfg.NodeKey(p.PublicKey)
|
||||
}
|
||||
@@ -1556,7 +1337,7 @@ func (e *userspaceEngine) peerForIP(ip netaddr.IP) (n *tailcfg.Node, err error)
|
||||
if bestInNM == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if bestInNMPrefix.Bits == 0 {
|
||||
if bestInNMPrefix.Bits() == 0 {
|
||||
return nil, errors.New("exit node found but not enabled")
|
||||
}
|
||||
return nil, fmt.Errorf("node %q found, but not using its %v route", bestInNM.ComputedNameWithHost, bestInNMPrefix)
|
||||
|
||||
@@ -102,9 +102,9 @@ func TestUserspaceEngineReconfig(t *testing.T) {
|
||||
Peers: []wgcfg.Peer{
|
||||
{
|
||||
AllowedIPs: []netaddr.IPPrefix{
|
||||
{IP: netaddr.IPv4(100, 100, 99, 1), Bits: 32},
|
||||
netaddr.IPPrefixFrom(netaddr.IPv4(100, 100, 99, 1), 32),
|
||||
},
|
||||
Endpoints: discoHex + ".disco.tailscale:12345",
|
||||
Endpoints: wgcfg.Endpoints{DiscoKey: dkFromHex(discoHex)},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
101
wgengine/wgcfg/clone.go
Normal file
101
wgengine/wgcfg/clone.go
Normal file
@@ -0,0 +1,101 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Code generated by tailscale.com/cmd/cloner -type Config,Peer,Endpoints,IPPortSet; DO NOT EDIT.
|
||||
|
||||
package wgcfg
|
||||
|
||||
import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
// Clone makes a deep copy of Config.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Config) Clone() *Config {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Config)
|
||||
*dst = *src
|
||||
dst.Addresses = append(src.Addresses[:0:0], src.Addresses...)
|
||||
dst.DNS = append(src.DNS[:0:0], src.DNS...)
|
||||
dst.Peers = make([]Peer, len(src.Peers))
|
||||
for i := range dst.Peers {
|
||||
dst.Peers[i] = *src.Peers[i].Clone()
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with command:
|
||||
// tailscale.com/cmd/cloner -type Config,Peer,Endpoints,IPPortSet
|
||||
var _ConfigNeedsRegeneration = Config(struct {
|
||||
Name string
|
||||
PrivateKey wgkey.Private
|
||||
Addresses []netaddr.IPPrefix
|
||||
MTU uint16
|
||||
DNS []netaddr.IP
|
||||
Peers []Peer
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of Peer.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Peer) Clone() *Peer {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Peer)
|
||||
*dst = *src
|
||||
dst.AllowedIPs = append(src.AllowedIPs[:0:0], src.AllowedIPs...)
|
||||
dst.Endpoints = *src.Endpoints.Clone()
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with command:
|
||||
// tailscale.com/cmd/cloner -type Config,Peer,Endpoints,IPPortSet
|
||||
var _PeerNeedsRegeneration = Peer(struct {
|
||||
PublicKey wgkey.Key
|
||||
AllowedIPs []netaddr.IPPrefix
|
||||
Endpoints Endpoints
|
||||
PersistentKeepalive uint16
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of Endpoints.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Endpoints) Clone() *Endpoints {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Endpoints)
|
||||
*dst = *src
|
||||
dst.IPPorts = *src.IPPorts.Clone()
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with command:
|
||||
// tailscale.com/cmd/cloner -type Config,Peer,Endpoints,IPPortSet
|
||||
var _EndpointsNeedsRegeneration = Endpoints(struct {
|
||||
PublicKey wgkey.Key
|
||||
DiscoKey tailcfg.DiscoKey
|
||||
IPPorts IPPortSet
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of IPPortSet.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *IPPortSet) Clone() *IPPortSet {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(IPPortSet)
|
||||
*dst = *src
|
||||
dst.ipp = append(src.ipp[:0:0], src.ipp...)
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with command:
|
||||
// tailscale.com/cmd/cloner -type Config,Peer,Endpoints,IPPortSet
|
||||
var _IPPortSetNeedsRegeneration = IPPortSet(struct {
|
||||
ipp []netaddr.IPPort
|
||||
}{})
|
||||
@@ -6,14 +6,15 @@
|
||||
package wgcfg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/wgkey"
|
||||
)
|
||||
|
||||
// EndpointDiscoSuffix is appended to the hex representation of a peer's discovery key
|
||||
// and is then the sole wireguard endpoint for peers with a non-zero discovery key.
|
||||
// This form is then recognize by magicsock's CreateEndpoint.
|
||||
const EndpointDiscoSuffix = ".disco.tailscale:12345"
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=Config,Peer,Endpoints,IPPortSet -output=clone.go
|
||||
|
||||
// Config is a WireGuard configuration.
|
||||
// It only supports the set of things Tailscale uses.
|
||||
@@ -29,36 +30,90 @@ type Config struct {
|
||||
type Peer struct {
|
||||
PublicKey wgkey.Key
|
||||
AllowedIPs []netaddr.IPPrefix
|
||||
Endpoints string // comma-separated host/port pairs: "1.2.3.4:56,[::]:80"
|
||||
Endpoints Endpoints
|
||||
PersistentKeepalive uint16
|
||||
}
|
||||
|
||||
// Copy makes a deep copy of Config.
|
||||
// The result aliases no memory with the original.
|
||||
func (cfg Config) Copy() Config {
|
||||
res := cfg
|
||||
if res.Addresses != nil {
|
||||
res.Addresses = append([]netaddr.IPPrefix{}, res.Addresses...)
|
||||
}
|
||||
if res.DNS != nil {
|
||||
res.DNS = append([]netaddr.IP{}, res.DNS...)
|
||||
}
|
||||
peers := make([]Peer, 0, len(res.Peers))
|
||||
for _, peer := range res.Peers {
|
||||
peers = append(peers, peer.Copy())
|
||||
}
|
||||
res.Peers = peers
|
||||
return res
|
||||
// Endpoints represents the routes to reach a remote node.
|
||||
// It is serialized and provided to wireguard-go as a conn.Endpoint.
|
||||
type Endpoints struct {
|
||||
// PublicKey is the public key for the remote node.
|
||||
PublicKey wgkey.Key `json:"pk"`
|
||||
// DiscoKey is the disco key associated with the remote node.
|
||||
DiscoKey tailcfg.DiscoKey `json:"dk,omitempty"`
|
||||
// IPPorts is a set of possible ip+ports the remote node can be reached at.
|
||||
// This is used only for legacy connections to pre-disco (pre-0.100) peers.
|
||||
IPPorts IPPortSet `json:"ipp,omitempty"`
|
||||
}
|
||||
|
||||
// Copy makes a deep copy of Peer.
|
||||
// The result aliases no memory with the original.
|
||||
func (peer Peer) Copy() Peer {
|
||||
res := peer
|
||||
if res.AllowedIPs != nil {
|
||||
res.AllowedIPs = append([]netaddr.IPPrefix{}, res.AllowedIPs...)
|
||||
func (e Endpoints) Equal(f Endpoints) bool {
|
||||
if e.PublicKey != f.PublicKey {
|
||||
return false
|
||||
}
|
||||
return res
|
||||
if e.DiscoKey != f.DiscoKey {
|
||||
return false
|
||||
}
|
||||
return e.IPPorts.EqualUnordered(f.IPPorts)
|
||||
}
|
||||
|
||||
// IPPortSet is an immutable slice of netaddr.IPPorts.
|
||||
type IPPortSet struct {
|
||||
ipp []netaddr.IPPort
|
||||
}
|
||||
|
||||
// NewIPPortSet returns an IPPortSet containing the ports in ipp.
|
||||
func NewIPPortSet(ipps ...netaddr.IPPort) IPPortSet {
|
||||
return IPPortSet{ipp: append(ipps[:0:0], ipps...)}
|
||||
}
|
||||
|
||||
// String returns a comma-separated list of all IPPorts in s.
|
||||
func (s IPPortSet) String() string {
|
||||
buf := new(strings.Builder)
|
||||
for i, ipp := range s.ipp {
|
||||
if i > 0 {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
buf.WriteString(ipp.String())
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// IPPorts returns a slice of netaddr.IPPorts containing the IPPorts in s.
|
||||
func (s IPPortSet) IPPorts() []netaddr.IPPort {
|
||||
return append(s.ipp[:0:0], s.ipp...)
|
||||
}
|
||||
|
||||
// EqualUnordered reports whether s and t contain the same IPPorts, regardless of order.
|
||||
func (s IPPortSet) EqualUnordered(t IPPortSet) bool {
|
||||
if len(s.ipp) != len(t.ipp) {
|
||||
return false
|
||||
}
|
||||
// Check whether the endpoints are the same, regardless of order.
|
||||
ipps := make(map[netaddr.IPPort]int, len(s.ipp))
|
||||
for _, ipp := range s.ipp {
|
||||
ipps[ipp]++
|
||||
}
|
||||
for _, ipp := range t.ipp {
|
||||
ipps[ipp]--
|
||||
}
|
||||
for _, n := range ipps {
|
||||
if n != 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// MarshalJSON marshals s into JSON.
|
||||
// It is necessary so that IPPortSet's fields can be unexported, to guarantee immutability.
|
||||
func (s IPPortSet) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(s.ipp)
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals s from JSON.
|
||||
// It is necessary so that IPPortSet's fields can be unexported, to guarantee immutability.
|
||||
func (s *IPPortSet) UnmarshalJSON(b []byte) error {
|
||||
return json.Unmarshal(b, &s.ipp)
|
||||
}
|
||||
|
||||
// PeerWithKey returns the Peer with key k and reports whether it was found.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user