Compare commits

...

33 Commits

Author SHA1 Message Date
Brad Fitzpatrick
c1024a5de2 net/netns, net/interfaces: move defaultRouteInterface, add Android fallback
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-08-10 13:01:49 -07:00
Josh Bleecher Snyder
d65e2632ab derp: add basic benchmark
This benchmark is far from perfect: It mixes together
client and server. Still, it provides a starting point
for easy profiling.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2020-08-10 09:58:34 -07:00
Brad Fitzpatrick
87cbc067c2 cmd/tailscale/cli: validate advertised routes' IP address-vs-network bits
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-08-10 09:16:49 -07:00
Brad Fitzpatrick
a275b9d7aa control/controlclient: use less battery when stopped, stop map requests
Updates #604

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-08-09 09:36:35 -07:00
Brad Fitzpatrick
dd97111d06 backoff: update to Go style, document a bit, make 30s explicit
Also, bit of behavior change: on non-nil err but expired context,
don't reset the consecutive failure count. I don't think the old
behavior was intentional.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-08-09 09:36:26 -07:00
Brad Fitzpatrick
696020227c tailcfg, control/controlclient: support delta-encoded netmaps
Should greatly reduce bandwidth for large networks (including our
hello.ipn.dev node).

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-08-07 21:49:49 -07:00
Josh Bleecher Snyder
b23f2263c1 derp: add server version to /debug, expvars
This will make it easier for a human to tell what
version is deployed, for (say) correlating line numbers
in profiles or panics to corresponding source code.

It'll also let us observe version changes in prometheus.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2020-08-07 12:46:02 -07:00
Brad Fitzpatrick
c64a43a734 wgengine/router: set MTU on Windows to min(configured,possible)
Fixes tailscale/corp#542

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-08-07 12:16:12 -07:00
Brad Fitzpatrick
9318b4758c README: update contributing section 2020-08-07 08:28:56 -07:00
Brad Fitzpatrick
6818bb843d Update README, remove old relaynode dredge 2020-08-07 08:25:25 -07:00
Brad Fitzpatrick
24f78eff62 version: new week, new date 2020-08-06 21:30:59 -07:00
Brad Fitzpatrick
5590daa97d control/controlclient: reset timeout timer on non-keepalive map updates
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-08-06 21:30:17 -07:00
Brad Fitzpatrick
b840e7dd5b go mod tidy 2020-08-06 21:24:09 -07:00
Josh Bleecher Snyder
1b27eb431a go.mod: update to newly rebased wireguard-go 2020-08-06 17:50:31 -07:00
Josh Bleecher Snyder
2622e8e082 wgenginer/router: fix build
Rebasing github.com/tailscale/wireguard-go to upstream
wireguard-go changed the API.

This commit is analogous to
https://git.zx2c4.com/wireguard-windows/commit/?id=6823cc10ffe193c0cb1d61a5d1828d563d3d0e5f
2020-08-06 17:37:05 -07:00
Brad Fitzpatrick
b62b07bc2d ipn: jack up the MaxMessageSize from 1MB to 10MB
hello.ipn.dev has a 2.5MB network map
2020-08-06 15:42:23 -07:00
Dmytro Shynkevych
cb01058a53 wgengine: stop giving tsdns a buffer that will be reused
Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>
2020-08-06 18:11:50 -04:00
Brad Fitzpatrick
9a346fd8b4 wgengine,magicsock: fix two lazy wireguard config issues
1) we weren't waking up a discoEndpoint that once existed and
   went idle for 5 minutes and then got a disco message again.

2) userspaceEngine.noteReceiveActivity had a buggy check; fixed
   and added a test
