Compare commits

...

56 Commits

Author SHA1 Message Date
Brad Fitzpatrick
1d9ab6d484 wgengine/filter: omit logging for all v6 multicast, remove debug panic :(
(cherry picked from commit da3b50ad88)
2020-08-01 19:43:16 +00:00
David Anderson
e98ed6319a Merge branch 'main' into release-branch/1.0 2020-08-01 02:44:40 +00:00
David Anderson
9e26ffecf8 cmd/tailscaled: ignore SIGPIPE.
SIGPIPE can be generated when CLIs disconnect from tailscaled. This
should not terminate the process.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-07-31 19:12:45 -07:00
David Anderson
d64de1ddf7 Revert "cmd/tailscaled: exit gracefully on SIGPIPE"
tailscaled receives a SIGPIPE when CLIs disconnect from it. We shouldn't
shut down in that case.

This reverts commit 43b271cb26.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-07-31 19:12:45 -07:00
David Anderson
358cd3fd92 ipn: fix incorrect change tracking for packet filter.
ORder of operations to trigger a problem:
 - Start an already authed tailscaled, verify you can ping stuff.
 - Run `tailscale up`. Notice you can no longer ping stuff.

The problem is that `tailscale up` stops the IPN state machine before
restarting it, which zeros out the packet filter but _not_ the packet
filter hash. Then, upon restarting IPN, the uncleared hash incorrectly
makes the code conclude that the filter doesn't need updating, and so
we stay with a zero filter (reject everything) for ever.

The fix is simply to update the filterHash correctly in all cases,
so that running -> stopped -> running correctly changes the filter
at every transition.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-07-31 19:12:45 -07:00
Dmytro Shynkevych
28e52a0492 all: dns refactor, add Proxied and PerDomain flags from control (#615)
Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>
2020-07-31 16:27:09 -04:00
Dmytro Shynkevych
43b271cb26 cmd/tailscaled: exit gracefully on SIGPIPE
Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>
2020-07-31 16:02:42 -04:00
Brad Fitzpatrick
3e493e0417 wgengine: fix lazy wireguard config bug on sent packet minute+ later
A comparison operator was backwards.

The bad case went:

* device A send packet to B at t=1s
* B gets added to A's wireguard config
* B gets packet

(5 minutes pass)

* some other activity happens, causing B to expire
  to be removed from A's network map, since it's
  been over 5 minutes since sent or received activity
* device A sends packet to B at t=5m1s
* normally, B would get added back, but the old send
  time was not zero (we sent earlier!) and the time
  comparison was backwards, so we never regenerated
  the wireguard config.

This also refactors the code for legibility and moves constants up
top, with comments.
2020-07-31 12:56:37 -07:00
Brad Fitzpatrick
c253d4f948 net/interfaces: don't try to fork on iOS in likelyHomeRouterIPDarwin
No subprocesses allowed on iOS. Will need to do this differently later.
2020-07-31 10:35:15 -07:00
Dmytro Shynkevych
8c850947db router: split off sandboxed path from router_darwin (#624)
Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>
2020-07-31 01:10:14 -04:00
Brad Fitzpatrick
cb970539a6 wgengine/magicsock: remove TODO comment that's no longer applicable 2020-07-30 21:33:37 -07:00
David Crawshaw
92e9a5ac15 tailscaled.service: use default restart limiting
It appears that systemd has sensible defaults for limiting
crash loops:

	DefaultStartLimitIntervalSec=10s
	DefaultStartLimitBurst=5

Remove our insta-restart configuration so that it works.

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2020-07-31 12:55:07 +10:00
Brad Fitzpatrick
915f65ddae wgengine/magicsock: stop disco activity on IPN stop
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-07-30 14:01:33 -07:00
Brad Fitzpatrick
c180abd7cf wgengine/magicsock: merge errClosed and errConnClosed 2020-07-30 13:59:30 -07:00
Brad Fitzpatrick
7cc8fcb784 wgengine/filter: remove leftover debug knob that staticcheck doesn't like 2020-07-30 11:21:37 -07:00
Brad Fitzpatrick
b4d97d2532 wgengine/filter: fix IPv4 IGMP spam omission, also omit ff02::16 spam
And add tests.

Fixes #618
Updates #402
2020-07-30 11:00:20 -07:00
Brad Fitzpatrick
ff8c8db9d3 cmd/tailscaled: log on shutdown signal 2020-07-30 08:49:17 -07:00
Brad Fitzpatrick
2072dcc127 version: revert the filepath change from earlier commit
f81233524f changed a use of package 'path' to 'filepath'.
Restore it back to 'path', with a comment.

Also, use the os.Executable-based fallback name in the case where the
binary itself doesn't have Go module information. That was overlooked in
the original code.
2020-07-30 08:03:33 -07:00
Brad Fitzpatrick
6013462e9e logpolicy: remove inaccurate comment, conditional tryFixLogStateLocation call
What I was probably actually hitting was exe caching issues where the
binary was updated on a SMB shared drive and I tried to run it with
the GUI exe still open, so Windows blends the two pages together and
causes all sorts of random corruption. I didn't know about that at the time.

Now, just call tryFixLogStateLocation unconditionally. The func itself will
bail out early on non-applicable OSes. (And rearrange it to return even a bit
earlier.)
2020-07-30 07:47:19 -07:00
Avery Pennarun
60c00605d3 ipn/setClientStatus: fix inverted prefsChanged check.
We need to emit Prefs when it *has* changed, not when it hasn't.

Test is added in our e2e test, separately.

Fixes: #620

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2020-07-30 04:52:58 -04:00
Avery Pennarun
f81233524f version/cmdname: s/path/filepath/ and fix version.ReadExe() fallback.
We were using the Go 'path' module, which apparently doesn't handle
backslashes correctly. path/filepath does.

However, the main bug turned out to be that we were not calling .Base()
on the path if version.ReadExe() fails, which it seems to do at least
on Windows 7. As a result, our logfile persistence was not working on
Windows, and logids would be regenerated on every restart.

Affects: #620

Signed-off-by: Avery Pennarun <apenwarr@tailscale.com>
2020-07-30 04:52:20 -04:00
Dmytro Shynkevych
2ce2b63239 router: stop iOS subprocess sandbox violations (#617)
Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>
2020-07-29 21:09:18 -04:00
Dmytro Shynkevych
154d1cde05 router: reload systemd-resolved after changing /etc/resolv.conf (#619)
Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>
2020-07-29 20:57:25 -04:00
Brad Fitzpatrick
cbf71d5eba ipn/ipnserver: fix bug in earlier commit where conn can be stranded
If a connection causes getEngine to transition from broken to fixed,
that connection was getting lost.
2020-07-29 17:46:58 -07:00
Brad Fitzpatrick
b3fc61b132 wgengine: disable wireguard config trimming for now except iOS w/ many peers
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-07-29 16:29:30 -07:00
Brad Fitzpatrick
9ff5b380cb ipn/ipnserver: staticcheck is not wrong
shamecube.gif
2020-07-29 15:15:05 -07:00
Brad Fitzpatrick
4aba86cc03 ipn/ipnserver: make Engine argument a func that tries again for each connection
So a backend in server-an-error state (as used by Windows) can try to
create a new Engine again each time somebody re-connects, relaunching
the GUI app.

(The proper fix is actually fixing Windows issues, but this makes things better
in the short term)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-07-29 14:33:33 -07:00
Brad Fitzpatrick
d55fdd4669 wgengine/magicsock: update, flesh out a TODO 2020-07-29 12:59:25 -07:00
Brad Fitzpatrick
d96d26c22a wgengine/filter: don't spam logs on dropped outgoing IPv6 ICMP or IPv4 IGMP
The OS (tries) to send these but we drop them. No need to worry the
user with spam that we're dropping it.

Fixes #402

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-07-29 08:32:55 -07:00
Dmytro Shynkevych
c7582dc234 ipn: fix netmap change tracking and dns map generation (#609)
Signed-off-by: Dmytro Shynkevych <dmytro@tailscale.com>
2020-07-28 21:47:23 -04:00
Brad Fitzpatrick
3e3c24b8f6 wgengine/packet: add IPVersion field, don't use IPProto to note version
As prep for IPv6 log spam fixes in a future change.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-07-28 16:29:28 -07:00
Brad Fitzpatrick
91d95dafd2 control/controlclient: remove an 'unexpected' log that no longer is
Fixes #611
2020-07-28 15:13:34 -07:00
Brad Fitzpatrick
77cad13c70 portlist: avoid syscall audit violation logspam on Android
If we don't have access, don't try, don't log, don't continue trying.

Fixes #521
2020-07-28 13:21:42 -07:00
Brad Fitzpatrick
84f2320972 go.sum: update 2020-07-28 11:49:56 -07:00
David Anderson
f8e4c75f6b wgengine/magicsock: check slightly less aggressively for connectivity.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-07-28 17:04:48 +00:00
Brad Fitzpatrick
33a748bec1 net/interfaces: fix likelyHomeRouterIP on Android 2020-07-28 09:12:04 -07:00
Brad Fitzpatrick
b77d752623 control/controlclient: populate OSVersion on Windows 2020-07-27 21:46:07 -07:00
Brad Fitzpatrick
cd21ba0a71 tailcfg, control/controlclient: add GoArch, populate OSVersion on Linux 2020-07-27 21:14:28 -07:00
Brad Fitzpatrick
58b721f374 wgengine/magicsock: deflake some tests with an ugly hack
Starting with fe68841dc7, some e2e tests
got flaky. Rather than debug them (they're gnarly), just revert to the old
behavior as far as those tests are concerned. The tests were somehow
using magicsock without a private key and expecting it to do ... something.

My goal with fe68841dc7 was to stop log spam
and unnecessary work I saw on the iOS app when when stopping the app.

Instead, only stop doing that work on any transition from
once-had-a-private-key to no-longer-have-a-private-key. That fixes
what I wanted to fix while still making the mysterious e2e tests
happy.
2020-07-27 16:32:35 -07:00
Brad Fitzpatrick
ec4feaf31c cmd/cloner, tailcfg: fix nil vs len 0 issues, add tests, use for Hostinfo
Also use go:generate and https://golang.org/s/generatedcode header style.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2020-07-27 14:11:41 -07:00
David Anderson
41d0c81859 wgengine/magicsock: make disco subtest name more precise.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-07-27 14:09:54 -07:00
David Anderson
9beea8b314 wgengine/magicsock: remove unnecessary use of context.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-07-27 14:09:54 -07:00
David Anderson
b62341d308 wgengine/magicsock: add docstring.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-07-27 14:09:54 -07:00
David Anderson
9265296b33 wgengine/magicsock: don't deadlock on shutdown if sending blocks.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-07-27 14:09:54 -07:00
David Anderson
0249236cc0 ipn/ipnstate: record assigned Tailscale IPs.
wgengine/magicsock: use ipnstate to find assigned Tailscale IPs.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-07-27 14:09:54 -07:00
David Anderson
c3958898f1 tstest/natlab: be a bit more lenient during test shutdown.
There is a race in natlab where we might start shutdown while natlab is still running
a goroutine or two to deliver packets. This adds a small grace period to try and receive
it before continuing shutdown.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-07-27 14:09:54 -07:00
David Anderson
7578c815be wgengine/magicsock: give pinger a more generous packet timeout.
The first packet to transit may take several seconds to do so, because
setup rates in wgengine may result in the initial WireGuard handshake
init to get dropped. So, we have to wait at least long enough for a
retransmit to correct the fault.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-07-27 14:09:54 -07:00
David Anderson
c3994fd77c derp: remove OnlyDisco option.
Active discovery lets us introspect the state of the network stack precisely
enough that it's unnecessary, and dropping the initial DERP packets greatly
slows down tests. Additionally, it's unrealistic since our production network
will never deliver _only_ discovery packets, it'll be all or nothing.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-07-27 14:09:54 -07:00
David Anderson
5455c64f1d wgengine/magicsock: add a test for two facing endpoint-independent NATs.
Signed-off-by: David Anderson <danderson@tailscale.com>
2020-07-27 14:09:54 -07:00
David Anderson
f794493b4f wgengine/magicsock: explicitly check path discovery, add a firewall test.
The test proves that active discovery can traverse two facing firewalls.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-07-27 14:09:54 -07:00
David Anderson
f582eeabd1 wgengine/magicsock: add a test for active path discovery.
Uses natlab only, because the point of this active discovery test is going to be
that it should get through a lot of obstacles.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-07-27 14:09:54 -07:00
David Anderson
a2b4ad839b net/netcheck: lower the hairpin check timeout to 100ms.
This single check is the long pole for netcheck, and significantly slows down magicsock
tests.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-07-27 14:09:54 -07:00
David Anderson
25288567ec net/netcheck: centralize all clock values in one place.
This makes it easier to see how long a netcheck might take, and what
the slow bits might be.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-07-27 14:09:54 -07:00
David Anderson
5a370d545a tstest/natlab: drop packets that can't be routed in a LAN.
LANs are authoritative for their prefixes, so we should not bounce
packets back and forth to the default gateway in that case.

Signed-off-by: David Anderson <danderson@tailscale.com>
2020-07-27 14:09:54 -07:00
Brad Fitzpatrick
37903a9056 wgengine/magicsock: fix occasional deadlock on Conn.Close on c.derpStarted
The deadlock was:

* Conn.Close was called, which acquired c.mu
* Then this goroutine scheduled:

    if firstDerp {
        startGate = c.derpStarted
        go func() {
            dc.Connect(ctx)
            close(c.derpStarted)
        }()
    }

* The getRegion hook for that derphttp.Client then ran, which also
  tries to acquire c.mu.

This change makes that hook first see if we're already in a closing
state and then it can pretend that region doesn't exist.
2020-07-27 12:27:10 -07:00
Elias Naur
bca9fe35ba logtail: return correct write size from logger.Write
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2020-07-27 11:06:41 -07:00
66 changed files with 2432 additions and 868 deletions

View File

@@ -123,7 +123,7 @@ const header = `// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserve
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// AUTO-GENERATED by: tailscale.com/cmd/cloner -type %s
// Code generated by tailscale.com/cmd/cloner -type %s; DO NOT EDIT.
package %s
@@ -168,8 +168,8 @@ func gen(buf *bytes.Buffer, imports map[string]struct{}, name string, typ *types
}
switch ft := ft.Underlying().(type) {
case *types.Slice:
n := importedName(ft.Elem())
if containsPointers(ft.Elem()) {
n := importedName(ft.Elem())
writef("dst.%s = make([]%s, len(src.%s))", fname, n, fname)
writef("for i := range dst.%s {", fname)
if _, isPtr := ft.Elem().(*types.Pointer); isPtr {
@@ -179,7 +179,7 @@ func gen(buf *bytes.Buffer, imports map[string]struct{}, name string, typ *types
}
writef("}")
} else {
writef("dst.%s = append([]%s(nil), src.%s...)", fname, n, fname)
writef("dst.%s = append(src.%s[:0:0], src.%s...)", fname, fname, fname)
}
case *types.Pointer:
if named, _ := ft.Elem().(*types.Named); named != nil && containsPointers(ft.Elem()) {

View File

@@ -149,11 +149,16 @@ func run() error {
ctx, cancel := context.WithCancel(context.Background())
// Exit gracefully by cancelling the ipnserver context in most common cases:
// interrupted from the TTY or killed by a service manager.
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
// SIGPIPE sometimes gets generated when CLIs disconnect from
// tailscaled. The default action is to terminate the process, we
// want to keep running.
signal.Ignore(syscall.SIGPIPE)
go func() {
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
select {
case <-interrupt:
case s := <-interrupt:
logf("tailscaled got signal %v; shutting down", s)
cancel()
case <-ctx.Done():
// continue
@@ -169,7 +174,7 @@ func run() error {
SurviveDisconnects: true,
DebugMux: debugMux,
}
err = ipnserver.Run(ctx, logf, pol.PublicID.String(), opts, e)
err = ipnserver.Run(ctx, logf, pol.PublicID.String(), ipnserver.FixedEngine(e), opts)
// Cancelation is not an error: it is the only way to stop ipnserver.
if err != nil && err != context.Canceled {
logf("ipnserver.Run: %v", err)

View File

@@ -3,8 +3,6 @@ Description=Tailscale node agent
Documentation=https://tailscale.com/kb/
Wants=network-pre.target
After=network-pre.target
StartLimitIntervalSec=0
StartLimitBurst=0
[Service]
EnvironmentFile=/etc/default/tailscaled

View File

@@ -517,7 +517,7 @@ func (c *Client) SetHostinfo(hi *tailcfg.Hostinfo) {
panic("nil Hostinfo")
}
if !c.direct.SetHostinfo(hi) {
c.logf("[unexpected] duplicate Hostinfo: %v", hi)
// No changes. Don't log.
return
}
c.logf("Hostinfo: %v", hi)

View File

@@ -70,3 +70,10 @@ func TestStatusEqual(t *testing.T) {
}
}
}
func TestOSVersion(t *testing.T) {
if osVersion == nil {
t.Skip("not available for OS")
}
t.Logf("Got: %#q", osVersion())
}

View File

@@ -4,6 +4,8 @@
package controlclient
//go:generate go run tailscale.com/cmd/cloner -type=Persist -output=direct_clone.go
import (
"bytes"
"context"
@@ -19,6 +21,7 @@ import (
"net/url"
"os"
"reflect"
"runtime"
"strconv"
"strings"
"sync"
@@ -27,6 +30,7 @@ import (
"github.com/tailscale/wireguard-go/wgcfg"
"golang.org/x/crypto/nacl/box"
"golang.org/x/oauth2"
"inet.af/netaddr"
"tailscale.com/log/logheap"
"tailscale.com/net/netns"
"tailscale.com/net/tlsdial"
@@ -165,12 +169,20 @@ func NewDirect(opts Options) (*Direct, error) {
return c, nil
}
var osVersion func() string // non-nil on some platforms
func NewHostinfo() *tailcfg.Hostinfo {
hostname, _ := os.Hostname()
var osv string
if osVersion != nil {
osv = osVersion()
}
return &tailcfg.Hostinfo{
IPNVersion: version.LONG,
Hostname: hostname,
OS: version.OS(),
OSVersion: osv,
GoArch: runtime.GOARCH,
}
}
@@ -619,6 +631,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
PrivateKey: persist.PrivateNodeKey,
Expiry: resp.Node.KeyExpiry,
Name: resp.Node.Name,
Addresses: resp.Node.Addresses,
Peers: resp.Peers,
LocalPort: localPort,
@@ -626,8 +639,7 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
Domain: resp.Domain,
Roles: resp.Roles,
DNS: resp.DNS,
DNSDomains: resp.SearchPaths,
DNS: resp.DNSConfig,
Hostinfo: resp.Node.Hostinfo,
PacketFilter: c.parsePacketFilter(resp.PacketFilter),
DERPMap: lastDERPMap,
@@ -641,6 +653,15 @@ func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkM
} else {
nm.MachineStatus = tailcfg.MachineUnauthorized
}
if len(resp.DNS) > 0 {
nm.DNS.Nameservers = wgIPToNetaddr(resp.DNS)
}
if len(resp.SearchPaths) > 0 {
nm.DNS.Domains = resp.SearchPaths
}
if Debug.ProxyDNS {
nm.DNS.Proxied = true
}
// Printing the netmap can be extremely verbose, but is very
// handy for debugging. Let's limit how often we do it.
@@ -780,12 +801,24 @@ func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (w
return key, nil
}
func wgIPToNetaddr(ips []wgcfg.IP) (ret []netaddr.IP) {
for _, ip := range ips {
nip, ok := netaddr.FromStdIP(ip.IP())
if !ok {
panic(fmt.Sprintf("conversion of %s from wgcfg to netaddr IP failed", ip))
}
ret = append(ret, nip.Unmap())
}
return ret
}
// Debug contains temporary internal-only debug knobs.
// They're unexported to not draw attention to them.
var Debug = initDebug()
type debug struct {
NetMap bool
ProxyDNS bool
OnlyDisco bool
Disco bool
ForceDisco bool // ask control server to not filter out our disco key
@@ -794,6 +827,7 @@ type debug struct {
func initDebug() debug {
d := debug{
NetMap: envBool("TS_DEBUG_NETMAP"),
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
OnlyDisco: os.Getenv("TS_DEBUG_USE_DISCO") == "only",
ForceDisco: os.Getenv("TS_DEBUG_USE_DISCO") == "only" || envBool("TS_DEBUG_USE_DISCO"),
}

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.
// Code generated by tailscale.com/cmd/cloner -type Persist; DO NOT EDIT.
package controlclient
import ()
// Clone makes a deep copy of Persist.
// The result aliases no memory with the original.
func (src *Persist) Clone() *Persist {
if src == nil {
return nil
}
dst := new(Persist)
*dst = *src
return dst
}

View File

@@ -0,0 +1,87 @@
// 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 linux,!android
package controlclient
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"strings"
"syscall"
"go4.org/mem"
"tailscale.com/util/lineread"
)
func init() {
osVersion = osVersionLinux
}
func osVersionLinux() string {
m := map[string]string{}
lineread.File("/etc/os-release", func(line []byte) error {
eq := bytes.IndexByte(line, '=')
if eq == -1 {
return nil
}
k, v := string(line[:eq]), strings.Trim(string(line[eq+1:]), `"`)
m[k] = v
return nil
})
var un syscall.Utsname
syscall.Uname(&un)
var attrBuf strings.Builder
attrBuf.WriteString("; kernel=")
for _, b := range un.Release {
if b == 0 {
break
}
attrBuf.WriteByte(byte(b))
}
if inContainer() {
attrBuf.WriteString("; container")
}
attr := attrBuf.String()
id := m["ID"]
switch id {
case "debian":
slurp, _ := ioutil.ReadFile("/etc/debian_version")
return fmt.Sprintf("Debian %s (%s)%s", bytes.TrimSpace(slurp), m["VERSION_CODENAME"], attr)
case "ubuntu":
return fmt.Sprintf("Ubuntu %s%s", m["VERSION"], attr)
case "", "centos": // CentOS 6 has no /etc/os-release, so its id is ""
if cr, _ := ioutil.ReadFile("/etc/centos-release"); len(cr) > 0 { // "CentOS release 6.10 (Final)
return fmt.Sprintf("%s%s", bytes.TrimSpace(cr), attr)
}
fallthrough
case "fedora", "rhel", "alpine":
// Their PRETTY_NAME is fine as-is for all versions I tested.
fallthrough
default:
if v := m["PRETTY_NAME"]; v != "" {
return fmt.Sprintf("%s%s", v, attr)
}
}
return fmt.Sprintf("Other%s", attr)
}
func inContainer() (ret bool) {
lineread.File("/proc/1/cgroup", func(line []byte) error {
if mem.Contains(mem.B(line), mem.S("/docker/")) ||
mem.Contains(mem.B(line), mem.S("/lxc/")) {
ret = true
return io.EOF // arbitrary non-nil error to stop loop
}
return nil
})
return
}

View File

@@ -0,0 +1,26 @@
// 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 (
"os/exec"
"strings"
"syscall"
)
func init() {
osVersion = osVersionWindows
}
func osVersionWindows() string {
cmd := exec.Command("cmd", "/c", "ver")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
out, _ := cmd.Output() // "\nMicrosoft Windows [Version 10.0.19041.388]\n\n"
s := strings.TrimSpace(string(out))
s = strings.TrimPrefix(s, "Microsoft Windows [")
s = strings.TrimSuffix(s, "]")
s = strings.TrimPrefix(s, "Version ") // is this localized? do it last in case.
return s // "10.0.19041.388", ideally
}

View File

@@ -23,15 +23,16 @@ import (
type NetworkMap struct {
// Core networking
NodeKey tailcfg.NodeKey
PrivateKey wgcfg.PrivateKey
Expiry time.Time
NodeKey tailcfg.NodeKey
PrivateKey wgcfg.PrivateKey
Expiry time.Time
// Name is the DNS name assigned to this node.
Name string
Addresses []wgcfg.CIDR
LocalPort uint16 // used for debugging
MachineStatus tailcfg.MachineStatus
Peers []*tailcfg.Node // sorted by Node.ID
DNS []wgcfg.IP
DNSDomains []string
DNS tailcfg.DNSConfig
Hostinfo tailcfg.Hostinfo
PacketFilter filter.Matches
@@ -217,8 +218,8 @@ const (
// 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, dnsOverride []wgcfg.IP) string {
wgcfg, err := nm.WGCfg(log.Printf, flags, dnsOverride)
func (nm *NetworkMap) UAPI(flags WGConfigFlags) string {
wgcfg, err := nm.WGCfg(log.Printf, flags)
if err != nil {
log.Fatalf("WGCfg() failed unexpectedly: %v", err)
}
@@ -235,13 +236,12 @@ func (nm *NetworkMap) UAPI(flags WGConfigFlags, dnsOverride []wgcfg.IP) string {
const EndpointDiscoSuffix = ".disco.tailscale:12345"
// WGCfg returns the NetworkMaps's Wireguard configuration.
func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags, dnsOverride []wgcfg.IP) (*wgcfg.Config, error) {
func (nm *NetworkMap) WGCfg(logf logger.Logf, flags WGConfigFlags) (*wgcfg.Config, error) {
cfg := &wgcfg.Config{
Name: "tailscale",
PrivateKey: nm.PrivateKey,
Addresses: nm.Addresses,
ListenPort: nm.LocalPort,
DNS: append([]wgcfg.IP(nil), dnsOverride...),
Peers: make([]wgcfg.Peer, 0, len(nm.Peers)),
}

View File

@@ -26,7 +26,6 @@ import (
"golang.org/x/crypto/nacl/box"
"golang.org/x/sync/errgroup"
"tailscale.com/disco"
"tailscale.com/metrics"
"tailscale.com/types/key"
"tailscale.com/types/logger"
@@ -52,13 +51,6 @@ type Server struct {
// before failing when writing to a client.
WriteTimeout time.Duration
// OnlyDisco controls whether, for tests, non-discovery packets
// are dropped. This is used by magicsock tests to verify that
// NAT traversal works (using DERP for out-of-band messaging)
// but the packets themselves aren't going via DERP.
OnlyDisco bool
_ [pad32bit]byte
privateKey key.Private
publicKey key.Public
logf logger.Logf
@@ -559,11 +551,6 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
return fmt.Errorf("client %x: recvPacket: %v", c.key, err)
}
if s.OnlyDisco && !disco.LooksLikeDiscoWrapper(contents) {
s.packetsDropped.Add(1)
return nil
}
var fwd PacketForwarder
s.mu.Lock()
dst := s.clients[dstKey]

2
go.sum
View File

@@ -86,6 +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/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=

View File

@@ -18,13 +18,23 @@ import (
"reflect"
)
func Hash(v interface{}) string {
func Hash(v ...interface{}) string {
h := sha256.New()
Print(h, v)
return fmt.Sprintf("%x", h.Sum(nil))
}
func Print(w io.Writer, v interface{}) {
// UpdateHash sets last to the hash of v and reports whether its value changed.
func UpdateHash(last *string, v ...interface{}) (changed bool) {
sig := Hash(v)
if *last != sig {
*last = sig
return true
}
return false
}
func Print(w io.Writer, v ...interface{}) {
print(w, reflect.ValueOf(v), make(map[uintptr]bool))
}

View File

@@ -11,6 +11,7 @@ import (
"github.com/tailscale/wireguard-go/wgcfg"
"inet.af/netaddr"
"tailscale.com/wgengine/router"
"tailscale.com/wgengine/router/dns"
)
func TestDeepPrint(t *testing.T) {
@@ -50,7 +51,7 @@ func getVal() []interface{} {
},
},
&router.Config{
DNSConfig: router.DNSConfig{
DNS: dns.Config{
Nameservers: []netaddr.IP{netaddr.IPv4(8, 8, 8, 8)},
Domains: []string{"tailscale.net"},
},

View File

@@ -69,10 +69,6 @@ type Options struct {
// DebugMux, if non-nil, specifies an HTTP ServeMux in which
// to register a debug handler.
DebugMux *http.ServeMux
// ErrorMessage, if not empty, signals that the server will exist
// only to relay the provided critical error message to the user.
ErrorMessage string
}
// server is an IPN backend and its set of 0 or more active connections
@@ -152,7 +148,9 @@ func (s *server) writeToClients(b []byte) {
}
}
func Run(ctx context.Context, logf logger.Logf, logid string, opts Options, e wgengine.Engine) error {
// Run runs a Tailscale backend service.
// The getEngine func is called repeatedly, once per connection, until it returns an engine successfully.
func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (wgengine.Engine, error), opts Options) error {
runDone := make(chan struct{})
defer close(runDone)
@@ -179,25 +177,38 @@ func Run(ctx context.Context, logf logger.Logf, logid string, opts Options, e wg
bo := backoff.NewBackoff("ipnserver", logf)
if opts.ErrorMessage != "" {
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
eng, err := getEngine()
if err != nil {
logf("Initial getEngine call: %v", err)
for i := 1; ctx.Err() == nil; i++ {
s, err := listen.Accept()
c, err := listen.Accept()
if err != nil {
logf("%d: Accept: %v", i, err)
bo.BackOff(ctx, err)
continue
}
serverToClient := func(b []byte) {
ipn.WriteMsg(s, b)
logf("%d: trying getEngine again...", i)
eng, err = getEngine()
if err == nil {
logf("%d: GetEngine worked; exiting failure loop", i)
unservedConn = c
break
}
logf("%d: getEngine failed again: %v", i, err)
errMsg := err.Error()
go func() {
defer s.Close()
defer c.Close()
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
bs := ipn.NewBackendServer(logf, nil, serverToClient)
bs.SendErrorMessage(opts.ErrorMessage)
s.Read(make([]byte, 1))
bs.SendErrorMessage(errMsg)
time.Sleep(time.Second)
}()
}
return ctx.Err()
if err := ctx.Err(); err != nil {
return err
}
}
var store ipn.StateStore
@@ -210,7 +221,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, opts Options, e wg
store = &ipn.MemoryStore{}
}
b, err := ipn.NewLocalBackend(logf, logid, store, e)
b, err := ipn.NewLocalBackend(logf, logid, store, eng)
if err != nil {
return fmt.Errorf("NewLocalBackend: %v", err)
}
@@ -243,7 +254,14 @@ func Run(ctx context.Context, logf logger.Logf, logid string, opts Options, e wg
}
for i := 1; ctx.Err() == nil; i++ {
c, err := listen.Accept()
var c net.Conn
var err error
if unservedConn != nil {
c = unservedConn
unservedConn = nil
} else {
c, err = listen.Accept()
}
if err != nil {
if ctx.Err() == nil {
logf("ipnserver: Accept: %v", err)
@@ -371,3 +389,8 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
}
}
}
// FixedEngine returns a func that returns eng and a nil error.
func FixedEngine(eng wgengine.Engine) func() (wgengine.Engine, error) {
return func() (wgengine.Engine, error) { return eng, nil }
}

View File

@@ -72,6 +72,6 @@ func TestRunMultipleAccepts(t *testing.T) {
SocketPath: socketPath,
}
t.Logf("pre-Run")
err = ipnserver.Run(ctx, logTriggerTestf, "dummy_logid", opts, eng)
err = ipnserver.Run(ctx, logTriggerTestf, "dummy_logid", ipnserver.FixedEngine(eng), opts)
t.Logf("ipnserver.Run = %v", err)
}

View File

@@ -18,6 +18,7 @@ import (
"sync"
"time"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
@@ -25,6 +26,7 @@ import (
// Status represents the entire state of the IPN network.
type Status struct {
BackendState string
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
Peer map[key.Public]*PeerStatus
User map[tailcfg.UserID]tailcfg.UserProfile
}
@@ -109,6 +111,18 @@ func (sb *StatusBuilder) AddUser(id tailcfg.UserID, up tailcfg.UserProfile) {
sb.st.User[id] = up
}
// AddIP adds a Tailscale IP address to the status.
func (sb *StatusBuilder) AddTailscaleIP(ip netaddr.IP) {
sb.mu.Lock()
defer sb.mu.Unlock()
if sb.locked {
log.Printf("[unexpected] ipnstate: AddIP after Locked")
return
}
sb.st.TailscaleIPs = append(sb.st.TailscaleIPs, ip)
}
// AddPeer adds a peer node to the status.
//
// Its PeerStatus is mixed with any previous status already added.
@@ -218,6 +232,12 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
//f("<p><b>logid:</b> %s</p>\n", logid)
//f("<p><b>opts:</b> <code>%s</code></p>\n", html.EscapeString(fmt.Sprintf("%+v", opts)))
ips := make([]string, 0, len(st.TailscaleIPs))
for _, ip := range st.TailscaleIPs {
ips = append(ips, ip.String())
}
f("<p>Tailscale IP: %s", strings.Join(ips, ", "))
f("<table>\n<thead>\n")
f("<tr><th>Peer</th><th>Node</th><th>Owner</th><th>Rx</th><th>Tx</th><th>Activity</th><th>Endpoints</th></tr>\n")
f("</thead>\n<tbody>\n")

View File

@@ -16,8 +16,10 @@ import (
"golang.org/x/oauth2"
"inet.af/netaddr"
"tailscale.com/control/controlclient"
"tailscale.com/internal/deepprint"
"tailscale.com/ipn/ipnstate"
"tailscale.com/ipn/policy"
"tailscale.com/net/tsaddr"
"tailscale.com/portlist"
"tailscale.com/tailcfg"
"tailscale.com/types/empty"
@@ -27,6 +29,7 @@ import (
"tailscale.com/wgengine"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/router"
"tailscale.com/wgengine/router/dns"
"tailscale.com/wgengine/tsdns"
)
@@ -51,12 +54,10 @@ type LocalBackend struct {
backendLogID string
portpoll *portlist.Poller // may be nil
portpollOnce sync.Once
serverURL string // tailcontrol URL
newDecompressor func() (controlclient.Decompressor, error)
// TODO: these fields are accessed unsafely by concurrent
// goroutines. They need to be protected.
serverURL string // tailcontrol URL
lastFilterPrint time.Time
filterHash string
// The mutex protects the following elements.
mu sync.Mutex
@@ -186,21 +187,56 @@ func (b *LocalBackend) SetDecompressor(fn func() (controlclient.Decompressor, er
// setClientStatus is the callback invoked by the control client whenever it posts a new status.
// Among other things, this is where we update the netmap, packet filters, DNS and DERP maps.
func (b *LocalBackend) setClientStatus(st controlclient.Status) {
// The following do not depend on any data for which we need to lock b.
if st.Err != "" {
// TODO(crawshaw): display in the UI.
b.logf("Received error: %v", st.Err)
return
}
if st.LoginFinished != nil {
// Auth completed, unblock the engine
b.blockEngineUpdates(false)
b.authReconfig()
b.send(Notify{LoginFinished: &empty.Message{}})
}
prefsChanged := false
// Lock b once and do only the things that require locking.
b.mu.Lock()
prefs := b.prefs
stateKey := b.stateKey
netMap := b.netMap
interact := b.interact
if st.Persist != nil {
persist := *st.Persist // copy
if !b.prefs.Persist.Equals(st.Persist) {
prefsChanged = true
b.prefs.Persist = st.Persist.Clone()
}
}
if st.NetMap != nil {
b.netMap = st.NetMap
}
if st.URL != "" {
b.authURL = st.URL
}
if b.state == NeedsLogin {
if !b.prefs.WantRunning {
prefsChanged = true
}
b.prefs.WantRunning = true
}
// Prefs will be written out; this is not safe unless locked or cloned.
if prefsChanged {
prefs = b.prefs.Clone()
}
b.mu.Lock()
b.prefs.Persist = &persist
prefs := b.prefs.Clone()
stateKey := b.stateKey
b.mu.Unlock()
b.mu.Unlock()
// Now complete the lock-free parts of what we started while locked.
if prefsChanged {
if stateKey != "" {
if err := b.store.WriteState(stateKey, prefs.ToBytes()); err != nil {
b.logf("Failed to save new controlclient state: %v", err)
@@ -209,63 +245,41 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
b.send(Notify{Prefs: prefs})
}
if st.NetMap != nil {
// Netmap is unchanged only when the diff is empty.
changed := true
b.mu.Lock()
if b.netMap != nil {
diff := st.NetMap.ConciseDiffFrom(b.netMap)
if netMap != nil {
diff := st.NetMap.ConciseDiffFrom(netMap)
if strings.TrimSpace(diff) == "" {
changed = false
b.logf("netmap diff: (none)")
} else {
b.logf("netmap diff:\n%v", diff)
}
}
disableDERP := b.prefs != nil && b.prefs.DisableDERP
b.netMap = st.NetMap
b.mu.Unlock()
b.send(Notify{NetMap: st.NetMap})
// There is nothing to update if the map hasn't changed.
if changed {
b.updateFilter(st.NetMap)
b.updateFilter(st.NetMap, prefs)
b.e.SetNetworkMap(st.NetMap)
if !dnsMapsEqual(st.NetMap, netMap) {
b.updateDNSMap(st.NetMap)
b.e.SetNetworkMap(st.NetMap)
}
disableDERP := prefs != nil && prefs.DisableDERP
if disableDERP {
b.e.SetDERPMap(nil)
} else {
b.e.SetDERPMap(st.NetMap.DERPMap)
}
b.send(Notify{NetMap: st.NetMap})
}
if st.URL != "" {
b.logf("Received auth URL: %.20v...", st.URL)
b.mu.Lock()
interact := b.interact
b.authURL = st.URL
b.mu.Unlock()
if interact > 0 {
b.popBrowserAuthNow()
}
}
if st.Err != "" {
// TODO(crawshaw): display in the UI.
b.logf("Received error: %v", st.Err)
return
}
if st.NetMap != nil {
b.mu.Lock()
if b.state == NeedsLogin {
b.prefs.WantRunning = true
}
prefs := b.prefs
b.mu.Unlock()
b.SetPrefs(prefs)
}
b.stateMachine()
// This is currently (2020-07-28) necessary; conditionally disabling it is fragile!
// This is where netmap information gets propagated to router and magicsock.
b.authReconfig()
}
// setWgengineStatus is the callback by the wireguard engine whenever it posts a new status.
@@ -360,7 +374,7 @@ func (b *LocalBackend) Start(opts Options) error {
persist := b.prefs.Persist
b.mu.Unlock()
b.updateFilter(nil)
b.updateFilter(nil, nil)
var discoPublic tailcfg.DiscoKey
if controlclient.Debug.Disco {
@@ -424,65 +438,121 @@ func (b *LocalBackend) Start(opts Options) error {
// updateFilter updates the packet filter in wgengine based on the
// given netMap and user preferences.
func (b *LocalBackend) updateFilter(netMap *controlclient.NetworkMap) {
if netMap == nil {
// Not configured yet, block everything
func (b *LocalBackend) updateFilter(netMap *controlclient.NetworkMap, prefs *Prefs) {
// NOTE(danderson): keep change detection as the first thing in
// this function. Don't try to optimize by returning early, more
// likely than not you'll just end up breaking the change
// detection and end up with the wrong filter installed. This is
// quite hard to debug, so save yourself the trouble.
var (
haveNetmap = netMap != nil
addrs []wgcfg.CIDR
packetFilter filter.Matches
advRoutes []wgcfg.CIDR
shieldsUp = prefs == nil || prefs.ShieldsUp // Be conservative when not ready
)
if haveNetmap {
addrs = netMap.Addresses
packetFilter = netMap.PacketFilter
}
if prefs != nil {
advRoutes = prefs.AdvertiseRoutes
}
changed := deepprint.UpdateHash(&b.filterHash, haveNetmap, addrs, packetFilter, advRoutes, shieldsUp)
if !changed {
return
}
if !haveNetmap {
b.logf("netmap packet filter: (not ready yet)")
b.e.SetFilter(filter.NewAllowNone(b.logf))
return
}
b.mu.Lock()
advRoutes := b.prefs.AdvertiseRoutes
b.mu.Unlock()
localNets := wgCIDRsToFilter(netMap.Addresses, advRoutes)
if b.shieldsAreUp() {
// Shields up, block everything
if shieldsUp {
b.logf("netmap packet filter: (shields up)")
var prevFilter *filter.Filter // don't reuse old filter state
b.e.SetFilter(filter.New(filter.Matches{}, localNets, prevFilter, b.logf))
return
} else {
b.logf("netmap packet filter: %v", packetFilter)
b.e.SetFilter(filter.New(packetFilter, localNets, b.e.GetFilter(), b.logf))
}
}
// dnsCIDRsEqual determines whether two CIDR lists are equal
// for DNS map construction purposes (that is, only the first entry counts).
func dnsCIDRsEqual(newAddr, oldAddr []wgcfg.CIDR) bool {
if len(newAddr) != len(oldAddr) {
return false
}
if len(newAddr) == 0 || newAddr[0] == oldAddr[0] {
return true
}
return false
}
// dnsMapsEqual determines whether the new and the old network map
// induce the same DNS map. It does so without allocating memory,
// at the expense of giving false negatives if peers are reordered.
func dnsMapsEqual(new, old *controlclient.NetworkMap) bool {
if (old == nil) != (new == nil) {
return false
}
if old == nil && new == nil {
return true
}
// TODO(apenwarr): don't replace filter at all if unchanged.
// TODO(apenwarr): print a diff instead of full filter.
now := time.Now()
if now.Sub(b.lastFilterPrint) > 1*time.Minute {
b.logf("netmap packet filter: %v", netMap.PacketFilter)
b.lastFilterPrint = now
} else {
b.logf("netmap packet filter: (length %d)", len(netMap.PacketFilter))
if len(new.Peers) != len(old.Peers) {
return false
}
b.e.SetFilter(filter.New(netMap.PacketFilter, localNets, b.e.GetFilter(), b.logf))
if new.Name != old.Name {
return false
}
if !dnsCIDRsEqual(new.Addresses, old.Addresses) {
return false
}
for i, newPeer := range new.Peers {
oldPeer := old.Peers[i]
if newPeer.Name != oldPeer.Name {
return false
}
if !dnsCIDRsEqual(newPeer.Addresses, oldPeer.Addresses) {
return false
}
}
return true
}
// updateDNSMap updates the domain map in the DNS resolver in wgengine
// based on the given netMap and user preferences.
func (b *LocalBackend) updateDNSMap(netMap *controlclient.NetworkMap) {
if netMap == nil {
b.logf("dns map: (not ready)")
return
}
domainToIP := make(map[string]netaddr.IP)
set := func(hostname string, addrs []wgcfg.CIDR) {
nameToIP := make(map[string]netaddr.IP)
set := func(name string, addrs []wgcfg.CIDR) {
if len(addrs) == 0 {
return
}
domain := hostname
// Like PeerStatus.SimpleHostName()
domain = strings.TrimSuffix(domain, ".local")
domain = strings.TrimSuffix(domain, ".localdomain")
domain = domain + ".b.tailscale.net"
domainToIP[domain] = netaddr.IPFrom16(addrs[0].IP.Addr)
nameToIP[name] = netaddr.IPFrom16(addrs[0].IP.Addr)
}
for _, peer := range netMap.Peers {
set(peer.Hostinfo.Hostname, peer.Addresses)
set(peer.Name, peer.Addresses)
}
set(netMap.Hostinfo.Hostname, netMap.Addresses)
set(netMap.Name, netMap.Addresses)
b.e.SetDNSMap(tsdns.NewMap(domainToIP))
dnsMap := tsdns.NewMap(nameToIP)
// map diff will be logged in tsdns.Resolver.SetMap.
b.e.SetDNSMap(dnsMap)
}
// readPoller is a goroutine that receives service lists from
@@ -721,37 +791,45 @@ func (b *LocalBackend) SetPrefs(new *Prefs) {
}
b.mu.Lock()
netMap := b.netMap
stateKey := b.stateKey
old := b.prefs
new.Persist = old.Persist // caller isn't allowed to override this
b.prefs = new
if b.stateKey != "" {
if err := b.store.WriteState(b.stateKey, b.prefs.ToBytes()); err != nil {
b.logf("Failed to save new controlclient state: %v", err)
}
}
// We do this to avoid holding the lock while doing everything else.
new = b.prefs.Clone()
oldHi := b.hostinfo
newHi := oldHi.Clone()
newHi.RoutableIPs = append([]wgcfg.CIDR(nil), b.prefs.AdvertiseRoutes...)
applyPrefsToHostinfo(newHi, new)
b.hostinfo = newHi
hostInfoChanged := !oldHi.Equal(newHi)
b.mu.Unlock()
if stateKey != "" {
if err := b.store.WriteState(stateKey, new.ToBytes()); err != nil {
b.logf("Failed to save new controlclient state: %v", err)
}
}
b.logf("SetPrefs: %v", new.Pretty())
if old.ShieldsUp != new.ShieldsUp || hostInfoChanged {
b.doSetHostinfoFilterServices(newHi)
}
b.updateFilter(b.netMap)
// TODO(dmytro): when Prefs gain an EnableTailscaleDNS toggle, updateDNSMap here.
b.updateFilter(netMap, new)
turnDERPOff := new.DisableDERP && !old.DisableDERP
turnDERPOn := !new.DisableDERP && old.DisableDERP
if turnDERPOff {
b.e.SetDERPMap(nil)
} else if turnDERPOn && b.netMap != nil {
b.e.SetDERPMap(b.netMap.DERPMap)
} else if turnDERPOn && netMap != nil {
b.e.SetDERPMap(netMap.DERPMap)
}
if old.WantRunning != new.WantRunning {
@@ -844,28 +922,71 @@ func (b *LocalBackend) authReconfig() {
flags |= controlclient.AllowSingleHosts
}
dns := nm.DNS
dom := nm.DNSDomains
if !uc.CorpDNS {
dns = []wgcfg.IP{}
dom = []string{}
}
cfg, err := nm.WGCfg(b.logf, flags, dns)
cfg, err := nm.WGCfg(b.logf, flags)
if err != nil {
b.logf("wgcfg: %v", err)
return
}
err = b.e.Reconfig(cfg, routerConfig(cfg, uc, dom))
rcfg := routerConfig(cfg, uc)
// If CorpDNS is false, rcfg.DNS remains the zero value.
if uc.CorpDNS {
domains := nm.DNS.Domains
proxied := nm.DNS.Proxied
if proxied {
if len(nm.DNS.Nameservers) == 0 {
b.logf("[unexpected] dns proxied but no nameservers")
proxied = false
} else {
domains = append(domains, domainsForProxying(nm)...)
}
}
rcfg.DNS = dns.Config{
Nameservers: nm.DNS.Nameservers,
Domains: domains,
PerDomain: nm.DNS.PerDomain,
Proxied: proxied,
}
}
err = b.e.Reconfig(cfg, rcfg)
if err == wgengine.ErrNoChanges {
return
}
b.logf("authReconfig: ra=%v dns=%v 0x%02x: %v", uc.RouteAll, uc.CorpDNS, flags, err)
}
// routerConfig produces a router.Config from a wireguard config,
// IPN prefs, and the dnsDomains pulled from control's network map.
func routerConfig(cfg *wgcfg.Config, prefs *Prefs, dnsDomains []string) *router.Config {
// domainsForProxying produces a list of search domains for proxied DNS.
func domainsForProxying(nm *controlclient.NetworkMap) []string {
var domains []string
if idx := strings.IndexByte(nm.Name, '.'); idx != -1 {
domains = append(domains, nm.Name[idx+1:])
}
for _, peer := range nm.Peers {
idx := strings.IndexByte(peer.Name, '.')
if idx == -1 {
continue
}
domain := peer.Name[idx+1:]
seen := false
// In theory this makes the function O(n^2) worst case,
// but in practice we expect domains to contain very few elements
// (only one until invitations are introduced).
for _, seenDomain := range domains {
if domain == seenDomain {
seen = true
}
}
if !seen {
domains = append(domains, domain)
}
}
return domains
}
// routerConfig produces a router.Config from a wireguard config and IPN prefs.
func routerConfig(cfg *wgcfg.Config, prefs *Prefs) *router.Config {
var addrs []wgcfg.CIDR
for _, addr := range cfg.Addresses {
addrs = append(addrs, wgcfg.CIDR{
@@ -879,20 +1000,14 @@ func routerConfig(cfg *wgcfg.Config, prefs *Prefs, dnsDomains []string) *router.
SubnetRoutes: wgCIDRToNetaddr(prefs.AdvertiseRoutes),
SNATSubnetRoutes: !prefs.NoSNAT,
NetfilterMode: prefs.NetfilterMode,
DNSConfig: router.DNSConfig{
Nameservers: wgIPToNetaddr(cfg.DNS),
Domains: dnsDomains,
},
}
for _, peer := range cfg.Peers {
rs.Routes = append(rs.Routes, wgCIDRToNetaddr(peer.AllowedIPs)...)
}
// The Tailscale DNS IP.
// TODO(dmytro): make this configurable.
rs.Routes = append(rs.Routes, netaddr.IPPrefix{
IP: netaddr.IPv4(100, 100, 100, 100),
IP: tsaddr.TailscaleServiceIP(),
Bits: 32,
})
@@ -916,17 +1031,6 @@ func wgCIDRsToFilter(cidrLists ...[]wgcfg.CIDR) (ret []filter.Net) {
return ret
}
func wgIPToNetaddr(ips []wgcfg.IP) (ret []netaddr.IP) {
for _, ip := range ips {
nip, ok := netaddr.FromStdIP(ip.IP())
if !ok {
panic(fmt.Sprintf("conversion of %s from wgcfg to netaddr IP failed", ip))
}
ret = append(ret, nip.Unmap())
}
return ret
}
func wgCIDRToNetaddr(cidrs []wgcfg.CIDR) (ret []netaddr.IPPrefix) {
for _, cidr := range cidrs {
ncidr, ok := netaddr.FromStdIPNet(cidr.IPNet())

View File

@@ -149,6 +149,14 @@ func runningUnderSystemd() bool {
// moved from whereever it does exist, into dir. Leftover logs state
// in / and $CACHE_DIRECTORY is deleted.
func tryFixLogStateLocation(dir, cmdname string) {
switch runtime.GOOS {
case "linux", "freebsd", "openbsd":
// These are the OSes where we might have written stuff into
// root. Others use different logic to find the logs storage
// dir.
default:
return
}
if cmdname == "" {
log.Printf("[unexpected] no cmdname given to tryFixLogStateLocation, please file a bug at https://github.com/tailscale/tailscale")
return
@@ -163,14 +171,6 @@ func tryFixLogStateLocation(dir, cmdname string) {
// Only root could have written log configs to weird places.
return
}
switch runtime.GOOS {
case "linux", "freebsd", "openbsd":
// These are the OSes where we might have written stuff into
// root. Others use different logic to find the logs storage
// dir.
default:
return
}
// We stored logs in 2 incorrect places: either /, or CACHE_DIR
// (aka /var/cache/tailscale). We want to move files into the
@@ -305,11 +305,10 @@ func New(collection string) *Policy {
dir := logsDir()
if runtime.GOOS != "windows" { // version.CmdName call was blowing some Windows stack limit via goversion DLL loading
tryFixLogStateLocation(dir, version.CmdName())
}
cmdName := version.CmdName()
tryFixLogStateLocation(dir, cmdName)
cfgPath := filepath.Join(dir, fmt.Sprintf("%s.log.conf", version.CmdName()))
cfgPath := filepath.Join(dir, fmt.Sprintf("%s.log.conf", cmdName))
var oldc *Config
data, err := ioutil.ReadFile(cfgPath)
if err != nil {
@@ -359,7 +358,7 @@ func New(collection string) *Policy {
HTTPC: &http.Client{Transport: newLogtailTransport(logtail.DefaultHost)},
}
filchBuf, filchErr := filch.New(filepath.Join(dir, version.CmdName()), filch.Options{})
filchBuf, filchErr := filch.New(filepath.Join(dir, cmdName), filch.Options{})
if filchBuf != nil {
c.Buffer = filchBuf
}

View File

@@ -462,5 +462,6 @@ func (l *logger) Write(buf []byte) (int, error) {
}
}
b := l.encode(buf)
return l.send(b)
_, err := l.send(b)
return len(buf), err
}

View File

@@ -32,3 +32,18 @@ func TestLoggerEncodeTextAllocs(t *testing.T) {
t.Logf("allocs = %d; want 1", int(n))
}
}
func TestLoggerWriteLength(t *testing.T) {
lg := &logger{
timeNow: time.Now,
buffer: NewMemoryBuffer(1024),
}
inBuf := []byte("some text to encode")
n, err := lg.Write(inBuf)
if err != nil {
t.Error(err)
}
if n != len(inBuf) {
t.Errorf("logger.Write wrote %d bytes, expected %d", n, len(inBuf))
}
}

View File

@@ -10,6 +10,7 @@ import (
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/util/lineread"
"tailscale.com/version"
)
func init() {
@@ -32,6 +33,13 @@ default link#14 UCSI utun2
*/
func likelyHomeRouterIPDarwin() (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.
return ret, false
}
cmd := exec.Command("/usr/sbin/netstat", "-r", "-n", "-f", "inet")
stdout, err := cmd.StdoutPipe()
if err != nil {

View File

@@ -5,8 +5,14 @@
package interfaces
import (
"bytes"
"log"
"os/exec"
"runtime"
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/syncs"
"tailscale.com/util/lineread"
)
@@ -14,6 +20,8 @@ func init() {
likelyHomeRouterIP = likelyHomeRouterIPLinux
}
var procNetRouteErr syncs.AtomicBool
/*
Parse 10.0.0.1 out of:
@@ -23,9 +31,17 @@ ens18 00000000 0100000A 0003 0 0 0 00000000
ens18 0000000A 00000000 0001 0 0 0 0000FFFF 0 0 0
*/
func likelyHomeRouterIPLinux() (ret netaddr.IP, ok bool) {
if procNetRouteErr.Get() {
// If we failed to read /proc/net/route previously, don't keep trying.
// But if we're on Android, go into the Android path.
if runtime.GOOS == "android" {
return likelyHomeRouterIPAndroid()
}
return ret, false
}
lineNum := 0
var f []mem.RO
lineread.File("/proc/net/route", func(line []byte) error {
err := lineread.File("/proc/net/route", func(line []byte) error {
lineNum++
if lineNum == 1 {
// Skip header line.
@@ -55,5 +71,47 @@ func likelyHomeRouterIPLinux() (ret netaddr.IP, ok bool) {
}
return nil
})
if err != nil {
procNetRouteErr.Set(true)
if runtime.GOOS == "android" {
return likelyHomeRouterIPAndroid()
}
log.Printf("interfaces: failed to read /proc/net/route: %v", err)
}
return ret, !ret.IsZero()
}
// Android apps don't have permission to read /proc/net/route, at
// least on Google devices and the Android emulator.
func likelyHomeRouterIPAndroid() (ret netaddr.IP, ok bool) {
cmd := exec.Command("/system/bin/ip", "route", "show", "table", "0")
out, err := cmd.StdoutPipe()
if err != nil {
return
}
if err := cmd.Start(); err != nil {
log.Printf("interfaces: running /system/bin/ip: %v", err)
return
}
// 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
}
line = line[len(pfx):]
sp := bytes.IndexByte(line, ' ')
if sp == -1 {
return nil
}
ipb := line[:sp]
if ip, err := netaddr.ParseIP(string(ipb)); err == nil && ip.Is4() {
ret = ip
log.Printf("interfaces: found Android default route %v", ip)
}
return nil
})
cmd.Process.Kill()
cmd.Wait()
return ret, !ret.IsZero()
}

View File

@@ -18,7 +18,9 @@ import (
"log"
"net"
"net/http"
"os"
"sort"
"strconv"
"sync"
"time"
@@ -36,6 +38,45 @@ import (
"tailscale.com/types/opt"
)
// Debugging and experimentation tweakables.
var (
debugNetcheck, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_NETCHECK"))
)
// The various default timeouts for things.
const (
// overallProbeTimeout is the maximum amount of time netcheck will
// spend gathering a single report.
overallProbeTimeout = 5 * time.Second
// stunTimeout is the maximum amount of time netcheck will spend
// probing with STUN packets without getting a reply before
// switching to HTTP probing, on the assumption that outbound UDP
// is blocked.
stunProbeTimeout = 3 * time.Second
// hairpinCheckTimeout is the amount of time we wait for a
// hairpinned packet to come back.
hairpinCheckTimeout = 100 * time.Millisecond
// defaultActiveRetransmitTime is the retransmit interval we use
// for STUN probes when we're in steady state (not in start-up),
// but don't have previous latency information for a DERP
// node. This is a somewhat conservative guess because if we have
// no data, likely the DERP node is very far away and we have no
// data because we timed out the last time we probed it.
defaultActiveRetransmitTime = 200 * time.Millisecond
// defaultInitialRetransmitTime is the retransmit interval used
// when netcheck first runs. We have no past context to work with,
// and we want answers relatively quickly, so it's biased slightly
// more aggressive than defaultActiveRetransmitTime. A few extra
// packets at startup is fine.
defaultInitialRetransmitTime = 100 * time.Millisecond
// portMapServiceProbeTimeout is the time we wait for port mapping
// services (UPnP, NAT-PMP, PCP) to respond before we give up and
// decide that they're not there. Since these services are on the
// same LAN as this machine and a single L3 hop away, we don't
// give them much time to respond.
portMapServiceProbeTimeout = 100 * time.Millisecond
)
type Report struct {
UDP bool // UDP works
IPv6 bool // IPv6 works
@@ -139,7 +180,7 @@ func (c *Client) logf(format string, a ...interface{}) {
}
func (c *Client) vlogf(format string, a ...interface{}) {
if c.Verbose {
if c.Verbose || debugNetcheck {
c.logf(format, a...)
}
}
@@ -170,6 +211,8 @@ func (c *Client) MakeNextReportFull() {
}
func (c *Client) ReceiveSTUNPacket(pkt []byte, src netaddr.IPPort) {
c.vlogf("received STUN packet from %s", src)
c.mu.Lock()
if c.handleHairSTUNLocked(pkt, src) {
c.mu.Unlock()
@@ -330,7 +373,7 @@ func makeProbePlan(dm *tailcfg.DERPMap, ifState *interfaces.State, last *Report)
n := reg.Nodes[try%len(reg.Nodes)]
prevLatency := last.RegionLatency[reg.RegionID] * 120 / 100
if prevLatency == 0 {
prevLatency = 200 * time.Millisecond
prevLatency = defaultActiveRetransmitTime
}
delay := time.Duration(try) * prevLatency
if do4 {
@@ -353,16 +396,12 @@ func makeProbePlan(dm *tailcfg.DERPMap, ifState *interfaces.State, last *Report)
func makeProbePlanInitial(dm *tailcfg.DERPMap, ifState *interfaces.State) (plan probePlan) {
plan = make(probePlan)
// initialSTUNTimeout is only 100ms because some extra retransmits
// when starting up is tolerable.
const initialSTUNTimeout = 100 * time.Millisecond
for _, reg := range dm.Regions {
var p4 []probe
var p6 []probe
for try := 0; try < 3; try++ {
n := reg.Nodes[try%len(reg.Nodes)]
delay := time.Duration(try) * initialSTUNTimeout
delay := time.Duration(try) * defaultInitialRetransmitTime
if ifState.HaveV4 && nodeMight4(n) {
p4 = append(p4, probe{delay: delay, node: n.Name, proto: probeIPv4})
}
@@ -518,7 +557,7 @@ func (rs *reportState) startHairCheckLocked(dst netaddr.IPPort) {
ua := dst.UDPAddr()
rs.pc4Hair.WriteTo(stun.Request(rs.hairTX), ua)
rs.c.vlogf("sent haircheck to %v", ua)
time.AfterFunc(500*time.Millisecond, func() { close(rs.hairTimeout) })
time.AfterFunc(hairpinCheckTimeout, func() { close(rs.hairTimeout) })
}
func (rs *reportState) waitHairCheck(ctx context.Context) {
@@ -539,6 +578,7 @@ func (rs *reportState) waitHairCheck(ctx context.Context) {
case <-rs.gotHairSTUN:
ret.HairPinning.Set(true)
case <-rs.hairTimeout:
rs.c.vlogf("hairCheck timeout")
ret.HairPinning.Set(false)
default:
select {
@@ -649,7 +689,7 @@ func (rs *reportState) probePortMapServices() {
}
defer uc.Close()
tempPort := uc.LocalAddr().(*net.UDPAddr).Port
uc.SetReadDeadline(time.Now().Add(100 * time.Millisecond))
uc.SetReadDeadline(time.Now().Add(portMapServiceProbeTimeout))
// Send request packets for all three protocols.
uc.WriteTo(uPnPPacket, port1900)
@@ -727,15 +767,10 @@ func newReport() *Report {
//
// It may not be called concurrently with itself.
func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, error) {
// Wait for STUN for 3 seconds, but then give HTTP probing
// another 2 seconds if all UDP failed.
const overallTimeout = 5 * time.Second
const stunTimeout = 3 * time.Second
// Mask user context with ours that we guarantee to cancel so
// we can depend on it being closed in goroutines later.
// (User ctx might be context.Background, etc)
ctx, cancel := context.WithTimeout(ctx, overallTimeout)
ctx, cancel := context.WithTimeout(ctx, overallProbeTimeout)
defer cancel()
if dm == nil {
@@ -844,7 +879,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
}(probeSet)
}
stunTimer := time.NewTimer(stunTimeout)
stunTimer := time.NewTimer(stunProbeTimeout)
defer stunTimer.Stop()
select {
@@ -857,7 +892,9 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
}
rs.waitHairCheck(ctx)
c.vlogf("hairCheck done")
rs.waitPortMap.Wait()
c.vlogf("portMap done")
rs.stopTimers()
// Try HTTPS latency check if all STUN probes failed due to UDP presumably being blocked.
@@ -912,7 +949,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegion) (time.Duration, netaddr.IP, error) {
var result httpstat.Result
ctx, cancel := context.WithTimeout(httpstat.WithHTTPStat(ctx, &result), 5*time.Second)
ctx, cancel := context.WithTimeout(httpstat.WithHTTPStat(ctx, &result), overallProbeTimeout)
defer cancel()
var ip netaddr.IP

View File

@@ -32,6 +32,15 @@ func CGNATRange() netaddr.IPPrefix {
var cgnatRange oncePrefix
// TailscaleServiceIP returns the listen address of services
// provided by Tailscale itself such as the Magic DNS proxy.
func TailscaleServiceIP() netaddr.IP {
serviceIP.Do(func() { mustIP(&serviceIP.v, "100.100.100.100") })
return serviceIP.v
}
var serviceIP onceIP
// IsTailscaleIP reports whether ip is an IP address in a range that
// Tailscale assigns from.
func IsTailscaleIP(ip netaddr.IP) bool {
@@ -50,3 +59,16 @@ type oncePrefix struct {
sync.Once
v netaddr.IPPrefix
}
func mustIP(v *netaddr.IP, ip string) {
var err error
*v, err = netaddr.ParseIP(ip)
if err != nil {
panic(err)
}
}
type onceIP struct {
sync.Once
v netaddr.IP
}

View File

@@ -10,12 +10,15 @@ import (
"io"
"io/ioutil"
"os"
"runtime"
"sort"
"strconv"
"strings"
"syscall"
"time"
"golang.org/x/sys/unix"
"tailscale.com/syncs"
)
// Reading the sockfiles on Linux is very fast, so we can do it often.
@@ -26,13 +29,30 @@ const pollInterval = 1 * time.Second
var sockfiles = []string{"/proc/net/tcp", "/proc/net/udp"}
var protos = []string{"tcp", "udp"}
var sawProcNetPermissionErr syncs.AtomicBool
func listPorts() (List, error) {
if sawProcNetPermissionErr.Get() {
return nil, nil
}
l := []Port{}
for pi, fname := range sockfiles {
proto := protos[pi]
// Android 10+ doesn't allow access to this anymore.
// https://developer.android.com/about/versions/10/privacy/changes#proc-net-filesystem
// Ignore it rather than have the system log about our violation.
if runtime.GOOS == "android" && syscall.Access(fname, unix.R_OK) != nil {
sawProcNetPermissionErr.Set(true)
return nil, nil
}
f, err := os.Open(fname)
if os.IsPermission(err) {
sawProcNetPermissionErr.Set(true)
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("%s: %s", fname, err)
}
@@ -96,7 +116,18 @@ func addProcesses(pl []Port) ([]Port, error) {
}
err := foreachPID(func(pid string) error {
fdDir, err := os.Open(fmt.Sprintf("/proc/%s/fd", pid))
fdPath := fmt.Sprintf("/proc/%s/fd", pid)
// Android logs a bunch of audit violations in logcat
// if we try to open things we don't have access
// to. So on Android only, ask if we have permission
// rather than just trying it to determine whether we
// have permission.
if runtime.GOOS == "android" && syscall.Access(fdPath, unix.R_OK) != nil {
return nil
}
fdDir, err := os.Open(fdPath)
if err != nil {
// Can't open fd list for this pid. Maybe
// don't have access. Ignore it.

View File

@@ -4,6 +4,8 @@
package tailcfg
//go:generate go run tailscale.com/cmd/cloner -type=User,Node,Hostinfo,NetInfo -output=tailcfg_clone.go
import (
"bytes"
"errors"
@@ -15,6 +17,7 @@ import (
"github.com/tailscale/wireguard-go/wgcfg"
"go4.org/mem"
"golang.org/x/oauth2"
"inet.af/netaddr"
"tailscale.com/types/key"
"tailscale.com/types/opt"
"tailscale.com/types/structs"
@@ -258,6 +261,7 @@ type Hostinfo struct {
OSVersion string // operating system version, with optional distro prefix ("Debian 10.4", "Windows 10 Pro 10.0.19041")
DeviceModel string // mobile phone model ("Pixel 3a", "iPhone 11 Pro")
Hostname string // name of the host the client runs on
GoArch string // the host's GOARCH value (of the running binary)
RoutableIPs []wgcfg.CIDR `json:",omitempty"` // set of IP ranges this client can route
RequestTags []string `json:",omitempty"` // set of ACL tags this node wants to claim
Services []Service `json:",omitempty"` // services advertised by this machine
@@ -371,20 +375,6 @@ func (ni *NetInfo) BasicallyEqual(ni2 *NetInfo) bool {
ni.LinkType == ni2.LinkType
}
// Clone makes a deep copy of Hostinfo.
// The result aliases no memory with the original.
//
// TODO: use cmd/cloner, reconcile len(0) vs. nil.
func (h *Hostinfo) Clone() (res *Hostinfo) {
res = new(Hostinfo)
*res = *h
res.RoutableIPs = append([]wgcfg.CIDR{}, h.RoutableIPs...)
res.Services = append([]Service{}, h.Services...)
res.NetInfo = h.NetInfo.Clone()
return res
}
// Equal reports whether h and h2 are equal.
func (h *Hostinfo) Equal(h2 *Hostinfo) bool {
if h == nil && h2 == nil {
@@ -503,15 +493,31 @@ var FilterAllowAll = []FilterRule{
},
}
// DNSConfig is the DNS configuration.
type DNSConfig struct {
Nameservers []netaddr.IP `json:",omitempty"`
Domains []string `json:",omitempty"`
PerDomain bool
Proxied bool
}
type MapResponse struct {
KeepAlive bool // if set, all other fields are ignored
// Networking
Node *Node
Peers []*Node
DNS []wgcfg.IP
Node *Node
Peers []*Node
DERPMap *DERPMap
// DNS is the same as DNSConfig.Nameservers.
//
// TODO(dmytro): should be sent in DNSConfig.Nameservers once clients have updated.
DNS []wgcfg.IP
// SearchPaths are the same as DNSConfig.Domains.
//
// TODO(dmytro): should be sent in DNSConfig.Domains once clients have updated.
SearchPaths []string
DERPMap *DERPMap
DNSConfig DNSConfig
// ACLs
Domain string

View File

@@ -2,12 +2,11 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// AUTO-GENERATED by tailscale.com/cmd/cloner -type User,Node,NetInfo
// Code generated by tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo; DO NOT EDIT.
package tailcfg
import (
"github.com/tailscale/wireguard-go/wgcfg"
"time"
)
@@ -19,8 +18,8 @@ func (src *User) Clone() *User {
}
dst := new(User)
*dst = *src
dst.Logins = append([]LoginID(nil), src.Logins...)
dst.Roles = append([]RoleID(nil), src.Roles...)
dst.Logins = append(src.Logins[:0:0], src.Logins...)
dst.Roles = append(src.Roles[:0:0], src.Roles...)
return dst
}
@@ -32,9 +31,9 @@ func (src *Node) Clone() *Node {
}
dst := new(Node)
*dst = *src
dst.Addresses = append([]wgcfg.CIDR(nil), src.Addresses...)
dst.AllowedIPs = append([]wgcfg.CIDR(nil), src.AllowedIPs...)
dst.Endpoints = append([]string(nil), src.Endpoints...)
dst.Addresses = append(src.Addresses[:0:0], src.Addresses...)
dst.AllowedIPs = append(src.AllowedIPs[:0:0], src.AllowedIPs...)
dst.Endpoints = append(src.Endpoints[:0:0], src.Endpoints...)
dst.Hostinfo = *src.Hostinfo.Clone()
if dst.LastSeen != nil {
dst.LastSeen = new(time.Time)
@@ -43,6 +42,21 @@ func (src *Node) Clone() *Node {
return dst
}
// Clone makes a deep copy of Hostinfo.
// The result aliases no memory with the original.
func (src *Hostinfo) Clone() *Hostinfo {
if src == nil {
return nil
}
dst := new(Hostinfo)
*dst = *src
dst.RoutableIPs = append(src.RoutableIPs[:0:0], src.RoutableIPs...)
dst.RequestTags = append(src.RequestTags[:0:0], src.RequestTags...)
dst.Services = append(src.Services[:0:0], src.Services...)
dst.NetInfo = src.NetInfo.Clone()
return dst
}
// Clone makes a deep copy of NetInfo.
// The result aliases no memory with the original.
func (src *NetInfo) Clone() *NetInfo {

View File

@@ -24,7 +24,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
func TestHostinfoEqual(t *testing.T) {
hiHandles := []string{
"IPNVersion", "FrontendLogID", "BackendLogID", "OS", "OSVersion",
"DeviceModel", "Hostname", "RoutableIPs", "RequestTags", "Services",
"DeviceModel", "Hostname", "GoArch", "RoutableIPs", "RequestTags", "Services",
"NetInfo",
}
if have := fieldsOf(reflect.TypeOf(Hostinfo{})); !reflect.DeepEqual(have, hiHandles) {
@@ -390,3 +390,43 @@ func testKey(t *testing.T, prefix string, in keyIn, out encoding.TextUnmarshaler
t.Errorf("mismatch after unmarshal")
}
}
func TestCloneUser(t *testing.T) {
tests := []struct {
name string
u *User
}{
{"nil_logins", &User{}},
{"zero_logins", &User{Logins: make([]LoginID, 0)}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
u2 := tt.u.Clone()
if !reflect.DeepEqual(tt.u, u2) {
t.Errorf("not equal")
}
})
}
}
func TestCloneNode(t *testing.T) {
tests := []struct {
name string
v *Node
}{
{"nil_fields", &Node{}},
{"zero_fields", &Node{
Addresses: make([]wgcfg.CIDR, 0),
AllowedIPs: make([]wgcfg.CIDR, 0),
Endpoints: make([]string, 0),
}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
v2 := tt.v.Clone()
if !reflect.DeepEqual(tt.v, v2) {
t.Errorf("not equal")
}
})
}
}

View File

@@ -93,9 +93,10 @@ func mustPrefix(s string) netaddr.IPPrefix {
// NewInternet returns a network that simulates the internet.
func NewInternet() *Network {
return &Network{
Name: "internet",
Prefix4: mustPrefix("203.0.113.0/24"), // documentation netblock that looks Internet-y
Prefix6: mustPrefix("fc00:52::/64"),
Name: "internet",
// easily recognizable internett-y addresses
Prefix4: mustPrefix("1.0.0.0/24"),
Prefix6: mustPrefix("1111::/64"),
}
}
@@ -184,6 +185,17 @@ func (n *Network) write(p *Packet) (num int, err error) {
defer n.mu.Unlock()
iface, ok := n.machine[p.Dst.IP]
if !ok {
// If the destination is within the network's authoritative
// range, no route to host.
if p.Dst.IP.Is4() && n.Prefix4.Contains(p.Dst.IP) {
p.Trace("no route to %v", p.Dst.IP)
return len(p.Payload), nil
}
if p.Dst.IP.Is6() && n.Prefix6.Contains(p.Dst.IP) {
p.Trace("no route to %v", p.Dst.IP)
return len(p.Payload), nil
}
if n.defaultGW == nil {
p.Trace("no route to %v", p.Dst.IP)
return len(p.Payload), nil

View File

@@ -9,6 +9,7 @@ package version
import (
"os"
"path"
"path/filepath"
"strings"
"rsc.io/goversion/version"
@@ -22,22 +23,27 @@ func CmdName() string {
if err != nil {
return "cmd"
}
// fallbackName, the lowercase basename of the executable, is what we return if
// we can't find the Go module metadata embedded in the file.
fallbackName := filepath.Base(strings.TrimSuffix(strings.ToLower(e), ".exe"))
var ret string
v, err := version.ReadExe(e)
if err != nil {
ret = strings.TrimSuffix(strings.ToLower(e), ".exe")
} else {
// v is like:
// "path\ttailscale.com/cmd/tailscale\nmod\ttailscale.com\t(devel)\t\ndep\tgithub.com/apenwarr/fixconsole\tv0.0.0-20191012055117-5a9f6489cc29\th1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4=\ndep\tgithub....
for _, line := range strings.Split(v.ModuleInfo, "\n") {
if strings.HasPrefix(line, "path\t") {
ret = path.Base(strings.TrimPrefix(line, "path\t"))
break
}
return fallbackName
}
// v is like:
// "path\ttailscale.com/cmd/tailscale\nmod\ttailscale.com\t(devel)\t\ndep\tgithub.com/apenwarr/fixconsole\tv0.0.0-20191012055117-5a9f6489cc29\th1:muXWUcay7DDy1/hEQWrYlBy+g0EuwT70sBHg65SeUc4=\ndep\tgithub....
for _, line := range strings.Split(v.ModuleInfo, "\n") {
if strings.HasPrefix(line, "path\t") {
goPkg := strings.TrimPrefix(line, "path\t") // like "tailscale.com/cmd/tailscale"
ret = path.Base(goPkg) // goPkg is always forward slashes; use path, not filepath
break
}
}
if ret == "" {
return "cmd"
return fallbackName
}
return ret
}

View File

@@ -6,6 +6,7 @@
package filter
import (
"fmt"
"sync"
"time"
@@ -136,9 +137,13 @@ func maybeHexdump(flag RunFlags, b []byte) string {
var acceptBucket = rate.NewLimiter(rate.Every(10*time.Second), 3)
var dropBucket = rate.NewLimiter(rate.Every(5*time.Second), 10)
func (f *Filter) logRateLimit(runflags RunFlags, q *packet.ParsedPacket, r Response, why string) {
func (f *Filter) logRateLimit(runflags RunFlags, q *packet.ParsedPacket, dir direction, r Response, why string) {
var verdict string
if r == Drop && omitDropLogging(q, dir) {
return
}
if r == Drop && (runflags&LogDrops) != 0 && dropBucket.Allow() {
verdict = "Drop"
runflags &= HexdumpDrops
@@ -157,26 +162,28 @@ func (f *Filter) logRateLimit(runflags RunFlags, q *packet.ParsedPacket, r Respo
// RunIn determines whether this node is allowed to receive q from a Tailscale peer.
func (f *Filter) RunIn(q *packet.ParsedPacket, rf RunFlags) Response {
r := f.pre(q, rf)
dir := in
r := f.pre(q, rf, dir)
if r == Accept || r == Drop {
// already logged
return r
}
r, why := f.runIn(q)
f.logRateLimit(rf, q, r, why)
f.logRateLimit(rf, q, dir, r, why)
return r
}
// RunOut determines whether this node is allowed to send q to a Tailscale peer.
func (f *Filter) RunOut(q *packet.ParsedPacket, rf RunFlags) Response {
r := f.pre(q, rf)
dir := out
r := f.pre(q, rf, dir)
if r == Drop || r == Accept {
// already logged
return r
}
r, why := f.runOut(q)
f.logRateLimit(rf, q, r, why)
f.logRateLimit(rf, q, dir, r, why)
return r
}
@@ -188,6 +195,11 @@ func (f *Filter) runIn(q *packet.ParsedPacket) (r Response, why string) {
return Drop, "destination not allowed"
}
if q.IPVersion == 6 {
// TODO: support IPv6.
return Drop, "no rules matched"
}
switch q.IPProto {
case packet.ICMP:
if q.IsEchoResponse() || q.IsError() {
@@ -247,30 +259,106 @@ func (f *Filter) runOut(q *packet.ParsedPacket) (r Response, why string) {
return Accept, "ok out"
}
func (f *Filter) pre(q *packet.ParsedPacket, rf RunFlags) Response {
// direction is whether a packet was flowing in to this machine, or flowing out.
type direction int
const (
in direction = iota
out
)
func (d direction) String() string {
switch d {
case in:
return "in"
case out:
return "out"
default:
return fmt.Sprintf("[??dir=%d]", int(d))
}
}
func (f *Filter) pre(q *packet.ParsedPacket, rf RunFlags, dir direction) Response {
if len(q.Buffer()) == 0 {
// wireguard keepalive packet, always permit.
return Accept
}
if len(q.Buffer()) < 20 {
f.logRateLimit(rf, q, Drop, "too short")
f.logRateLimit(rf, q, dir, Drop, "too short")
return Drop
}
if q.IPVersion == 6 {
f.logRateLimit(rf, q, dir, Drop, "ipv6")
return Drop
}
switch q.IPProto {
case packet.Unknown:
// Unknown packets are dangerous; always drop them.
f.logRateLimit(rf, q, Drop, "unknown")
return Drop
case packet.IPv6:
f.logRateLimit(rf, q, Drop, "ipv6")
f.logRateLimit(rf, q, dir, Drop, "unknown")
return Drop
case packet.Fragment:
// Fragments after the first always need to be passed through.
// Very small fragments are considered Junk by ParsedPacket.
f.logRateLimit(rf, q, Accept, "fragment")
f.logRateLimit(rf, q, dir, Accept, "fragment")
return Accept
}
return noVerdict
}
const (
// ipv6AllRoutersLinkLocal is ff02::2 (All link-local routers)
ipv6AllRoutersLinkLocal = "\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02"
// ipv6AllMLDv2CapableRouters is ff02::16 (All MLDv2-capable routers)
ipv6AllMLDv2CapableRouters = "\xff\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x16"
)
// omitDropLogging reports whether packet p, which has already been
// deemded a packet to Drop, should bypass the [rate-limited] logging.
// We don't want to log scary & spammy reject warnings for packets that
// are totally normal, like IPv6 route announcements.
func omitDropLogging(p *packet.ParsedPacket, dir direction) bool {
b := p.Buffer()
switch dir {
case out:
switch p.IPVersion {
case 4:
// ParsedPacket.Decode zeros out ParsedPacket.IPProtocol for protocols
// it doesn't know about, so parse it out ourselves if needed.
ipProto := p.IPProto
if ipProto == 0 && len(b) > 8 {
ipProto = packet.IPProto(b[9])
}
// Omit logging about outgoing IGMP.
if ipProto == packet.IGMP {
return true
}
case 6:
if len(b) < 40 {
return false
}
src, dst := b[8:8+16], b[24:24+16]
// Omit logging for outgoing IPv6 ICMP-v6 queries to ff02::2,
// as sent by the OS, looking for routers.
if p.IPProto == packet.ICMPv6 {
if isLinkLocalV6(src) && string(dst) == ipv6AllRoutersLinkLocal {
return true
}
}
if string(dst) == ipv6AllMLDv2CapableRouters {
return true
}
// Actually, just catch all multicast.
if dst[0] == 0xff {
return true
}
}
}
return false
}
// isLinkLocalV6 reports whether src is in fe80::/10.
func isLinkLocalV6(src []byte) bool {
return len(src) == 16 && src[0] == 0xfe && src[1]>>6 == 0x80>>6
}

View File

@@ -6,7 +6,9 @@ package filter
import (
"encoding/binary"
"encoding/hex"
"encoding/json"
"strings"
"testing"
"tailscale.com/types/logger"
@@ -219,7 +221,7 @@ func TestPreFilter(t *testing.T) {
for _, testPacket := range packets {
p := &ParsedPacket{}
p.Decode(testPacket.b)
got := f.pre(p, LogDrops|LogAccepts)
got := f.pre(p, LogDrops|LogAccepts, in)
if got != testPacket.want {
t.Errorf("%q got=%v want=%v packet:\n%s", testPacket.desc, got, testPacket.want, packet.Hexdump(testPacket.b))
}
@@ -298,3 +300,63 @@ func rawdefault(proto packet.IPProto, trimLength int) []byte {
port := uint16(53)
return rawpacket(proto, ip, ip, port, port, trimLength)
}
func parseHexPkt(t *testing.T, h string) *packet.ParsedPacket {
t.Helper()
b, err := hex.DecodeString(strings.ReplaceAll(h, " ", ""))
if err != nil {
t.Fatalf("failed to read hex %q: %v", h, err)
}
p := new(packet.ParsedPacket)
p.Decode(b)
return p
}
func TestOmitDropLogging(t *testing.T) {
tests := []struct {
name string
pkt *packet.ParsedPacket
dir direction
want bool
}{
{
name: "v4_tcp_out",
pkt: &packet.ParsedPacket{IPVersion: 4, IPProto: packet.TCP},
dir: out,
want: false,
},
{
name: "v6_icmp_out", // as seen on Linux
pkt: parseHexPkt(t, "60 00 00 00 00 00 3a 00 fe800000000000000000000000000000 ff020000000000000000000000000002"),
dir: out,
want: true,
},
{
name: "v6_to_MLDv2_capable_routers", // as seen on Windows
pkt: parseHexPkt(t, "60 00 00 00 00 24 00 01 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ff 02 00 00 00 00 00 00 00 00 00 00 00 00 00 16 3a 00 05 02 00 00 01 00 8f 00 6e 80 00 00 00 01 04 00 00 00 ff 02 00 00 00 00 00 00 00 00 00 00 00 00 00 0c"),
dir: out,
want: true,
},
{
name: "v4_igmp_out", // on Windows, from https://github.com/tailscale/tailscale/issues/618
pkt: parseHexPkt(t, "46 00 00 30 37 3a 00 00 01 02 10 0e a9 fe 53 6b e0 00 00 16 94 04 00 00 22 00 14 05 00 00 00 02 04 00 00 00 e0 00 00 fb 04 00 00 00 e0 00 00 fc"),
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 {
t.Run(tt.name, func(t *testing.T) {
got := omitDropLogging(tt.pkt, tt.dir)
if got != tt.want {
t.Errorf("got %v; want %v\npacket: %#v\n%s", got, tt.want, tt.pkt, packet.Hexdump(tt.pkt.Buffer()))
}
})
}
}

View File

@@ -55,6 +55,43 @@ import (
"tailscale.com/version"
)
// Various debugging and experimental tweakables, set by environment
// variable.
var (
// logPacketDests prints the known addresses for a peer every time
// they change, in the legacy (non-discovery) endpoint code only.
logPacketDests, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_LOG_PACKET_DESTS"))
// debugDisco prints verbose logs of active discovery events as
// they happen.
debugDisco, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_DISCO"))
// debugOmitLocalAddresses removes all local interface addresses
// from magicsock's discovered local endpoints. Used in some tests.
debugOmitLocalAddresses, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_OMIT_LOCAL_ADDRS"))
// debugUseDerpRoute temporarily (2020-03-22) controls whether DERP
// reverse routing is enabled (Issue 150). It will become always true
// later.
debugUseDerpRoute, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_ENABLE_DERP_ROUTE"))
// logDerpVerbose logs all received DERP packets, including their
// full payload.
logDerpVerbose, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_DERP"))
// debugReSTUNStopOnIdle unconditionally enables the "shut down
// STUN if magicsock is idle" behavior that normally only triggers
// on mobile devices, lowers the shutdown interval, and logs more
// verbosely about idle measurements.
debugReSTUNStopOnIdle, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_RESTUN_STOP_ON_IDLE"))
)
// inTest reports whether the running program is a test that set the
// IN_TS_TEST environment variable.
//
// Unlike the other debug tweakables above, this one needs to be
// checked every time at runtime, because tests set this after program
// startup.
func inTest() bool {
inTest, _ := strconv.ParseBool(os.Getenv("IN_TS_TEST"))
return inTest
}
// A Conn routes UDP packets and actively manages a list of its endpoints.
// It implements wireguard/conn.Bind.
type Conn struct {
@@ -138,8 +175,9 @@ type Conn struct {
derpMap *tailcfg.DERPMap // nil (or zero regions/nodes) means DERP is disabled
netMap *controlclient.NetworkMap
privateKey key.Private
everHadKey bool // whether we ever had a non-zero private key
myDerp int // nearest DERP region ID; 0 means none/unknown
derpStarted chan struct{} // closed on first connection to DERP; for tests
derpStarted chan struct{} // closed on first connection to DERP; for tests & cleaner Close
activeDerp map[int]activeDerp // DERP regionID -> connection to a node in that region
prevDerp map[int]*syncs.WaitGroupChan
@@ -602,8 +640,6 @@ func (c *Conn) goDerpConnect(node int) {
go c.derpWriteChanOfAddr(netaddr.IPPort{IP: derpMagicIPAddr, Port: uint16(node)}, key.Public{})
}
var debugOmitLocalAddresses, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_OMIT_LOCAL_ADDRS"))
// determineEndpoints returns the machine's endpoint addresses. It
// does a STUN lookup (via netcheck) to determine its public address.
//
@@ -705,10 +741,6 @@ func shouldSprayPacket(b []byte) bool {
return false
}
var logPacketDests, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_LOG_PACKET_DESTS"))
var debugDisco, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_DISCO"))
const sprayPeriod = 3 * time.Second
// appendDests appends to dsts the destinations that b should be
@@ -933,11 +965,6 @@ func (c *Conn) sendAddr(addr netaddr.IPPort, pubKey key.Public, b []byte) (sent
// TODO: this is currently arbitrary. Figure out something better?
const bufferedDerpWritesBeforeDrop = 32
// debugUseDerpRoute temporarily (2020-03-22) controls whether DERP
// reverse routing is enabled (Issue 150). It will become always true
// later.
var debugUseDerpRoute, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_ENABLE_DERP_ROUTE"))
// derpWriteChanOfAddr returns a DERP client for fake UDP addresses that
// represent DERP servers, creating them as necessary. For real UDP
// addresses, it returns nil.
@@ -1006,6 +1033,11 @@ func (c *Conn) derpWriteChanOfAddr(addr netaddr.IPPort, peer key.Public) chan<-
// Note that derphttp.NewClient does not dial the server
// so it is safe to do under the mu lock.
dc := derphttp.NewRegionClient(c.privateKey, c.logf, func() *tailcfg.DERPRegion {
if c.connCtx.Err() != nil {
// If we're closing, don't try to acquire the lock.
// We might already be in Conn.Close and the Lock would deadlock.
return nil
}
c.mu.Lock()
defer c.mu.Unlock()
if c.derpMap == nil {
@@ -1108,8 +1140,6 @@ type derpReadResult struct {
copyBuf func(dst []byte) int
}
var logDerpVerbose, _ = strconv.ParseBool(os.Getenv("DEBUG_DERP_VERBOSE"))
// runDerpReader runs in a goroutine for the life of a DERP
// connection, handling received packets.
func (c *Conn) runDerpReader(ctx context.Context, derpFakeAddr netaddr.IPPort, dc *derphttp.Client, wg *syncs.WaitGroupChan, startGate <-chan struct{}) {
@@ -1232,8 +1262,11 @@ func (c *Conn) runDerpWriter(ctx context.Context, dc *derphttp.Client, ch <-chan
// The provided addr and ipp must match.
//
// TODO(bradfitz): add a fast path that returns nil here for normal
// wireguard-go transport packets; IIRC wireguard-go only uses this
// Endpoint for the relatively rare non-data packets.
// wireguard-go transport packets; wireguard-go only uses this
// Endpoint for the relatively rare non-data packets; but we need the
// Endpoint to find the UDPAddr to return to wireguard anyway, so no
// benefit unless we can, say, always return the same fake UDPAddr for
// all packets.
func (c *Conn) findEndpoint(ipp netaddr.IPPort, addr *net.UDPAddr) conn.Endpoint {
c.mu.Lock()
defer c.mu.Unlock()
@@ -1498,7 +1531,7 @@ func (c *Conn) sendDiscoMessage(dst netaddr.IPPort, dstKey tailcfg.NodeKey, dstD
c.mu.Lock()
if c.closed {
c.mu.Unlock()
return false, errClosed
return false, errConnClosed
}
var nonce [disco.NonceLen]byte
if _, err := crand.Read(nonce[:]); err != nil {
@@ -1554,6 +1587,10 @@ func (c *Conn) handleDiscoMessage(msg []byte, src netaddr.IPPort) bool {
if debugDisco {
c.logf("magicsock: disco: got disco-looking frame from %v", sender.ShortString())
}
if c.privateKey.IsZero() {
// Ignore disco messages when we're stopped.
return false
}
if c.discoPrivate.IsZero() {
if debugDisco {
c.logf("magicsock: disco: ignoring disco-looking frame, no local key")
@@ -1786,6 +1823,7 @@ func (c *Conn) SetPrivateKey(privateKey wgcfg.PrivateKey) error {
c.privateKey = newKey
if oldKey.IsZero() {
c.everHadKey = true
c.logf("magicsock: SetPrivateKey called (init)")
go c.ReSTUN("set-private-key")
} else if newKey.IsZero() {
@@ -1802,6 +1840,12 @@ func (c *Conn) SetPrivateKey(privateKey wgcfg.PrivateKey) error {
c.goDerpConnect(c.myDerp)
}
if newKey.IsZero() {
for _, de := range c.endpointOfDisco {
de.stopAndReset()
}
}
return nil
}
@@ -1903,7 +1947,6 @@ func (c *Conn) SetNetworkMap(nm *controlclient.NetworkMap) {
c.nodeOfDisco[n.DiscoKey] = n
if old, ok := c.discoOfNode[n.Key]; ok && old != n.DiscoKey {
c.logf("magicsock: node %s changed discovery key from %x to %x", n.Key.ShortString(), old[:8], n.DiscoKey[:8])
// TODO: reset AddrSet states, reset wireguard session key, etc.
}
c.discoOfNode[n.Key] = n.DiscoKey
}
@@ -1916,7 +1959,7 @@ func (c *Conn) SetNetworkMap(nm *controlclient.NetworkMap) {
// Clean c.endpointOfDisco for discovery keys that are no longer present.
for dk, de := range c.endpointOfDisco {
if _, ok := c.nodeOfDisco[dk]; !ok {
de.cleanup()
de.stopAndReset()
delete(c.endpointOfDisco, dk)
delete(c.sharedDiscoKey, dk)
}
@@ -2034,7 +2077,7 @@ func (c *Conn) Close() error {
defer c.mu.Unlock()
for _, ep := range c.endpointOfDisco {
ep.cleanup()
ep.stopAndReset()
}
c.closed = true
@@ -2065,8 +2108,6 @@ func (c *Conn) Close() error {
return err
}
var debugReSTUNStopOnIdle, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_RESTUN_STOP_ON_IDLE"))
func maxIdleBeforeSTUNShutdown() time.Duration {
if debugReSTUNStopOnIdle {
return time.Minute
@@ -2157,8 +2198,19 @@ func (c *Conn) ReSTUN(why string) {
// raced with a shutdown.
return
}
if c.privateKey.IsZero() {
c.logf("magicsock: ReSTUN(%q) ignored; no private key", why)
// If the user stopped the app, stop doing work. (When the
// user stops Tailscale via the GUI apps, ipn/local.go
// reconfigures the engine with a zero private key.)
//
// This used to just check c.privateKey.IsZero, but that broke
// some end-to-end tests tests that didn't ever set a private
// key somehow. So for now, only stop doing work if we ever
// had a key, which helps real users, but appeases tests for
// now. TODO: rewrite those tests to be less brittle or more
// realistic.
if c.privateKey.IsZero() && c.everHadKey {
c.logf("magicsock: ReSTUN(%q) ignored; stopped, no private key", why)
return
}
@@ -2192,7 +2244,7 @@ func (c *Conn) listenPacket(ctx context.Context, network, addr string) (net.Pack
func (c *Conn) bind1(ruc **RebindingUDPConn, which string) error {
host := ""
if v, _ := strconv.ParseBool(os.Getenv("IN_TS_TEST")); v {
if inTest() {
host = "127.0.0.1"
}
var pc net.PacketConn
@@ -2222,7 +2274,7 @@ func (c *Conn) bind1(ruc **RebindingUDPConn, which string) error {
// It should be followed by a call to ReSTUN.
func (c *Conn) Rebind() {
host := ""
if v, _ := strconv.ParseBool(os.Getenv("IN_TS_TEST")); v {
if inTest() {
host = "127.0.0.1"
}
listenCtx := context.Background() // unused without DNS name to resolve
@@ -2832,6 +2884,17 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) {
c.mu.Lock()
defer c.mu.Unlock()
if c.netMap != nil {
for _, addr := range c.netMap.Addresses {
if (addr.IP.Is4() && addr.Mask != 32) || (addr.IP.Is6() && addr.Mask != 128) {
continue
}
if ip, ok := netaddr.FromStdIP(addr.IP.IP()); ok {
sb.AddTailscaleIP(ip)
}
}
}
for dk, n := range c.nodeOfDisco {
ps := &ipnstate.PeerStatus{InMagicSock: true}
ps.Addrs = append(ps.Addrs, n.Endpoints...)
@@ -3384,14 +3447,27 @@ func (de *discoEndpoint) populatePeerStatus(ps *ipnstate.PeerStatus) {
}
}
// cleanup is called when a discovery endpoint is no longer present in the NetworkMap.
// This is where we can do cleanup such as closing goroutines or canceling timers.
func (de *discoEndpoint) cleanup() {
// stopAndReset stops timers associated with de and resets its state back to zero.
// It's called when a discovery endpoint is no longer present in the NetworkMap,
// or when magicsock is transition from running to stopped state (via SetPrivateKey(zero))
func (de *discoEndpoint) stopAndReset() {
de.mu.Lock()
defer de.mu.Unlock()
de.c.logf("magicsock: doing cleanup for discovery key %x", de.discoKey[:])
// Zero these fields so if the user re-starts the network, the discovery
// state isn't a mix of before & after two sessions.
de.lastSend = time.Time{}
de.lastFullPing = time.Time{}
de.bestAddr = netaddr.IPPort{}
de.bestAddrLatency = 0
de.bestAddrAt = time.Time{}
de.trustBestAddrUntil = time.Time{}
for _, es := range de.endpointState {
es.lastPing = time.Time{}
}
for txid, sp := range de.sentPing {
de.removeSentPingLocked(txid, sp)
}
@@ -3443,5 +3519,3 @@ type ippCacheKey struct {
// derpStr replaces DERP IPs in s with "derp-".
func derpStr(s string) string { return strings.ReplaceAll(s, "127.3.3.40:", "derp-") }
var errClosed = errors.New("conn is closed")

View File

@@ -6,6 +6,7 @@ package magicsock
import (
"bytes"
"context"
crand "crypto/rand"
"crypto/tls"
"encoding/binary"
@@ -26,9 +27,11 @@ import (
"github.com/tailscale/wireguard-go/wgcfg"
"golang.org/x/crypto/nacl/box"
"inet.af/netaddr"
"tailscale.com/control/controlclient"
"tailscale.com/derp"
"tailscale.com/derp/derphttp"
"tailscale.com/derp/derpmap"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/stun/stuntest"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
@@ -66,11 +69,6 @@ func runDERPAndStun(t *testing.T, logf logger.Logf, l nettype.PacketListener, st
t.Fatal(err)
}
d := derp.NewServer(serverPrivateKey, logf)
if l != (nettype.Std{}) {
// When using virtual networking, only allow DERP to forward
// discovery traffic, not actual packets.
d.OnlyDisco = true
}
httpsrv := httptest.NewUnstartedServer(derphttp.Handler(d))
httpsrv.Config.ErrorLog = logger.StdLogger(logf)
@@ -172,7 +170,7 @@ func newMagicStack(t *testing.T, logf logger.Logf, l nettype.PacketListener, der
// Wait for first endpoint update to be available
deadline := time.Now().Add(2 * time.Second)
for len(epCh) == 0 && time.Now().Before(deadline) {
time.Sleep(10 * time.Millisecond)
time.Sleep(100 * time.Millisecond)
}
return &magicStack{
@@ -185,11 +183,136 @@ func newMagicStack(t *testing.T, logf logger.Logf, l nettype.PacketListener, der
}
}
func (s *magicStack) String() string {
pub := s.Public()
return pub.ShortString()
}
func (s *magicStack) Close() {
s.dev.Close()
s.conn.Close()
}
func (s *magicStack) Public() key.Public {
return key.Public(s.privateKey.Public())
}
func (s *magicStack) Status() *ipnstate.Status {
var sb ipnstate.StatusBuilder
s.conn.UpdateStatus(&sb)
return sb.Status()
}
// IP returns the Tailscale IP address assigned to this magicStack.
//
// Something external needs to provide a NetworkMap and WireGuard
// configs to the magicStack in order for it to acquire an IP
// address. See meshStacks for one possible source of netmaps and IPs.
func (s *magicStack) IP(t *testing.T) netaddr.IP {
for deadline := time.Now().Add(5 * time.Second); time.Now().Before(deadline); time.Sleep(10 * time.Millisecond) {
st := s.Status()
if len(st.TailscaleIPs) > 0 {
return st.TailscaleIPs[0]
}
}
t.Fatal("timed out waiting for magicstack to get an IP assigned")
panic("unreachable") // compiler doesn't know t.Fatal panics
}
// meshStacks monitors epCh on all given ms, and plumbs network maps
// and WireGuard configs into everyone to form a full mesh that has up
// to date endpoint info. Think of it as an extremely stripped down
// and purpose-built Tailscale control plane.
//
// meshStacks only supports disco connections, not legacy logic.
func meshStacks(logf logger.Logf, ms []*magicStack) (cleanup func()) {
ctx, cancel := context.WithCancel(context.Background())
// Serialize all reconfigurations globally, just to keep things
// simpler.
var (
mu sync.Mutex
eps = make([][]string, len(ms))
)
buildNetmapLocked := func(myIdx int) *controlclient.NetworkMap {
me := ms[myIdx]
nm := &controlclient.NetworkMap{
PrivateKey: me.privateKey,
NodeKey: tailcfg.NodeKey(me.privateKey.Public()),
Addresses: []wgcfg.CIDR{{IP: wgcfg.IPv4(1, 0, 0, byte(myIdx+1)), Mask: 32}},
}
for i, peer := range ms {
if i == myIdx {
continue
}
addrs := []wgcfg.CIDR{{IP: wgcfg.IPv4(1, 0, 0, byte(i+1)), Mask: 32}}
peer := &tailcfg.Node{
ID: tailcfg.NodeID(i + 1),
Name: fmt.Sprintf("node%d", i+1),
Key: tailcfg.NodeKey(peer.privateKey.Public()),
DiscoKey: peer.conn.DiscoPublicKey(),
Addresses: addrs,
AllowedIPs: addrs,
Endpoints: eps[i],
DERP: "127.3.3.40:1",
}
nm.Peers = append(nm.Peers, peer)
}
return nm
}
updateEps := func(idx int, newEps []string) {
mu.Lock()
defer mu.Unlock()
eps[idx] = newEps
for i, m := range ms {
netmap := buildNetmapLocked(i)
m.conn.SetNetworkMap(netmap)
peerSet := make(map[key.Public]struct{}, len(netmap.Peers))
for _, peer := range netmap.Peers {
peerSet[key.Public(peer.Key)] = struct{}{}
}
m.conn.UpdatePeers(peerSet)
wg, err := netmap.WGCfg(logf, controlclient.AllowSingleHosts)
if err != nil {
// We're too far from the *testing.T to be graceful,
// blow up. Shouldn't happen anyway.
panic(fmt.Sprintf("failed to construct wgcfg from netmap: %v", err))
}
if err := m.dev.Reconfig(wg); err != nil {
panic(fmt.Sprintf("device reconfig failed: %v", err))
}
}
}
var wg sync.WaitGroup
wg.Add(len(ms))
for i := range ms {
go func(myIdx int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case eps := <-ms[myIdx].epCh:
logf("conn%d endpoints update", myIdx+1)
updateEps(myIdx, eps)
}
}
}(i)
}
return func() {
cancel()
wg.Wait()
}
}
func TestNewConn(t *testing.T) {
tstest.PanicOnLog()
rc := tstest.NewResourceCheck()
@@ -433,65 +556,136 @@ func makeNestable(t *testing.T) (logf logger.Logf, setT func(t *testing.T)) {
}
func TestTwoDevicePing(t *testing.T) {
t.Run("real", func(t *testing.T) {
l, ip := nettype.Std{}, netaddr.IPv4(127, 0, 0, 1)
l, ip := nettype.Std{}, netaddr.IPv4(127, 0, 0, 1)
n := &devices{
m1: l,
m1IP: ip,
m2: l,
m2IP: ip,
stun: l,
stunIP: ip,
}
testTwoDevicePing(t, n)
}
func TestActiveDiscovery(t *testing.T) {
t.Run("simple_internet", func(t *testing.T) {
t.Parallel()
mstun := &natlab.Machine{Name: "stun"}
m1 := &natlab.Machine{Name: "m1"}
m2 := &natlab.Machine{Name: "m2"}
inet := natlab.NewInternet()
sif := mstun.Attach("eth0", inet)
m1if := m1.Attach("eth0", inet)
m2if := m2.Attach("eth0", inet)
n := &devices{
m1: l,
m1IP: ip,
m2: l,
m2IP: ip,
stun: l,
stunIP: ip,
m1: m1,
m1IP: m1if.V4(),
m2: m2,
m2IP: m2if.V4(),
stun: mstun,
stunIP: sif.V4(),
}
testTwoDevicePing(t, n)
testActiveDiscovery(t, n)
})
t.Run("natlab", func(t *testing.T) {
t.Run("simple internet", func(t *testing.T) {
mstun := &natlab.Machine{Name: "stun"}
m1 := &natlab.Machine{Name: "m1"}
m2 := &natlab.Machine{Name: "m2"}
inet := natlab.NewInternet()
sif := mstun.Attach("eth0", inet)
m1if := m1.Attach("eth0", inet)
m2if := m2.Attach("eth0", inet)
n := &devices{
m1: m1,
m1IP: m1if.V4(),
m2: m2,
m2IP: m2if.V4(),
stun: mstun,
stunIP: sif.V4(),
}
testTwoDevicePing(t, n)
})
t.Run("facing_easy_firewalls", func(t *testing.T) {
mstun := &natlab.Machine{Name: "stun"}
m1 := &natlab.Machine{
Name: "m1",
PacketHandler: &natlab.Firewall{},
}
m2 := &natlab.Machine{
Name: "m2",
PacketHandler: &natlab.Firewall{},
}
inet := natlab.NewInternet()
sif := mstun.Attach("eth0", inet)
m1if := m1.Attach("eth0", inet)
m2if := m2.Attach("eth0", inet)
t.Run("facing firewalls", func(t *testing.T) {
mstun := &natlab.Machine{Name: "stun"}
m1 := &natlab.Machine{
Name: "m1",
PacketHandler: &natlab.Firewall{},
}
m2 := &natlab.Machine{
Name: "m2",
PacketHandler: &natlab.Firewall{},
}
inet := natlab.NewInternet()
sif := mstun.Attach("eth0", inet)
m1if := m1.Attach("eth0", inet)
m2if := m2.Attach("eth0", inet)
n := &devices{
m1: m1,
m1IP: m1if.V4(),
m2: m2,
m2IP: m2if.V4(),
stun: mstun,
stunIP: sif.V4(),
}
testTwoDevicePing(t, n)
})
n := &devices{
m1: m1,
m1IP: m1if.V4(),
m2: m2,
m2IP: m2if.V4(),
stun: mstun,
stunIP: sif.V4(),
}
testActiveDiscovery(t, n)
})
t.Run("facing_nats", func(t *testing.T) {
mstun := &natlab.Machine{Name: "stun"}
m1 := &natlab.Machine{
Name: "m1",
PacketHandler: &natlab.Firewall{},
}
nat1 := &natlab.Machine{
Name: "nat1",
}
m2 := &natlab.Machine{
Name: "m2",
PacketHandler: &natlab.Firewall{},
}
nat2 := &natlab.Machine{
Name: "nat2",
}
inet := natlab.NewInternet()
lan1 := &natlab.Network{
Name: "lan1",
Prefix4: mustPrefix("192.168.0.0/24"),
}
lan2 := &natlab.Network{
Name: "lan2",
Prefix4: mustPrefix("192.168.1.0/24"),
}
sif := mstun.Attach("eth0", inet)
nat1WAN := nat1.Attach("wan", inet)
nat1LAN := nat1.Attach("lan1", lan1)
nat2WAN := nat2.Attach("wan", inet)
nat2LAN := nat2.Attach("lan2", lan2)
m1if := m1.Attach("eth0", lan1)
m2if := m2.Attach("eth0", lan2)
lan1.SetDefaultGateway(nat1LAN)
lan2.SetDefaultGateway(nat2LAN)
nat1.PacketHandler = &natlab.SNAT44{
Machine: nat1,
ExternalInterface: nat1WAN,
Firewall: &natlab.Firewall{
TrustedInterface: nat1LAN,
},
}
nat2.PacketHandler = &natlab.SNAT44{
Machine: nat2,
ExternalInterface: nat2WAN,
Firewall: &natlab.Firewall{
TrustedInterface: nat2LAN,
},
}
n := &devices{
m1: m1,
m1IP: m1if.V4(),
m2: m2,
m2IP: m2if.V4(),
stun: mstun,
stunIP: sif.V4(),
}
testActiveDiscovery(t, n)
})
}
func mustPrefix(s string) netaddr.IPPrefix {
pfx, err := netaddr.ParseIPPrefix(s)
if err != nil {
panic(err)
}
return pfx
}
type devices struct {
@@ -505,6 +699,131 @@ type devices struct {
stunIP netaddr.IP
}
// newPinger starts continuously sending test packets from srcM to
// dstM, until cleanup is invoked to stop it. Each ping has 1 second
// to transit the network. It is a test failure to lose a ping.
func newPinger(t *testing.T, logf logger.Logf, src, dst *magicStack) (cleanup func()) {
ctx, cancel := context.WithCancel(context.Background())
done := make(chan struct{})
one := func() bool {
// TODO(danderson): requiring exactly zero packet loss
// will probably be too strict for some tests we'd like to
// run (e.g. discovery switching to a new path on
// failure). Figure out what kind of thing would be
// acceptable to test instead of "every ping must
// transit".
pkt := tuntest.Ping(dst.IP(t).IPAddr().IP, src.IP(t).IPAddr().IP)
select {
case src.tun.Outbound <- pkt:
case <-ctx.Done():
return false
}
select {
case <-dst.tun.Inbound:
return true
case <-time.After(10 * time.Second):
// Very generous timeout here because depending on
// magicsock setup races, the first handshake might get
// eaten by the receiving end (if wireguard-go hasn't been
// configured quite yet), so we have to wait for at least
// the first retransmit from wireguard before we declare
// failure.
t.Errorf("timed out waiting for ping to transit")
return true
case <-ctx.Done():
// Try a little bit longer to consume the packet we're
// waiting for. This is to deal with shutdown races, where
// natlab may still be delivering a packet to us from a
// goroutine.
select {
case <-dst.tun.Inbound:
case <-time.After(time.Second):
}
return false
}
}
cleanup = func() {
cancel()
<-done
}
// Synchronously transit one ping to get things started. This is
// nice because it means that newPinger returning means we've
// worked through initial connectivity.
if !one() {
cleanup()
return
}
go func() {
logf("sending ping stream from %s (%s) to %s (%s)", src, src.IP(t), dst, dst.IP(t))
defer close(done)
for one() {
}
}()
return cleanup
}
// testActiveDiscovery verifies that two magicStacks tied to the given
// devices can establish a direct p2p connection with each other. See
// TestActiveDiscovery for the various configurations of devices that
// get exercised.
func testActiveDiscovery(t *testing.T, d *devices) {
tstest.PanicOnLog()
rc := tstest.NewResourceCheck()
defer rc.Assert(t)
tlogf, setT := makeNestable(t)
setT(t)
start := time.Now()
logf := func(msg string, args ...interface{}) {
msg = fmt.Sprintf("%s: %s", time.Since(start), msg)
tlogf(msg, args...)
}
derpMap, cleanup := runDERPAndStun(t, logf, d.stun, d.stunIP)
defer cleanup()
m1 := newMagicStack(t, logger.WithPrefix(logf, "conn1: "), d.m1, derpMap)
defer m1.Close()
m2 := newMagicStack(t, logger.WithPrefix(logf, "conn2: "), d.m2, derpMap)
defer m2.Close()
cleanup = meshStacks(logf, []*magicStack{m1, m2})
defer cleanup()
m1IP := m1.IP(t)
m2IP := m2.IP(t)
logf("IPs: %s %s", m1IP, m2IP)
cleanup = newPinger(t, logf, m1, m2)
defer cleanup()
// Everything is now up and running, active discovery should find
// a direct path between our peers. Wait for it to switch away
// from DERP.
mustDirect := func(m1, m2 *magicStack) {
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)
}
t.Errorf("magicsock did not find a direct path from %s to %s", m1, m2)
}
mustDirect(m1, m2)
mustDirect(m2, m1)
logf("starting cleanup")
}
func testTwoDevicePing(t *testing.T, d *devices) {
tstest.PanicOnLog()
rc := tstest.NewResourceCheck()
@@ -943,6 +1262,7 @@ func initAddrSet(as *AddrSet) {
func TestDiscoMessage(t *testing.T) {
c := newConn()
c.logf = t.Logf
c.privateKey = key.NewPrivate()
peer1Pub := c.DiscoPublicKey()
peer1Priv := c.discoPrivate

View File

@@ -47,12 +47,13 @@ const (
// Unknown represents an unknown or unsupported protocol; it's deliberately the zero value.
Unknown IPProto = 0x00
ICMP IPProto = 0x01
IGMP IPProto = 0x02
ICMPv6 IPProto = 0x3a
TCP IPProto = 0x06
UDP IPProto = 0x11
// IPv6 and Fragment are special values. They're not really IPProto values
// so we're using the unassigned 0xFE and 0xFF values for them.
// Fragment is a special value. It's not really an IPProto value
// so we're using the unassigned 0xFF value.
// TODO(dmytro): special values should be taken out of here.
IPv6 IPProto = 0xFE
Fragment IPProto = 0xFF
)
@@ -66,8 +67,6 @@ func (p IPProto) String() string {
return "UDP"
case TCP:
return "TCP"
case IPv6:
return "IPv6"
default:
return "Unknown"
}

View File

@@ -30,6 +30,8 @@ var (
)
// ParsedPacket is a minimal decoding of a packet suitable for use in filters.
//
// In general, it only supports IPv4. The IPv6 parsing is very minimal.
type ParsedPacket struct {
// b is the byte buffer that this decodes.
b []byte
@@ -41,27 +43,32 @@ type ParsedPacket struct {
// This is not the same as len(b) because b can have trailing zeros.
length int
IPProto IPProto // IP subprotocol (UDP, TCP, etc)
SrcIP IP // IP source address
DstIP IP // IP destination address
SrcPort uint16 // TCP/UDP source port
DstPort uint16 // TCP/UDP destination port
TCPFlags uint8 // TCP flags (SYN, ACK, etc)
IPVersion uint8 // 4, 6, or 0
IPProto IPProto // IP subprotocol (UDP, TCP, etc); the NextHeader field for IPv6
SrcIP IP // IP source address (not used for IPv6)
DstIP IP // IP destination address (not used for IPv6)
SrcPort uint16 // TCP/UDP source port
DstPort uint16 // TCP/UDP destination port
TCPFlags uint8 // TCP flags (SYN, ACK, etc)
}
func (q *ParsedPacket) String() string {
switch q.IPProto {
case IPv6:
return "IPv6{???}"
// NextHeader
type NextHeader uint8
func (p *ParsedPacket) String() string {
if p.IPVersion == 6 {
return fmt.Sprintf("IPv6{Proto=%d}", p.IPProto)
}
switch p.IPProto {
case Unknown:
return "Unknown{???}"
}
sb := strbuilder.Get()
sb.WriteString(q.IPProto.String())
sb.WriteString(p.IPProto.String())
sb.WriteByte('{')
writeIPPort(sb, q.SrcIP, q.SrcPort)
writeIPPort(sb, p.SrcIP, p.SrcPort)
sb.WriteString(" > ")
writeIPPort(sb, q.DstIP, q.DstPort)
writeIPPort(sb, p.DstIP, p.DstPort)
sb.WriteByte('}')
return sb.String()
}
@@ -105,20 +112,22 @@ func (q *ParsedPacket) Decode(b []byte) {
q.b = b
if len(b) < ipHeaderLength {
q.IPVersion = 0
q.IPProto = Unknown
return
}
// Check that it's IPv4.
// TODO(apenwarr): consider IPv6 support
switch (b[0] & 0xF0) >> 4 {
q.IPVersion = (b[0] & 0xF0) >> 4
switch q.IPVersion {
case 4:
q.IPProto = IPProto(b[9])
// continue
case 6:
q.IPProto = IPv6
q.IPProto = IPProto(b[6]) // "Next Header" field
return
default:
q.IPVersion = 0
q.IPProto = Unknown
return
}

View File

@@ -47,11 +47,12 @@ var icmpRequestDecode = ParsedPacket{
dataofs: 24,
length: len(icmpRequestBuffer),
IPProto: ICMP,
SrcIP: NewIP(net.ParseIP("1.2.3.4")),
DstIP: NewIP(net.ParseIP("5.6.7.8")),
SrcPort: 0,
DstPort: 0,
IPVersion: 4,
IPProto: ICMP,
SrcIP: NewIP(net.ParseIP("1.2.3.4")),
DstIP: NewIP(net.ParseIP("5.6.7.8")),
SrcPort: 0,
DstPort: 0,
}
var icmpReplyBuffer = []byte{
@@ -72,11 +73,12 @@ var icmpReplyDecode = ParsedPacket{
dataofs: 24,
length: len(icmpReplyBuffer),
IPProto: ICMP,
SrcIP: NewIP(net.ParseIP("1.2.3.4")),
DstIP: NewIP(net.ParseIP("5.6.7.8")),
SrcPort: 0,
DstPort: 0,
IPVersion: 4,
IPProto: ICMP,
SrcIP: NewIP(net.ParseIP("1.2.3.4")),
DstIP: NewIP(net.ParseIP("5.6.7.8")),
SrcPort: 0,
DstPort: 0,
}
// IPv6 Router Solicitation
@@ -90,8 +92,9 @@ var ipv6PacketBuffer = []byte{
}
var ipv6PacketDecode = ParsedPacket{
b: ipv6PacketBuffer,
IPProto: IPv6,
b: ipv6PacketBuffer,
IPVersion: 6,
IPProto: ICMPv6,
}
// This is a malformed IPv4 packet.
@@ -101,8 +104,9 @@ var unknownPacketBuffer = []byte{
}
var unknownPacketDecode = ParsedPacket{
b: unknownPacketBuffer,
IPProto: Unknown,
b: unknownPacketBuffer,
IPVersion: 0,
IPProto: Unknown,
}
var tcpPacketBuffer = []byte{
@@ -125,12 +129,13 @@ var tcpPacketDecode = ParsedPacket{
dataofs: 40,
length: len(tcpPacketBuffer),
IPProto: TCP,
SrcIP: NewIP(net.ParseIP("1.2.3.4")),
DstIP: NewIP(net.ParseIP("5.6.7.8")),
SrcPort: 123,
DstPort: 567,
TCPFlags: TCPSynAck,
IPVersion: 4,
IPProto: TCP,
SrcIP: NewIP(net.ParseIP("1.2.3.4")),
DstIP: NewIP(net.ParseIP("5.6.7.8")),
SrcPort: 123,
DstPort: 567,
TCPFlags: TCPSynAck,
}
var udpRequestBuffer = []byte{
@@ -152,11 +157,12 @@ var udpRequestDecode = ParsedPacket{
dataofs: 28,
length: len(udpRequestBuffer),
IPProto: UDP,
SrcIP: NewIP(net.ParseIP("1.2.3.4")),
DstIP: NewIP(net.ParseIP("5.6.7.8")),
SrcPort: 123,
DstPort: 567,
IPVersion: 4,
IPProto: UDP,
SrcIP: NewIP(net.ParseIP("1.2.3.4")),
DstIP: NewIP(net.ParseIP("5.6.7.8")),
SrcPort: 123,
DstPort: 567,
}
var udpReplyBuffer = []byte{
@@ -194,7 +200,7 @@ func TestParsedPacket(t *testing.T) {
{"tcp", tcpPacketDecode, "TCP{1.2.3.4:123 > 5.6.7.8:567}"},
{"icmp", icmpRequestDecode, "ICMP{1.2.3.4:0 > 5.6.7.8:0}"},
{"unknown", unknownPacketDecode, "Unknown{???}"},
{"ipv6", ipv6PacketDecode, "IPv6{???}"},
{"ipv6", ipv6PacketDecode, "IPv6{Proto=58}"},
}
for _, tt := range tests {
@@ -234,7 +240,7 @@ func TestDecode(t *testing.T) {
var got ParsedPacket
got.Decode(tt.buf)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("got %v; want %v", got, tt.want)
t.Errorf("mismatch\n got: %#v\nwant: %#v", got, tt.want)
}
})
}

View File

@@ -1,74 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package router
import (
"inet.af/netaddr"
)
// DNSConfig is the subset of Config that contains DNS parameters.
type DNSConfig struct {
// Nameservers are the IP addresses of the nameservers to use.
Nameservers []netaddr.IP
// Domains are the search domains to use.
Domains []string
}
// EquivalentTo determines whether its argument and receiver
// represent equivalent DNS configurations (then DNS reconfig is a no-op).
func (lhs DNSConfig) EquivalentTo(rhs DNSConfig) bool {
if len(lhs.Nameservers) != len(rhs.Nameservers) {
return false
}
if len(lhs.Domains) != len(rhs.Domains) {
return false
}
// With how we perform resolution order shouldn't matter,
// but it is unlikely that we will encounter different orders.
for i, server := range lhs.Nameservers {
if rhs.Nameservers[i] != server {
return false
}
}
for i, domain := range lhs.Domains {
if rhs.Domains[i] != domain {
return false
}
}
return true
}
// dnsMode determines how DNS settings are managed.
type dnsMode uint8
const (
// dnsDirect indicates that /etc/resolv.conf is edited directly.
dnsDirect dnsMode = iota
// dnsResolvconf indicates that a resolvconf binary is used.
dnsResolvconf
// dnsNetworkManager indicates that the NetworkManaer DBus API is used.
dnsNetworkManager
// dnsResolved indicates that the systemd-resolved DBus API is used.
dnsResolved
)
func (m dnsMode) String() string {
switch m {
case dnsDirect:
return "direct"
case dnsResolvconf:
return "resolvconf"
case dnsNetworkManager:
return "networkmanager"
case dnsResolved:
return "resolved"
default:
return "???"
}
}

View File

@@ -0,0 +1,75 @@
// 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 dns
import (
"inet.af/netaddr"
"tailscale.com/types/logger"
)
// Config is the set of parameters that uniquely determine
// the state to which a manager should bring system DNS settings.
type Config struct {
// Nameservers are the IP addresses of the nameservers to use.
Nameservers []netaddr.IP
// Domains are the search domains to use.
Domains []string
// PerDomain indicates whether it is preferred to use Nameservers
// only for DNS queries for subdomains of Domains.
// Note that Nameservers may still be applied to all queries
// if the manager does not support per-domain settings.
PerDomain bool
// Proxied indicates whether DNS requests are proxied through a tsdns.Resolver.
Proxied bool
}
// Equal determines whether its argument and receiver
// represent equivalent DNS configurations (then DNS reconfig is a no-op).
func (lhs Config) Equal(rhs Config) bool {
if lhs.Proxied != rhs.Proxied || lhs.PerDomain != rhs.PerDomain {
return false
}
if len(lhs.Nameservers) != len(rhs.Nameservers) {
return false
}
if len(lhs.Domains) != len(rhs.Domains) {
return false
}
// With how we perform resolution order shouldn't matter,
// but it is unlikely that we will encounter different orders.
for i, server := range lhs.Nameservers {
if rhs.Nameservers[i] != server {
return false
}
}
// The order of domains, on the other hand, is significant.
for i, domain := range lhs.Domains {
if rhs.Domains[i] != domain {
return false
}
}
return true
}
// ManagerConfig is the set of parameters from which
// a manager implementation is chosen and initialized.
type ManagerConfig struct {
// logf is the logger for the manager to use.
Logf logger.Logf
// InterfaceNAme is the name of the interface with which DNS settings should be associated.
InterfaceName string
// Cleanup indicates that the manager is created for cleanup only.
// A no-op manager will be instantiated if the system needs no cleanup.
Cleanup bool
// PerDomain indicates that a manager capable of per-domain configuration is preferred.
// Certain managers are per-domain only; they will not be considered if this is false.
PerDomain bool
}

View File

@@ -4,7 +4,7 @@
// +build linux freebsd openbsd
package router
package dns
import (
"bufio"
@@ -13,6 +13,8 @@ import (
"io"
"io/ioutil"
"os"
"os/exec"
"runtime"
"strings"
"inet.af/netaddr"
@@ -25,8 +27,8 @@ const (
resolvConf = "/etc/resolv.conf"
)
// dnsWriteConfig writes DNS configuration in resolv.conf format to the given writer.
func dnsWriteConfig(w io.Writer, servers []netaddr.IP, domains []string) {
// writeResolvConf writes DNS configuration in resolv.conf format to the given writer.
func writeResolvConf(w io.Writer, servers []netaddr.IP, domains []string) {
io.WriteString(w, "# resolv.conf(5) file generated by tailscale\n")
io.WriteString(w, "# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN\n\n")
for _, ns := range servers {
@@ -44,9 +46,9 @@ func dnsWriteConfig(w io.Writer, servers []netaddr.IP, domains []string) {
}
}
// dnsReadConfig reads DNS configuration from /etc/resolv.conf.
func dnsReadConfig() (DNSConfig, error) {
var config DNSConfig
// readResolvConf reads DNS configuration from /etc/resolv.conf.
func readResolvConf() (Config, error) {
var config Config
f, err := os.Open("/etc/resolv.conf")
if err != nil {
@@ -79,17 +81,43 @@ func dnsReadConfig() (DNSConfig, error) {
return config, nil
}
// dnsDirectUp replaces /etc/resolv.conf with a file generated
// from the given configuration, creating a backup of its old state.
// isResolvedRunning reports whether systemd-resolved is running on the system,
// even if it is not managing the system DNS settings.
func isResolvedRunning() bool {
if runtime.GOOS != "linux" {
return false
}
// systemd-resolved is never installed without systemd.
_, err := exec.LookPath("systemctl")
if err != nil {
return false
}
// is-active exits with code 3 if the service is not active.
err = exec.Command("systemctl", "is-active", "systemd-resolved.service").Run()
return err == nil
}
// directManager is a managerImpl which replaces /etc/resolv.conf with a file
// generated from the given configuration, creating a backup of its old state.
//
// This way of configuring DNS is precarious, since it does not react
// to the disappearance of the Tailscale interface.
// The caller must call dnsDirectDown before program shutdown
// and ensure that router.Cleanup is run if the program terminates unexpectedly.
func dnsDirectUp(config DNSConfig) error {
// The caller must call Down before program shutdown
// or as cleanup if the program terminates unexpectedly.
type directManager struct{}
func newDirectManager(mconfig ManagerConfig) managerImpl {
return directManager{}
}
// Up implements managerImpl.
func (m directManager) Up(config Config) error {
// Write the tsConf file.
buf := new(bytes.Buffer)
dnsWriteConfig(buf, config.Nameservers, config.Domains)
writeResolvConf(buf, config.Nameservers, config.Domains)
if err := atomicfile.WriteFile(tsConf, buf.Bytes(), 0644); err != nil {
return err
}
@@ -124,12 +152,15 @@ func dnsDirectUp(config DNSConfig) error {
return err
}
if isResolvedRunning() {
exec.Command("systemctl", "restart", "systemd-resolved.service").Run() // Best-effort.
}
return nil
}
// dnsDirectDown restores /etc/resolv.conf to its state before dnsDirectUp.
// It is idempotent and behaves correctly even if dnsDirectUp has never been run.
func dnsDirectDown() error {
// Down implements managerImpl.
func (m directManager) Down() error {
if _, err := os.Stat(backupConf); err != nil {
// If the backup file does not exist, then Up never ran successfully.
if os.IsNotExist(err) {
@@ -147,5 +178,10 @@ func dnsDirectDown() error {
return err
}
os.Remove(tsConf)
if isResolvedRunning() {
exec.Command("systemctl", "restart", "systemd-resolved.service").Run() // Best-effort.
}
return nil
}

View File

@@ -0,0 +1,94 @@
// 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 dns
import (
"time"
"tailscale.com/types/logger"
)
// reconfigTimeout is the time interval within which Manager.{Up,Down} should complete.
//
// This is particularly useful because certain conditions can cause indefinite hangs
// (such as improper dbus auth followed by contextless dbus.Object.Call).
// Such operations should be wrapped in a timeout context.
const reconfigTimeout = time.Second
type managerImpl interface {
// Up updates system DNS settings to match the given configuration.
Up(Config) error
// Down undoes the effects of Up.
// It is idempotent and performs no action if Up has never been called.
Down() error
}
// Manager manages system DNS settings.
type Manager struct {
logf logger.Logf
impl managerImpl
config Config
mconfig ManagerConfig
}
// NewManagers created a new manager from the given config.
func NewManager(mconfig ManagerConfig) *Manager {
mconfig.Logf = logger.WithPrefix(mconfig.Logf, "dns: ")
m := &Manager{
logf: mconfig.Logf,
impl: newManager(mconfig),
config: Config{PerDomain: mconfig.PerDomain},
mconfig: mconfig,
}
m.logf("using %T", m.impl)
return m
}
func (m *Manager) Set(config Config) error {
if config.Equal(m.config) {
return nil
}
m.logf("Set: %+v", config)
if len(config.Nameservers) == 0 {
err := m.impl.Down()
// If we save the config, we will not retry next time. Only do this on success.
if err == nil {
m.config = config
}
return err
}
// Switching to and from per-domain mode may require a change of manager.
if config.PerDomain != m.config.PerDomain {
if err := m.impl.Down(); err != nil {
return err
}
m.mconfig.PerDomain = config.PerDomain
m.impl = newManager(m.mconfig)
m.logf("switched to %T", m.impl)
}
err := m.impl.Up(config)
// If we save the config, we will not retry next time. Only do this on success.
if err == nil {
m.config = config
}
return err
}
func (m *Manager) Up() error {
return m.impl.Up(m.config)
}
func (m *Manager) Down() error {
return m.impl.Down()
}

View File

@@ -0,0 +1,14 @@
// 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 !linux,!freebsd,!openbsd,!windows
package dns
func newManager(mconfig ManagerConfig) managerImpl {
// TODO(dmytro): on darwin, we should use a macOS-specific method such as scutil.
// This is currently not implemented. Editing /etc/resolv.conf does not work,
// as most applications use the system resolver, which disregards it.
return newNoopManager(mconfig)
}

View File

@@ -0,0 +1,14 @@
// 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 dns
func newManager(mconfig ManagerConfig) managerImpl {
switch {
case isResolvconfActive():
return newResolvconfManager(mconfig)
default:
return newDirectManager(mconfig)
}
}

View File

@@ -0,0 +1,27 @@
// 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 dns
func newManager(mconfig ManagerConfig) managerImpl {
switch {
// systemd-resolved should only activate per-domain.
case isResolvedActive() && mconfig.PerDomain:
if mconfig.Cleanup {
return newNoopManager(mconfig)
} else {
return newResolvedManager(mconfig)
}
case isNMActive():
if mconfig.Cleanup {
return newNoopManager(mconfig)
} else {
return newNMManager(mconfig)
}
case isResolvconfActive():
return newResolvconfManager(mconfig)
default:
return newDirectManager(mconfig)
}
}

View File

@@ -0,0 +1,9 @@
// 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 dns
func newManager(mconfig ManagerConfig) managerImpl {
return newDirectManager(mconfig)
}

View File

@@ -0,0 +1,83 @@
// 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 dns
import (
"fmt"
"strings"
"github.com/tailscale/wireguard-go/tun"
"golang.org/x/sys/windows/registry"
"tailscale.com/types/logger"
)
type windowsManager struct {
logf logger.Logf
guid string
}
func newManager(mconfig ManagerConfig) managerImpl {
return windowsManager{
logf: mconfig.Logf,
guid: tun.WintunGUID,
}
}
func setRegistry(path, nameservers, domains string) error {
key, err := registry.OpenKey(registry.LOCAL_MACHINE, path, registry.READ|registry.SET_VALUE)
if err != nil {
return fmt.Errorf("opening %s: %w", path, err)
}
defer key.Close()
err = key.SetStringValue("NameServer", nameservers)
if err != nil {
return fmt.Errorf("setting %s/NameServer: %w", path, err)
}
err = key.SetStringValue("Domain", domains)
if err != nil {
return fmt.Errorf("setting %s/Domain: %w", path, err)
}
return nil
}
func (m windowsManager) Up(config Config) error {
var ipsv4 []string
var ipsv6 []string
for _, ip := range config.Nameservers {
if ip.Is4() {
ipsv4 = append(ipsv4, ip.String())
} else {
ipsv6 = append(ipsv6, ip.String())
}
}
nsv4 := strings.Join(ipsv4, ",")
nsv6 := strings.Join(ipsv6, ",")
var domains string
if len(config.Domains) > 0 {
if len(config.Domains) > 1 {
m.logf("only a single search domain is supported")
}
domains = config.Domains[0]
}
v4Path := `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\` + m.guid
if err := setRegistry(v4Path, nsv4, domains); err != nil {
return err
}
v6Path := `SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters\Interfaces\` + m.guid
if err := setRegistry(v6Path, nsv6, domains); err != nil {
return err
}
return nil
}
func (m windowsManager) Down() error {
return m.Up(Config{Nameservers: nil, Domains: nil})
}

View File

@@ -4,7 +4,7 @@
// +build linux
package router
package dns
import (
"bufio"
@@ -20,8 +20,8 @@ import (
type nmConnectionSettings map[string]map[string]dbus.Variant
// nmIsActive determines if NetworkManager is currently managing system DNS settings.
func nmIsActive() bool {
// isNMActive determines if NetworkManager is currently managing system DNS settings.
func isNMActive() bool {
// This is somewhat tricky because NetworkManager supports a number
// of DNS configuration modes. In all cases, we expect it to be installed
// and /etc/resolv.conf to contain a mention of NetworkManager in the comments.
@@ -50,10 +50,20 @@ func nmIsActive() bool {
return false
}
// dnsNetworkManagerUp updates the DNS config for the Tailscale interface
// through the NetworkManager DBus API.
func dnsNetworkManagerUp(config DNSConfig, interfaceName string) error {
ctx, cancel := context.WithTimeout(context.Background(), dnsReconfigTimeout)
// nmManager uses the NetworkManager DBus API.
type nmManager struct {
interfaceName string
}
func newNMManager(mconfig ManagerConfig) managerImpl {
return nmManager{
interfaceName: mconfig.InterfaceName,
}
}
// Up implements managerImpl.
func (m nmManager) Up(config Config) error {
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
defer cancel()
// conn is a shared connection whose lifecycle is managed by the dbus package.
@@ -90,7 +100,7 @@ func dnsNetworkManagerUp(config DNSConfig, interfaceName string) error {
var devicePath dbus.ObjectPath
err = nm.CallWithContext(
ctx, "org.freedesktop.NetworkManager.GetDeviceByIpIface", 0,
interfaceName,
m.interfaceName,
).Store(&devicePath)
if err != nil {
return fmt.Errorf("getDeviceByIpIface: %w", err)
@@ -189,7 +199,7 @@ func dnsNetworkManagerUp(config DNSConfig, interfaceName string) error {
return nil
}
// dnsNetworkManagerDown undoes the changes made by dnsNetworkManagerUp.
func dnsNetworkManagerDown(interfaceName string) error {
return dnsNetworkManagerUp(DNSConfig{Nameservers: nil, Domains: nil}, interfaceName)
// Down implements managerImpl.
func (m nmManager) Down() error {
return m.Up(Config{Nameservers: nil, Domains: nil})
}

View File

@@ -0,0 +1,17 @@
// 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 dns
type noopManager struct{}
// Up implements managerImpl.
func (m noopManager) Up(Config) error { return nil }
// Down implements managerImpl.
func (m noopManager) Down() error { return nil }
func newNoopManager(mconfig ManagerConfig) managerImpl {
return noopManager{}
}

View File

@@ -4,7 +4,7 @@
// +build linux freebsd
package router
package dns
import (
"bufio"
@@ -14,10 +14,10 @@ import (
"os/exec"
)
// resolvconfIsActive indicates whether the system appears to be using resolvconf.
// If this is true, then dnsManualUp should be avoided:
// isResolvconfActive indicates whether the system appears to be using resolvconf.
// If this is true, then directManager should be avoided:
// resolvconf has exclusive ownership of /etc/resolv.conf.
func resolvconfIsActive() bool {
func isResolvconfActive() bool {
// Sanity-check first: if there is no resolvconf binary, then this is fruitless.
//
// However, this binary may be a shim like the one systemd-resolved provides.
@@ -57,21 +57,31 @@ func resolvconfIsActive() bool {
return false
}
// resolvconfImplementation enumerates supported implementations of the resolvconf CLI.
type resolvconfImplementation uint8
// resolvconfImpl enumerates supported implementations of the resolvconf CLI.
type resolvconfImpl uint8
const (
// resolvconfOpenresolv is the implementation packaged as "openresolv" on Ubuntu.
// It supports exclusive mode and interface metrics.
resolvconfOpenresolv resolvconfImplementation = iota
resolvconfOpenresolv resolvconfImpl = iota
// resolvconfLegacy is the implementation by Thomas Hood packaged as "resolvconf" on Ubuntu.
// It does not support exclusive mode or interface metrics.
resolvconfLegacy
)
// getResolvconfImplementation returns the implementation of resolvconf
// that appears to be in use.
func getResolvconfImplementation() resolvconfImplementation {
func (impl resolvconfImpl) String() string {
switch impl {
case resolvconfOpenresolv:
return "openresolv"
case resolvconfLegacy:
return "legacy"
default:
return "unknown"
}
}
// getResolvconfImpl returns the implementation of resolvconf that appears to be in use.
func getResolvconfImpl() resolvconfImpl {
err := exec.Command("resolvconf", "-v").Run()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
@@ -85,21 +95,31 @@ func getResolvconfImplementation() resolvconfImplementation {
return resolvconfOpenresolv
}
type resolvconfManager struct {
impl resolvconfImpl
}
func newResolvconfManager(mconfig ManagerConfig) managerImpl {
impl := getResolvconfImpl()
mconfig.Logf("resolvconf implementation is %s", impl)
return resolvconfManager{
impl: impl,
}
}
// resolvconfConfigName is the name of the config submitted to resolvconf.
// It has this form to match the "tun*" rule in interface-order
// when running resolvconfLegacy, hopefully placing our config first.
const resolvconfConfigName = "tun-tailscale.inet"
// dnsResolvconfUp invokes the resolvconf binary to associate
// the given DNS configuration the Tailscale interface.
func dnsResolvconfUp(config DNSConfig, interfaceName string) error {
implementation := getResolvconfImplementation()
// Up implements managerImpl.
func (m resolvconfManager) Up(config Config) error {
stdin := new(bytes.Buffer)
dnsWriteConfig(stdin, config.Nameservers, config.Domains) // dns_direct.go
writeResolvConf(stdin, config.Nameservers, config.Domains) // dns_direct.go
var cmd *exec.Cmd
switch implementation {
switch m.impl {
case resolvconfOpenresolv:
// Request maximal priority (metric 0) and exclusive mode.
cmd = exec.Command("resolvconf", "-m", "0", "-x", "-a", resolvconfConfigName)
@@ -117,12 +137,10 @@ func dnsResolvconfUp(config DNSConfig, interfaceName string) error {
return nil
}
// dnsResolvconfDown undoes the action of dnsResolvconfUp.
func dnsResolvconfDown(interfaceName string) error {
implementation := getResolvconfImplementation()
// Down implements managerImpl.
func (m resolvconfManager) Down() error {
var cmd *exec.Cmd
switch implementation {
switch m.impl {
case resolvconfOpenresolv:
cmd = exec.Command("resolvconf", "-f", "-d", resolvconfConfigName)
case resolvconfLegacy:

View File

@@ -4,14 +4,13 @@
// +build linux
package router
package dns
import (
"context"
"errors"
"fmt"
"os/exec"
"time"
"github.com/godbus/dbus/v5"
"golang.org/x/sys/unix"
@@ -23,7 +22,7 @@ import (
//
// We only consider resolved to be the system resolver if the stub resolver is;
// that is, if this address is the sole nameserver in /etc/resolved.conf.
// In other cases, resolved may still be managing the system DNS configuration directly.
// In other cases, resolved may be managing the system DNS configuration directly.
// Then the nameserver list will be a concatenation of those for all
// the interfaces that register their interest in being a default resolver with
// SetLinkDomains([]{{"~.", true}, ...})
@@ -36,13 +35,6 @@ import (
// this address is, in fact, hard-coded into resolved.
var resolvedListenAddr = netaddr.IPv4(127, 0, 0, 53)
// dnsReconfigTimeout is the timeout for DNS reconfiguration.
//
// This is useful because certain conditions can cause indefinite hangs
// (such as improper dbus auth followed by contextless dbus.Object.Call).
// Such operations should be wrapped in a timeout context.
const dnsReconfigTimeout = time.Second
var errNotReady = errors.New("interface not ready")
type resolvedLinkNameserver struct {
@@ -55,8 +47,8 @@ type resolvedLinkDomain struct {
RoutingOnly bool
}
// resolvedIsActive determines if resolved is currently managing system DNS settings.
func resolvedIsActive() bool {
// isResolvedActive determines if resolved is currently managing system DNS settings.
func isResolvedActive() bool {
// systemd-resolved is never installed without systemd.
_, err := exec.LookPath("systemctl")
if err != nil {
@@ -69,7 +61,7 @@ func resolvedIsActive() bool {
return false
}
config, err := dnsReadConfig()
config, err := readResolvConf()
if err != nil {
return false
}
@@ -82,10 +74,16 @@ func resolvedIsActive() bool {
return false
}
// dnsResolvedUp sets the DNS parameters for the Tailscale interface
// to given nameservers and search domains using the resolved DBus API.
func dnsResolvedUp(config DNSConfig) error {
ctx, cancel := context.WithTimeout(context.Background(), dnsReconfigTimeout)
// resolvedManager uses the systemd-resolved DBus API.
type resolvedManager struct{}
func newResolvedManager(mconfig ManagerConfig) managerImpl {
return resolvedManager{}
}
// Up implements managerImpl.
func (m resolvedManager) Up(config Config) error {
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
defer cancel()
// conn is a shared connection whose lifecycle is managed by the dbus package.
@@ -100,6 +98,8 @@ func dnsResolvedUp(config DNSConfig) error {
dbus.ObjectPath("/org/freedesktop/resolve1"),
)
// In principle, we could persist this in the manager struct
// if we knew that interface indices are persistent. This does not seem to be the case.
_, iface, err := interfaces.Tailscale()
if err != nil {
return fmt.Errorf("getting interface index: %w", err)
@@ -129,7 +129,7 @@ func dnsResolvedUp(config DNSConfig) error {
iface.Index, linkNameservers,
).Store()
if err != nil {
return fmt.Errorf("SetLinkDNS: %w", err)
return fmt.Errorf("setLinkDNS: %w", err)
}
var linkDomains = make([]resolvedLinkDomain, len(config.Domains))
@@ -145,15 +145,15 @@ func dnsResolvedUp(config DNSConfig) error {
iface.Index, linkDomains,
).Store()
if err != nil {
return fmt.Errorf("SetLinkDomains: %w", err)
return fmt.Errorf("setLinkDomains: %w", err)
}
return nil
}
// dnsResolvedDown undoes the changes made by dnsResolvedUp.
func dnsResolvedDown() error {
ctx, cancel := context.WithTimeout(context.Background(), dnsReconfigTimeout)
// Down implements managerImpl.
func (m resolvedManager) Down() error {
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
defer cancel()
// conn is a shared connection whose lifecycle is managed by the dbus package.

View File

@@ -21,7 +21,6 @@ import (
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
"tailscale.com/wgengine/winnet"
)
@@ -157,28 +156,6 @@ func monitorDefaultRoutes(device *device.Device, autoMTU bool, tun *tun.NativeTu
return cb, nil
}
func setDNSDomains(g windows.GUID, dnsDomains []string) {
gs := g.String()
log.Printf("setDNSDomains(%v) guid=%v\n", dnsDomains, gs)
p := `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces\` + gs
key, err := registry.OpenKey(registry.LOCAL_MACHINE, p, registry.READ|registry.SET_VALUE)
if err != nil {
log.Printf("setDNSDomains(%v): open: %v\n", p, err)
return
}
defer key.Close()
// Windows only supports a single per-interface DNS domain.
dom := ""
if len(dnsDomains) > 0 {
dom = dnsDomains[0]
}
err = key.SetStringValue("Domain", dom)
if err != nil {
log.Printf("setDNSDomains(%v): SetStringValue: %v\n", p, err)
}
}
func setFirewall(ifcGUID *windows.GUID) (bool, error) {
c := ole.Connection{}
err := c.Initialize()
@@ -262,8 +239,6 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
}
}()
setDNSDomains(guid, cfg.Domains)
routes := []winipcfg.RouteData{}
var firstGateway4 *net.IP
var firstGateway6 *net.IP
@@ -358,16 +333,6 @@ func configureInterface(cfg *Config, tun *tun.NativeTun) error {
errAcc = err
}
var dnsIPs []net.IP
for _, ip := range cfg.Nameservers {
dnsIPs = append(dnsIPs, ip.IPAddr().IP)
}
err = iface.SetDNS(dnsIPs)
if err != nil && errAcc == nil {
log.Printf("setdns: %v\n", err)
errAcc = err
}
ipif, err := iface.GetIpInterface(winipcfg.AF_INET)
if err != nil {
log.Printf("getipif: %v\n", err)

View File

@@ -11,6 +11,7 @@ import (
"github.com/tailscale/wireguard-go/tun"
"inet.af/netaddr"
"tailscale.com/types/logger"
"tailscale.com/wgengine/router/dns"
)
// Router is responsible for managing the system network stack.
@@ -40,6 +41,15 @@ func New(logf logger.Logf, wgdev *device.Device, tundev tun.Device) (Router, err
// in case the Tailscale daemon terminated without closing the router.
// No other state needs to be instantiated before this runs.
func Cleanup(logf logger.Logf, interfaceName string) {
mconfig := dns.ManagerConfig{
Logf: logf,
InterfaceName: interfaceName,
Cleanup: true,
}
dns := dns.NewManager(mconfig)
if err := dns.Down(); err != nil {
logf("dns down: %v", err)
}
cleanup(logf, interfaceName)
}
@@ -72,7 +82,7 @@ type Config struct {
LocalAddrs []netaddr.IPPrefix
Routes []netaddr.IPPrefix // routes to point into the Tailscale interface
DNSConfig
DNS dns.Config
// Linux-only things below, ignored on other platforms.

View File

@@ -10,55 +10,10 @@ import (
"tailscale.com/types/logger"
)
type darwinRouter struct {
logf logger.Logf
tunname string
Router
func newUserspaceRouter(logf logger.Logf, wgdev *device.Device, tundev tun.Device) (Router, error) {
return newUserspaceBSDRouter(logf, wgdev, tundev)
}
func newUserspaceRouter(logf logger.Logf, _ *device.Device, tundev tun.Device) (Router, error) {
tunname, err := tundev.Name()
if err != nil {
return nil, err
}
userspaceRouter, err := newUserspaceBSDRouter(logf, nil, tundev)
if err != nil {
return nil, err
}
return &darwinRouter{
logf: logf,
tunname: tunname,
Router: userspaceRouter,
}, nil
}
func (r *darwinRouter) Set(cfg *Config) error {
if cfg == nil {
cfg = &shutdownConfig
}
if SetRoutesFunc != nil {
return SetRoutesFunc(cfg)
}
return r.Router.Set(cfg)
}
func (r *darwinRouter) Up() error {
if SetRoutesFunc != nil {
return nil // bringing up the tunnel is handled externally
}
return r.Router.Up()
}
func upDNS(config DNSConfig, interfaceName string) error {
// Handled by IPNExtension
return nil
}
func downDNS(interfaceName string) error {
// Handled by IPNExtension
return nil
func cleanup(logger.Logf, string) {
// Nothing to do.
}

View File

@@ -1,23 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package router
// SetRoutesFunc applies the given router settings to the OS network
// stack. cfg is guaranteed to be non-nil.
//
// This is logically part of the router_darwin.go implementation, and
// should not be used on other platforms.
//
// The code to reconfigure the network stack on MacOS and iOS is in
// the non-open `ipn-go-bridge` package, which bridges between the Go
// and Swift pieces of the application. The ipn-go-bridge sets
// SetRoutesFunc at startup.
//
// So why isn't this in router_darwin.go? Because in the non-oss
// repository, we build ipn-go-bridge when developing on Linux as well
// as MacOS, so that we don't have to wait until the Mac CI to
// discover that we broke it. So this one definition needs to exist in
// both the darwin and linux builds. Hence this file and build tag.
var SetRoutesFunc func(cfg *Config) error

View File

@@ -16,6 +16,6 @@ func newUserspaceRouter(logf logger.Logf, tunname string, dev *device.Device, tu
return NewFakeRouter(logf, tunname, dev, tuntap, netChanged)
}
func cleanup() error {
return nil
func cleanup(logf logger.Logf, interfaceName string) {
// Nothing to do here.
}

View File

@@ -5,8 +5,6 @@
package router
import (
"fmt"
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
"tailscale.com/types/logger"
@@ -21,34 +19,13 @@ func newUserspaceRouter(logf logger.Logf, _ *device.Device, tundev tun.Device) (
return newUserspaceBSDRouter(logf, nil, tundev)
}
func upDNS(config DNSConfig, interfaceName string) error {
if len(config.Nameservers) == 0 {
return downDNS(interfaceName)
func cleanup(logf logger.Logf, interfaceName string) {
// If the interface was left behind, ifconfig down will not remove it.
// In fact, this will leave a system in a tainted state where starting tailscaled
// will result in "interface tailscale0 already exists"
// until the defunct interface is ifconfig-destroyed.
ifup := []string{"ifconfig", interfaceName, "destroy"}
if out, err := cmd(ifup...).CombinedOutput(); err != nil {
logf("ifconfig destroy: %v\n%s", err, out)
}
if resolvconfIsActive() {
if err := dnsResolvconfUp(config, interfaceName); err != nil {
return fmt.Errorf("resolvconf: %w")
}
return nil
}
if err := dnsDirectUp(config); err != nil {
return fmt.Errorf("direct: %w")
}
return nil
}
func downDNS(interfaceName string) error {
if resolvconfIsActive() {
if err := dnsResolvconfDown(interfaceName); err != nil {
return fmt.Errorf("resolvconf: %w")
}
return nil
}
if err := dnsDirectDown(); err != nil {
return fmt.Errorf("direct: %w")
}
return nil
}

View File

@@ -15,6 +15,7 @@ import (
"inet.af/netaddr"
"tailscale.com/net/tsaddr"
"tailscale.com/types/logger"
"tailscale.com/wgengine/router/dns"
)
// The following bits are added to packet marks for Tailscale use.
@@ -84,8 +85,7 @@ type linuxRouter struct {
snatSubnetRoutes bool
netfilterMode NetfilterMode
dnsMode dnsMode
dnsConfig DNSConfig
dns *dns.Manager
ipt4 netfilterRunner
cmd commandRunner
@@ -109,6 +109,11 @@ func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netfilter netf
_, err := exec.Command("ip", "rule").Output()
ipRuleAvailable := (err == nil)
mconfig := dns.ManagerConfig{
Logf: logf,
InterfaceName: tunname,
}
return &linuxRouter{
logf: logf,
ipRuleAvailable: ipRuleAvailable,
@@ -116,6 +121,7 @@ func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, netfilter netf
netfilterMode: NetfilterOff,
ipt4: netfilter,
cmd: cmd,
dns: dns.NewManager(mconfig),
}, nil
}
@@ -133,26 +139,12 @@ func (r *linuxRouter) Up() error {
return err
}
switch {
// TODO(dmytro): enable resolved when per-domain resolvers are desired.
case resolvedIsActive():
r.dnsMode = dnsDirect
// r.dnsMode = dnsResolved
case nmIsActive():
r.dnsMode = dnsNetworkManager
case resolvconfIsActive():
r.dnsMode = dnsResolvconf
default:
r.dnsMode = dnsDirect
}
r.logf("dns mode: %v", r.dnsMode)
return nil
}
func (r *linuxRouter) Close() error {
if err := r.downDNS(); err != nil {
return err
if err := r.dns.Down(); err != nil {
return fmt.Errorf("dns down: %v", err)
}
if err := r.downInterface(); err != nil {
return err
@@ -206,12 +198,8 @@ func (r *linuxRouter) Set(cfg *Config) error {
}
r.snatSubnetRoutes = cfg.SNATSubnetRoutes
if !r.dnsConfig.EquivalentTo(cfg.DNSConfig) {
if err := r.upDNS(cfg.DNSConfig); err != nil {
r.logf("dns up: %v", err)
} else {
r.dnsConfig = cfg.DNSConfig
}
if err := r.dns.Set(cfg.DNS); err != nil {
return fmt.Errorf("dns set: %v", err)
}
return nil
@@ -855,68 +843,6 @@ func normalizeCIDR(cidr netaddr.IPPrefix) string {
return fmt.Sprintf("%s/%d", nip, cidr.Bits)
}
// upDNS updates the system DNS configuration to the given one.
func (r *linuxRouter) upDNS(config DNSConfig) error {
if len(config.Nameservers) == 0 {
return r.downDNS()
}
switch r.dnsMode {
case dnsResolved:
if err := dnsResolvedUp(config); err != nil {
return fmt.Errorf("resolved: %w", err)
}
case dnsResolvconf:
if err := dnsResolvconfUp(config, r.tunname); err != nil {
return fmt.Errorf("resolvconf: %w", err)
}
case dnsNetworkManager:
if err := dnsNetworkManagerUp(config, r.tunname); err != nil {
return fmt.Errorf("network manager: %w", err)
}
case dnsDirect:
if err := dnsDirectUp(config); err != nil {
return fmt.Errorf("direct: %w", err)
}
}
return nil
}
// downDNS restores system DNS configuration to its state before upDNS.
// It is idempotent (in particular, it does nothing if upDNS was never run).
func (r *linuxRouter) downDNS() error {
switch r.dnsMode {
case dnsResolved:
if err := dnsResolvedDown(); err != nil {
return fmt.Errorf("resolved: %w", err)
}
case dnsResolvconf:
if err := dnsResolvconfDown(r.tunname); err != nil {
return fmt.Errorf("resolvconf: %w", err)
}
case dnsNetworkManager:
if err := dnsNetworkManagerDown(r.tunname); err != nil {
return fmt.Errorf("network manager: %w", err)
}
case dnsDirect:
if err := dnsDirectDown(); err != nil {
return fmt.Errorf("direct: %w", err)
}
}
return nil
}
func cleanup(logf logger.Logf, interfaceName string) {
// Note: we need not do anything for dnsResolved,
// as its settings are interface-bound and get cleaned up for us.
switch {
case resolvconfIsActive():
if err := dnsResolvconfDown(interfaceName); err != nil {
logf("down down: resolvconf: %v", err)
}
default:
if err := dnsDirectDown(); err != nil {
logf("dns down: direct: %v", err)
}
}
// TODO(dmytro): clean up iptables.
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/tailscale/wireguard-go/tun"
"inet.af/netaddr"
"tailscale.com/types/logger"
"tailscale.com/wgengine/router/dns"
)
// For now this router only supports the WireGuard userspace implementation.
@@ -26,7 +27,7 @@ type openbsdRouter struct {
local netaddr.IPPrefix
routes map[netaddr.IPPrefix]struct{}
dnsConfig DNSConfig
dns *dns.Manager
}
func newUserspaceRouter(logf logger.Logf, _ *device.Device, tundev tun.Device) (Router, error) {
@@ -34,9 +35,16 @@ func newUserspaceRouter(logf logger.Logf, _ *device.Device, tundev tun.Device) (
if err != nil {
return nil, err
}
mconfig := dns.ManagerConfig{
Logf: logf,
InterfaceName: tunname,
}
return &openbsdRouter{
logf: logf,
tunname: tunname,
dns: dns.NewManager(mconfig),
}, nil
}
@@ -62,7 +70,10 @@ func (r *openbsdRouter) Set(cfg *Config) error {
}
// TODO: support configuring multiple local addrs on interface.
if len(cfg.LocalAddrs) != 1 {
if len(cfg.LocalAddrs) == 0 {
return nil
}
if len(cfg.LocalAddrs) > 1 {
return errors.New("freebsd doesn't support setting multiple local addrs yet")
}
localAddr := cfg.LocalAddrs[0]
@@ -155,26 +166,22 @@ func (r *openbsdRouter) Set(cfg *Config) error {
r.local = localAddr
r.routes = newRoutes
if !r.dnsConfig.EquivalentTo(cfg.DNSConfig) {
if err := dnsDirectUp(cfg.DNSConfig); err != nil {
errq = fmt.Errorf("dns up: direct: %v", err)
} else {
r.dnsConfig = cfg.DNSConfig
}
if err := r.dns.Set(cfg.DNS); err != nil {
errq = fmt.Errorf("dns set: %v", err)
}
return errq
}
func (r *openbsdRouter) Close() error {
if err := r.dns.Down(); err != nil {
return fmt.Errorf("dns down: %v", err)
}
cleanup(r.logf, r.tunname)
return nil
}
func cleanup(logf logger.Logf, interfaceName string) {
if err := dnsDirectDown(); err != nil {
logf("dns down: direct: %v", err)
}
out, err := cmd("ifconfig", interfaceName, "down").CombinedOutput()
if err != nil {
logf("ifconfig down: %v\n%s", err, out)

View File

@@ -16,6 +16,7 @@ import (
"github.com/tailscale/wireguard-go/tun"
"inet.af/netaddr"
"tailscale.com/types/logger"
"tailscale.com/wgengine/router/dns"
)
type userspaceBSDRouter struct {
@@ -24,7 +25,7 @@ type userspaceBSDRouter struct {
local netaddr.IPPrefix
routes map[netaddr.IPPrefix]struct{}
dnsConfig DNSConfig
dns *dns.Manager
}
func newUserspaceBSDRouter(logf logger.Logf, _ *device.Device, tundev tun.Device) (Router, error) {
@@ -32,9 +33,16 @@ func newUserspaceBSDRouter(logf logger.Logf, _ *device.Device, tundev tun.Device
if err != nil {
return nil, err
}
mconfig := dns.ManagerConfig{
Logf: logf,
InterfaceName: tunname,
}
return &userspaceBSDRouter{
logf: logf,
tunname: tunname,
dns: dns.NewManager(mconfig),
}, nil
}
@@ -141,29 +149,17 @@ func (r *userspaceBSDRouter) Set(cfg *Config) error {
r.local = localAddr
r.routes = newRoutes
if !r.dnsConfig.EquivalentTo(cfg.DNSConfig) {
if err := upDNS(cfg.DNSConfig, r.tunname); err != nil {
errq = fmt.Errorf("dns up: %v", err)
} else {
r.dnsConfig = cfg.DNSConfig
}
if err := r.dns.Set(cfg.DNS); err != nil {
errq = fmt.Errorf("dns set: %v", err)
}
return errq
}
func (r *userspaceBSDRouter) Close() error {
cleanup(r.logf, r.tunname)
if err := r.dns.Down(); err != nil {
r.logf("dns down: %v", err)
}
// No interface cleanup is necessary during normal shutdown.
return nil
}
func cleanup(logf logger.Logf, interfaceName string) {
if err := downDNS(interfaceName); err != nil {
logf("dns down: %v", err)
}
ifup := []string{"ifconfig", interfaceName, "down"}
if out, err := cmd(ifup...).CombinedOutput(); err != nil {
logf("ifconfig down: %v\n%s", err, out)
}
}

View File

@@ -5,12 +5,14 @@
package router
import (
"fmt"
"log"
winipcfg "github.com/tailscale/winipcfg-go"
"github.com/tailscale/wireguard-go/device"
"github.com/tailscale/wireguard-go/tun"
"tailscale.com/types/logger"
"tailscale.com/wgengine/router/dns"
)
type winRouter struct {
@@ -19,6 +21,7 @@ type winRouter struct {
nativeTun *tun.NativeTun
wgdev *device.Device
routeChangeCallback *winipcfg.RouteChangeCallback
dns *dns.Manager
}
func newUserspaceRouter(logf logger.Logf, wgdev *device.Device, tundev tun.Device) (Router, error) {
@@ -26,11 +29,20 @@ func newUserspaceRouter(logf logger.Logf, wgdev *device.Device, tundev tun.Devic
if err != nil {
return nil, err
}
nativeTun := tundev.(*tun.NativeTun)
guid := nativeTun.GUID().String()
mconfig := dns.ManagerConfig{
Logf: logf,
InterfaceName: guid,
}
return &winRouter{
logf: logf,
wgdev: wgdev,
tunname: tunname,
nativeTun: tundev.(*tun.NativeTun),
nativeTun: nativeTun,
dns: dns.NewManager(mconfig),
}, nil
}
@@ -55,10 +67,18 @@ func (r *winRouter) Set(cfg *Config) error {
r.logf("ConfigureInterface: %v\n", err)
return err
}
if err := r.dns.Set(cfg.DNS); err != nil {
return fmt.Errorf("dns set: %w", err)
}
return nil
}
func (r *winRouter) Close() error {
if err := r.dns.Down(); err != nil {
return fmt.Errorf("dns down: %w", err)
}
if r.routeChangeCallback != nil {
r.routeChangeCallback.Unregister()
}
@@ -66,5 +86,5 @@ func (r *winRouter) Close() error {
}
func cleanup(logf logger.Logf, interfaceName string) {
// DNS is interface-bound, so nothing to do here.
// Nothing to do here.
}

118
wgengine/tsdns/map.go Normal file
View File

@@ -0,0 +1,118 @@
// 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 tsdns
import (
"fmt"
"sort"
"strings"
"inet.af/netaddr"
)
// Map is all the data Resolver needs to resolve DNS queries within the Tailscale network.
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
// 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 {
names = append(names, name)
}
sort.Strings(names)
return &Map{
nameToIP: nameToIP,
names: names,
}
}
func printSingleNameIP(buf *strings.Builder, name string, ip netaddr.IP) {
// Output width is exactly 80 columns.
fmt.Fprintf(buf, "%s\t%s\n", name, ip)
}
func (m *Map) Pretty() string {
buf := new(strings.Builder)
for _, name := range m.names {
printSingleNameIP(buf, name, m.nameToIP[name])
}
return buf.String()
}
func (m *Map) PrettyDiffFrom(old *Map) string {
var (
oldNameToIP map[string]netaddr.IP
newNameToIP map[string]netaddr.IP
oldNames []string
newNames []string
)
if old != nil {
oldNameToIP = old.nameToIP
oldNames = old.names
}
if m != nil {
newNameToIP = m.nameToIP
newNames = m.names
}
buf := new(strings.Builder)
for len(oldNames) > 0 && len(newNames) > 0 {
var name string
newName, oldName := newNames[0], oldNames[0]
switch {
case oldName < newName:
name = oldName
oldNames = oldNames[1:]
case oldName > newName:
name = newName
newNames = newNames[1:]
case oldNames[0] == newNames[0]:
name = oldNames[0]
oldNames = oldNames[1:]
newNames = newNames[1:]
}
ipOld, inOld := oldNameToIP[name]
ipNew, inNew := newNameToIP[name]
switch {
case !inOld:
buf.WriteByte('+')
printSingleNameIP(buf, name, ipNew)
case !inNew:
buf.WriteByte('-')
printSingleNameIP(buf, name, ipOld)
case ipOld != ipNew:
buf.WriteByte('-')
printSingleNameIP(buf, name, ipOld)
buf.WriteByte('+')
printSingleNameIP(buf, name, ipNew)
}
}
for _, name := range oldNames {
if _, ok := newNameToIP[name]; !ok {
buf.WriteByte('-')
printSingleNameIP(buf, name, oldNameToIP[name])
}
}
for _, name := range newNames {
if _, ok := oldNameToIP[name]; !ok {
buf.WriteByte('+')
printSingleNameIP(buf, name, newNameToIP[name])
}
}
return buf.String()
}

138
wgengine/tsdns/map_test.go Normal file
View File

@@ -0,0 +1,138 @@
// 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 tsdns
import (
"testing"
"inet.af/netaddr"
)
func TestPretty(t *testing.T) {
tests := []struct {
name string
dmap *Map
want string
}{
{"empty", NewMap(nil), ""},
{
"single",
NewMap(map[string]netaddr.IP{
"hello.ipn.dev": netaddr.IPv4(100, 101, 102, 103),
}),
"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\t100.101.102.103\ntest2.sub.domain\t100.99.9.1\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.dmap.Pretty()
if tt.want != got {
t.Errorf("want %v; got %v", tt.want, got)
}
})
}
}
func TestPrettyDiffFrom(t *testing.T) {
tests := []struct {
name string
map1 *Map
map2 *Map
want string
}{
{
"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\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),
}),
NewMap(map[string]netaddr.IP{
"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),
}),
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\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),
}),
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\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),
}),
NewMap(map[string]netaddr.IP{
"test1.ipn.dev": netaddr.IPv4(100, 101, 102, 103),
}),
"-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),
}),
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),
}),
"-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",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.map2.PrettyDiffFrom(tt.map1)
if tt.want != got {
t.Errorf("want %v; got %v", tt.want, got)
}
})
}
}

View File

@@ -40,23 +40,12 @@ var ErrClosed = errors.New("closed")
var (
errAllFailed = errors.New("all upstream nameservers failed")
errFullQueue = errors.New("request queue full")
errNoNameservers = errors.New("no upstream nameservers set")
errMapNotSet = errors.New("domain map not set")
errNotImplemented = errors.New("query type not implemented")
errNotQuery = errors.New("not a DNS query")
)
// Map is all the data Resolver needs to resolve DNS queries within the Tailscale network.
type Map struct {
// domainToIP is a mapping of Tailscale domains to their IP addresses.
// For example, monitoring.tailscale.us -> 100.64.0.1.
domainToIP map[string]netaddr.IP
}
// NewMap returns a new Map with domain to address mapping given by domainToIP.
func NewMap(domainToIP map[string]netaddr.IP) *Map {
return &Map{domainToIP: domainToIP}
}
// Packet represents a DNS payload together with the address of its origin.
type Packet struct {
// Payload is the application layer DNS payload.
@@ -142,8 +131,10 @@ func (r *Resolver) Close() {
// SetMap sets the resolver's DNS map, taking ownership of it.
func (r *Resolver) SetMap(m *Map) {
r.mu.Lock()
oldMap := r.dnsMap
r.dnsMap = m
r.mu.Unlock()
r.logf("map diff:\n%s", m.PrettyDiffFrom(oldMap))
}
// SetUpstreamNameservers sets the addresses of the resolver's
@@ -189,7 +180,7 @@ func (r *Resolver) Resolve(domain string) (netaddr.IP, dns.RCode, error) {
r.mu.RUnlock()
return netaddr.IP{}, dns.RCodeServerFailure, errMapNotSet
}
addr, found := r.dnsMap.domainToIP[domain]
addr, found := r.dnsMap.nameToIP[domain]
r.mu.RUnlock()
if !found {
@@ -267,7 +258,7 @@ func (r *Resolver) delegate(query []byte) ([]byte, error) {
r.mu.RUnlock()
if len(nameservers) == 0 {
return nil, errAllFailed
return nil, errNoNameservers
}
ctx, cancel := context.WithTimeout(context.Background(), delegateTimeout)

View File

@@ -23,7 +23,7 @@ var testipv6 = netaddr.IPv6Raw([16]byte{
})
var dnsMap = &Map{
domainToIP: map[string]netaddr.IP{
nameToIP: map[string]netaddr.IP{
"test1.ipn.dev": testipv4,
"test2.ipn.dev": testipv6,
},

View File

@@ -13,9 +13,11 @@ import (
"fmt"
"io"
"log"
"net"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
@@ -30,9 +32,11 @@ import (
"tailscale.com/internal/deepprint"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/interfaces"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/version"
"tailscale.com/wgengine/filter"
"tailscale.com/wgengine/magicsock"
"tailscale.com/wgengine/monitor"
@@ -67,19 +71,28 @@ const (
// (This includes peers that have never been idle, which
// effectively have infinite idleness)
lazyPeerIdleThreshold = 5 * time.Minute
// packetSendTimeUpdateFrequency controls how often we record
// the time that we wrote a packet to an IP address.
packetSendTimeUpdateFrequency = 10 * time.Second
// packetSendRecheckWireguardThreshold controls how long we can go
// between packet sends to an IP before checking to see
// whether this IP address needs to be added back to the
// Wireguard peer oconfig.
packetSendRecheckWireguardThreshold = 1 * time.Minute
)
type userspaceEngine struct {
logf logger.Logf
reqCh chan struct{}
waitCh chan struct{} // chan is closed when first Close call completes; contrast with closing bool
tundev *tstun.TUN
wgdev *device.Device
router router.Router
resolver *tsdns.Resolver
useTailscaleDNS bool
magicConn *magicsock.Conn
linkMon *monitor.Mon
logf logger.Logf
reqCh chan struct{}
waitCh chan struct{} // chan is closed when first Close call completes; contrast with closing bool
tundev *tstun.TUN
wgdev *device.Device
router router.Router
resolver *tsdns.Resolver
magicConn *magicsock.Conn
linkMon *monitor.Mon
// localAddrs is the set of IP addresses assigned to the local
// tunnel interface. It's used to reflect local packets
@@ -119,13 +132,9 @@ type EngineConfig struct {
RouterGen RouterGen
// ListenPort is the port on which the engine will listen.
ListenPort uint16
// EchoRespondToAll determines whether ICMP Echo requests incoming from Tailscale peers
// will be intercepted and responded to, regardless of the source host.
EchoRespondToAll bool
// UseTailscaleDNS determines whether DNS requests for names of the form <mynode>.<mydomain>.<root>
// directed to the designated Taislcale DNS address (see wgengine/tsdns)
// will be intercepted and resolved by a tsdns.Resolver.
UseTailscaleDNS bool
// Fake determines whether this engine is running in fake mode,
// which disables such features as DNS configuration and unrestricted ICMP Echo responses.
Fake bool
}
type Loggify struct {
@@ -140,11 +149,11 @@ func (l *Loggify) Write(b []byte) (int, error) {
func NewFakeUserspaceEngine(logf logger.Logf, listenPort uint16) (Engine, error) {
logf("Starting userspace wireguard engine (FAKE tuntap device).")
conf := EngineConfig{
Logf: logf,
TUN: tstun.NewFakeTUN(),
RouterGen: router.NewFake,
ListenPort: listenPort,
EchoRespondToAll: true,
Logf: logf,
TUN: tstun.NewFakeTUN(),
RouterGen: router.NewFake,
ListenPort: listenPort,
Fake: true,
}
return NewUserspaceEngineAdvanced(conf)
}
@@ -171,8 +180,6 @@ func NewUserspaceEngine(logf logger.Logf, tunname string, listenPort uint16) (En
TUN: tun,
RouterGen: router.New,
ListenPort: listenPort,
// TODO(dmytro): plumb this down.
UseTailscaleDNS: true,
}
e, err := NewUserspaceEngineAdvanced(conf)
@@ -192,19 +199,18 @@ func newUserspaceEngineAdvanced(conf EngineConfig) (_ Engine, reterr error) {
logf := conf.Logf
e := &userspaceEngine{
logf: logf,
reqCh: make(chan struct{}, 1),
waitCh: make(chan struct{}),
tundev: tstun.WrapTUN(logf, conf.TUN),
resolver: tsdns.NewResolver(logf, magicDNSDomain),
useTailscaleDNS: conf.UseTailscaleDNS,
pingers: make(map[wgcfg.Key]*pinger),
logf: logf,
reqCh: make(chan struct{}, 1),
waitCh: make(chan struct{}),
tundev: tstun.WrapTUN(logf, conf.TUN),
resolver: tsdns.NewResolver(logf, magicDNSDomain),
pingers: make(map[wgcfg.Key]*pinger),
}
e.localAddrs.Store(map[packet.IP]bool{})
e.linkState, _ = getLinkState()
// Respond to all pings only in fake mode.
if conf.EchoRespondToAll {
if conf.Fake {
e.tundev.PostFilterIn = echoRespondToAll
}
e.tundev.PreFilterOut = e.handleLocalPackets
@@ -364,11 +370,9 @@ func echoRespondToAll(p *packet.ParsedPacket, t *tstun.TUN) filter.Response {
// tailscaled directly. Other packets are allowed to proceed into the
// main ACL filter.
func (e *userspaceEngine) handleLocalPackets(p *packet.ParsedPacket, t *tstun.TUN) filter.Response {
if e.useTailscaleDNS {
if verdict := e.handleDNS(p, t); verdict == filter.Drop {
// local DNS handled the packet.
return filter.Drop
}
if verdict := e.handleDNS(p, t); verdict == filter.Drop {
// local DNS handled the packet.
return filter.Drop
}
if runtime.GOOS == "darwin" && e.isLocalAddr(p.DstIP) {
@@ -560,13 +564,27 @@ func (e *userspaceEngine) pinger(peerKey wgcfg.Key, ips []wgcfg.IP) {
p.run(ctx, peerKey, ips, srcIP)
}
func updateSig(last *string, v interface{}) (changed bool) {
sig := deepprint.Hash(v)
if *last != sig {
*last = sig
return true
var debugTrimWireguard, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_TRIM_WIREGUARD"))
// forceFullWireguardConfig reports whether we should give wireguard
// our full network map, even for inactive peers
//
// TODO(bradfitz): remove this after our 1.0 launch; we don't want to
// enable wireguard config trimming quite yet because it just landed
// and we haven't got enough time testing it.
func forceFullWireguardConfig(numPeers int) bool {
// Did the user explicitly enable trimmming via the environment variable knob?
if debugTrimWireguard {
return false
}
return false
// On iOS with large networks, it's critical, so turn on trimming.
// Otherwise we run out of memory from wireguard-go goroutine stacks+buffers.
// This will be the default later for all platforms and network sizes.
iOS := runtime.GOOS == "darwin" && version.IsMobile()
if iOS && numPeers > 50 {
return false
}
return true
}
// isTrimmablePeer reports whether p is a peer that we can trim out of the
@@ -578,7 +596,10 @@ func updateSig(last *string, v interface{}) (changed bool) {
// simplicity, have only one IP address (an IPv4 /32), which is the
// common case for most peers. Subnet router nodes will just always be
// created in the wireguard-go config.
func isTrimmablePeer(p *wgcfg.Peer) bool {
func isTrimmablePeer(p *wgcfg.Peer, numPeers int) bool {
if forceFullWireguardConfig(numPeers) {
return false
}
if len(p.AllowedIPs) != 1 || len(p.Endpoints) != 1 {
return false
}
@@ -680,7 +701,7 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked() error {
for i := range full.Peers {
p := &full.Peers[i]
if !isTrimmablePeer(p) {
if !isTrimmablePeer(p, len(full.Peers)) {
min.Peers = append(min.Peers, *p)
continue
}
@@ -693,7 +714,7 @@ func (e *userspaceEngine) maybeReconfigWireguardLocked() error {
}
}
if !updateSig(&e.lastEngineSigTrim, min) {
if !deepprint.UpdateHash(&e.lastEngineSigTrim, min) {
// No changes
return nil
}
@@ -744,12 +765,19 @@ 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, old := time.Now().Unix(), atomic.LoadInt64(timePtr)
if old > now-10 {
return
now := time.Now().Unix()
old := atomic.LoadInt64(timePtr)
// How long's it been since we last sent a packet?
// For our first packet, old is Unix epoch time 0 (1970).
elapsedSec := now - old
if elapsedSec >= int64(packetSendTimeUpdateFrequency/time.Second) {
atomic.StoreInt64(timePtr, now)
}
atomic.StoreInt64(timePtr, now)
if old == 0 || (now-old) <= 60 {
// On a big jump, assume we might no longer be in the wireguard
// config and go check.
if elapsedSec >= int64(packetSendRecheckWireguardThreshold/time.Second) {
e.wgLock.Lock()
defer e.wgLock.Unlock()
e.maybeReconfigWireguardLocked()
@@ -788,15 +816,8 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config)
}
e.mu.Unlock()
// If the only nameserver is quad 100 (Magic DNS), set up the resolver appropriately.
if len(routerCfg.Nameservers) == 1 && routerCfg.Nameservers[0] == packet.IP(magicDNSIP).Netaddr() {
// TODO(dmytro): plumb dnsReadConfig here instead of hardcoding this.
e.resolver.SetNameservers([]string{"8.8.8.8:53"})
routerCfg.Domains = append([]string{magicDNSDomain}, routerCfg.Domains...)
}
engineChanged := updateSig(&e.lastEngineSigFull, cfg)
routerChanged := updateSig(&e.lastRouterSig, routerCfg)
engineChanged := deepprint.UpdateHash(&e.lastEngineSigFull, cfg)
routerChanged := deepprint.UpdateHash(&e.lastRouterSig, routerCfg)
if !engineChanged && !routerChanged {
return ErrNoChanges
}
@@ -816,6 +837,15 @@ func (e *userspaceEngine) Reconfig(cfg *wgcfg.Config, routerCfg *router.Config)
}
if routerChanged {
if routerCfg.DNS.Proxied {
ips := routerCfg.DNS.Nameservers
nameservers := make([]string, len(ips))
for i, ip := range ips {
nameservers[i] = net.JoinHostPort(ip.String(), "53")
}
e.resolver.SetNameservers(nameservers)
routerCfg.DNS.Nameservers = []netaddr.IP{tsaddr.TailscaleServiceIP()}
}
e.logf("wgengine: Reconfig: configuring router")
if err := e.router.Set(routerCfg); err != nil {
return err