2020-08-06 15:02:29 -07:00
Dmytro Shynkevych
78c2e1ff83 tsdns: implement reverse DNS lookups, canonicalize names everywhere. (#640)
Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>
2020-08-06 14:25:28 -04:00
Brad Fitzpatrick
41c4560592 control/controlclient: remove unused NetworkMap.UAPI method
And remove last remaining use of wgcfg.ToUAPI in a test's debug
output; replace it with JSON.
2020-08-06 10:30:18 -07:00
Brad Fitzpatrick
cff737786e wgengine/magicsock: fix lazy config deadlock, document more lock ordering
This removes the atomic bool that tried to track whether we needed to acquire
the lock on a future recursive call back into magicsock. Unfortunately that
hack doesn't work because we also had a lock ordering issue between magicsock
and userspaceEngine (see issue). This documents that too.

Fixes #644
2020-08-06 08:43:48 -07:00
Brad Fitzpatrick
43bc86588e wgengine/monitor: log RTM_DELROUTE details, fix format strings
Updates #643
2020-08-05 20:44:05 -07:00
Brad Fitzpatrick
2bd9ad4b40 wgengine: fix deadlock between engine and magicsock 2020-08-05 16:37:15 -07:00
Brad Fitzpatrick
5db529a655 logpolicy: upload early logpolicy output, log where we decide to write logs
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-08-05 15:04:28 -07:00
Dmytro Shynkevych
934c63115e ipn: put Magic DNS domains first
Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>
2020-08-04 20:13:30 -04:00
Brad Fitzpatrick
7c38db0c97 wgengine/magicsock: don't deadlock on pre-disco Endpoints w/ lazy wireguard configs
Fixes tailscale/tailscale#637
2020-08-04 17:06:05 -07:00
Josh Bleecher Snyder
a16a793605 net/interfaces: use syscalls to find private gateway IP address
iOS doesn't let you run subprocesses,
which means we can't use netstat to get routing information.
Instead, use syscalls and grub around in the results.
We keep the old netstat version around,
both for use in non-cgo builds,
and for use testing the syscall-based version.

Note that iOS doesn't ship route.h,
so we include a copy here from the macOS 10.15 SDK
(which is itself unchanged from the 10.14 SDK).

I have tested manually that this yields the correct
gateway IP address on my own macOS and iOS devices.
More coverage would be most welcome.

Signed-off-by: Josh Bleecher Snyder <josharian@gmail.com>
2020-08-04 15:45:56 -07:00
Mike Kramlich
08949d4ef1 --advertise-routes option enabled in Mac tailscale CLI; it checks for IP forwarding enabled
Signed-off-by: Mike Kramlich <groglogic@gmail.com>
2020-08-04 10:49:34 -07:00
Brad Fitzpatrick
4987a7d46c wgengine/magicsock: when hard NAT, add stun-ipv4:static-port as candidate
If a node is behind a hard NAT and is using an explicit local port
number, assume they might've mapped a port and add their public IPv4
address with the local tailscaled's port number as a candidate endpoint.
2020-08-04 09:48:34 -07:00
Brad Fitzpatrick
bfcb0aa0be wgengine/magicsock: deflake tests, Close deadlock again
Better fix than 37903a9056

Fixes tailscale/corp#533
2020-08-04 09:36:38 -07:00
David Anderson
c3467fbadb version: adjust to a pure semver version number, per bradfitz's proposal.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-08-03 12:49:42 -07:00
Brad Fitzpatrick
6298018704 control/controlclient: print disco keys NetworkMap diffs (debug change only)
NetworkMap text diffs being empty were currently used to short-circuit
calling magicsock's SetNetworkMap (via Engine.SetNetworkMap), but that
went away in c7582dc2 (0.100.0-230)

Prior to c7582dc2 (notably, in 0.100.0-225 and below, down to
0.100.0), a change in only disco key (as when a node restarts) but
without endpoint changes (as would happen for a client not behind a
NAT with random ports) could result in a "netmap diff: (none)" being
printed, as well as Engine.SetNetworkMap being skipped, leading to
broken discovery endpoints.

c7582dc2 fixed the Engine.SetNetworkMap skippage.

This change fixes the "netmap diff: (none)" print so we'll actually see when a peer
restarts with identical endpoints but a new discovery key.
2020-08-03 10:03:01 -07:00
Brad Fitzpatrick
da3b50ad88 wgengine/filter: omit logging for all v6 multicast, remove debug panic :( 2020-08-01 12:40:32 -07:00
45 changed files with 1944 additions and 469 deletions

View File

@@ -6,17 +6,24 @@ Private WireGuard® networks made easy
## Overview
This repository contains all the open source Tailscale code.
It currently includes the Linux client.
This repository contains all the open source Tailscale client code and
the `tailscaled` daemon and `tailscale` CLI tool. The `tailscaled`
daemon runs primarily on Linux; it also works to varying degrees on
FreeBSD, OpenBSD, Darwin, and Windows.
The Linux client is currently `cmd/relaynode`, but will
soon be replaced by `cmd/tailscaled`.
The Android app is at https://github.com/tailscale/tailscale-android
## Using
We serve packages for a variety of distros at
https://pkgs.tailscale.com .
## Other clients
The [macOS, iOS, and Windows clients](https://tailscale.com/download)
use the code in this repository but additionally include small GUI
wrappers that are not open source.
## Building
```
@@ -35,10 +42,8 @@ Please file any issues about this code or the hosted service on
## Contributing
`under_construction.gif`
PRs welcome, but we are still working out our contribution process and
tooling.
PRs welcome! But please file bugs. Commit messages should [reference
bugs](https://docs.github.com/en/github/writing-on-github/autolinked-references-and-urls).
We require [Developer Certificate of
Origin](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin)
@@ -46,7 +51,7 @@ Origin](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin)
## About Us
We are apenwarr, bradfitz, crawshaw, danderson, dfcarney,
We are apenwarr, bradfitz, crawshaw, danderson, dfcarney, josharian
from Tailscale Inc.
You can learn more about us from [our website](https://tailscale.com).

View File

@@ -33,6 +33,7 @@ import (
"tailscale.com/net/stun"
"tailscale.com/tsweb"
"tailscale.com/types/key"
"tailscale.com/version"
)
var (
@@ -221,6 +222,7 @@ func debugHandler(s *derp.Server) http.Handler {
f("<li><b>Hostname:</b> %v</li>\n", *hostname)
f("<li><b>Uptime:</b> %v</li>\n", tsweb.Uptime())
f("<li><b>Mesh Key:</b> %v</li>\n", s.HasMeshKey())
f("<li><b>Version:</b> %v</li>\n", version.LONG)
f(`<li><a href="/debug/vars">/debug/vars</a> (Go)</li>
<li><a href="/debug/varz">/debug/varz</a> (Prometheus)</li>

View File

@@ -1 +0,0 @@
# placeholder to work around redo bug

View File

@@ -76,6 +76,11 @@ change in the future.
return err
}
func fatalf(format string, a ...interface{}) {
log.SetFlags(0)
log.Fatalf(format, a...)
}
var rootArgs struct {
socket string
}
@@ -84,9 +89,9 @@ func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context
c, err := safesocket.Connect(rootArgs.socket, 41112)
if err != nil {
if runtime.GOOS != "windows" && rootArgs.socket == "" {
log.Fatalf("--socket cannot be empty")
fatalf("--socket cannot be empty")
}
log.Fatalf("Failed to connect to connect to tailscaled. (safesocket.Connect: %v)\n", err)
fatalf("Failed to connect to connect to tailscaled. (safesocket.Connect: %v)\n", err)
}
clientToServer := func(b []byte) {
ipn.WriteMsg(c, b)

View File

@@ -18,8 +18,10 @@ import (
"github.com/peterbourgon/ff/v2/ffcli"
"github.com/tailscale/wireguard-go/wgcfg"
"inet.af/netaddr"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/version"
"tailscale.com/wgengine/router"
)
@@ -55,7 +57,7 @@ specify any flags, options are reset to their default.
upf.StringVar(&upArgs.authKey, "authkey", "", "node authorization key")
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
upf.BoolVar(&upArgs.enableDERP, "enable-derp", true, "enable the use of DERP servers")
if runtime.GOOS == "linux" || isBSD(runtime.GOOS) {
if runtime.GOOS == "linux" || isBSD(runtime.GOOS) || version.OS() == "macOS" {
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. 10.0.0.0/8,192.168.0.0/24)")
}
if runtime.GOOS == "linux" {
@@ -109,18 +111,18 @@ func isBSD(s string) bool {
return s == "dragonfly" || s == "freebsd" || s == "netbsd" || s == "openbsd"
}
func warning(format string, args ...interface{}) {
func warnf(format string, args ...interface{}) {
fmt.Printf("Warning: "+format+"\n", args...)
}
// checkIPForwarding prints warnings on linux if IP forwarding is not
// checkIPForwarding prints warnings if IP forwarding is not
// enabled, or if we were unable to verify the state of IP forwarding.
func checkIPForwarding() {
var key string
if runtime.GOOS == "linux" {
key = "net.ipv4.ip_forward"
} else if isBSD(runtime.GOOS) {
} else if isBSD(runtime.GOOS) || version.OS() == "macOS" {
key = "net.inet.ip.forwarding"
} else {
return
@@ -128,16 +130,16 @@ func checkIPForwarding() {
bs, err := exec.Command("sysctl", "-n", key).Output()
if err != nil {
warning("couldn't check %s (%v).\nSubnet routes won't work without IP forwarding.", key, err)
warnf("couldn't check %s (%v).\nSubnet routes won't work without IP forwarding.", key, err)
return
}
on, err := strconv.ParseBool(string(bytes.TrimSpace(bs)))
if err != nil {
warning("couldn't parse %s (%v).\nSubnet routes won't work without IP forwarding.", key, err)
warnf("couldn't parse %s (%v).\nSubnet routes won't work without IP forwarding.", key, err)
return
}
if !on {
warning("%s is disabled. Subnet routes won't work.", key)
warnf("%s is disabled. Subnet routes won't work.", key)
}
}
@@ -148,15 +150,19 @@ func runUp(ctx context.Context, args []string) error {
var routes []wgcfg.CIDR
if upArgs.advertiseRoutes != "" {
checkIPForwarding()
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
for _, s := range advroutes {
cidr, ok := parseIPOrCIDR(s)
if !ok {
log.Fatalf("%q is not a valid IP address or CIDR prefix", s)
ipp, err := netaddr.ParseIPPrefix(s) // parse it with other pawith both packages
if !ok || err != nil {
fatalf("%q is not a valid IP address or CIDR prefix", s)
}
if ipp != ipp.Masked() {
fatalf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
}
routes = append(routes, cidr)
}
checkIPForwarding()
}
var tags []string
@@ -165,13 +171,13 @@ func runUp(ctx context.Context, args []string) error {
for _, tag := range tags {
err := tailcfg.CheckTag(tag)
if err != nil {
log.Fatalf("tag: %q: %s", tag, err)
fatalf("tag: %q: %s", tag, err)
}
}
}
if len(upArgs.hostname) > 256 {
log.Fatalf("hostname too long: %d bytes (max 256)", len(upArgs.hostname))
fatalf("hostname too long: %d bytes (max 256)", len(upArgs.hostname))
}
// TODO(apenwarr): fix different semantics between prefs and uflags
@@ -194,12 +200,12 @@ func runUp(ctx context.Context, args []string) error {
prefs.NetfilterMode = router.NetfilterOn
case "nodivert":
prefs.NetfilterMode = router.NetfilterNoDivert
warning("netfilter=nodivert; add iptables calls to ts-* chains manually.")
warnf("netfilter=nodivert; add iptables calls to ts-* chains manually.")
case "off":
prefs.NetfilterMode = router.NetfilterOff
warning("netfilter=off; configure iptables yourself.")
warnf("netfilter=off; configure iptables yourself.")
default:
log.Fatalf("invalid value --netfilter-mode: %q", upArgs.netfilterMode)
fatalf("invalid value --netfilter-mode: %q", upArgs.netfilterMode)
}
}
@@ -214,7 +220,7 @@ func runUp(ctx context.Context, args []string) error {
AuthKey: upArgs.authKey,
Notify: func(n ipn.Notify) {
if n.ErrMessage != nil {
log.Fatalf("backend error: %v\n", *n.ErrMessage)
fatalf("backend error: %v\n", *n.ErrMessage)
}
if s := n.State; s != nil {
switch *s {

View File

@@ -117,13 +117,15 @@ type Client struct {
mu sync.Mutex // mutex guards the following fields
statusFunc func(Status) // called to update Client status
loggedIn bool // true if currently logged in
loginGoal *LoginGoal // non-nil if some login activity is desired
synced bool // true if our netmap is up-to-date
hostinfo *tailcfg.Hostinfo
inPollNetMap bool // true if currently running a PollNetMap
inSendStatus int // number of sendStatus calls currently in progress
state State
paused bool // whether we should stop making HTTP requests
unpauseWaiters []chan struct{}
loggedIn bool // true if currently logged in
loginGoal *LoginGoal // non-nil if some login activity is desired
synced bool // true if our netmap is up-to-date
hostinfo *tailcfg.Hostinfo
inPollNetMap bool // true if currently running a PollNetMap
inSendStatus int // number of sendStatus calls currently in progress
state State
authCtx context.Context // context used for auth requests
mapCtx context.Context // context used for netmap requests
@@ -169,6 +171,27 @@ func NewNoStart(opts Options) (*Client, error) {
return c, nil
}
// SetPaused controls whether HTTP activity should be paused.
//
// The client can be paused and unpaused repeatedly, unlike Start and Shutdown, which can only be used once.
func (c *Client) SetPaused(paused bool) {
c.mu.Lock()
defer c.mu.Unlock()
if paused == c.paused {
return
}
c.paused = paused
if paused {
// Just cancel the map routine. The auth routine isn't expensive.
c.cancelMapLocked()
} else {
for _, ch := range c.unpauseWaiters {
close(ch)
}
c.unpauseWaiters = nil
}
}
// Start starts the client's goroutines.
//
// It should only be called for clients created by NewNoStart.
@@ -241,7 +264,7 @@ func (c *Client) cancelMapSafely() {
func (c *Client) authRoutine() {
defer close(c.authDone)
bo := backoff.NewBackoff("authRoutine", c.logf)
bo := backoff.NewBackoff("authRoutine", c.logf, 30*time.Second)
for {
c.mu.Lock()
@@ -272,6 +295,7 @@ func (c *Client) authRoutine() {
if goal == nil {
// Wait for something interesting to happen
var exp <-chan time.Time
var expTimer *time.Timer
if expiry != nil && !expiry.IsZero() {
// if expiry is in the future, don't delay
// past that time.
@@ -284,11 +308,15 @@ func (c *Client) authRoutine() {
if delay > 5*time.Second {
delay = time.Second
}
exp = time.After(delay)
expTimer = time.NewTimer(delay)
exp = expTimer.C
}
}
select {
case <-ctx.Done():
if expTimer != nil {
expTimer.Stop()
}
c.logf("authRoutine: context done.")
case <-exp:
// Unfortunately the key expiry isn't provided
@@ -310,7 +338,7 @@ func (c *Client) authRoutine() {
}
}
} else if !goal.wantLoggedIn {
err := c.direct.TryLogout(c.authCtx)
err := c.direct.TryLogout(ctx)
if err != nil {
report(err, "TryLogout")
bo.BackOff(ctx, err)
@@ -399,12 +427,35 @@ func (c *Client) Direct() *Direct {
return c.direct
}
// unpausedChanLocked returns a new channel that is closed when the
// current Client pause is unpaused.
//
// c.mu must be held
func (c *Client) unpausedChanLocked() <-chan struct{} {
unpaused := make(chan struct{})
c.unpauseWaiters = append(c.unpauseWaiters, unpaused)
return unpaused
}
func (c *Client) mapRoutine() {
defer close(c.mapDone)
bo := backoff.NewBackoff("mapRoutine", c.logf)
bo := backoff.NewBackoff("mapRoutine", c.logf, 30*time.Second)
for {
c.mu.Lock()
if c.paused {
unpaused := c.unpausedChanLocked()
c.mu.Unlock()
c.logf("mapRoutine: awaiting unpause")
select {
case <-unpaused:
c.logf("mapRoutine: unpaused")
case <-c.quit:
c.logf("mapRoutine: quit")
return
}
continue
}
c.logf("mapRoutine: %s", c.state)
loggedIn := c.loggedIn
ctx := c.mapCtx
@@ -487,8 +538,14 @@ func (c *Client) mapRoutine() {
if c.state == StateSynchronized {
c.state = StateAuthenticated
}
paused := c.paused
c.mu.Unlock()
if paused {
c.logf("mapRoutine: paused")
continue
}
if err != nil {
report(err, "PollNetMap")
bo.BackOff(ctx, err)

View File

@@ -22,6 +22,7 @@ import (
"os"
"reflect"
"runtime"
"sort"
"strconv"
"strings"
"sync"
@@ -489,6 +490,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
request := tailcfg.MapRequest{
Version: 4,
IncludeIPv6: true,
DeltaPeers: true,
KeepAlive: c.keepAlive,
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
DiscoKey: c.discoPubKey,
@@ -573,6 +575,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
// the same format before just closing the connection.
// We can use this same read loop either way.
var msg []byte
var previousPeers []*tailcfg.Node // for delta-purposes
for i := 0; i < maxPolls || maxPolls < 0; i++ {
vlogf("netmap: starting size read after %v (poll %v)", time.Since(t0).Round(time.Millisecond), i)
var siz [4]byte
@@ -594,18 +597,25 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
vlogf("netmap: decode error: %v")
return err
}
if resp.KeepAlive {
vlogf("netmap: got keep-alive")
select {
case timeoutReset <- struct{}{}:
vlogf("netmap: sent keep-alive timer reset")
case <-ctx.Done():
c.logf("netmap: not resetting timer for keep-alive due to: %v", ctx.Err())
return ctx.Err()
}
} else {
vlogf("netmap: got new map")
}
select {
case timeoutReset <- struct{}{}:
vlogf("netmap: sent timer reset")
case <-ctx.Done():
c.logf("netmap: not resetting timer; context done: %v", ctx.Err())
return ctx.Err()
}
if resp.KeepAlive {
continue
}
vlogf("netmap: got new map")
undeltaPeers(&resp, previousPeers)
previousPeers = cloneNodes(resp.Peers) // defensive/lazy clone, since this escapes to who knows where
if resp.DERPMap != nil {
vlogf("netmap: new map contains DERP map")
@@ -849,3 +859,97 @@ func envBool(k string) bool {
}
return v
}
// undeltaPeers updates mapRes.Peers to be complete based on the provided previous peer list
// and the PeersRemoved and PeersChanged fields in mapRes.
// It then also nils out the delta fields.
func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
if len(mapRes.Peers) > 0 {
// Not delta encoded.
if !nodesSorted(mapRes.Peers) {
log.Printf("netmap: undeltaPeers: MapResponse.Peers not sorted; sorting")
sortNodes(mapRes.Peers)
}
return
}
var removed map[tailcfg.NodeID]bool
if pr := mapRes.PeersRemoved; len(pr) > 0 {
removed = make(map[tailcfg.NodeID]bool, len(pr))
for _, id := range pr {
removed[id] = true
}
}
changed := mapRes.PeersChanged
if len(removed) == 0 && len(changed) == 0 {
// No changes fast path.
mapRes.Peers = prev
return
}
if !nodesSorted(changed) {
log.Printf("netmap: undeltaPeers: MapResponse.PeersChanged not sorted; sorting")
sortNodes(changed)
}
if !nodesSorted(prev) {
// Internal error (unrelated to the network) if we get here.
log.Printf("netmap: undeltaPeers: [unexpected] prev not sorted; sorting")
sortNodes(prev)
}
newFull := make([]*tailcfg.Node, 0, len(prev)-len(removed))
for len(prev) > 0 && len(changed) > 0 {
pID := prev[0].ID
cID := changed[0].ID
if removed[pID] {
prev = prev[1:]
continue
}
switch {
case pID < cID:
newFull = append(newFull, prev[0])
prev = prev[1:]
case pID == cID:
newFull = append(newFull, changed[0])
prev, changed = prev[1:], changed[1:]
case cID < pID:
newFull = append(newFull, changed[0])
changed = changed[1:]
}
}
newFull = append(newFull, changed...)
for _, n := range prev {
if !removed[n.ID] {
newFull = append(newFull, n)
}
}
sortNodes(newFull)
mapRes.Peers = newFull
mapRes.PeersChanged = nil
mapRes.PeersRemoved = nil
}
func nodesSorted(v []*tailcfg.Node) bool {
for i, n := range v {
if i > 0 && n.ID <= v[i-1].ID {
return false
}
}
return true
}
func sortNodes(v []*tailcfg.Node) {
sort.Slice(v, func(i, j int) bool { return v[i].ID < v[j].ID })
}
func cloneNodes(v1 []*tailcfg.Node) []*tailcfg.Node {
if v1 == nil {
return nil
}
v2 := make([]*tailcfg.Node, len(v1))
for i, n := range v1 {
v2[i] = n.Clone()
}
return v2
}

View File

@@ -0,0 +1,93 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package controlclient
import (
"fmt"
"reflect"
"strings"
"testing"
"tailscale.com/tailcfg"
)
func TestUndeltaPeers(t *testing.T) {
n := func(id tailcfg.NodeID, name string) *tailcfg.Node {
return &tailcfg.Node{ID: id, Name: name}
}
peers := func(nv ...*tailcfg.Node) []*tailcfg.Node { return nv }
tests := []struct {
name string
mapRes *tailcfg.MapResponse
prev []*tailcfg.Node
want []*tailcfg.Node
}{
{
name: "full_peers",
mapRes: &tailcfg.MapResponse{
Peers: peers(n(1, "foo"), n(2, "bar")),
},
want: peers(n(1, "foo"), n(2, "bar")),
},
{
name: "full_peers_ignores_deltas",
mapRes: &tailcfg.MapResponse{
Peers: peers(n(1, "foo"), n(2, "bar")),
PeersRemoved: []tailcfg.NodeID{2},
},
want: peers(n(1, "foo"), n(2, "bar")),
},
{
name: "add_and_update",
prev: peers(n(1, "foo"), n(2, "bar")),
mapRes: &tailcfg.MapResponse{
PeersChanged: peers(n(0, "zero"), n(2, "bar2"), n(3, "three")),
},
want: peers(n(0, "zero"), n(1, "foo"), n(2, "bar2"), n(3, "three")),
},
{
name: "remove",
prev: peers(n(1, "foo"), n(2, "bar")),
mapRes: &tailcfg.MapResponse{
PeersRemoved: []tailcfg.NodeID{1},
},
want: peers(n(2, "bar")),
},
{
name: "add_and_remove",
prev: peers(n(1, "foo"), n(2, "bar")),
mapRes: &tailcfg.MapResponse{
PeersChanged: peers(n(1, "foo2")),
PeersRemoved: []tailcfg.NodeID{2},
},
want: peers(n(1, "foo2")),
},
{
name: "unchanged",
prev: peers(n(1, "foo"), n(2, "bar")),
mapRes: &tailcfg.MapResponse{},
want: peers(n(1, "foo"), n(2, "bar")),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
undeltaPeers(tt.mapRes, tt.prev)
if !reflect.DeepEqual(tt.mapRes.Peers, tt.want) {
t.Errorf("wrong results\n got: %s\nwant: %s", formatNodes(tt.mapRes.Peers), formatNodes(tt.want))
}
})
}
}
func formatNodes(nodes []*tailcfg.Node) string {
var sb strings.Builder
for i, n := range nodes {
if i > 0 {
sb.WriteString(", ")
}
fmt.Fprintf(&sb, "(%d, %q)", n.ID, n.Name)
}
return sb.String()
}

View File

@@ -7,7 +7,6 @@ package controlclient
import (
"encoding/json"
"fmt"
"log"
"net"
"reflect"
"strconv"
@@ -132,12 +131,18 @@ func printPeerConcise(buf *strings.Builder, p *tailcfg.Node) {
if strings.HasPrefix(derp, derpPrefix) {
derp = "D" + derp[len(derpPrefix):]
}
var discoShort string
if !p.DiscoKey.IsZero() {
discoShort = p.DiscoKey.ShortString() + " "
}
// Most of the time, aip is just one element, so format the
// table to look good in that case. This will also make multi-
// subnet nodes stand out visually.
fmt.Fprintf(buf, " %v %-2v %-15v : %v\n",
p.Key.ShortString(), derp,
fmt.Fprintf(buf, " %v %s%-2v %-15v : %v\n",
p.Key.ShortString(),
discoShort,
derp,
strings.Join(aip, " "),
strings.Join(ep, " "))
}
@@ -146,6 +151,7 @@ func printPeerConcise(buf *strings.Builder, p *tailcfg.Node) {
func nodeConciseEqual(a, b *tailcfg.Node) bool {
return a.Key == b.Key &&
a.DERP == b.DERP &&
a.DiscoKey == b.DiscoKey &&
eqCIDRsIgnoreNil(a.AllowedIPs, b.AllowedIPs) &&
eqStringsIgnoreNil(a.Endpoints, b.Endpoints)
}
@@ -216,20 +222,6 @@ const (
HackDefaultRoute
)
// TODO(bradfitz): UAPI seems to only be used by the old confnode and
// pingnode; delete this when those are deleted/rewritten?
func (nm *NetworkMap) UAPI(flags WGConfigFlags) string {
wgcfg, err := nm.WGCfg(log.Printf, flags)
if err != nil {
log.Fatalf("WGCfg() failed unexpectedly: %v", err)
}
s, err := wgcfg.ToUAPI()
if err != nil {
log.Fatalf("ToUAPI() failed unexpectedly: %v", err)
}
return s
}
// 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.

View File

@@ -5,8 +5,10 @@
package controlclient
import (
"encoding/hex"
"testing"
"github.com/tailscale/wireguard-go/wgcfg"
"tailscale.com/tailcfg"
)
@@ -17,6 +19,15 @@ func testNodeKey(b byte) (ret tailcfg.NodeKey) {
return
}
func testDiscoKey(hexPrefix string) (ret tailcfg.DiscoKey) {
b, err := hex.DecodeString(hexPrefix)
if err != nil {
panic(err)
}
copy(ret[:], b)
return
}
func TestNetworkMapConcise(t *testing.T) {
for _, tt := range []struct {
name string
@@ -202,6 +213,62 @@ func TestConciseDiffFrom(t *testing.T) {
},
want: "- [AQEBA] D1 : 192.168.0.100:12 192.168.0.100:12354\n- [AwMDA] D3 : 192.168.0.100:12 192.168.0.100:12354\n",
},
{
name: "peer_port_change",
a: &NetworkMap{
NodeKey: testNodeKey(1),
Peers: []*tailcfg.Node{
{
ID: 2,
Key: testNodeKey(2),
DERP: "127.3.3.40:2",
Endpoints: []string{"192.168.0.100:12", "1.1.1.1:1"},
},
},
},
b: &NetworkMap{
NodeKey: testNodeKey(1),
Peers: []*tailcfg.Node{
{
ID: 2,
Key: testNodeKey(2),
DERP: "127.3.3.40:2",
Endpoints: []string{"192.168.0.100:12", "1.1.1.1:2"},
},
},
},
want: "- [AgICA] D2 : 192.168.0.100:12 1.1.1.1:1 \n+ [AgICA] D2 : 192.168.0.100:12 1.1.1.1:2 \n",
},
{
name: "disco_key_only_change",
a: &NetworkMap{
NodeKey: testNodeKey(1),
Peers: []*tailcfg.Node{
{
ID: 2,
Key: testNodeKey(2),
DERP: "127.3.3.40:2",
Endpoints: []string{"192.168.0.100:41641", "1.1.1.1:41641"},
DiscoKey: testDiscoKey("f00f00f00f"),
AllowedIPs: []wgcfg.CIDR{{IP: wgcfg.IPv4(100, 102, 103, 104), Mask: 32}},
},
},
},
b: &NetworkMap{
NodeKey: testNodeKey(1),
Peers: []*tailcfg.Node{
{
ID: 2,
Key: testNodeKey(2),
DERP: "127.3.3.40:2",
Endpoints: []string{"192.168.0.100:41641", "1.1.1.1:41641"},
DiscoKey: testDiscoKey("ba4ba4ba4b"),
AllowedIPs: []wgcfg.CIDR{{IP: wgcfg.IPv4(100, 102, 103, 104), Mask: 32}},
},
},
},
want: "- [AgICA] d:f00f00f00f000000 D2 100.102.103.104 : 192.168.0.100:41641 1.1.1.1:41641\n+ [AgICA] d:ba4ba4ba4b000000 D2 100.102.103.104 : 192.168.0.100:41641 1.1.1.1:41641\n",
},
} {
t.Run(tt.name, func(t *testing.T) {
var got string

View File

@@ -29,6 +29,7 @@ import (
"tailscale.com/metrics"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/version"
)
var debug, _ = strconv.ParseBool(os.Getenv("DERP_DEBUG_LOGS"))
@@ -1191,6 +1192,9 @@ func (s *Server) ExpVar() expvar.Var {
m.Set("multiforwarder_created", &s.multiForwarderCreated)
m.Set("multiforwarder_deleted", &s.multiForwarderDeleted)
m.Set("packet_forwarder_delete_other_value", &s.removePktForwardOther)
var expvarVersion expvar.String
expvarVersion.Set(version.LONG)
m.Set("version", &expvarVersion)
return m
}

View File

@@ -23,10 +23,10 @@ import (
"tailscale.com/types/logger"
)
func newPrivateKey(t *testing.T) (k key.Private) {
t.Helper()
func newPrivateKey(tb testing.TB) (k key.Private) {
tb.Helper()
if _, err := crand.Read(k[:]); err != nil {
t.Fatal(err)
tb.Fatal(err)
}
return
}
@@ -742,3 +742,64 @@ func TestForwarderRegistration(t *testing.T) {
u1: testFwd(3),
})
}
func BenchmarkSendRecv(b *testing.B) {
for _, size := range []int{10, 100, 1000, 10000} {
b.Run(fmt.Sprintf("msgsize=%d", size), func(b *testing.B) { benchmarkSendRecvSize(b, size) })
}
}
func benchmarkSendRecvSize(b *testing.B, packetSize int) {
serverPrivateKey := newPrivateKey(b)
s := NewServer(serverPrivateKey, logger.Discard)
defer s.Close()
key := newPrivateKey(b)
clientKey := key.Public()
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
b.Fatal(err)
}
defer ln.Close()
connOut, err := net.Dial("tcp", ln.Addr().String())
if err != nil {
b.Fatal(err)
}
defer connOut.Close()
connIn, err := ln.Accept()
if err != nil {
b.Fatal(err)
}
defer connIn.Close()
brwServer := bufio.NewReadWriter(bufio.NewReader(connIn), bufio.NewWriter(connIn))
go s.Accept(connIn, brwServer, "test-client")
brw := bufio.NewReadWriter(bufio.NewReader(connOut), bufio.NewWriter(connOut))
client, err := NewClient(key, connOut, brw, logger.Discard)
if err != nil {
b.Fatalf("client: %v", err)
}
go func() {
for {
_, err := client.Recv()
if err != nil {
return
}
}
}()
msg := make([]byte, packetSize)
b.SetBytes(int64(len(msg)))
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := client.Send(clientKey, msg); err != nil {
b.Fatal(err)
}
}
}

10
go.mod
View File

@@ -21,18 +21,18 @@ require (
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3
github.com/peterbourgon/ff/v2 v2.0.0
github.com/tailscale/winipcfg-go v0.0.0-20200413171540-609dcf2df55f
github.com/tailscale/wireguard-go v0.0.0-20200724155040-d554a2a5e7e1
github.com/tailscale/wireguard-go v0.0.0-20200806235025-91988cfbaa3a
github.com/tcnksm/go-httpstat v0.2.0
github.com/toqueteos/webbrowser v1.2.0
go4.org/mem v0.0.0-20200706164138-185c595c3ecc
golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6
golang.org/x/net v0.0.0-20200301022130-244492dfa37a
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425
honnef.co/go/tools v0.0.1-2020.1.4
inet.af/netaddr v0.0.0-20200718043157-99321d6ad24c
inet.af/netaddr v0.0.0-20200810144936-56928fe48a98
rsc.io/goversion v1.2.0
)

21
go.sum
View File

@@ -86,8 +86,8 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/tailscale/winipcfg-go v0.0.0-20200413171540-609dcf2df55f h1:uFj5bslHsMzxIM8UTjAhq4VXeo6GfNW91rpoh/WMJaY=
github.com/tailscale/winipcfg-go v0.0.0-20200413171540-609dcf2df55f/go.mod h1:x880GWw5fvrl2DVTQ04ttXQD4DuppTt1Yz6wLibbjNE=
github.com/tailscale/wireguard-go v0.0.0-20200724155040-d554a2a5e7e1 h1:Ga895WFYzI9VOXyps7t9ax9P5zSKsP5Yqpiv2euuyeU=
github.com/tailscale/wireguard-go v0.0.0-20200724155040-d554a2a5e7e1/go.mod h1:JPm5cTfu1K+qDFRbiHy0sOlHUylYQbpl356sdYFD8V4=
github.com/tailscale/wireguard-go v0.0.0-20200806235025-91988cfbaa3a h1:dQEgNpoOJf+8MswlvXJicb8ZDQqZAGe8f/WfzbDMvtE=
github.com/tailscale/wireguard-go v0.0.0-20200806235025-91988cfbaa3a/go.mod h1:WXq+IkSOJGIgfF1XW+4z4oW+LX/TXzU9DcKlT5EZLi4=
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=
@@ -102,8 +102,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191002192127-34f69633bfdc/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6 h1:TjszyFsQsyZNHwdVdZ5m7bjmreu0znc2kRYsEml9/Ww=
golang.org/x/crypto v0.0.0-20200317142112-1b76d66859c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88=
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -113,12 +113,11 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0=
golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5 h1:WQ8q63x+f/zpC8Ac1s9wLElVoHhm32p6tudrU72n1QA=
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
@@ -134,12 +133,12 @@ golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191003212358-c178f38b412c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0=
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e h1:hq86ru83GdWTlfQFZGO4nZJTU4Bs2wfHl8oFHRaXsfc=
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3 h1:5B6i6EAiSYyejWfvc5Rc9BbI3rzIsrrXfAQBWnYfn+w=
golang.org/x/sys v0.0.0-20200501145240-bc7a7d42d5c3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
@@ -169,5 +168,7 @@ honnef.co/go/tools v0.0.1-2020.1.4 h1:UoveltGrhghAA7ePc+e+QYDHXrBps2PqFZiHkGR/xK
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
inet.af/netaddr v0.0.0-20200718043157-99321d6ad24c h1:si3Owrfem175Ry6gKqnh59eOXxDojyBTIHxUKuvK/Eo=
inet.af/netaddr v0.0.0-20200718043157-99321d6ad24c/go.mod h1:qqYzz/2whtrbWJvt+DNWQyvekNN4ePQZcg2xc2/Yjww=
inet.af/netaddr v0.0.0-20200810144936-56928fe48a98 h1:bWyWDZP0l6VnQ1TDKf6yNwuiEDV6Q3q1Mv34m+lzT1I=
inet.af/netaddr v0.0.0-20200810144936-56928fe48a98/go.mod h1:qqYzz/2whtrbWJvt+DNWQyvekNN4ePQZcg2xc2/Yjww=
rsc.io/goversion v1.2.0 h1:SPn+NLTiAG7w30IRK/DKp1BjvpWabYgxlLp/+kx5J8w=
rsc.io/goversion v1.2.0/go.mod h1:Eih9y/uIBS3ulggl7KNJ09xGSLcuNaLgmvvqa07sgfo=

View File

@@ -175,7 +175,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
}()
logf("Listening on %v", listen.Addr())
bo := backoff.NewBackoff("ipnserver", logf)
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
@@ -306,7 +306,7 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
proc.mu.Unlock()
}()
bo := backoff.NewBackoff("BabysitProc", logf)
bo := backoff.NewBackoff("BabysitProc", logf, 30*time.Second)
for {
startTime := time.Now()

View File

@@ -939,7 +939,8 @@ func (b *LocalBackend) authReconfig() {
b.logf("[unexpected] dns proxied but no nameservers")
proxied = false
} else {
domains = append(domains, domainsForProxying(nm)...)
// Domains for proxying should come first to avoid leaking queries.
domains = append(domainsForProxying(nm), domains...)
}
}
rcfg.DNS = dns.Config{
@@ -1068,6 +1069,7 @@ func (b *LocalBackend) enterState(newState State) {
b.state = newState
prefs := b.prefs
notify := b.notify
bc := b.c
b.mu.Unlock()
if state == newState {
@@ -1079,6 +1081,10 @@ func (b *LocalBackend) enterState(newState State) {
b.send(Notify{State: &newState})
}
if bc != nil {
bc.SetPaused(newState == Stopped)
}
switch newState {
case NeedsLogin:
b.blockEngineUpdates(true)

View File

@@ -255,7 +255,7 @@ func (bc *BackendClient) FakeExpireAfter(x time.Duration) {
}
// MaxMessageSize is the maximum message size, in bytes.
const MaxMessageSize = 1 << 20
const MaxMessageSize = 10 << 20
// TODO(apenwarr): incremental json decode?
// That would let us avoid storing the whole byte array uselessly in RAM.

View File

@@ -32,6 +32,7 @@ import (
"tailscale.com/net/netns"
"tailscale.com/net/tlsdial"
"tailscale.com/smallzstd"
"tailscale.com/types/logger"
"tailscale.com/version"
)
@@ -54,7 +55,7 @@ type Policy struct {
func (c *Config) ToBytes() []byte {
data, err := json.MarshalIndent(c, "", "\t")
if err != nil {
log.Fatalf("logpolicy.Config marshal: %v\n", err)
log.Fatalf("logpolicy.Config marshal: %v", err)
}
return data
}
@@ -101,21 +102,25 @@ func (l logWriter) Write(buf []byte) (int, error) {
// logsDir returns the directory to use for log configuration and
// buffer storage.
func logsDir() string {
func logsDir(logf logger.Logf) string {
systemdStateDir := os.Getenv("STATE_DIRECTORY")
if systemdStateDir != "" {
logf("logpolicy: using $STATE_DIRECTORY, %q", systemdStateDir)
return systemdStateDir
}
cacheDir, err := os.UserCacheDir()
if err == nil {
return filepath.Join(cacheDir, "Tailscale")
d := filepath.Join(cacheDir, "Tailscale")
logf("logpolicy: using UserCacheDir, %q", d)
return d
}
// Use the current working directory, unless we're being run by a
// service manager that sets it to /.
wd, err := os.Getwd()
if err == nil && wd != "/" {
logf("logpolicy: using current directory, %q", wd)
return wd
}
@@ -126,6 +131,7 @@ func logsDir() string {
if err != nil {
panic("no safe place found to store log state")
}
logf("logpolicy: using temp directory, %q", tmp)
return tmp
}
@@ -303,7 +309,13 @@ func New(collection string) *Policy {
}
console := log.New(stderrWriter{}, "", lflags)
dir := logsDir()
var earlyErrBuf bytes.Buffer
earlyLogf := func(format string, a ...interface{}) {
fmt.Fprintf(&earlyErrBuf, format, a...)
earlyErrBuf.WriteByte('\n')
}
dir := logsDir(earlyLogf)
cmdName := version.CmdName()
tryFixLogStateLocation(dir, cmdName)
@@ -312,13 +324,13 @@ func New(collection string) *Policy {
var oldc *Config
data, err := ioutil.ReadFile(cfgPath)
if err != nil {
log.Printf("logpolicy.Read %v: %v\n", cfgPath, err)
earlyLogf("logpolicy.Read %v: %v", cfgPath, err)
oldc = &Config{}
oldc.Collection = collection
} else {
oldc, err = ConfigFromBytes(data)
if err != nil {
log.Printf("logpolicy.Config unmarshal: %v\n", err)
earlyLogf("logpolicy.Config unmarshal: %v", err)
oldc = &Config{}
}
}
@@ -340,7 +352,7 @@ func New(collection string) *Policy {
newc.PublicID = newc.PrivateID.Public()
if newc != *oldc {
if err := newc.save(cfgPath); err != nil {
log.Printf("logpolicy.Config.Save: %v\n", err)
earlyLogf("logpolicy.Config.Save: %v", err)
}
}
@@ -366,14 +378,17 @@ func New(collection string) *Policy {
log.SetFlags(0) // other logflags are set on console, not here
log.SetOutput(lw)
log.Printf("Program starting: v%v, Go %v: %#v\n",
log.Printf("Program starting: v%v, Go %v: %#v",
version.LONG,
strings.TrimPrefix(runtime.Version(), "go"),
os.Args)
log.Printf("LogID: %v\n", newc.PublicID)
log.Printf("LogID: %v", newc.PublicID)
if filchErr != nil {
log.Printf("filch failed: %v", filchErr)
}
if earlyErrBuf.Len() != 0 {
log.Printf("%s", earlyErrBuf.Bytes())
}
return &Policy{
Logtail: lw,
@@ -392,7 +407,7 @@ func (p *Policy) Close() {
// log upload if it can be done before ctx is canceled.
func (p *Policy) Shutdown(ctx context.Context) error {
if p.Logtail != nil {
log.Printf("flushing log.\n")
log.Printf("flushing log.")
return p.Logtail.Shutdown(ctx)
}
return nil

View File

@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package backoff provides a back-off timer type.
package backoff
import (
@@ -12,54 +13,70 @@ import (
"tailscale.com/types/logger"
)
const MAX_BACKOFF_MSEC = 30000
// Backoff tracks state the history of consecutive failures and sleeps
// an increasing amount of time, up to a provided limit.
type Backoff struct {
n int
n int // number of consecutive failures
maxBackoff time.Duration
// Name is the name of this backoff timer, for logging purposes.
name string
// logf is the function used for log messages when backing off.
logf logger.Logf
// NewTimer is the function that acts like time.NewTimer().
// You can override this in unit tests.
NewTimer func(d time.Duration) *time.Timer
// NewTimer is the function that acts like time.NewTimer.
// It's for use in unit tests.
NewTimer func(time.Duration) *time.Timer
// LogLongerThan sets the minimum time of a single backoff interval
// before we mention it in the log.
LogLongerThan time.Duration
}
func NewBackoff(name string, logf logger.Logf) Backoff {
return Backoff{
name: name,
logf: logf,
NewTimer: time.NewTimer,
// NewBackoff returns a new Backoff timer with the provided name (for logging), logger,
// and max backoff time. By default, all failures (calls to BackOff with a non-nil err)
// are logged unless the returned Backoff.LogLongerThan is adjusted.
func NewBackoff(name string, logf logger.Logf, maxBackoff time.Duration) *Backoff {
return &Backoff{
name: name,
logf: logf,
maxBackoff: maxBackoff,
NewTimer: time.NewTimer,
}
}
// Backoff sleeps an increasing amount of time if err is non-nil.
// and the context is not a
// It resets the backoff schedule once err is nil.
func (b *Backoff) BackOff(ctx context.Context, err error) {
if ctx.Err() == nil && err != nil {
b.n++
// n^2 backoff timer is a little smoother than the
// common choice of 2^n.
msec := b.n * b.n * 10
if msec > MAX_BACKOFF_MSEC {
msec = MAX_BACKOFF_MSEC
}
// Randomize the delay between 0.5-1.5 x msec, in order
// to prevent accidental "thundering herd" problems.
msec = rand.Intn(msec) + msec/2
dur := time.Duration(msec) * time.Millisecond
if dur >= b.LogLongerThan {
b.logf("%s: backoff: %d msec\n", b.name, msec)
}
t := b.NewTimer(dur)
select {
case <-ctx.Done():
t.Stop()
case <-t.C:
}
} else {
// not a regular error
if err == nil {
// No error. Reset number of consecutive failures.
b.n = 0
return
}
if ctx.Err() != nil {
// Fast path.
return
}
b.n++
// n^2 backoff timer is a little smoother than the
// common choice of 2^n.
d := time.Duration(b.n*b.n) * 10 * time.Millisecond
if d > b.maxBackoff {
d = b.maxBackoff
}
// Randomize the delay between 0.5-1.5 x msec, in order
// to prevent accidental "thundering herd" problems.
d = time.Duration(float64(d) * (rand.Float64() + 0.5))
if d >= b.LogLongerThan {
b.logf("%s: backoff: %d msec", b.name, d.Milliseconds())
}
t := b.NewTimer(d)
select {
case <-ctx.Done():
t.Stop()
case <-t.C:
}
}

View File

@@ -105,7 +105,7 @@ func Log(cfg Config, logf tslogger.Logf) Logger {
sentinel: make(chan int32, 16),
drainLogs: cfg.DrainLogs,
timeNow: cfg.TimeNow,
bo: backoff.NewBackoff("logtail", logf),
bo: backoff.NewBackoff("logtail", logf, 30*time.Second),
shutdownStart: make(chan struct{}),
shutdownDone: make(chan struct{}),
@@ -133,7 +133,7 @@ type logger struct {
drainLogs <-chan struct{} // if non-nil, external signal to attempt a drain
sentinel chan int32
timeNow func() time.Time
bo backoff.Backoff
bo *backoff.Backoff
zstdEncoder Encoder
uploadCancel func()

View File

@@ -13,10 +13,6 @@ import (
"tailscale.com/version"
)
func init() {
likelyHomeRouterIP = likelyHomeRouterIPDarwin
}
/*
Parse out 10.0.0.1 from:
@@ -32,12 +28,11 @@ default link#14 UCSI utun2
...
*/
func likelyHomeRouterIPDarwin() (ret netaddr.IP, ok bool) {
func likelyHomeRouterIPDarwinExec() (ret netaddr.IP, ok bool) {
if version.IsMobile() {
// Don't try to do subprocesses on iOS. Ends up with log spam like:
// kernel: "Sandbox: IPNExtension(86580) deny(1) process-fork"
// TODO(bradfitz): let our iOS app register a func with this package
// and have it call into C/Swift to get the routing table.
// This is why we have likelyHomeRouterIPDarwinSyscall.
return ret, false
}
cmd := exec.Command("/usr/sbin/netstat", "-r", "-n", "-f", "inet")

View File

@@ -0,0 +1,127 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin,cgo
package interfaces
/*
#import "route.h"
#import <netinet/in.h>
#import <sys/sysctl.h>
#import <stdlib.h>
#import <stdio.h>
// privateGatewayIPFromRoute returns the private gateway ip address from rtm, if it exists.
// Otherwise, it returns 0.
int privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
{
// sockaddrs are after the message header
struct sockaddr* dst_sa = (struct sockaddr *)(rtm + 1);
if((rtm->rtm_addrs & (RTA_DST|RTA_GATEWAY)) != (RTA_DST|RTA_GATEWAY))
return 0; // missing dst or gateway addr
if (dst_sa->sa_family != AF_INET)
return 0; // dst not IPv4
if ((rtm->rtm_flags & RTF_GATEWAY) == 0)
return 0; // gateway flag not set
struct sockaddr_in* dst_si = (struct sockaddr_in *)dst_sa;
if (dst_si->sin_addr.s_addr != INADDR_ANY)
return 0; // not default route
#define ROUNDUP(a) ((a) > 0 ? (1 + (((a) - 1) | (sizeof(long) - 1))) : sizeof(long))
struct sockaddr* gateway_sa = (struct sockaddr *)((char *)dst_sa + ROUNDUP(dst_sa->sa_len));
if (gateway_sa->sa_family != AF_INET)
return 0; // gateway not IPv4
struct sockaddr_in* gateway_si= (struct sockaddr_in *)gateway_sa;
int ip;
ip = gateway_si->sin_addr.s_addr;
unsigned char a, b;
a = (ip >> 0) & 0xff;
b = (ip >> 8) & 0xff;
// Check whether ip is private, that is, whether it is
// in one of 10.0.0.0/8, 172.16.0.0/12, or 192.168.0.0/16.
if (a == 10)
return ip; // matches 10.0.0.0/8
if (a == 172 && (b >> 4) == 1)
return ip; // matches 172.16.0.0/12
if (a == 192 && b == 168)
return ip; // matches 192.168.0.0/16
// Not a private IP.
return 0;
}
// privateGatewayIP returns the private gateway IP address, if it exists.
// If no private gateway IP address was found, it returns 0.
// On an error, it returns an error code in (0, 255].
// Any private gateway IP address is > 255.
int privateGatewayIP()
{
size_t needed;
int mib[6];
char *buf;
mib[0] = CTL_NET;
mib[1] = PF_ROUTE;
mib[2] = 0;
mib[3] = 0;
mib[4] = NET_RT_DUMP2;
mib[5] = 0;
if (sysctl(mib, 6, NULL, &needed, NULL, 0) < 0)
return 1; // route dump size estimation failed
if ((buf = malloc(needed)) == 0)
return 2; // malloc failed
if (sysctl(mib, 6, buf, &needed, NULL, 0) < 0) {
free(buf);
return 3; // route dump failed
}
// Loop over all routes.
char *next, *lim;
lim = buf + needed;
struct rt_msghdr2 *rtm;
for (next = buf; next < lim; next += rtm->rtm_msglen) {
rtm = (struct rt_msghdr2 *)next;
int ip;
ip = privateGatewayIPFromRoute(rtm);
if (ip) {
free(buf);
return ip;
}
}
free(buf);
return 0; // no gateway found
}
*/
import "C"
import (
"encoding/binary"
"fmt"
"os"
"inet.af/netaddr"
)
func init() {
likelyHomeRouterIP = likelyHomeRouterIPDarwinSyscall
}
func likelyHomeRouterIPDarwinSyscall() (ret netaddr.IP, ok bool) {
ip := C.privateGatewayIP()
fmt.Fprintln(os.Stderr, "likelyHomeRouterIPDarwinSyscall", ip)
if ip < 255 {
return netaddr.IP{}, false
}
var q [4]byte
binary.LittleEndian.PutUint32(q[:], uint32(ip))
return netaddr.IPv4(q[0], q[1], q[2], q[3]), true
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build cgo,darwin
package interfaces
import "testing"
func TestLikelyHomeRouterIPSyscallExec(t *testing.T) {
syscallIP, syscallOK := likelyHomeRouterIPDarwinSyscall()
netstatIP, netstatOK := likelyHomeRouterIPDarwinExec()
if syscallOK != netstatOK || syscallIP != netstatIP {
t.Errorf("syscall() = %v, %v, netstat = %v, %v",
syscallIP, syscallOK,
netstatIP, netstatOK,
)
}
}

View File

@@ -0,0 +1,11 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin,!cgo
package interfaces
func init() {
likelyHomeRouterIP = likelyHomeRouterIPDarwinExec
}

View File

@@ -5,10 +5,15 @@
package interfaces
import (
"bufio"
"bytes"
"errors"
"io"
"log"
"os"
"os/exec"
"runtime"
"strings"
"go4.org/mem"
"inet.af/netaddr"
@@ -115,3 +120,91 @@ func likelyHomeRouterIPAndroid() (ret netaddr.IP, ok bool) {
cmd.Wait()
return ret, !ret.IsZero()
}
// DefaultRouteInterface returns the name of the network interface that owns
// the default route, not including any tailscale interfaces.
func DefaultRouteInterface() (string, error) {
v, err := defaultRouteInterfaceProcNet()
if err == nil {
return v, nil
}
if runtime.GOOS == "android" {
return defaultRouteInterfaceAndroidIPRoute()
}
return v, err
}
var zeroRouteBytes = []byte("00000000")
func defaultRouteInterfaceProcNet() (string, error) {
f, err := os.Open("/proc/net/route")
if err != nil {
return "", err
}
defer f.Close()
br := bufio.NewReaderSize(f, 128)
for {
line, err := br.ReadSlice('\n')
if err == io.EOF {
break
}
if err != nil {
return "", err
}
if !bytes.Contains(line, zeroRouteBytes) {
continue
}
fields := strings.Fields(string(line))
ifc := fields[0]
ip := fields[1]
netmask := fields[7]
if strings.HasPrefix(ifc, "tailscale") ||
strings.HasPrefix(ifc, "wg") {
continue
}
if ip == "00000000" && netmask == "00000000" {
// default route
return ifc, nil // interface name
}
}
return "", errors.New("no default routes found")
}
// defaultRouteInterfaceAndroidIPRoute tries to find the machine's default route interface name
// by parsing the "ip route" command output. We use this on Android where /proc/net/route
// can be missing entries or have locked-down permissions.
// See also comments in https://github.com/tailscale/tailscale/pull/666.
func defaultRouteInterfaceAndroidIPRoute() (ifname string, err error) {
cmd := exec.Command("/system/bin/ip", "route", "show", "table", "0")
out, err := cmd.StdoutPipe()
if err != nil {
return "", err
}
if err := cmd.Start(); err != nil {
log.Printf("interfaces: running /system/bin/ip: %v", err)
return "", err
}
// Search for line like "default via 10.0.2.2 dev radio0 table 1016 proto static mtu 1500 "
lineread.Reader(out, func(line []byte) error {
const pfx = "default via "
if !mem.HasPrefix(mem.B(line), mem.S(pfx)) {
return nil
}
ff := strings.Fields(string(line))
for i, v := range ff {
if i > 0 && ff[i-1] == "dev" && ifname == "" {
ifname = v
}
}
return nil
})
cmd.Process.Kill()
cmd.Wait()
if ifname == "" {
return "", errors.New("no default routes found")
}
return ifname, nil
}

View File

@@ -0,0 +1,16 @@
// 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 interfaces
import "testing"
func BenchmarkDefaultRouteInterface(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if _, err := DefaultRouteInterface(); err != nil {
b.Fatal(err)
}
}
}

257
net/interfaces/route.h Normal file
View File

@@ -0,0 +1,257 @@
/*
* Copyright (c) 2000-2017 Apple Inc. All rights reserved.
*
* @APPLE_OSREFERENCE_LICENSE_HEADER_START@
*
* This file contains Original Code and/or Modifications of Original Code
* as defined in and that are subject to the Apple Public Source License
* Version 2.0 (the 'License'). You may not use this file except in
* compliance with the License. The rights granted to you under the License
* may not be used to create, or enable the creation or redistribution of,
* unlawful or unlicensed copies of an Apple operating system, or to
* circumvent, violate, or enable the circumvention or violation of, any
* terms of an Apple operating system software license agreement.
*
* Please obtain a copy of the License at
* http://www.opensource.apple.com/apsl/ and read it before using this file.
*
* The Original Code and all software distributed under the License are
* distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
* EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
* INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
* Please see the License for the specific language governing rights and
* limitations under the License.
*
* @APPLE_OSREFERENCE_LICENSE_HEADER_END@
*/
/*
* Copyright (c) 1980, 1986, 1993
* The Regents of the University of California. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. All advertising materials mentioning features or use of this software
* must display the following acknowledgement:
* This product includes software developed by the University of
* California, Berkeley and its contributors.
* 4. Neither the name of the University nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* @(#)route.h 8.3 (Berkeley) 4/19/94
* $FreeBSD: src/sys/net/route.h,v 1.36.2.1 2000/08/16 06:14:23 jayanth Exp $
*/
#ifndef _NET_ROUTE_H_
#define _NET_ROUTE_H_
#include <sys/appleapiopts.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/socket.h>
/*
* These numbers are used by reliable protocols for determining
* retransmission behavior and are included in the routing structure.
*/
struct rt_metrics {
u_int32_t rmx_locks; /* Kernel leaves these values alone */
u_int32_t rmx_mtu; /* MTU for this path */
u_int32_t rmx_hopcount; /* max hops expected */
int32_t rmx_expire; /* lifetime for route, e.g. redirect */
u_int32_t rmx_recvpipe; /* inbound delay-bandwidth product */
u_int32_t rmx_sendpipe; /* outbound delay-bandwidth product */
u_int32_t rmx_ssthresh; /* outbound gateway buffer limit */
u_int32_t rmx_rtt; /* estimated round trip time */
u_int32_t rmx_rttvar; /* estimated rtt variance */
u_int32_t rmx_pksent; /* packets sent using this route */
u_int32_t rmx_state; /* route state */
u_int32_t rmx_filler[3]; /* will be used for T/TCP later */
};
/*
* rmx_rtt and rmx_rttvar are stored as microseconds;
*/
#define RTM_RTTUNIT 1000000 /* units for rtt, rttvar, as units per sec */
#define RTF_UP 0x1 /* route usable */
#define RTF_GATEWAY 0x2 /* destination is a gateway */
#define RTF_HOST 0x4 /* host entry (net otherwise) */
#define RTF_REJECT 0x8 /* host or net unreachable */
#define RTF_DYNAMIC 0x10 /* created dynamically (by redirect) */
#define RTF_MODIFIED 0x20 /* modified dynamically (by redirect) */
#define RTF_DONE 0x40 /* message confirmed */
#define RTF_DELCLONE 0x80 /* delete cloned route */
#define RTF_CLONING 0x100 /* generate new routes on use */
#define RTF_XRESOLVE 0x200 /* external daemon resolves name */
#define RTF_LLINFO 0x400 /* DEPRECATED - exists ONLY for backward
* compatibility */
#define RTF_LLDATA 0x400 /* used by apps to add/del L2 entries */
#define RTF_STATIC 0x800 /* manually added */
#define RTF_BLACKHOLE 0x1000 /* just discard pkts (during updates) */
#define RTF_NOIFREF 0x2000 /* not eligible for RTF_IFREF */
#define RTF_PROTO2 0x4000 /* protocol specific routing flag */
#define RTF_PROTO1 0x8000 /* protocol specific routing flag */
#define RTF_PRCLONING 0x10000 /* protocol requires cloning */
#define RTF_WASCLONED 0x20000 /* route generated through cloning */
#define RTF_PROTO3 0x40000 /* protocol specific routing flag */
/* 0x80000 unused */
#define RTF_PINNED 0x100000 /* future use */
#define RTF_LOCAL 0x200000 /* route represents a local address */
#define RTF_BROADCAST 0x400000 /* route represents a bcast address */
#define RTF_MULTICAST 0x800000 /* route represents a mcast address */
#define RTF_IFSCOPE 0x1000000 /* has valid interface scope */
#define RTF_CONDEMNED 0x2000000 /* defunct; no longer modifiable */
#define RTF_IFREF 0x4000000 /* route holds a ref to interface */
#define RTF_PROXY 0x8000000 /* proxying, no interface scope */
#define RTF_ROUTER 0x10000000 /* host is a router */
#define RTF_DEAD 0x20000000 /* Route entry is being freed */
/* 0x40000000 and up unassigned */
#define RTPRF_OURS RTF_PROTO3 /* set on routes we manage */
#define RTF_BITS \
"\020\1UP\2GATEWAY\3HOST\4REJECT\5DYNAMIC\6MODIFIED\7DONE" \
"\10DELCLONE\11CLONING\12XRESOLVE\13LLINFO\14STATIC\15BLACKHOLE" \
"\16NOIFREF\17PROTO2\20PROTO1\21PRCLONING\22WASCLONED\23PROTO3" \
"\25PINNED\26LOCAL\27BROADCAST\30MULTICAST\31IFSCOPE\32CONDEMNED" \
"\33IFREF\34PROXY\35ROUTER"
#define IS_DIRECT_HOSTROUTE(rt) \
(((rt)->rt_flags & (RTF_HOST | RTF_GATEWAY)) == RTF_HOST)
/*
* Routing statistics.
*/
struct rtstat {
short rts_badredirect; /* bogus redirect calls */
short rts_dynamic; /* routes created by redirects */
short rts_newgateway; /* routes modified by redirects */
short rts_unreach; /* lookups which failed */
short rts_wildcard; /* lookups satisfied by a wildcard */
short rts_badrtgwroute; /* route to gateway is not direct */
};
/*
* Structures for routing messages.
*/
struct rt_msghdr {
u_short rtm_msglen; /* to skip over non-understood messages */
u_char rtm_version; /* future binary compatibility */
u_char rtm_type; /* message type */
u_short rtm_index; /* index for associated ifp */
int rtm_flags; /* flags, incl. kern & message, e.g. DONE */
int rtm_addrs; /* bitmask identifying sockaddrs in msg */
pid_t rtm_pid; /* identify sender */
int rtm_seq; /* for sender to identify action */
int rtm_errno; /* why failed */
int rtm_use; /* from rtentry */
u_int32_t rtm_inits; /* which metrics we are initializing */
struct rt_metrics rtm_rmx; /* metrics themselves */
};
struct rt_msghdr2 {
u_short rtm_msglen; /* to skip over non-understood messages */
u_char rtm_version; /* future binary compatibility */
u_char rtm_type; /* message type */
u_short rtm_index; /* index for associated ifp */
int rtm_flags; /* flags, incl. kern & message, e.g. DONE */
int rtm_addrs; /* bitmask identifying sockaddrs in msg */
int32_t rtm_refcnt; /* reference count */
int rtm_parentflags; /* flags of the parent route */
int rtm_reserved; /* reserved field set to 0 */
int rtm_use; /* from rtentry */
u_int32_t rtm_inits; /* which metrics we are initializing */
struct rt_metrics rtm_rmx; /* metrics themselves */
};
#define RTM_VERSION 5 /* Up the ante and ignore older versions */
/*
* Message types.
*/
#define RTM_ADD 0x1 /* Add Route */
#define RTM_DELETE 0x2 /* Delete Route */
#define RTM_CHANGE 0x3 /* Change Metrics or flags */
#define RTM_GET 0x4 /* Report Metrics */
#define RTM_LOSING 0x5 /* RTM_LOSING is no longer generated by xnu
* and is deprecated */
#define RTM_REDIRECT 0x6 /* Told to use different route */
#define RTM_MISS 0x7 /* Lookup failed on this address */
#define RTM_LOCK 0x8 /* fix specified metrics */
#define RTM_OLDADD 0x9 /* caused by SIOCADDRT */
#define RTM_OLDDEL 0xa /* caused by SIOCDELRT */
#define RTM_RESOLVE 0xb /* req to resolve dst to LL addr */
#define RTM_NEWADDR 0xc /* address being added to iface */
#define RTM_DELADDR 0xd /* address being removed from iface */
#define RTM_IFINFO 0xe /* iface going up/down etc. */
#define RTM_NEWMADDR 0xf /* mcast group membership being added to if */
#define RTM_DELMADDR 0x10 /* mcast group membership being deleted */
#define RTM_IFINFO2 0x12 /* */
#define RTM_NEWMADDR2 0x13 /* */
#define RTM_GET2 0x14 /* */
/*
* Bitmask values for rtm_inits and rmx_locks.
*/
#define RTV_MTU 0x1 /* init or lock _mtu */
#define RTV_HOPCOUNT 0x2 /* init or lock _hopcount */
#define RTV_EXPIRE 0x4 /* init or lock _expire */
#define RTV_RPIPE 0x8 /* init or lock _recvpipe */
#define RTV_SPIPE 0x10 /* init or lock _sendpipe */
#define RTV_SSTHRESH 0x20 /* init or lock _ssthresh */
#define RTV_RTT 0x40 /* init or lock _rtt */
#define RTV_RTTVAR 0x80 /* init or lock _rttvar */
/*
* Bitmask values for rtm_addrs.
*/
#define RTA_DST 0x1 /* destination sockaddr present */
#define RTA_GATEWAY 0x2 /* gateway sockaddr present */
#define RTA_NETMASK 0x4 /* netmask sockaddr present */
#define RTA_GENMASK 0x8 /* cloning mask sockaddr present */
#define RTA_IFP 0x10 /* interface name sockaddr present */
#define RTA_IFA 0x20 /* interface addr sockaddr present */
#define RTA_AUTHOR 0x40 /* sockaddr for author of redirect */
#define RTA_BRD 0x80 /* for NEWADDR, broadcast or p-p dest addr */
/*
* Index offsets for sockaddr array for alternate internal encoding.
*/
#define RTAX_DST 0 /* destination sockaddr present */
#define RTAX_GATEWAY 1 /* gateway sockaddr present */
#define RTAX_NETMASK 2 /* netmask sockaddr present */
#define RTAX_GENMASK 3 /* cloning mask sockaddr present */
#define RTAX_IFP 4 /* interface name sockaddr present */
#define RTAX_IFA 5 /* interface addr sockaddr present */
#define RTAX_AUTHOR 6 /* sockaddr for author of redirect */
#define RTAX_BRD 7 /* for NEWADDR, broadcast or p-p dest addr */
#define RTAX_MAX 8 /* size of array to allocate */
struct rt_addrinfo {
int rti_addrs;
struct sockaddr *rti_info[RTAX_MAX];
};
#endif /* _NET_ROUTE_H_ */

View File

@@ -5,19 +5,15 @@
package netns
import (
"bufio"
"bytes"
"errors"
"flag"
"fmt"
"io"
"os"
"os/exec"
"strings"
"sync"
"syscall"
"golang.org/x/sys/unix"
"tailscale.com/net/interfaces"
)
// tailscaleBypassMark is the mark indicating that packets originating
@@ -43,47 +39,6 @@ func ipRuleAvailable() bool {
return ipRuleOnce.v
}
var zeroRouteBytes = []byte("00000000")
// defaultRouteInterface returns the name of the network interface that owns
// the default route, not including any tailscale interfaces. We only use
// this in SO_BINDTODEVICE mode.
func defaultRouteInterface() (string, error) {
f, err := os.Open("/proc/net/route")
if err != nil {
return "", err
}
defer f.Close()
br := bufio.NewReaderSize(f, 128)
for {
line, err := br.ReadSlice('\n')
if err == io.EOF {
break
}
if err != nil {
return "", err
}
if !bytes.Contains(line, zeroRouteBytes) {
continue
}
fields := strings.Fields(string(line))
ifc := fields[0]
ip := fields[1]
netmask := fields[7]
if strings.HasPrefix(ifc, "tailscale") ||
strings.HasPrefix(ifc, "wg") {
continue
}
if ip == "00000000" && netmask == "00000000" {
// default route
return ifc, nil // interface name
}
}
return "", errors.New("no default routes found")
}
// ignoreErrors returns true if we should ignore setsocketopt errors in
// this instance.
func ignoreErrors() bool {
@@ -133,7 +88,7 @@ func setBypassMark(fd uintptr) error {
}
func bindToDevice(fd uintptr) error {
ifc, err := defaultRouteInterface()
ifc, err := interfaces.DefaultRouteInterface()
if err != nil {
// Make sure we bind to *some* interface,
// or we could get a routing loop.

View File

@@ -49,12 +49,3 @@ func TestBypassMarkInSync(t *testing.T) {
}
t.Errorf("tailscaleBypassMark not found in router_linux.go")
}
func BenchmarkDefaultRouteInterface(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
if _, err := defaultRouteInterface(); err != nil {
b.Fatal(err)
}
}
}

View File

@@ -443,11 +443,12 @@ type RegisterResponse struct {
type MapRequest struct {
Version int // current version is 4
Compress string // "zstd" or "" (no compression)
KeepAlive bool // server sends keep-alives
KeepAlive bool // whether server should send keep-alives back to us
NodeKey NodeKey
DiscoKey DiscoKey
Endpoints []string // caller's endpoints (IPv4 or IPv6)
IncludeIPv6 bool // include IPv6 endpoints in returned Node Endpoints
DeltaPeers bool // whether the 2nd+ network map in response should be deltas, using PeersChanged, PeersRemoved
Stream bool // if true, multiple MapResponse objects are returned
Hostinfo *Hostinfo
@@ -502,22 +503,36 @@ type DNSConfig struct {
}
type MapResponse struct {
KeepAlive bool // if set, all other fields are ignored
KeepAlive bool `json:",omitempty"` // if set, all other fields are ignored
// Networking
Node *Node
Peers []*Node
DERPMap *DERPMap
DERPMap *DERPMap `json:",omitempty"` // if non-empty, a change in the DERP map.
// Peers, if non-empty, is the complete list of peers.
// It will be set in the first MapResponse for a long-polled request/response.
// Subsequent responses will be delta-encoded if DeltaPeers was set in the request.
// If Peers is non-empty, PeersChanged and PeersRemoved should
// be ignored (and should be empty).
// Peers is always returned sorted by Node.ID.
Peers []*Node `json:",omitempty"`
// PeersChanged are the Nodes (identified by their ID) that
// have changed or been added since the past update on the
// HTTP response. It's only set if MapRequest.DeltaPeers was true.
// PeersChanged is always returned sorted by Node.ID.
PeersChanged []*Node `json:",omitempty"`
// PeersRemoved are the NodeIDs that are no longer in the peer list.
PeersRemoved []NodeID `json:",omitempty"`
// DNS is the same as DNSConfig.Nameservers.
//
// TODO(dmytro): should be sent in DNSConfig.Nameservers once clients have updated.
DNS []wgcfg.IP
DNS []wgcfg.IP `json:",omitempty"`
// SearchPaths are the same as DNSConfig.Domains.
//
// TODO(dmytro): should be sent in DNSConfig.Domains once clients have updated.
SearchPaths []string
DNSConfig DNSConfig
SearchPaths []string `json:",omitempty"`
DNSConfig DNSConfig `json:",omitempty"`
// ACLs
Domain string

View File

@@ -5,89 +5,107 @@ set -eu
mode=$1
describe=$2
long() {
ver="${describe#v}"
stem="${ver%%-*}"
case "$stem" in
*.*.*)
# Full SemVer, nothing to do.
semver="${stem}"
# Git describe output overall looks like
# MAJOR.MINOR.PATCH-NUMCOMMITS-GITHASH. Depending on the tag being
# described and the state of the repo, ver can be missing PATCH,
# NUMCOMMITS, or both.
#
# Valid values look like: 1.2.3-1234-abcdef, 0.98-1234-abcdef,
# 1.0.0-abcdef, 0.99-abcdef.
ver="${describe#v}"
stem="${ver%%-*}" # Just the semver-ish bit e.g. 1.2.3, 0.98
suffix="${ver#$stem}" # The rest e.g. -23-abcdef, -abcdef
# Normalize the stem into a full major.minor.patch semver. We might
# not use all those pieces depending on what kind of version we're
# making, but it's good to have them all on hand.
case "$stem" in
*.*.*)
# Full SemVer, nothing to do
stem="$stem"
;;
*.*)
# Old style major.minor, add a .0
semver="${stem}.0"
;;
*)
echo "Unparseable version $stem" >&2
exit 1
;;
esac
suffix="${ver#$stem}"
case "$suffix" in
-*-*)
# Has a change count in addition to the commit hash.
*.*)
# Old style major.minor, add a .0
stem="${stem}.0"
;;
*)
echo "Unparseable version $stem" >&2
exit 1
;;
esac
major=$(echo "$stem" | cut -f1 -d.)
minor=$(echo "$stem" | cut -f2 -d.)
patch=$(echo "$stem" | cut -f3 -d.)
# Extract the change count and git ID from the suffix.
case "$suffix" in
-*-*)
# Has both a change count and a commit hash.
changecount=$(echo "$suffix" | cut -f2 -d-)
githash=$(echo "$suffix" | cut -f3 -d-)
;;
-*)
# Git hash only, change count is zero.
changecount="0"
githash=$(echo "$suffix" | cut -f2 -d-)
;;
*)
echo "Unparseable version suffix $suffix" >&2
exit 1
;;
esac
# Validate that the version data makes sense. Rules:
# - Odd number minors are unstable. Patch must be 0, and gets
# replaced by changecount.
# - Even number minors are stable. Changecount must be 0, and
# gets removed.
#
# After this section, we only use major/minor/patch, which have been
# tweaked as needed.
if expr "$minor" : "[0-9]*[13579]$" >/dev/null; then
# Unstable
if [ "$patch" != "0" ]; then
# This is a fatal error, because a non-zero patch number
# indicates that we created an unstable git tag in violation
# of our versioning policy, and we want to blow up loudly to
# get that fixed.
echo "Unstable release $describe has a non-zero patch number, which is not allowed" >&2
exit 1
fi
patch="$changecount"
else
# Stable
if [ "$changecount" != "0" ]; then
# This is a commit that's sitting between two stable
# releases. We never want to release such a commit to the
# pbulic, but it's useful to be able to build it for
# debugging. Just force the version to 0.0.0, so that we're
# forced to rely on the git commit hash.
major=0
minor=0
patch=0
fi
fi
case "$1" in
long)
echo "${major}.${minor}.${patch}-${githash}"
;;
short)
echo "${major}.${minor}.${patch}"
;;
xcode)
# CFBundleShortVersionString: the "short name" used in the App
# Store. eg. 0.92.98
echo "VERSION_NAME = ${major}.${minor}.${patch}"
# CFBundleVersion: the build number. Needs to be 3 numeric
# sections that increment for each release according to SemVer
# rules.
#
# We start counting at 100 because we submitted using raw
# build numbers before, and Apple doesn't let you start over.
# e.g. 0.98.3 -> 100.98.3
echo "VERSION_ID = $((major + 100)).${minor}.${patch}"
;;
-*)
# Missing change count, add one.
suffix="-0${suffix}"
;;
*)
echo "Unexpected version suffix" >&2
exit 1
esac
echo "${semver}${suffix}"
}
short() {
ver="$(long)"
case "$ver" in
*-*-*)
echo "${ver%-*}"
;;
*-*)
echo "$ver"
;;
*)
echo "Long version in invalid format" >&2
exit 1
;;
esac
}
xcode() {
ver=$(short | sed -e 's/-/./')
major=$(echo "$ver" | cut -f1 -d.)
minor=$(echo "$ver" | cut -f2 -d.)
patch=$(echo "$ver" | cut -f3 -d.)
changecount=$(echo "$ver" | cut -f4 -d.)
# Apple version numbers must be major.minor.patch. We have 4 fields
# because we need major.minor.patch for go module compatibility, and
# changecount for automatic version numbering of unstable builds. To
# resolve this, for Apple builds, we combine changecount into patch:
patch=$((patch*10000 + changecount))
# CFBundleShortVersionString: the "short name" used in the App Store.
# eg. 0.92.98
echo "VERSION_NAME = $major.$minor.$patch"
# CFBundleVersion: the build number. Needs to be 3 numeric sections
# that increment for each release according to SemVer rules.
#
# We start counting at 100 because we submitted using raw build
# numbers before, and Apple doesn't let you start over.
# e.g. 0.98.3-123 -> 100.98.3123
major=$((major + 100))
echo "VERSION_ID = $major.$minor.$patch"
}
case "$mode" in
long)
long
;;
short)
short
;;
xcode)
xcode
;;
esac

View File

@@ -15,42 +15,60 @@ func xcode(short, long string) string {
return fmt.Sprintf("VERSION_NAME = %s\nVERSION_ID = %s", short, long)
}
func mkversion(t *testing.T, mode, in string) string {
func mkversion(t *testing.T, mode, in string) (string, bool) {
t.Helper()
bs, err := exec.Command("./mkversion.sh", mode, in).CombinedOutput()
if err != nil {
t.Logf("mkversion.sh output: %s", string(bs))
t.Fatalf("mkversion.sh %s %s: %v", mode, in, err)
return "", false
}
return strings.TrimSpace(string(bs))
return strings.TrimSpace(string(bs)), true
}
func TestMkversion(t *testing.T) {
tests := []struct {
in string
ok bool
long string
short string
xcode string
}{
{"v0.98-abcdef", "0.98.0-0-abcdef", "0.98.0-0", xcode("0.98.0", "100.98.0")},
{"v0.98-123-abcdef", "0.98.0-123-abcdef", "0.98.0-123", xcode("0.98.123", "100.98.123")},
{"v0.99.5-123-abcdef", "0.99.5-123-abcdef", "0.99.5-123", xcode("0.99.50123", "100.99.50123")},
{"v0.99.5-123-abcdef", "0.99.5-123-abcdef", "0.99.5-123", xcode("0.99.50123", "100.99.50123")},
{"v2.3-0-abcdef", "2.3.0-0-abcdef", "2.3.0-0", xcode("2.3.0", "102.3.0")},
{"1.2.3-4-abcdef", "1.2.3-4-abcdef", "1.2.3-4", xcode("1.2.30004", "101.2.30004")},
{"v0.98-abcdef", true, "0.98.0-abcdef", "0.98.0", xcode("0.98.0", "100.98.0")},
{"v0.98.1-abcdef", true, "0.98.1-abcdef", "0.98.1", xcode("0.98.1", "100.98.1")},
{"v1.1.0-37-abcdef", true, "1.1.37-abcdef", "1.1.37", xcode("1.1.37", "101.1.37")},
{"v1.2.9-abcdef", true, "1.2.9-abcdef", "1.2.9", xcode("1.2.9", "101.2.9")},
{"v1.2.9-0-abcdef", true, "1.2.9-abcdef", "1.2.9", xcode("1.2.9", "101.2.9")},
{"v1.15.0-129-abcdef", true, "1.15.129-abcdef", "1.15.129", xcode("1.15.129", "101.15.129")},
{"v0.98-123-abcdef", true, "0.0.0-abcdef", "0.0.0", xcode("0.0.0", "100.0.0")},
{"v1.0.0-37-abcdef", true, "0.0.0-abcdef", "0.0.0", xcode("0.0.0", "100.0.0")},
{"v0.99.5-0-abcdef", false, "", "", ""}, // unstable, patch not allowed
{"v0.99.5-123-abcdef", false, "", "", ""}, // unstable, patch not allowed
{"v1-abcdef", false, "", "", ""}, // bad semver
{"v1.0", false, "", "", ""}, // missing suffix
}
for _, test := range tests {
gotlong := mkversion(t, "long", test.in)
gotshort := mkversion(t, "short", test.in)
gotxcode := mkversion(t, "xcode", test.in)
if gotlong != test.long {
gotlong, longOK := mkversion(t, "long", test.in)
if longOK != test.ok {
t.Errorf("mkversion.sh long %q ok=%v, want %v", test.in, longOK, test.ok)
}
gotshort, shortOK := mkversion(t, "short", test.in)
if shortOK != test.ok {
t.Errorf("mkversion.sh short %q ok=%v, want %v", test.in, shortOK, test.ok)
}
gotxcode, xcodeOK := mkversion(t, "xcode", test.in)
if xcodeOK != test.ok {
t.Errorf("mkversion.sh xcode %q ok=%v, want %v", test.in, xcodeOK, test.ok)
}
if longOK && gotlong != test.long {
t.Errorf("mkversion.sh long %q: got %q, want %q", test.in, gotlong, test.long)
}
if gotshort != test.short {
if shortOK && gotshort != test.short {
t.Errorf("mkversion.sh short %q: got %q, want %q", test.in, gotshort, test.short)
}
if gotxcode != test.xcode {
if xcodeOK && gotxcode != test.xcode {
t.Errorf("mkversion.sh xcode %q: got %q, want %q", test.in, gotxcode, test.xcode)
}
}

View File

@@ -7,5 +7,5 @@
// Package version provides the version that the binary was built at.
package version
const LONG = "date.20200727"
const LONG = "date.20200806"
const SHORT = LONG

View File

@@ -349,7 +349,10 @@ func omitDropLogging(p *packet.ParsedPacket, dir direction) bool {
if string(dst) == ipv6AllMLDv2CapableRouters {
return true
}
panic(fmt.Sprintf("Got proto=%2x; src=%x dst=%x", int(p.IPProto), src, dst))
// Actually, just catch all multicast.
if dst[0] == 0xff {
return true
}
}
}
return false

View File

@@ -343,6 +343,12 @@ func TestOmitDropLogging(t *testing.T) {
dir: out,
want: true,
},
{
name: "v6_udp_multicast",
pkt: parseHexPkt(t, "60 00 00 00 00 00 11 00 fe800000000000007dc6bc04499262a3 ff120000000000000000000000008384"),
dir: out,
want: true,
},
}
for _, tt := range tests {

View File

@@ -125,19 +125,12 @@ type Conn struct {
packetListener nettype.PacketListener
// ============================================================
mu sync.Mutex // guards all following fields
// canCreateEPUnlocked tracks at one place whether mu is
// already held. It's then checked in CreateEndpoint to avoid
// double-locking mu and thus deadlocking. mu should be held
// while setting this; but can be read without mu held.
// TODO(bradfitz): delete this shameful hack; refactor the one use
canCreateEPUnlocked syncs.AtomicBool
mu sync.Mutex // guards all following fields; see userspaceEngine lock ordering rules
muCond *sync.Cond
started bool // Start was called
closed bool // Close was called
endpointsUpdateWaiter *sync.Cond
endpointsUpdateActive bool
wantEndpointsUpdate string // true if non-empty; string is reason
lastEndpoints []string
@@ -289,8 +282,10 @@ type Options struct {
// sole user just doesn't need or want it called on every
// packet, just every minute or two for Wireguard timeouts,
// and 10 seconds seems like a good trade-off between often
// enough and not too often.) The provided func likely calls
// Conn.CreateEndpoint, which acquires Conn.mu.
// 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 should
// not hold
NoteRecvActivity func(tailcfg.DiscoKey)
}
@@ -323,7 +318,7 @@ func newConn() *Conn {
sharedDiscoKey: make(map[tailcfg.DiscoKey]*[32]byte),
discoOfAddr: make(map[netaddr.IPPort]tailcfg.DiscoKey),
}
c.endpointsUpdateWaiter = sync.NewCond(&c.mu)
c.muCond = sync.NewCond(&c.mu)
return c
}
@@ -395,7 +390,7 @@ func (c *Conn) updateEndpoints(why string) {
go c.updateEndpoints(why)
} else {
c.endpointsUpdateActive = false
c.endpointsUpdateWaiter.Broadcast()
c.muCond.Broadcast()
}
}()
@@ -666,6 +661,17 @@ func (c *Conn) determineEndpoints(ctx context.Context) (ipPorts []string, reason
if nr.GlobalV4 != "" {
addAddr(nr.GlobalV4, "stun")
// If they're behind a hard NAT and are using a fixed
// port locally, assume they might've added a static
// port mapping on their router to the same explicit
// port that tailscaled is running with. Worst case
// it's an invalid candidate mapping.
if nr.MappingVariesByDestIP.EqualBool(true) && c.pconnPort != 0 {
if ip, _, err := net.SplitHostPort(nr.GlobalV4); err == nil {
addAddr(net.JoinHostPort(ip, strconv.Itoa(int(c.pconnPort))), "port_in")
}
}
}
if nr.GlobalV6 != "" {
addAddr(nr.GlobalV6, "stun")
@@ -1079,6 +1085,7 @@ func (c *Conn) derpWriteChanOfAddr(addr netaddr.IPPort, peer key.Public) chan<-
go func() {
dc.Connect(ctx)
close(c.derpStarted)
c.muCond.Broadcast()
}()
}
@@ -1353,13 +1360,15 @@ func wgRecvAddr(e conn.Endpoint, ipp netaddr.IPPort, addr *net.UDPAddr) *net.UDP
return ipp.UDPAddr()
}
// noteRecvActivity calls the magicsock.Conn.noteRecvActivity hook if
// e is a discovery-capable peer.
// noteRecvActivityFromEndpoint calls the c.noteRecvActivity hook if
// e is a discovery-capable peer and this is the first receive activity
// it's got in awhile (in last 10 seconds).
//
// This should be called whenever a packet arrives from e.
func noteRecvActivity(e conn.Endpoint) {
if de, ok := e.(*discoEndpoint); ok {
de.onRecvActivity()
func (c *Conn) noteRecvActivityFromEndpoint(e conn.Endpoint) {
de, ok := e.(*discoEndpoint)
if ok && c.noteRecvActivity != nil && de.isFirstRecvActivityInAwhile() {
c.noteRecvActivity(de.discoKey)
}
}
@@ -1370,7 +1379,7 @@ Top:
c.bufferedIPv4From = netaddr.IPPort{}
addr = from.UDPAddr()
ep := c.findEndpoint(from, addr)
noteRecvActivity(ep)
c.noteRecvActivityFromEndpoint(ep)
return copy(b, c.bufferedIPv4Packet), ep, wgRecvAddr(ep, from, addr), nil
}
@@ -1482,7 +1491,7 @@ Top:
ep = c.findEndpoint(ipp, addr)
}
if !didNoteRecvActivity {
noteRecvActivity(ep)
c.noteRecvActivityFromEndpoint(ep)
}
return n, ep, wgRecvAddr(ep, ipp, addr), nil
}
@@ -1510,7 +1519,7 @@ func (c *Conn) ReceiveIPv6(b []byte) (int, conn.Endpoint, *net.UDPAddr, error) {
}
ep := c.findEndpoint(ipp, addr)
noteRecvActivity(ep)
c.noteRecvActivityFromEndpoint(ep)
return n, ep, wgRecvAddr(ep, ipp, addr), nil
}
}
@@ -1608,8 +1617,9 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) bool {
return false
}
de, ok := c.endpointOfDisco[sender]
if !ok {
needsRecvActivityCall := false
de, endpointFound0 := c.endpointOfDisco[sender]
if !endpointFound0 {
// 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
@@ -1621,20 +1631,37 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) bool {
c.logf("magicsock: [unexpected] have node without endpoint, without c.noteRecvActivity hook")
return false
}
// noteRecvActivity calls back into CreateEndpoint, which we can't easily control,
// and CreateEndpoint expects to be called with c.mu held, but we hold it here, and
// it's too invasive for now to release it here and recheck invariants. So instead,
// use this unfortunate hack: set canCreateEPUnlocked which CreateEndpoint then
// checks to conditionally acquire the mutex. I'm so sorry.
c.canCreateEPUnlocked.Set(true)
needsRecvActivityCall = true
} else {
needsRecvActivityCall = de.isFirstRecvActivityInAwhile()
}
if needsRecvActivityCall && c.noteRecvActivity != nil {
// 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
// Conn.mu.
c.mu.Unlock()
c.noteRecvActivity(sender)
c.canCreateEPUnlocked.Set(false)
c.mu.Lock() // re-acquire
// Now, recheck invariants that might've changed while we'd
// released the lock, which isn't much:
if c.closed || c.privateKey.IsZero() {
return true
}
de, ok = c.endpointOfDisco[sender]
if !ok {
if _, ok := c.nodeOfDisco[sender]; !ok {
// They just disappeared while we'd released the lock.
return false
}
c.logf("magicsock: [unexpected] lazy endpoint not created for %v, %v", peerNode.Key.ShortString(), sender.ShortString())
return false
}
c.logf("magicsock: lazy endpoint created via disco message for %v, %v", peerNode.Key.ShortString(), sender.ShortString())
if !endpointFound0 {
c.logf("magicsock: lazy endpoint created via disco message for %v, %v", peerNode.Key.ShortString(), sender.ShortString())
}
}
// First, do we even know (and thus care) about this sender? If not,
@@ -1825,7 +1852,9 @@ func (c *Conn) SetPrivateKey(privateKey wgcfg.PrivateKey) error {
if oldKey.IsZero() {
c.everHadKey = true
c.logf("magicsock: SetPrivateKey called (init)")
go c.ReSTUN("set-private-key")
if c.started {
go c.ReSTUN("set-private-key")
}
} else if newKey.IsZero() {
c.logf("magicsock: SetPrivateKey called (zeroed)")
c.closeAllDerpLocked("zero-private-key")
@@ -2087,6 +2116,21 @@ func (c *Conn) Close() error {
c.pconn6.Close()
}
err := c.pconn4.Close()
// Wait on goroutines updating right at the end, once everything is
// already closed. We want everything else in the Conn to be
// consistently in the closed state before we release mu to wait
// on the endpoint updater & derphttp.Connect.
for c.goroutinesRunningLocked() {
c.muCond.Wait()
}
return err
}
func (c *Conn) goroutinesRunningLocked() bool {
if c.endpointsUpdateActive {
return true
}
// The goroutine running dc.Connect in derpWriteChanOfAddr may linger
// and appear to leak, as observed in https://github.com/tailscale/tailscale/issues/554.
// This is despite the underlying context being cancelled by connCtxCancel above.
@@ -2096,16 +2140,14 @@ func (c *Conn) Close() error {
// on the first run, it sets firstDerp := true and spawns the aforementioned goroutine.
// To detect this, we check activeDerp, which is initialized to non-nil on the first run.
if c.activeDerp != nil {
<-c.derpStarted
select {
case <-c.derpStarted:
break
default:
return true
}
}
// Wait on endpoints updating right at the end, once everything is
// already closed. We want everything else in the Conn to be
// consistently in the closed state before we release mu to wait
// on the endpoint updater.
for c.endpointsUpdateActive {
c.endpointsUpdateWaiter.Wait()
}
return err
return false
}
func maxIdleBeforeSTUNShutdown() time.Duration {
@@ -2623,6 +2665,9 @@ func (c *Conn) CreateBind(uint16) (conn.Bind, uint16, error) {
//
func (c *Conn) CreateEndpoint(pubKey [32]byte, addrs string) (conn.Endpoint, error) {
c.mu.Lock()
defer c.mu.Unlock()
pk := key.Public(pubKey)
c.logf("magicsock: CreateEndpoint: key=%s: %s", pk.ShortString(), derpStr(addrs))
@@ -2632,10 +2677,6 @@ func (c *Conn) CreateEndpoint(pubKey [32]byte, addrs string) (conn.Endpoint, err
if err != nil {
return nil, fmt.Errorf("magicsock: invalid discokey endpoint %q for %v: %w", addrs, pk.ShortString(), err)
}
if !c.canCreateEPUnlocked.Get() { // sorry
c.mu.Lock()
defer c.mu.Unlock()
}
de := &discoEndpoint{
c: c,
publicKey: tailcfg.NodeKey(pk), // peer public key (for WireGuard + DERP)
@@ -2645,17 +2686,6 @@ func (c *Conn) CreateEndpoint(pubKey [32]byte, addrs string) (conn.Endpoint, err
sentPing: map[stun.TxID]sentPing{},
endpointState: map[netaddr.IPPort]*endpointState{},
}
lastRecvTime := new(int64) // atomic
de.onRecvActivity = func() {
now := time.Now().Unix()
old := atomic.LoadInt64(lastRecvTime)
if old == 0 || old <= now-10 {
atomic.StoreInt64(lastRecvTime, now)
if c.noteRecvActivity != nil {
c.noteRecvActivity(de.discoKey)
}
}
}
de.initFakeUDPAddr()
de.updateFromNode(c.nodeOfDisco[de.discoKey])
c.endpointOfDisco[de.discoKey] = de
@@ -2679,9 +2709,6 @@ func (c *Conn) CreateEndpoint(pubKey [32]byte, addrs string) (conn.Endpoint, err
}
}
c.mu.Lock()
defer c.mu.Unlock()
// If this endpoint is being updated, remember its old set of
// endpoints so we can remove any (from c.addrsByUDP) that are
// not in the new set.
@@ -2930,6 +2957,9 @@ func udpAddrDebugString(ua net.UDPAddr) string {
// discoEndpoint is a wireguard/conn.Endpoint for new-style peers that
// advertise a DiscoKey and participate in active discovery.
type discoEndpoint struct {
// atomically accessed; declared first for alignment reasons
lastRecvUnixAtomic int64
// These fields are initialized once and never modified.
c *Conn
publicKey tailcfg.NodeKey // peer public key (for WireGuard + DERP)
@@ -2938,7 +2968,6 @@ type discoEndpoint struct {
fakeWGAddr netaddr.IPPort // the UDP address we tell wireguard-go we're using
fakeWGAddrStd *net.UDPAddr // the *net.UDPAddr form of fakeWGAddr
wgEndpointHostPort string // string from CreateEndpoint: "<hex-discovery-key>.disco.tailscale:12345"
onRecvActivity func()
// Owned by Conn.mu:
lastPingFrom netaddr.IPPort
@@ -3035,6 +3064,19 @@ func (de *discoEndpoint) initFakeUDPAddr() {
de.fakeWGAddrStd = de.fakeWGAddr.UDPAddr()
}
// isFirstRecvActivityInAwhile notes that receive activity has occured for this
// endpoint and reports whether it's been at least 10 seconds since the last
// receive activity (including having never received from this peer before).
func (de *discoEndpoint) isFirstRecvActivityInAwhile() bool {
now := time.Now().Unix()
old := atomic.LoadInt64(&de.lastRecvUnixAtomic)
if old <= now-10 {
atomic.StoreInt64(&de.lastRecvUnixAtomic, now)
return true
}
return false
}
// String exists purely so wireguard-go internals can log.Printf("%v")
// its internal conn.Endpoints and we don't end up with data races
// from fmt (via log) reading mutex fields and such.

View File

@@ -10,16 +10,19 @@ import (
crand "crypto/rand"
"crypto/tls"
"encoding/binary"
"encoding/json"
"fmt"
"io/ioutil"
"net"
"net/http"
"net/http/httptest"
"os"
"strconv"
"strings"
"sync"
"testing"
"time"
"unsafe"
"github.com/google/go-cmp/cmp"
"github.com/tailscale/wireguard-go/device"
@@ -807,13 +810,17 @@ func testActiveDiscovery(t *testing.T, d *devices) {
// from DERP.
mustDirect := func(m1, m2 *magicStack) {
lastLog := time.Now().Add(-time.Minute)
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(10 * time.Millisecond) {
pst := m1.Status().Peer[m2.Public()]
if pst.CurAddr != "" {
logf("direct link %s->%s found with addr %s", m1, m2, pst.CurAddr)
return
}
logf("no direct path %s->%s yet, addrs %v", m1, m2, pst.Addrs)
if now := time.Now(); now.Sub(lastLog) > time.Second {
logf("no direct path %s->%s yet, addrs %v", m1, m2, pst.Addrs)
lastLog = now
}
}
t.Errorf("magicsock did not find a direct path from %s to %s", m1, m2)
}
@@ -923,7 +930,7 @@ func testTwoDevicePing(t *testing.T, d *devices) {
})
// TODO: Remove this once the following tests are reliable.
if os.Getenv("RUN_CURSED_TESTS") == "" {
if run, _ := strconv.ParseBool(os.Getenv("RUN_CURSED_TESTS")); !run {
t.Skip("skipping following tests because RUN_CURSED_TESTS is not set.")
}
@@ -1021,10 +1028,8 @@ func testTwoDevicePing(t *testing.T, d *devices) {
defer setT(outerT)
defer func() {
if t.Failed() || true {
uapi1, _ := cfgs[0].ToUAPI()
logf("cfg0: %v", uapi1)
uapi2, _ := cfgs[1].ToUAPI()
logf("cfg1: %v", uapi2)
logf("cfg0: %v", stringifyConfig(cfgs[0]))
logf("cfg1: %v", stringifyConfig(cfgs[1]))
}
}()
pingSeq(t, 20, 0, true)
@@ -1310,3 +1315,25 @@ func TestDiscoStringLogRace(t *testing.T) {
}()
wg.Wait()
}
func stringifyConfig(cfg wgcfg.Config) string {
j, err := json.Marshal(cfg)
if err != nil {
panic(err)
}
return string(j)
}
func TestDiscoEndpointAlignment(t *testing.T) {
var de discoEndpoint
off := unsafe.Offsetof(de.lastRecvUnixAtomic)
if off%8 != 0 {
t.Fatalf("lastRecvUnixAtomic is not 8-byte aligned")
}
if !de.isFirstRecvActivityInAwhile() { // verify this doesn't panic on 32-bit
t.Error("expected true")
}
if de.isFirstRecvActivityInAwhile() {
t.Error("expected false on second call")
}
}

View File

@@ -79,7 +79,7 @@ func (c *nlConn) Receive() (message, error) {
case unix.RTM_NEWADDR, unix.RTM_DELADDR:
var rmsg rtnetlink.AddressMessage
if err := rmsg.UnmarshalBinary(msg.Data); err != nil {
c.logf("failed to parse type 0x%x: %v", msg.Header.Type, err)
c.logf("failed to parse type %v: %v", msg.Header.Type, err)
return unspecifiedMessage{}, nil
}
return &newAddrMessage{
@@ -99,8 +99,18 @@ func (c *nlConn) Receive() (message, error) {
Dst: netaddrIP(rmsg.Attributes.Dst),
Gateway: netaddrIP(rmsg.Attributes.Gateway),
}, nil
case unix.RTM_DELROUTE:
var rmsg rtnetlink.RouteMessage
if err := rmsg.UnmarshalBinary(msg.Data); err != nil {
c.logf("RTM_DELROUTE: failed to parse: %v", err)
return unspecifiedMessage{}, nil
}
// Just log it for now, but don't bubble it up.
// (Debugging https://github.com/tailscale/tailscale/issues/643)
c.logf("RTM_DELROUTE: %+v", rmsg)
return unspecifiedMessage{}, nil
default:
c.logf("unhandled netlink msg type 0x%x: %+v, %q", msg.Header.Type, msg.Header, msg.Data)
c.logf("unhandled netlink msg type %+v, %q", msg.Header, msg.Data)
return unspecifiedMessage{}, nil
}
}

View File

@@ -18,6 +18,7 @@ import (
ole "github.com/go-ole/go-ole"
winipcfg "github.com/tailscale/winipcfg-go"
"github.com/tailscale/wireguard-go/conn"
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
"golang.org/x/sys/windows"
@@ -58,11 +59,15 @@ func bindSocketRoute(family winipcfg.AddressFamily, device *device.Device, ourLu
}
*lastLuid = luid
if false {
bind, ok := device.Bind().(conn.BindSocketToInterface)
if !ok {
return fmt.Errorf("unexpected device.Bind type %T", device.Bind())
}
// TODO(apenwarr): doesn't work with magic socket yet.
if family == winipcfg.AF_INET {
return device.BindSocketToInterface4(index, false)
return bind.BindSocketToInterface4(index, false)
} else if family == winipcfg.AF_INET6 {
return device.BindSocketToInterface6(index, false)
return bind.BindSocketToInterface6(index, false)
}
} else {
log.Printf("WARNING: skipping windows socket binding.\n")
@@ -116,6 +121,13 @@ func monitorDefaultRoutes(device *device.Device, autoMTU bool, tun *tun.NativeTu
return err
}
iface.NlMtu = mtu - 80
// If the TUN device was created with a smaller MTU,
// though, such as 1280, we don't want to go bigger than
// configured. (See the comment on minimalMTU in the
// wgengine package.)
if min, err := tun.MTU(); err == nil && min < int(iface.NlMtu) {
iface.NlMtu = uint32(min)
}
if iface.NlMtu < 576 {
iface.NlMtu = 576
}

View File

@@ -17,20 +17,37 @@ type Map struct {
// nameToIP is a mapping of Tailscale domain names to their IP addresses.
// For example, monitoring.tailscale.us -> 100.64.0.1.
nameToIP map[string]netaddr.IP
// ipToName is the inverse of nameToIP.
ipToName map[netaddr.IP]string
// names are the keys of nameToIP in sorted order.
names []string
}
// NewMap returns a new Map with name to address mapping given by nameToIP.
func NewMap(nameToIP map[string]netaddr.IP) *Map {
names := make([]string, 0, len(nameToIP))
for name := range nameToIP {
func NewMap(initNameToIP map[string]netaddr.IP) *Map {
// TODO(dmytro): we have to allocate names and ipToName, but nameToIP can be avoided.
// It is here because control sends us names not in canonical form. Change this.
names := make([]string, 0, len(initNameToIP))
nameToIP := make(map[string]netaddr.IP, len(initNameToIP))
ipToName := make(map[netaddr.IP]string, len(initNameToIP))
for name, ip := range initNameToIP {
if len(name) == 0 {
// Nothing useful can be done with empty names.
continue
}
if name[len(name)-1] != '.' {
name += "."
}
names = append(names, name)
nameToIP[name] = ip
ipToName[ip] = name
}
sort.Strings(names)
return &Map{
nameToIP: nameToIP,
ipToName: ipToName,
names: names,
}
}

View File

@@ -20,17 +20,17 @@ func TestPretty(t *testing.T) {
{
"single",
NewMap(map[string]netaddr.IP{
"hello.ipn.dev": netaddr.IPv4(100, 101, 102, 103),
"hello.ipn.dev.": netaddr.IPv4(100, 101, 102, 103),
}),
"hello.ipn.dev\t100.101.102.103\n",
"hello.ipn.dev.\t100.101.102.103\n",
},
{
"multiple",
NewMap(map[string]netaddr.IP{
"test1.domain": netaddr.IPv4(100, 101, 102, 103),
"test2.sub.domain": netaddr.IPv4(100, 99, 9, 1),
"test1.domain.": netaddr.IPv4(100, 101, 102, 103),
"test2.sub.domain.": netaddr.IPv4(100, 99, 9, 1),
}),
"test1.domain\t100.101.102.103\ntest2.sub.domain\t100.99.9.1\n",
"test1.domain.\t100.101.102.103\ntest2.sub.domain.\t100.99.9.1\n",
},
}
@@ -55,75 +55,75 @@ func TestPrettyDiffFrom(t *testing.T) {
"from_empty",
nil,
NewMap(map[string]netaddr.IP{
"test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103),
"test2.ipn.dev": netaddr.IPv4(100, 103, 102, 101),
"test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103),
"test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101),
}),
"+test1.ipn.dev\t100.101.102.103\n+test2.ipn.dev\t100.103.102.101\n",
"+test1.ipn.dev.\t100.101.102.103\n+test2.ipn.dev.\t100.103.102.101\n",
},
{
"equal",
NewMap(map[string]netaddr.IP{
"test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103),
"test2.ipn.dev": netaddr.IPv4(100, 103, 102, 101),
"test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103),
"test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101),
}),
NewMap(map[string]netaddr.IP{
"test2.ipn.dev": netaddr.IPv4(100, 103, 102, 101),
"test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103),
"test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101),
"test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103),
}),
"",
},
{
"changed_ip",
NewMap(map[string]netaddr.IP{
"test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103),
"test2.ipn.dev": netaddr.IPv4(100, 103, 102, 101),
"test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103),
"test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101),
}),
NewMap(map[string]netaddr.IP{
"test2.ipn.dev": netaddr.IPv4(100, 104, 102, 101),
"test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103),
"test2.ipn.dev.": netaddr.IPv4(100, 104, 102, 101),
"test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103),
}),
"-test2.ipn.dev\t100.103.102.101\n+test2.ipn.dev\t100.104.102.101\n",
"-test2.ipn.dev.\t100.103.102.101\n+test2.ipn.dev.\t100.104.102.101\n",
},
{
"new_domain",
NewMap(map[string]netaddr.IP{
"test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103),
"test2.ipn.dev": netaddr.IPv4(100, 103, 102, 101),
"test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103),
"test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101),
}),
NewMap(map[string]netaddr.IP{
"test3.ipn.dev": netaddr.IPv4(100, 105, 106, 107),
"test2.ipn.dev": netaddr.IPv4(100, 103, 102, 101),
"test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103),
"test3.ipn.dev.": netaddr.IPv4(100, 105, 106, 107),
"test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101),
"test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103),
}),
"+test3.ipn.dev\t100.105.106.107\n",
"+test3.ipn.dev.\t100.105.106.107\n",
},
{
"gone_domain",
NewMap(map[string]netaddr.IP{
"test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103),
"test2.ipn.dev": netaddr.IPv4(100, 103, 102, 101),
"test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103),
"test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101),
}),
NewMap(map[string]netaddr.IP{
"test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103),
"test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103),
}),
"-test2.ipn.dev\t100.103.102.101\n",
"-test2.ipn.dev.\t100.103.102.101\n",
},
{
"mixed",
NewMap(map[string]netaddr.IP{
"test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103),
"test4.ipn.dev": netaddr.IPv4(100, 107, 106, 105),
"test5.ipn.dev": netaddr.IPv4(100, 64, 1, 1),
"test2.ipn.dev": netaddr.IPv4(100, 103, 102, 101),
"test1.ipn.dev.": netaddr.IPv4(100, 101, 102, 103),
"test4.ipn.dev.": netaddr.IPv4(100, 107, 106, 105),
"test5.ipn.dev.": netaddr.IPv4(100, 64, 1, 1),
"test2.ipn.dev.": netaddr.IPv4(100, 103, 102, 101),
}),
NewMap(map[string]netaddr.IP{
"test2.ipn.dev": netaddr.IPv4(100, 104, 102, 101),
"test1.ipn.dev": netaddr.IPv4(100, 100, 101, 102),
"test3.ipn.dev": netaddr.IPv4(100, 64, 1, 1),
"test2.ipn.dev.": netaddr.IPv4(100, 104, 102, 101),
"test1.ipn.dev.": netaddr.IPv4(100, 100, 101, 102),
"test3.ipn.dev.": netaddr.IPv4(100, 64, 1, 1),
}),
"-test1.ipn.dev\t100.101.102.103\n+test1.ipn.dev\t100.100.101.102\n" +
"-test2.ipn.dev\t100.103.102.101\n+test2.ipn.dev\t100.104.102.101\n" +
"+test3.ipn.dev\t100.64.1.1\n-test4.ipn.dev\t100.107.106.105\n-test5.ipn.dev\t100.64.1.1\n",
"-test1.ipn.dev.\t100.101.102.103\n+test1.ipn.dev.\t100.100.101.102\n" +
"-test2.ipn.dev.\t100.103.102.101\n+test2.ipn.dev.\t100.104.102.101\n" +
"+test3.ipn.dev.\t100.64.1.1\n-test4.ipn.dev.\t100.107.106.105\n-test5.ipn.dev.\t100.64.1.1\n",
},
}

View File

@@ -9,6 +9,7 @@ package tsdns
import (
"bytes"
"context"
"encoding/hex"
"errors"
"sync"
"time"
@@ -84,7 +85,7 @@ type Resolver struct {
dialer netns.Dialer
// mu guards the following fields from being updated while used.
mu sync.RWMutex
mu sync.Mutex
// dnsMap is the map most recently received from the control server.
dnsMap *Map
// nameservers is the list of nameserver addresses that should be used
@@ -94,15 +95,15 @@ type Resolver struct {
}
// NewResolver constructs a resolver associated with the given root domain.
// The root domain must be in canonical form (with a trailing period).
func NewResolver(logf logger.Logf, rootDomain string) *Resolver {
r := &Resolver{
logf: logger.WithPrefix(logf, "tsdns: "),
queue: make(chan Packet, queueSize),
responses: make(chan Packet),
errors: make(chan error),
closed: make(chan struct{}),
// Conform to the name format dnsmessage uses (trailing period, bytes).
rootDomain: []byte(rootDomain + "."),
logf: logger.WithPrefix(logf, "tsdns: "),
queue: make(chan Packet, queueSize),
responses: make(chan Packet),
errors: make(chan error),
closed: make(chan struct{}),
rootDomain: []byte(rootDomain),
dialer: netns.NewDialer(),
}
@@ -173,22 +174,40 @@ func (r *Resolver) NextResponse() (Packet, error) {
}
// Resolve maps a given domain name to the IP address of the host that owns it.
// The domain name must not have a trailing period.
// The domain name must be in canonical form (with a trailing period).
func (r *Resolver) Resolve(domain string) (netaddr.IP, dns.RCode, error) {
r.mu.RLock()
if r.dnsMap == nil {
r.mu.RUnlock()
r.mu.Lock()
dnsMap := r.dnsMap
r.mu.Unlock()
if dnsMap == nil {
return netaddr.IP{}, dns.RCodeServerFailure, errMapNotSet
}
addr, found := r.dnsMap.nameToIP[domain]
r.mu.RUnlock()
addr, found := dnsMap.nameToIP[domain]
if !found {
return netaddr.IP{}, dns.RCodeNameError, nil
}
return addr, dns.RCodeSuccess, nil
}
// ResolveReverse returns the unique domain name that maps to the given address.
// The returned domain name is in canonical form (with a trailing period).
func (r *Resolver) ResolveReverse(ip netaddr.IP) (string, dns.RCode, error) {
r.mu.Lock()
dnsMap := r.dnsMap
r.mu.Unlock()
if dnsMap == nil {
return "", dns.RCodeServerFailure, errMapNotSet
}
name, found := dnsMap.ipToName[ip]
if !found {
return "", dns.RCodeNameError, nil
}
return name, dns.RCodeSuccess, nil
}
func (r *Resolver) poll() {
defer r.pollGroup.Done()
@@ -253,9 +272,9 @@ func (r *Resolver) queryServer(ctx context.Context, server string, query []byte)
// delegate forwards the query to all upstream nameservers and returns the first response.
func (r *Resolver) delegate(query []byte) ([]byte, error) {
r.mu.RLock()
r.mu.Lock()
nameservers := r.nameservers
r.mu.RUnlock()
r.mu.Unlock()
if len(nameservers) == 0 {
return nil, errNoNameservers
@@ -301,8 +320,10 @@ func (r *Resolver) delegate(query []byte) ([]byte, error) {
type response struct {
Header dns.Header
Question dns.Question
Name string
IP netaddr.IP
// Name is the response to a PTR query.
Name string
// IP is the response to an A, AAAA, or ANY query.
IP netaddr.IP
}
// parseQuery parses the query in given packet into a response struct.
@@ -359,6 +380,25 @@ func marshalAAAARecord(name dns.Name, ip netaddr.IP, builder *dns.Builder) error
return builder.AAAAResource(answerHeader, answer)
}
// marshalPTRRecord serializes a PTR record into an active builder.
// The caller may continue using the builder following the call.
func marshalPTRRecord(queryName dns.Name, name string, builder *dns.Builder) error {
var answer dns.PTRResource
var err error
answerHeader := dns.ResourceHeader{
Name: queryName,
Type: dns.TypePTR,
Class: dns.ClassINET,
TTL: uint32(defaultTTL / time.Second),
}
answer.PTR, err = dns.NewName(name)
if err != nil {
return err
}
return builder.PTRResource(answerHeader, answer)
}
// marshalResponse serializes the DNS response into a new buffer.
func marshalResponse(resp *response) ([]byte, error) {
resp.Header.Response = true
@@ -389,10 +429,15 @@ func marshalResponse(resp *response) ([]byte, error) {
return nil, err
}
if resp.IP.Is4() {
err = marshalARecord(resp.Question.Name, resp.IP, &builder)
} else {
err = marshalAAAARecord(resp.Question.Name, resp.IP, &builder)
switch resp.Question.Type {
case dns.TypeA, dns.TypeAAAA, dns.TypeALL:
if resp.IP.Is4() {
err = marshalARecord(resp.Question.Name, resp.IP, &builder)
} else {
err = marshalAAAARecord(resp.Question.Name, resp.IP, &builder)
}
case dns.TypePTR:
err = marshalPTRRecord(resp.Question.Name, resp.Name, &builder)
}
if err != nil {
return nil, err
@@ -401,6 +446,120 @@ func marshalResponse(resp *response) ([]byte, error) {
return builder.Finish()
}
var (
rdnsv4Suffix = []byte(".in-addr.arpa.")
rdnsv6Suffix = []byte(".ip6.arpa.")
)
// ptrNameToIPv4 transforms a PTR name representing an IPv4 address to said address.
// Such names are IPv4 labels in reverse order followed by .in-addr.arpa.
// For example,
// 4.3.2.1.in-addr.arpa
// is transformed to
// 1.2.3.4
func rdnsNameToIPv4(name []byte) (ip netaddr.IP, ok bool) {
name = bytes.TrimSuffix(name, rdnsv4Suffix)
ip, err := netaddr.ParseIP(string(name))
if err != nil {
return netaddr.IP{}, false
}
if !ip.Is4() {
return netaddr.IP{}, false
}
b := ip.As4()
return netaddr.IPv4(b[3], b[2], b[1], b[0]), true
}
// ptrNameToIPv6 transforms a PTR name representing an IPv6 address to said address.
// Such names are dot-separated nibbles in reverse order followed by .ip6.arpa.
// For example,
// b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.
// is transformed to
// 2001:db8::567:89ab
func rdnsNameToIPv6(name []byte) (ip netaddr.IP, ok bool) {
var b [32]byte
var ipb [16]byte
name = bytes.TrimSuffix(name, rdnsv6Suffix)
// 32 nibbles and 31 dots between them.
if len(name) != 63 {
return netaddr.IP{}, false
}
// Dots and hex digits alternate.
prevDot := true
// i ranges over name backward; j ranges over b forward.
for i, j := len(name)-1, 0; i >= 0; i-- {
thisDot := (name[i] == '.')
if prevDot == thisDot {
return netaddr.IP{}, false
}
prevDot = thisDot
if !thisDot {
// This is safe assuming alternation.
// We do not check that non-dots are hex digits: hex.Decode below will do that.
b[j] = name[i]
j++
}
}
_, err := hex.Decode(ipb[:], b[:])
if err != nil {
return netaddr.IP{}, false
}
return netaddr.IPFrom16(ipb), true
}
// respondReverse returns a DNS response to a PTR query.
// It is assumed that resp.Question is populated by respond before this is called.
func (r *Resolver) respondReverse(query []byte, resp *response) ([]byte, error) {
name := resp.Question.Name.Data[:resp.Question.Name.Length]
shouldDelegate := false
var ip netaddr.IP
var ok bool
var err error
switch {
case bytes.HasSuffix(name, rdnsv4Suffix):
ip, ok = rdnsNameToIPv4(name)
case bytes.HasSuffix(name, rdnsv6Suffix):
ip, ok = rdnsNameToIPv6(name)
default:
shouldDelegate = true
}
// It is more likely that we failed in parsing the name than that it is actually malformed.
// To avoid frustrating users, just log and delegate.
if !ok {
// Without this conversion, escape analysis rules that resp escapes.
r.logf("parsing rdns: malformed name: %s", resp.Question.Name.String())
shouldDelegate = true
}
if !shouldDelegate {
resp.Name, resp.Header.RCode, err = r.ResolveReverse(ip)
if err != nil {
r.logf("resolving rdns: %v", ip, err)
}
shouldDelegate = (resp.Header.RCode == dns.RCodeNameError)
}
if shouldDelegate {
out, err := r.delegate(query)
if err != nil {
r.logf("delegating rdns: %v", err)
resp.Header.RCode = dns.RCodeServerFailure
return marshalResponse(resp)
}
return out, nil
}
return marshalResponse(resp)
}
// respond returns a DNS response to query.
func (r *Resolver) respond(query []byte) ([]byte, error) {
resp := new(response)
@@ -416,7 +575,14 @@ func (r *Resolver) respond(query []byte) ([]byte, error) {
return marshalResponse(resp)
}
// Delegate only when not a subdomain of rootDomain.
// Always try to handle reverse lookups; delegate inside when not found.
// This way, queries for exitent nodes do not leak,
// but we behave gracefully if non-Tailscale nodes exist in CGNATRange.
if resp.Question.Type == dns.TypePTR {
return r.respondReverse(query, resp)
}
// Delegate forward lookups when not a subdomain of rootDomain.
// We do this on bytes because Name.String() allocates.
rawName := resp.Question.Name.Data[:resp.Question.Name.Length]
if !bytes.HasSuffix(rawName, r.rootDomain) {
@@ -431,11 +597,8 @@ func (r *Resolver) respond(query []byte) ([]byte, error) {
switch resp.Question.Type {
case dns.TypeA, dns.TypeAAAA, dns.TypeALL:
domain := resp.Question.Name.String()
// Strip off the trailing period.
// This is safe: Name is guaranteed to have a trailing period by construction.
domain = domain[:len(domain)-1]
resp.IP, resp.Header.RCode, err = r.Resolve(domain)
name := resp.Question.Name.String()
resp.IP, resp.Header.RCode, err = r.Resolve(name)
default:
resp.Header.RCode = dns.RCodeNotImplemented
err = errNotImplemented

View File

@@ -22,12 +22,12 @@ var testipv6 = netaddr.IPv6Raw([16]byte{
0x0c, 0x0d, 0x0e, 0x0f,
})
var dnsMap = &Map{
nameToIP: map[string]netaddr.IP{
var dnsMap = NewMap(
map[string]netaddr.IP{
"test1.ipn.dev": testipv4,
"test2.ipn.dev": testipv6,
},
}
)
func dnspacket(domain string, tp dns.Type) []byte {
var dnsHeader dns.Header
@@ -97,6 +97,86 @@ func syncRespond(r *Resolver, query []byte) ([]byte, error) {
return resp.Payload, err
}
func mustIP(str string) netaddr.IP {
ip, err := netaddr.ParseIP(str)
if err != nil {
panic(err)
}
return ip
}
func TestRDNSNameToIPv4(t *testing.T) {
tests := []struct {
name string
input string
wantIP netaddr.IP
wantOK bool
}{
{"valid", "4.123.24.1.in-addr.arpa.", netaddr.IPv4(1, 24, 123, 4), true},
{"double_dot", "1..2.3.in-addr.arpa.", netaddr.IP{}, false},
{"overflow", "1.256.3.4.in-addr.arpa.", netaddr.IP{}, false},
{"not_ip", "sub.do.ma.in.in-addr.arpa.", netaddr.IP{}, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
name := []byte(tt.input)
ip, ok := rdnsNameToIPv4(name)
if ok != tt.wantOK {
t.Errorf("ok = %v; want %v", ok, tt.wantOK)
} else if ok && ip != tt.wantIP {
t.Errorf("ip = %v; want %v", ip, tt.wantIP)
}
})
}
}
func TestRDNSNameToIPv6(t *testing.T) {
tests := []struct {
name string
input string
wantIP netaddr.IP
wantOK bool
}{
{
"valid",
"b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.",
mustIP("2001:db8::567:89ab"),
true,
},
{
"double_dot",
"b..9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.",
netaddr.IP{},
false,
},
{
"double_hex",
"b.a.98.0.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.",
netaddr.IP{},
false,
},
{
"not_hex",
"b.a.g.0.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.",
netaddr.IP{},
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
name := []byte(tt.input)
ip, ok := rdnsNameToIPv6(name)
if ok != tt.wantOK {
t.Errorf("ok = %v; want %v", ok, tt.wantOK)
} else if ok && ip != tt.wantIP {
t.Errorf("ip = %v; want %v", ip, tt.wantIP)
}
})
}
}
func TestResolve(t *testing.T) {
r := NewResolver(t.Logf, "ipn.dev")
r.SetMap(dnsMap)
@@ -108,10 +188,10 @@ func TestResolve(t *testing.T) {
ip netaddr.IP
code dns.RCode
}{
{"ipv4", "test1.ipn.dev", testipv4, dns.RCodeSuccess},
{"ipv6", "test2.ipn.dev", testipv6, dns.RCodeSuccess},
{"nxdomain", "test3.ipn.dev", netaddr.IP{}, dns.RCodeNameError},
{"foreign domain", "google.com", netaddr.IP{}, dns.RCodeNameError},
{"ipv4", "test1.ipn.dev.", testipv4, dns.RCodeSuccess},
{"ipv6", "test2.ipn.dev.", testipv6, dns.RCodeSuccess},
{"nxdomain", "test3.ipn.dev.", netaddr.IP{}, dns.RCodeNameError},
{"foreign domain", "google.com.", netaddr.IP{}, dns.RCodeNameError},
}
for _, tt := range tests {
@@ -131,6 +211,38 @@ func TestResolve(t *testing.T) {
}
}
func TestResolveReverse(t *testing.T) {
r := NewResolver(t.Logf, "ipn.dev")
r.SetMap(dnsMap)
r.Start()
tests := []struct {
name string
ip netaddr.IP
want string
code dns.RCode
}{
{"ipv4", testipv4, "test1.ipn.dev.", dns.RCodeSuccess},
{"ipv6", testipv6, "test2.ipn.dev.", dns.RCodeSuccess},
{"nxdomain", netaddr.IPv4(4, 3, 2, 1), "", dns.RCodeNameError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
name, code, err := r.ResolveReverse(tt.ip)
if err != nil {
t.Errorf("err = %v; want nil", err)
}
if code != tt.code {
t.Errorf("code = %v; want %v", code, tt.code)
}
if name != tt.want {
t.Errorf("ip = %v; want %v", name, tt.want)
}
})
}
}
func TestDelegate(t *testing.T) {
dnsHandleFunc("test.site.", resolveToIP(testipv4, testipv6))
dnsHandleFunc("nxdomain.site.", resolveToNXDOMAIN)
@@ -200,7 +312,7 @@ func TestDelegate(t *testing.T) {
}
func TestConcurrentSetMap(t *testing.T) {
r := NewResolver(t.Logf, "ipn.dev")
r := NewResolver(t.Logf, "ipn.dev.")
r.Start()
// This is purely to ensure that Resolve does not race with SetMap.
@@ -218,7 +330,7 @@ func TestConcurrentSetMap(t *testing.T) {
}
func TestConcurrentSetNameservers(t *testing.T) {
r := NewResolver(t.Logf, "ipn.dev")
r := NewResolver(t.Logf, "ipn.dev.")
r.Start()
packet := dnspacket("google.com.", dns.TypeA)
@@ -271,6 +383,26 @@ var validIPv6Response = []byte{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0xb, 0xc, 0xd, 0xe, 0xf,
}
var validPTRResponse = []byte{
0x00, 0x00, // transaction id: 0
0x84, 0x00, // flags: response, authoritative, no error
0x00, 0x01, // one question
0x00, 0x01, // one answer
0x00, 0x00, 0x00, 0x00, // no authority or additional RRs
// Question: 4.3.2.1.in-addr.arpa
0x01, 0x34, 0x01, 0x33, 0x01, 0x32, 0x01, 0x31, 0x07,
0x69, 0x6e, 0x2d, 0x61, 0x64, 0x64, 0x72, 0x04, 0x61, 0x72, 0x70, 0x61, 0x00,
0x00, 0x0c, 0x00, 0x01, // type PTR, class IN
// Answer: 4.3.2.1.in-addr.arpa
0x01, 0x34, 0x01, 0x33, 0x01, 0x32, 0x01, 0x31, 0x07,
0x69, 0x6e, 0x2d, 0x61, 0x64, 0x64, 0x72, 0x04, 0x61, 0x72, 0x70, 0x61, 0x00,
0x00, 0x0c, 0x00, 0x01, // type PTR, class IN
0x00, 0x00, 0x02, 0x58, // TTL: 600
0x00, 0x0f, // length: 15 bytes
// PTR: test1.ipn.dev
0x05, 0x74, 0x65, 0x73, 0x74, 0x31, 0x03, 0x69, 0x70, 0x6e, 0x03, 0x64, 0x65, 0x76, 0x00,
}
var nxdomainResponse = []byte{
0x00, 0x00, // transaction id: 0
0x84, 0x03, // flags: response, authoritative, error: nxdomain
@@ -283,7 +415,7 @@ var nxdomainResponse = []byte{
}
func TestFull(t *testing.T) {
r := NewResolver(t.Logf, "ipn.dev")
r := NewResolver(t.Logf, "ipn.dev.")
r.SetMap(dnsMap)
r.Start()
@@ -295,6 +427,7 @@ func TestFull(t *testing.T) {
}{
{"ipv4", dnspacket("test1.ipn.dev.", dns.TypeA), validIPv4Response},
{"ipv6", dnspacket("test2.ipn.dev.", dns.TypeAAAA), validIPv6Response},
{"ptr", dnspacket("4.3.2.1.in-addr.arpa.", dns.TypePTR), validPTRResponse},
{"error", dnspacket("test3.ipn.dev.", dns.TypeA), nxdomainResponse},
}
@@ -312,34 +445,44 @@ func TestFull(t *testing.T) {
}
func TestAllocs(t *testing.T) {
r := NewResolver(t.Logf, "ipn.dev")
r := NewResolver(t.Logf, "ipn.dev.")
r.SetMap(dnsMap)
r.Start()
// It is seemingly pointless to test allocs in the delegate path,
// as dialer.Dial -> Read -> Write alone comprise 12 allocs.
query := dnspacket("test1.ipn.dev.", dns.TypeA)
tests := []struct {
name string
query []byte
want int
}{
// The only alloc is the slice created by dns.NewBuilder.
{"forward", dnspacket("test1.ipn.dev.", dns.TypeA), 1},
// 3 extra allocs in rdnsNameToIPv4 and one in marshalPTRRecord (dns.NewName).
{"reverse", dnspacket("4.3.2.1.in-addr.arpa.", dns.TypePTR), 5},
}
allocs := testing.AllocsPerRun(100, func() {
syncRespond(r, query)
})
if allocs > 1 {
t.Errorf("allocs = %v; want 1", allocs)
for _, tt := range tests {
allocs := testing.AllocsPerRun(100, func() {
syncRespond(r, tt.query)
})
if int(allocs) > tt.want {
t.Errorf("%s: allocs = %v; want %v", tt.name, allocs, tt.want)
}
}
}
func BenchmarkFull(b *testing.B) {
r := NewResolver(b.Logf, "ipn.dev")
r := NewResolver(b.Logf, "ipn.dev.")
r.SetMap(dnsMap)
r.Start()
// One full packet and one error packet
tests := []struct {
name string
request []byte
}{
{"valid", dnspacket("test1.ipn.dev.", dns.TypeA)},
{"forward", dnspacket("test1.ipn.dev.", dns.TypeA)},
{"reverse", dnspacket("4.3.2.1.in-addr.arpa.", dns.TypePTR)},
{"nxdomain", dnspacket("test3.ipn.dev.", dns.TypeA)},
}

View File

@@ -62,7 +62,7 @@ const (
)
// magicDNSDomain is the parent domain for Tailscale nodes.
const magicDNSDomain = "b.tailscale.net"
const magicDNSDomain = "b.tailscale.net."
// Lazy wireguard-go configuration parameters.
const (
@@ -87,6 +87,7 @@ type userspaceEngine struct {
logf logger.Logf
reqCh chan struct{}
waitCh chan struct{} // chan is closed when first Close call completes; contrast with closing bool
timeNow func() time.Time
tundev *tstun.TUN
wgdev *device.Device
router router.Router
@@ -94,6 +95,8 @@ type userspaceEngine struct {
magicConn *magicsock.Conn
linkMon *monitor.Mon
testMaybeReconfigHook func() // for tests; if non-nil, fires if maybeReconfigWireguardLocked called
// localAddrs is the set of IP addresses assigned to the local
// tunnel interface. It's used to reflect local packets
// incorrectly sent to us.
@@ -116,7 +119,7 @@ type userspaceEngine struct {
pingers map[wgcfg.Key]*pinger // legacy pingers for pre-discovery peers
linkState *interfaces.State
// Lock ordering: wgLock, then mu.
// Lock ordering: magicsock.Conn.mu, wgLock, then mu.
}
// RouterGen is the signature for a function that creates a
@@ -199,6 +202,7 @@ func newUserspaceEngineAdvanced(conf EngineConfig) (_ Engine, reterr error) {
logf := conf.Logf
e := &userspaceEngine{
timeNow: time.Now,
logf: logf,
reqCh: make(chan struct{}, 1),
waitCh: make(chan struct{}),
@@ -401,7 +405,7 @@ func (e *userspaceEngine) isLocalAddr(ip packet.IP) bool {
func (e *userspaceEngine) handleDNS(p *packet.ParsedPacket, t *tstun.TUN) filter.Response {
if p.DstIP == magicDNSIP && p.DstPort == magicDNSPort && p.IPProto == packet.UDP {
request := tsdns.Packet{
Payload: p.Payload(),
Payload: append([]byte(nil), p.Payload()...),
Addr: netaddr.IPPort{IP: p.SrcIP.Netaddr(), Port: p.SrcPort},
}
err := e.resolver.EnqueueRequest(request)
@@ -622,12 +626,12 @@ func (e *userspaceEngine) noteReceiveActivity(dk tailcfg.DiscoKey) {
e.wgLock.Lock()
defer e.wgLock.Unlock()
now := time.Now()
was, ok := e.recvActivityAt[dk]
if !ok {
// Not a trimmable peer we care about tracking. (See isTrimmablePeer)
return
}
now := e.timeNow()
e.recvActivityAt[dk] = now
// If the last activity time jumped a bunch (say, at least
@@ -636,7 +640,7 @@ func (e *userspaceEngine) noteReceiveActivity(dk tailcfg.DiscoKey) {
// lazyPeerIdleThreshold without the divide by 2, but
// maybeReconfigWireguardLocked is cheap enough to call every
// couple minutes (just not on every packet).
if was.IsZero() || now.Sub(was) < -lazyPeerIdleThreshold/2 {
if was.IsZero() || now.Sub(was) > lazyPeerIdleThreshold/2 {
e.maybeReconfigWireguardLocked()
}
}
@@ -677,6 +681,11 @@ func discoKeyFromPeer(p *wgcfg.Peer) tailcfg.DiscoKey {
// e.wgLock must be held.
func (e *userspaceEngine) maybeReconfigWireguardLocked() error {
if hook := e.testMaybeReconfigHook; hook != nil {
hook()
return nil
}
full := e.lastCfgFull
// Compute a minimal config to pass to wireguard-go
@@ -689,7 +698,7 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked() error {
// the past 5 minutes. That's more than WireGuard's key
// rotation time anyway so it's no harm if we remove it
// later if it's been inactive.
activeCutoff := time.Now().Add(-lazyPeerIdleThreshold)
activeCutoff := e.timeNow().Add(-lazyPeerIdleThreshold)
// Not all peers can be trimmed from the network map (see
// isTrimmablePeer). For those are are trimmable, keep track
@@ -765,7 +774,7 @@ func (e *userspaceEngine) updateActivityMapsLocked(trackDisco []tailcfg.DiscoKey
if fn == nil {
// This is the func that gets run on every outgoing packet for tracked IPs:
fn = func() {
now := time.Now().Unix()
now := e.timeNow().Unix()
old := atomic.LoadInt64(timePtr)
// How long's it been since we last sent a packet?
@@ -883,6 +892,11 @@ func (e *userspaceEngine) getStatusCallback() StatusCallback {
// TODO: this function returns an error but it's always nil, and when
// there's actually a problem it just calls log.Fatal. Why?
func (e *userspaceEngine) getStatus() (*Status, error) {
// Grab derpConns before acquiring wgLock to not violate lock ordering;
// the DERPs method acquires magicsock.Conn.mu.
// (See comment in userspaceEngine's declaration.)
derpConns := e.magicConn.DERPs()
e.wgLock.Lock()
defer e.wgLock.Unlock()
@@ -998,7 +1012,7 @@ func (e *userspaceEngine) getStatus() (*Status, error) {
return &Status{
LocalAddrs: append([]string(nil), e.endpoints...),
Peers: peers,
DERPs: e.magicConn.DERPs(),
DERPs: derpConns,
}, nil
}

View File

@@ -0,0 +1,88 @@
// 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 wgengine
import (
"bytes"
"fmt"
"testing"
"time"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/wgengine/tstun"
)
func TestNoteReceiveActivity(t *testing.T) {
now := time.Unix(1, 0)
tick := func(d time.Duration) { now = now.Add(d) }
var logBuf bytes.Buffer
confc := make(chan bool, 1)
gotConf := func() bool {
select {
case <-confc:
return true
default:
return false
}
}
e := &userspaceEngine{
timeNow: func() time.Time { return now },
recvActivityAt: map[tailcfg.DiscoKey]time.Time{},
logf: func(format string, a ...interface{}) {
fmt.Fprintf(&logBuf, format, a...)
},
tundev: new(tstun.TUN),
testMaybeReconfigHook: func() { confc <- true },
}
ra := e.recvActivityAt
dk := tailcfg.DiscoKey(key.NewPrivate().Public())
// Activity on an untracked key should do nothing.
e.noteReceiveActivity(dk)
if len(ra) != 0 {
t.Fatalf("unexpected growth in map: now has %d keys; want 0", len(ra))
}
if logBuf.Len() != 0 {
t.Fatalf("unexpected log write (and thus activity): %s", logBuf.Bytes())
}
// Now track it and expect updates.
ra[dk] = time.Time{}
e.noteReceiveActivity(dk)
if len(ra) != 1 {
t.Fatalf("unexpected growth in map: now has %d keys; want 1", len(ra))
}
if got := ra[dk]; got != now {
t.Fatalf("time in map = %v; want %v", got, now)
}
if !gotConf() {
t.Fatalf("didn't get expected reconfig")
}
// With updates 1 second apart, don't expect a reconfig.
for i := 0; i < 300; i++ {
tick(time.Second)
e.noteReceiveActivity(dk)
if len(ra) != 1 {
t.Fatalf("map len = %d; want 1", len(ra))
}
if got := ra[dk]; got != now {
t.Fatalf("time in map = %v; want %v", got, now)
}
if gotConf() {
t.Fatalf("unexpected reconfig")
}
}
// But if there's a big jump it should get an update.
tick(3 * time.Minute)
e.noteReceiveActivity(dk)
if !gotConf() {
t.Fatalf("expected config")
}
}