Compare commits

...

167 Commits

Author SHA1 Message Date
Mike Kramlich
9d3cb8780f brew: added TODO; revised README; added to formula caveats and test
Several tweaks:
  * added correct 'services start' msg via formula caveats method added
  * added 'tailscaled -version' to formula test block
  * added TODO file, extracted from author's private Tailscale brew TODO notes
  * added more brew notes to README

Signed-off-by: Mike Kramlich <groglogic@gmail.com>
2021-02-27 11:31:34 -07:00
Mike Kramlich
06116b566e build_dist.sh: added comment on Homebrew version consistency
Signed-off-by: Mike Kramlich <groglogic@gmail.com>
2021-02-26 12:30:01 -07:00
Mike Kramlich
ed0080eac5 brew: formulae audits clean, restored (it required only refactors)
WIP

Signed-off-by: Mike Kramlich <groglogic@gmail.com>
2021-02-26 12:05:54 -07:00
Mike Kramlich
63e4034211 brew/README: changed branch of preview from mkramlich/macos-brew to mkramlich/macos-brew2
WIP

Signed-off-by: Mike Kramlich <groglogic@gmail.com>
2021-02-26 10:59:39 -07:00
Mike Kramlich
7c16062a53 brew: formula go builds embed version props into the tailscale{d} exes
it is now done correctly, and is exactly compatible to build_dist.sh

(WIP)

Signed-off-by: Mike Kramlich <groglogic@gmail.com>
2021-02-25 17:29:49 -07:00
Mike Kramlich
d2eeba5adb Homebrew packaging of the non-IPNExtension, unsandboxed tailscale{d} releases for macOS/darwin.
So the public can "brew install" tailscale and start it as a global boot daemon via brew services.

supported: Homebrew 3.0, go1.15 darwin/amd64, macOS Catalina 10.15.3, Intel 64-bit
probably also but unconfirmed: BigSur 11 and Apple M1 ARM64

NOTE: lots of upgrades and polish since 1st squashed WIP PR, and applies most prior feedback

Part of #177. (WIP)

Signed-off-by: Mike Kramlich <groglogic@gmail.com>
2021-02-24 02:09:39 -07:00
Brad Fitzpatrick
e9e4f1063d wgengine/magicsock: fix discoEndpoint caching bug when a node key changes
Fixes #1391

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-23 14:39:15 -08:00
Brad Fitzpatrick
f11952ad7f ipn/ipnserver: fix Windows connection auth regression
Regression from code movement in d3efe8caf6

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-23 13:27:04 -08:00
Brad Fitzpatrick
c64bd587ae net/portmapper: add NAT-PMP client, move port mapping service probing
* move probing out of netcheck into new net/portmapper package
* use PCP ANNOUNCE op codes for PCP discovery, rather than causing
  short-lived (sub-second) side effects with a 1-second-expiring map +
  delete.
* track when we heard things from the router so we can be less wasteful
  in querying the router's port mapping services in the future
* use portmapper from magicsock to map a public port

Fixes #1298
Fixes #1080
Fixes #1001
Updates #864

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-23 09:07:38 -08:00
David Anderson
d038a5295d wgengine/wglog: drop 1/s "interface is up" messages.
Fixes #1388.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-02-23 09:01:58 -08:00
Brad Fitzpatrick
188bb14269 wgengine: consistently close things when NewUserspaceEngineAdvanced errors
Fixes #1363

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-22 20:39:39 -08:00
David Anderson
6e42430ad8 wgengine/monitor: don't log any single-IP routes added to the tailscale table.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-02-22 20:21:51 -08:00
David Anderson
df5adb2e23 wgengine/monitor: on linux, also monitor for IPv6 changes.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-02-22 19:38:07 -08:00
David Anderson
b83c273737 wgengine/filter: use IPSet for localNets instead of prefixes.
Part of #1177, preparing for doing fancier set operations on
the allowed local nets.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-02-22 14:51:22 -08:00
Matt Layher
2c500cee23 go.mod: bump github.com/mdlayher/netlink, github.com/jsimonetti/rtnetlink
Signed-off-by: Matt Layher <mdlayher@gmail.com>
2021-02-22 06:20:56 -08:00
Filippo Valsorda
39f7a61e9c tstest/staticcheck: import the main package to fix "go mod tidy"
Importing the non-main package was missing some dependencies that
"go mod tidy" would then cleanup. Also added a non-ignore build tag to
avoid other tools getting upset about importing a main package.

Signed-off-by: Filippo Valsorda <hi@filippo.io>
2021-02-20 09:53:47 -08:00
Filippo Valsorda
87f2e4c12c go.mod: bump github.com/kr/pty to build on openbsd/arm64
$ GOOS=openbsd GOARCH=arm64 go install tailscale.com/cmd/...@latest
pkg/mod/github.com/kr/pty@v1.1.4-0.20190131011033-7dc38fb350b1/pty_openbsd.go:24:10: undefined: ptmget
pkg/mod/github.com/kr/pty@v1.1.4-0.20190131011033-7dc38fb350b1/pty_openbsd.go:25:34: undefined: ioctl_PTMGET

"go mod tidy" did some unrelated work in go.sum, maybe because it was
not run with Go 1.16 before.

Signed-off-by: Filippo Valsorda <hi@filippo.io>
2021-02-20 09:53:47 -08:00
Brad Fitzpatrick
86d3a6c9a6 Switch to Go 1.16.
Fixes #1370

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-19 13:18:31 -08:00
Brad Fitzpatrick
9748c5414e portlist: adjust build tags for iOS + Go 1.16
Updates #943
Updates #1370

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-19 10:03:33 -08:00
Brad Fitzpatrick
826f64e863 cmd/tailscale/cli: add netcheck dev knob TS_DEBUG_NETCHECK_UDP_BIND 2021-02-19 07:48:35 -08:00
Brad Fitzpatrick
7ad3af2141 cmd/tailscale/cli: remove outdated TODO
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-18 15:11:00 -08:00
Sonia Appasamy
76fb27bea7 dnsname,tailcfg: add hostname sanitation logic to node display names (#1304)
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2021-02-18 17:15:38 -05:00
Brad Fitzpatrick
c386496e4f version: bump date 2021-02-18 13:36:48 -08:00
Brad Fitzpatrick
fd8e070d01 health, control/controlclient, wgengine: report when router unhealthy
Updates tailscale/corp#1338

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-18 11:48:48 -08:00
Brad Fitzpatrick
2d96215d97 wgengine/router: make Linux delRoute idempotent, cidrDiff fail late as possible
This makes cidrDiff do as much as possible before failing, and makes a
delete of an already-deleted rule be a no-op. We should never do this
ourselves, but other things on the system can, and this should help us
recover a bit.

Also adds the start of root-requiring tests.

TODO: hook into wgengine/monitor and notice when routes are changed
behind our back, and invalidate our routes map and re-read from
kernel (via the ip command) at least on the next reconfig call.

Updates tailscale/corp#1338

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-18 10:36:00 -08:00
Brad Fitzpatrick
6a2c6541da net/tshttpproxy: support HTTP proxy environment credentials on Windows too
and some minor style nits.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-18 08:27:37 -08:00
Brad Fitzpatrick
96a488e37e wgengine/router: simplify func normalizeCIDR using netaddr method 2021-02-17 21:35:33 -08:00
Brad Fitzpatrick
38629b62fc cmd/tailscaled: on darwin, fail early if not root with nicer message
Don't do it on all platforms, as Linux folk might be playing
container + capability games.
2021-02-17 15:45:50 -08:00
Christine Dodrill
3e5c3e932c net/tshttpproxy: support basic auth when available (#1354)
This allows proxy URLs such as:

    http://azurediamond:hunter2@192.168.122.154:38274

to be used in order to dial out to control, logs or derp servers.

Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-02-17 16:01:47 -05:00
Brad Fitzpatrick
d98ef5699d wgengine/filter: remove redundant code
no generated code change.
2021-02-17 09:11:28 -08:00
Brad Fitzpatrick
7038c09bc9 ipn/ipnserver: on darwin, let users who are admins use CLI without sudo
Tangentially related to #987, #177, #594, #925, #505

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-16 21:09:27 -08:00
Brad Fitzpatrick
d3efe8caf6 safesocket, ipn/ipnserver: look up peer creds on Darwin
And open up socket permissions like Linux, now that we know who
connections are from.

This uses the new inet.af/peercred that supports Linux and Darwin at
the moment.

Fixes #1347
Fixes #1348

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-16 20:38:44 -08:00
Brad Fitzpatrick
65815cc1ac wgengine/tsdns: skip test that requires local IPv6 when IPv6 unavailable
Fixes #1292

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-16 10:50:37 -08:00
Brad Fitzpatrick
4ec01323c1 control/controlclient: note package type in Hostinfo
Fixes tailscale/corp#440

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-15 13:24:05 -08:00
Brad Fitzpatrick
73552eb32e tailcfg: add Hostinfo.Package
Updates tailscale/corp#440
2021-02-15 12:58:56 -08:00
Brad Fitzpatrick
dec01ef22b safesocket: make ConnectDefault use paths pkg, fixing tailscaled-on-macOS
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-15 11:33:12 -08:00
Brad Fitzpatrick
7e00100a0a cmd/hello: make whois client work on macOS against GUI client
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-15 11:33:09 -08:00
Brad Fitzpatrick
fdac0387a7 ipn/ipnserver, ipn/ipnlocal: move whois handler to new localapi package 2021-02-15 10:46:22 -08:00
Brad Fitzpatrick
36189e2704 wgengine/monitor: prevent shutdown hang in darwin link monitor 2021-02-15 08:59:53 -08:00
Brad Fitzpatrick
bbb4631e04 safesocket, wgengine: add some darwin failure diagnostic hints 2021-02-15 08:40:52 -08:00
Brad Fitzpatrick
f4ae745b0b net/{interfaces,netns}: add some new tests, missed from prior commit
I meant for these to be part of 52e24aa966.
2021-02-14 21:18:27 -08:00
Brad Fitzpatrick
e923639feb net/interfaces: fix staticcheck error on darwin 2021-02-14 21:17:12 -08:00
Brad Fitzpatrick
d7569863b5 cmd/tailscaled: fix up install-system-daemon on darwin, add uninstall too
Tangentially related to #987, #177, #594, #925, #505
2021-02-14 21:12:30 -08:00
Brad Fitzpatrick
52e24aa966 net/{interfaces,ns}: add tailscaled-mode darwin routing looping prevention
Fixes #1331

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-14 12:47:47 -08:00
Brad Fitzpatrick
4f7d60ad42 wgengine/monitor: add a darwin implementation for tailscaled mode
Tangentially related to #987, #177, #594, #925, #505

Motivated by rebooting a launchd-controlled tailscaled and it going
into SetNetworkUp(false) mode immediately because there really is no
network up at system boot, but then it got stuck in that paused state
forever, without a monitor implementation.
2021-02-13 21:09:27 -08:00
Brad Fitzpatrick
29b028b9c4 cmd/tailscaled: add subcommand on darwin to install+start tailscaled under launchd
Tangentially related to #987, #177, #594, #925.
2021-02-13 12:57:49 -08:00
Brad Fitzpatrick
54e108ff4e paths: update some default paths for darwin 2021-02-13 12:10:20 -08:00
Brad Fitzpatrick
20e66c5b92 net/interfaces: reconcile interface filtering with address printing in logs
The interface.State logging tried to only log interfaces which had
interesting IPs, but the what-is-interesting checks differed between
the code that gathered the interface names to print and the printing
of their addresses.
2021-02-12 18:42:45 -08:00
Josh Bleecher Snyder
c7e5ab8094 wgengine/magicsock: retry and re-send packets in TestTwoDevicePing
When a handshake race occurs, a queued data packet can get lost.
TestTwoDevicePing expected that the very first data packet would arrive.
This caused occasional flakes.

Change TestTwoDevicePing to repeatedly re-send packets
and succeed when one of them makes it through.

This is acceptable (vs making WireGuard not drop the packets)
because this only affects communication with extremely old clients.
And those extremely old clients will eventually connect,
because the kernel will retry sends on timeout.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-02-12 14:18:58 -08:00
Brad Fitzpatrick
ca51529b81 derp/derphttp: return nicer errors from Recv on Close 2021-02-12 12:04:16 -08:00
Brad Fitzpatrick
741d654aa3 derp/derphttp: add a context and infoLogger option to RunWatchConnectionLoop 2021-02-12 10:59:11 -08:00
Josh Bleecher Snyder
1632f9fd6b wgengine/magicsock: reduce log spam during tests
Only do the type assertion to *net.UDPAddr when addr is non-nil.
This prevents a bunch of log spam during tests.
2021-02-12 10:49:02 -08:00
Josh Bleecher Snyder
88586ec4a4 wgengine/magicsock: remove an alloc from ReceiveIPvN
We modified the standard net package to not allocate a *net.UDPAddr
during a call to (*net.UDPConn).ReadFromUDP if the caller's use
of the *net.UDPAddr does not cause it to escape.
That is https://golang.org/cl/291390.

This is the companion change to magicsock.
There are two changes required.
First, call ReadFromUDP instead of ReadFrom, if possible.
ReadFrom returns a net.Addr, which is an interface, which always allocates.
Second, reduce the lifetime of the returned *net.UDPAddr.
We do this by immediately converting it into a netaddr.IPPort.

We left the existing RebindingUDPConn.ReadFrom method in place,
as it is required to satisfy the net.PacketConn interface.

With the upstream change and both of these fixes in place,
we have removed one large allocation per packet received.

name           old time/op    new time/op    delta
ReceiveFrom-8    16.7µs ± 5%    16.4µs ± 8%     ~     (p=0.310 n=5+5)

name           old alloc/op   new alloc/op   delta
ReceiveFrom-8      112B ± 0%       64B ± 0%  -42.86%  (p=0.008 n=5+5)

name           old allocs/op  new allocs/op  delta
ReceiveFrom-8      3.00 ± 0%      2.00 ± 0%  -33.33%  (p=0.008 n=5+5)

Co-authored-by: Sonia Appasamy <sonia@tailscale.com>
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-02-12 09:52:43 -08:00
Josh Bleecher Snyder
0c673c1344 wgengine/magicsock: unify on netaddr types in addrSet
addrSet maintained duplicate lists of netaddr.IPPorts and net.UDPAddrs.
Unify to use the netaddr type only.

This makes (*Conn).ReceiveIPvN a bit uglier,
but that'll be cleaned up in a subsequent commit.

This is preparatory work to remove an allocation from ReceiveIPv4.

Co-authored-by: Sonia Appasamy <sonia@tailscale.com>
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-02-12 09:52:43 -08:00
Josh Bleecher Snyder
4cd9218351 wgengine/magicsock: prevent logging while running benchmarks
Co-authored-by: Sonia Appasamy <sonia@tailscale.com>
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-02-12 09:52:43 -08:00
Brad Fitzpatrick
be906dabd4 version: bump date 2021-02-11 20:11:00 -08:00
Brad Fitzpatrick
6680976b50 cmd/tailscaled: pick automatic tun device name on darwin 2021-02-11 20:10:07 -08:00
Brad Fitzpatrick
88ab0173a7 wgengine/router: fix BSD router to support multiple local addrs, IPv6
Fixes #1201
2021-02-11 19:13:03 -08:00
Ross Zurowski
25321cbd01 cmd/hello: truncate long strings (#1328)
Signed-off-by: Ross Zurowski <ross@rosszurowski.com>
2021-02-11 20:56:22 -05:00
Brad Fitzpatrick
5378776043 cmd/hello: chop DNS name at first dot 2021-02-11 16:38:26 -08:00
Ross Zurowski
6075135e0a cmd/hello: style welcome message (#1325)
Signed-off-by: Ross Zurowski <ross@rosszurowski.com>
2021-02-11 17:42:07 -05:00
Brad Fitzpatrick
917307a90c wgengine/tstun: reply to MagicDNS pings
Fixes #849

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-11 11:55:38 -08:00
Brad Fitzpatrick
34ffd4f7c6 cmd/hello: serve fake data in dev mode on whois failure 2021-02-11 10:57:08 -08:00
Brad Fitzpatrick
de3001bc79 cmd/hello: in dev mode, live reload template 2021-02-11 10:53:33 -08:00
Josh Bleecher Snyder
11bbfbd8bb go.mod: update to latest wireguard-go
All changes are trivial.
2021-02-10 14:14:11 -08:00
Josh Bleecher Snyder
635e4c7435 wgengine/magicsock: increase legacy ping timeout again
I based my estimation of the required timeout based on locally
observed behavior. But CI machines are worse than my local machine.
16s was enough to reduce flakiness but not eliminate it. Bump it up again.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-02-10 13:50:18 -08:00
Brad Fitzpatrick
1ec64bc94d wgengine/router: add another Windows firewall rule to allow incoming UDP
Based on @sailorfrag's research.

Fixes #1312

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-10 13:12:17 -08:00
Brad Fitzpatrick
7e201806b1 wgengine/magicsock: reconnect to DERP home after network comes back up
Updates #1310
2021-02-10 10:29:03 -08:00
Josh Bleecher Snyder
1f0fa8b814 go.mod: pull in upstream wireguard-go bug fixes 2021-02-10 08:04:12 -08:00
moncho
e101d8396d portlist, version: update build tags for Go 1.16, Apple M1
Build tags have been updated to build native Apple M1 binaries, existing build
tags for ios have been changed from darwin,arm64 to ios,arm64.

With this change, running go build cmd/tailscale{,d}/tailscale{,d}.go on an Apple
machine with the new processor works and resulting binaries show the expected
architecture, e.g. tailscale: Mach-O 64-bit executable arm64.

Tested using go version go1.16beta1 darwin/arm64.

Updates #943

Signed-off-by: moncho <50428+moncho@users.noreply.github.com>
2021-02-09 21:10:12 -08:00
Brad Fitzpatrick
cbd6224ca4 wgengine/winnet: don't build on non-windows
It only affects 'go install ./...', etc, and only on darwin/arm64 (M1 Macs) where
the go-ole package doesn't compile.

No need to build it.

Updates #943
2021-02-09 21:09:24 -08:00
Josh Bleecher Snyder
4a82e36491 go.mod: bump to latest wireguard-go
Stabilization and performance improvements.
2021-02-09 14:20:01 -08:00
Brad Fitzpatrick
9b4e50cec0 wgengine/magicsock: fix typo in comment 2021-02-09 09:37:24 -08:00
Naman Sood
07c3df13c6 wgengine/tstun: inform userspaceEngine about injected outbound packets in tundev
Signed-off-by: Naman Sood <mail@nsood.in>
2021-02-09 08:08:01 -08:00
Josh Bleecher Snyder
e7caad61fb wgengine: remove IpcGetOperation filter
This was in place because retrieved allowed_ips was very expensive.
Upstream changed the data structure to make them cheaper to compute.

This commit is an experiment to find out whether they're now cheap enough.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-02-08 20:22:55 -08:00
Brad Fitzpatrick
6b365b0239 wgengine/magicsock: fix DERP reader hang regression during concurrent reads
Fixes #1282

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-08 14:30:15 -08:00
Josh Bleecher Snyder
e1f773ebba wgengine/magicsock: allow more time for pings to transit
We removed the "fast retry" code from our wireguard-go fork.
As a result, pings can take longer to transit when retries are required. 
Allow that.

Fixes #1277

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-02-08 13:54:37 -08:00
Brad Fitzpatrick
6d2b8df06d wgengine/magicsock: add disabled failing (deadlocking) test for #1282
The fix can make this test run unconditionally.

This moves code from 5c619882bc for
testability but doesn't fix it yet. The #1282 problem remains (when I
wrote its wake-up mechanism, I forgot there were N DERP readers
funneling into 1 UDP reader, and the code just isn't correct at all
for that case).

Also factor out some test helper code from BenchmarkReceiveFrom.

The refactoring in magicsock.go for testability should have no
behavior change.
2021-02-06 21:34:16 -08:00
David Anderson
e86b39b73f ipn/ipnlocal: don't short-circuit default route filtering.
If no exit node is specified, the filter must still run to remove
offered default routes from all peers.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-02-05 20:33:18 -08:00
Brad Fitzpatrick
1e7a35b225 types/netmap: split controlclient.NetworkMap off into its own leaf package
Updates #1278

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-05 16:18:52 -08:00
Brad Fitzpatrick
ddfcc4326c types/persist: split controlclient.Persist into a small leaf package
This one alone doesn't modify the global dependency map much
(depaware.txt if anything looks slightly worse), but it leave
controlclient as only containing NetworkMap:

bradfitz@tsdev:~/src/tailscale.com/ipn$ grep -F "controlclient." *.go
backend.go:     NetMap        *controlclient.NetworkMap // new netmap received
fake_test.go:   b.notify(Notify{NetMap: &controlclient.NetworkMap{}})
fake_test.go:   b.notify(Notify{NetMap: &controlclient.NetworkMap{}})
handle.go:      netmapCache       *controlclient.NetworkMap
handle.go:func (h *Handle) NetMap() *controlclient.NetworkMap {

Once that goes into a leaf package, then ipn doesn't depend on
controlclient at all, and then the client gets smaller.

Updates #1278
2021-02-05 15:25:33 -08:00
David Anderson
a046b48593 cmd/tailscale/cli: display currently active exit node in tailscale status.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-02-05 14:53:17 -08:00
Brad Fitzpatrick
6064b6ff47 wgengine/wgcfg/nmcfg: split control/controlclient/netmap.go into own package
It couldn't move to ipnlocal due to test dependency cycles.

Updates #1278

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-05 14:21:30 -08:00
Josh Bleecher Snyder
138055dd70 tstest/natlab: use net.ErrClosed instead of a new error
Upstream wireguard-go decided to use errors.Is(err, net.ErrClosed)
instead of checking the error string.

It also provided an unsafe linknamed version of net.ErrClosed
for clients running Go 1.15. Switch to that.

This reduces the time required for the wgengine/magicsock tests
on my machine from ~35s back to the ~13s it was before
456cf8a376.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-02-05 13:44:46 -08:00
David Anderson
ace57d7627 wgengine/magicsock: set a dummy private key in benchmark.
Magicsock started dropping all traffic internally when Tailscale is
shut down, to avoid spurious wireguard logspam. This made the benchmark
not receive anything. Setting a dummy private key is sufficient to get
magicsock to pass traffic for benchmarking purposes.

Fixes #1270.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-02-05 13:36:05 -08:00
David Anderson
b9c2231fdf ipn: program exit node into the data plane according to user pref.
Part of #1153, #1154. Fixes #1224.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-02-05 13:07:11 -08:00
Brad Fitzpatrick
fb6b0e247c cmd/tailscaled: rename Windows service to just Tailscale
Updates #1232
2021-02-05 11:13:34 -08:00
Brad Fitzpatrick
98f9e82c62 logpolicy: on Windows, use tailscale-ipn log name if it already existed
For the migration to tailscaled.exe on Windows, don't create a new logid
if one existed under the old filename.

Updates #1232
2021-02-05 10:57:51 -08:00
Brad Fitzpatrick
e8d4afedd1 control/controlclient: don't call lite endpoint update path when logged out
This was the other half of the #1271 problem.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-05 10:00:35 -08:00
Brad Fitzpatrick
a7562be5e1 cmd/tailscaled: move more of the Windows server setup code into tailscaled
Updates #1232
2021-02-05 09:53:54 -08:00
Brad Fitzpatrick
6f7974b7f2 cmd/tailscaled: add missing depaware.txt update 2021-02-05 08:48:00 -08:00
Brad Fitzpatrick
6099ecf7f4 cmd/tailscaled: run as a service on Windows
Updates #1232
2021-02-05 08:46:12 -08:00
Brad Fitzpatrick
7529b74018 control/controlclient: avoid crash sending map request with zero node key
Fixes #1271

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-04 16:23:50 -08:00
Josh Bleecher Snyder
aa6856a9eb wgengine: adapt to wireguard-go changes
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-02-04 15:15:33 -08:00
Brad Fitzpatrick
d76334d2f0 ipn: split LocalBackend off into new ipn/ipnlocal package
And move a couple other types down into leafier packages.

Now cmd/tailscale doesn't bring in netlink, magicsock, wgengine, etc.

Fixes #1181

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-04 14:04:23 -08:00
Brad Fitzpatrick
6254efb9ef cmd/tailscale{,d}: move debug subcommand to tailscaled
Work on reducing the size of the tailscale binary, which is
currently pulling in most of the same code as tailscaled.

Updates #1181
2021-02-04 12:23:06 -08:00
Brad Fitzpatrick
70eb05fd47 wgengine: access flow pending problem with lock held
Missed review feedback from just-submitted d37058af72.
2021-02-04 11:18:32 -08:00
Brad Fitzpatrick
d37058af72 net/packet: add some more TSMP packet reject reasons and MaybeBroken bit
Unused for now, but I want to backport this commit to 1.4 so 1.6 can
start sending these and then at least 1.4 logs will stringify nicely.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-04 10:59:07 -08:00
Christine Dodrill
2f0cb98e50 logpolicy: rename target env var to TS_LOG_TARGET (#1267)
Signed-Off-By: Christine Dodrill <xe@tailscale.com>
2021-02-04 12:38:30 -05:00
Brad Fitzpatrick
f7eed25bb9 wgengine/magicsock: filter disco packets and packets when stopped from wireguard
Fixes #1167
Fixes tailscale/corp#219

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-04 09:38:02 -08:00
Christine Dodrill
81466eef81 Add an environment variable to enable customizing the log target (#1243)
Signed-off-by: Christine Dodrill <xe@tailscale.com>
2021-02-04 12:20:17 -05:00
David Anderson
45fe06a89f Revert "tailcfg: remove v6-overlay debug option."
This reverts commit da4ec54756.

Since v6 got disabled for Windows nodes, I need the debug flag back
to figure out why it was broken.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-02-03 16:11:56 -08:00
Josh Bleecher Snyder
e8cd7bb66f tstest: simplify goroutine leak tests
Use tb.Cleanup to simplify both the API and the implementation.

One behavior change: When the number of goroutines shrinks, don't log.
I've never found these logs to be useful, and they frequently add noise.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-02-03 13:06:40 -08:00
Brad Fitzpatrick
9a70789853 cmd/tailscale: fix IPN message reading stall in tailscale status -web
Fixes #1234
Updates #1254
2021-02-02 14:51:44 -08:00
Brad Fitzpatrick
a2aa6cd2ed wgengine/router: clarify disabled IPv6 message on Linux 2021-02-02 14:51:44 -08:00
David Crawshaw
d139fa9c92 net/interfaces: use a uint32_t for ipv4 address
The code was using a C "int", which is a signed 32-bit integer.
That means some valid IP addresses were negative numbers.
(In particular, the default router address handed out by AT&T
fiber: 192.168.1.254. No I don't know why they do that.)
A negative number is < 255, and so was treated by the Go code
as an error.

This fixes the unit test failure:

	$ go test -v -run=TestLikelyHomeRouterIPSyscallExec ./net/interfaces
	=== RUN   TestLikelyHomeRouterIPSyscallExec
	    interfaces_darwin_cgo_test.go:15: syscall() = invalid IP, false, netstat = 192.168.1.254, true
	--- FAIL: TestLikelyHomeRouterIPSyscallExec (0.00s)

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-02-02 13:32:58 -08:00
David Anderson
267531e4f8 wgengine/router: probe better for v6 policy routing support.
Previously we disabled v6 support if the disable_policy knob was
missing in /proc, but some kernels support policy routing without
exposing the toggle. So instead, treat disable_policy absence as a
"maybe", and make the direct `ip -6 rule` probing a bit more
elaborate to compensate.

Fixes #1241.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-02-01 16:12:17 -08:00
Josh Bleecher Snyder
717c715c96 wgengine/wglog: don't log failure to send data packets
Fixes #1239
2021-02-01 14:41:51 -08:00
Josh Bleecher Snyder
516e8a4838 tsweb: add num_goroutines expvar
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-02-01 14:38:59 -08:00
Josh Bleecher Snyder
dd10babaed wgenginer/magicsock: remove Addrs methods
They are now unused.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-02-01 14:05:05 -08:00
Brad Fitzpatrick
c7d4bf2333 cmd/tailscale/cli: recommend sudo for 'tailscale up' on failure
Fixes #1220
2021-02-01 13:53:57 -08:00
Brad Fitzpatrick
2889fabaef cmd/tailscaled/tailscaled.service: revert recent hardening for now
It broke Debian Stretch. We'll try again later.

Updates #1245

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-02-01 13:37:48 -08:00
Brad Fitzpatrick
761188e5d2 wgengine/wgcfg: fix validateEndpoints of empty string
Updates tailscale/corp#1238
2021-01-30 11:17:55 -08:00
Brad Fitzpatrick
914a486af6 safesocket: refactor macOS auth code, pull out separate LocalTCPPortAndToken 2021-01-29 14:34:57 -08:00
Brad Fitzpatrick
60e189f699 cmd/hello: use safesocket client to connect 2021-01-29 13:49:17 -08:00
Brad Fitzpatrick
006a224f50 ipn/ipnserver, cmd/hello: do whois over unix socket, not debug http
Start of a local HTTP API. Not a stable interface yet.
2021-01-29 13:23:13 -08:00
Josh Bleecher Snyder
fe7c3e9c17 all: move wgcfg from wireguard-go
This is mostly code movement from the wireguard-go repo.

Most of the new wgcfg package corresponds to the wireguard-go wgcfg package.

wgengine/wgcfg/device{_test}.go was device/config{_test}.go.
There were substantive but simple changes to device_test.go to remove
internal package device references.

The API of device.Config (now wgcfg.DeviceConfig) grew an error return;
we previously logged the error and threw it away.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-01-29 12:52:56 -08:00
Brad Fitzpatrick
0bc73f8e4f cmd/hello: new hello.ipn.dev server
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-29 12:23:13 -08:00
Brad Fitzpatrick
c611d8480b cmd/tailscaled: add whois/identd-ish debug handler 2021-01-28 15:31:52 -08:00
Brad Fitzpatrick
c7fc4a06da wgengine/router: don't configure IPv6 on Linux when IPv6 is unavailable
Fixes #1214

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-28 13:35:11 -08:00
David Anderson
de497358b8 cmd/tailscaled: add /run to the allowed paths for iptables.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-01-28 12:58:07 -08:00
Josh Bleecher Snyder
1e28207a15 types/logger: fix rateFree interaction with verbosity prefixes
We log lines like this:

c.logf("[v1] magicsock: disco: %v->%v (%v, %v) sent %v", c.discoShort, dstDisco.ShortString(), dstKey.ShortString(), derpStr(dst.String()), disco.MessageSummary(m))

The leading [v1] causes it to get unintentionally rate limited.
Until we have a proper fix, work around it.

Fixes #1216

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-01-28 10:15:56 -08:00
David Anderson
7a16ac80b7 VERSION.txt: this is 1.5.0. 2021-01-27 18:45:22 -08:00
Brad Fitzpatrick
4d943536f1 wgengine: don't leak TUN device in NewUserspaceEngine error path
Updates #1187

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-27 11:06:56 -08:00
Brad Fitzpatrick
9f5b0d058f wgengine: fix bugs from earlier fix
Fixes a regression from e970ed0995 that wasn't covered by tests
in this repo. (Our end-to-end tests in another repo caught this.)

Updates #1204
2021-01-27 10:32:08 -08:00
Sonia Appasamy
4dab0c1702 tailcfg: update node display name fields and methods (#1207)
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>

Consolidates the node display name logic from each of the clients into
tailcfg.Node. UI clients can use these names directly, rather than computing
them independently.
2021-01-27 11:50:31 -05:00
Brad Fitzpatrick
35e10c78fc net/interfaces: don't send over zt* interfaces
Fixes #1208

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-26 15:20:43 -08:00
David Anderson
692a011b54 net/interfaces: remove IsTailscaleIP, make callers use tsaddr.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-01-26 15:10:51 -08:00
Brad Fitzpatrick
e970ed0995 wgengine: fix crash reading long UAPI lines from legacy peers
Also don't log.Fatalf in a function returning an error.

Fixes #1204

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-26 11:39:13 -08:00
Brad Fitzpatrick
a7edcd0872 ipn/ipnstate: update tailscale status -web to match CLI 2021-01-26 08:29:59 -08:00
Brad Fitzpatrick
a98538f84a Merge branch 'main' of github.com:tailscale/tailscale into main 2021-01-25 15:53:13 -08:00
Brad Fitzpatrick
c3c59445ff ipn/ipnserver: on Windows in unattended mode, wait for Engine forever
Updates #1187
2021-01-25 15:52:24 -08:00
Brad Fitzpatrick
0dde8fa0a8 ipn/ipnserver: rearrange some code
No functional change. Make a future diff easier to read.
2021-01-25 15:46:39 -08:00
Brad Fitzpatrick
4d3c09ced4 ipn/ipnserver: on Windows in unattended mode, wait for Engine forever
Updates #1187
2021-01-25 15:32:13 -08:00
Sonia Appasamy
567c5a6d9e tailcfg, controlclient: add DisplayName field to tailcfg.Node and populate it from controlclient (#1191)
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
2021-01-25 17:41:39 -05:00
Brad Fitzpatrick
4fea604979 wgengine/router: stop setPrivateNetwork goroutine on configureInterface failure
On Windows, configureInterface starts a goroutine reconfiguring the
Windows firewall.

But if configureInterface fails later, that goroutine kept running and
likely failing forever, spamming logs. Make it stop quietly if its
launching goroutine filed.
2021-01-25 13:22:51 -08:00
Andrey Petrov
bf6205d200 LICENSE: Reformat for Github
Should be equivalent to the license before, but compatible with the library Github uses to detect the license for the project's metadata: https://docs.github.com/en/github/creating-cloning-and-archiving-repositories/licensing-a-repository#detecting-a-license

Signed-off-by: Andrey Petrov <andrey.petrov@shazow.net>
2021-01-24 16:20:22 -08:00
David Anderson
9f7cbf6cf1 wgengine/filter: add a Clone method.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-01-22 17:31:37 -08:00
Brad Fitzpatrick
9ce92aad3e cmd/tailscaled: update depaware.txt 2021-01-22 14:44:40 -08:00
Brad Fitzpatrick
fa3543d629 control/controlclient: use more direct way of getting the MagicDNS suffix
Suggested by Avery earlier. Ends up fixing bug in "tailscale status" when
MagicDNS if off too:
https://forum.tailscale.com/t/1-3-293-is-released-a-1-4-0-pre-release/349/11?u=bradfitz
2021-01-22 14:30:56 -08:00
Brad Fitzpatrick
e7bf144c3f ipn, wgengine/filter: fix Shields Up recent regression and old bug
Fixes #1192 (regression)
Fixes #1193 (old bug)
2021-01-22 13:39:53 -08:00
Brad Fitzpatrick
97496a83af wgengine/tstun: also support DropSilently on PostFilterIn
Not a problem (yet). But should be consistent with other places that support both
types of drops.
2021-01-22 13:22:32 -08:00
Brad Fitzpatrick
eb47cba435 cmd/tailscaled: don't require --state for --cleanup 2021-01-22 11:35:22 -08:00
Brad Fitzpatrick
daf2c70a08 go.mod: bump wireguard-go 2021-01-21 20:03:35 -08:00
Josh Bleecher Snyder
d5baeeed5c wgengine: use Tailscale-style peer identifiers in logs
Rewrite log lines on the fly, based on the set of known peers.

This enables us to use upstream wireguard-go logging,
but maintain the Tailscale-style peer public key identifiers
that the rest of our systems (and people) expect.

Fixes #1183

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-01-21 19:13:32 -08:00
Brad Fitzpatrick
4306433d1c cmd/tailscale: make "tailscale ping" also resolve names without DNS
This lets "tailscale ping $NAME" work even if MagicDNS is off, letting you
ping a name that shows up in "tailscale status".

More user friendly.
2021-01-21 15:45:36 -08:00
Brad Fitzpatrick
9541886856 wgengine/magicsock: disable regular STUNs for all platforms by default
Reduces background CPU & network.

Updates #1034

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-21 14:56:07 -08:00
David Anderson
49d00b6a28 tailcfg: add StableID to Node. #1178
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-01-21 13:33:19 -08:00
Brad Fitzpatrick
54d0d83b67 safesocket: on Linux, make /var/run/tailscale be 0755
Continuation of earlier two umask changes,
5611f290eb and
d6e9fb1df0.

This change mostly affects us, running tailscaled as root by hand (wit
a umask of 0077), not under systemd. End users running tailscaled
under systemd won't have a umask.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-21 13:23:14 -08:00
Steve Coffman
fec9490378 Add docker build script to inject version information
Signed-off-by: Steve Coffman <steve@khanacademy.org>
2021-01-21 12:42:53 -08:00
Brad Fitzpatrick
c55d26967b wgengine/magicsock: log more details of endpoints learned over disco
Also, don't try to use IPv6 LinkLocalUnicast addresses for now. Like endpoints
exchanged with control, we share them but don't yet use them.

Updates #1172
2021-01-21 08:06:14 -08:00
Brad Fitzpatrick
9f1b02699a tstime: add RandomDurationBetween helper
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-21 07:54:14 -08:00
Brad Fitzpatrick
a905ce5607 control/controlclient: add debug knob to not use control's endpoints 2021-01-20 21:31:06 -08:00
Brad Fitzpatrick
359055d3fa wgengine/magicsock: fix logging regression
c8c493f3d9 made it always say
`created=false` which scared me when I saw it, as that would've implied
things were broken much worse. Fortunately the logging was just wrong.
2021-01-20 20:48:02 -08:00
Brad Fitzpatrick
b5628cee4e control/controlclient: add detail to verbose log about route skips 2021-01-20 19:28:21 -08:00
Brad Fitzpatrick
edf64e0901 wgengine/magicsock: send, use endpoints in CallMeMaybe messages
Fixes #1172

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-20 14:59:41 -08:00
Brad Fitzpatrick
ec77b80c53 tailcfg, control/controlclient: add mapver 10: MapResponse.PeerSeenChange
This adds a more wire-efficient way of updating peers' Node.LastSeen times.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-20 13:20:21 -08:00
Brad Fitzpatrick
b5b4992eff disco: support parsing/encoding endpoints in call-me-maybe frames
Updates #1172

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-20 12:16:33 -08:00
Josh Bleecher Snyder
d3dd7c6270 wgengine/magicsock: make legacy DstToString match Addrs
DstToString is used in two places in wireguard-go: Logging and uapi.

We are switching to use uapi for wireguard-go config.
To preserve existing behavior, we need the full set of addrs.

And for logging, having the full set of addrs seems useful.

(The Addrs method itself is slated for removal. When that happens,
the implementation will move to DstToString.)


Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-01-20 10:31:51 -08:00
Brad Fitzpatrick
187e22a756 wgengine/magicsock: don't run the DERP cleanup so often
To save CPU and wakeups, don't run the DERP cleanup timer regularly
unless there is a non-home DERP connection open.

Also eliminates the goroutine, moving to a time.AfterFunc.

Updates #1034

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-19 18:14:25 -08:00
David Anderson
ab9cccb292 cmd/tailscale/cli: require v4 and v6 default routes to be advertised together.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-01-19 16:49:06 -08:00
David Anderson
78338ac029 types/logger: trim spaces from the rate-limited example message.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-01-19 16:48:44 -08:00
Brad Fitzpatrick
b405644f5d api.md: add TOC 2021-01-19 12:35:09 -08:00
Josh Bleecher Snyder
5fe5402fcd Revert "wgengine/magicsock: shortcircuit discoEndpoint.heartbeat when its connection is closed"
This reverts commit 08baa17d9a.
It caused deadlocks due to lock ordering violations.
It was not the right fix, and thus should simply be reverted
while we look for the right fix (if we haven't already found it
in the interim; we've fixed other logging-after-test issues).

Fixes #1161
2021-01-19 11:44:32 -08:00
Josh Bleecher Snyder
e4c075cd95 wgengine/magicsock: prevent log-after-test in TestTwoDevicePing 2021-01-19 11:04:17 -08:00
Brad Fitzpatrick
edce91a8a6 wgengine/magicsock: fix a naked return bug/crash where we returned (nil, true)
The 'ok' from 'ipp, ok :=' above was the result parameter ok. Whoops.
2021-01-19 10:57:40 -08:00
Brad Fitzpatrick
51bd1feae4 wgengine/magicsock: add single element IPPort->endpoint cache in receive path
name           old time/op  new time/op  delta
ReceiveFrom-4  21.8µs ± 2%  20.9µs ± 2%  -4.27%  (p=0.000 n=10+10)

Updates #414

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-01-18 21:40:58 -08:00
173 changed files with 8415 additions and 1911 deletions

View File

@@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.15
go-version: 1.16
id: go
- name: Check out code into the Go module directory

View File

@@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.15
go-version: 1.16
id: go
- name: Check out code into the Go module directory

View File

@@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.15
go-version: 1.16
id: go
- name: Check out code into the Go module directory

View File

@@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.15
go-version: 1.16
id: go
- name: Check out code into the Go module directory

View File

@@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.15
go-version: 1.16
id: go
- name: Check out code into the Go module directory

View File

@@ -16,7 +16,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.15
go-version: 1.16
- name: Check out code
uses: actions/checkout@v1

View File

@@ -16,7 +16,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.15
go-version: 1.16
- name: Check out code
uses: actions/checkout@v1

View File

@@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.15
go-version: 1.16
id: go
- name: Check out code into the Go module directory

View File

@@ -19,7 +19,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.15
go-version: 1.16
id: go
- name: Check out code into the Go module directory

View File

@@ -16,7 +16,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.15
go-version: 1.16
- name: Check out code
uses: actions/checkout@v1

View File

@@ -19,7 +19,7 @@ jobs:
- name: Install Go
uses: actions/setup-go@v2
with:
go-version: 1.15.x
go-version: 1.16.x
- name: Checkout code
uses: actions/checkout@v2

View File

@@ -38,7 +38,7 @@
# $ docker exec tailscaled tailscale status
FROM golang:1.15-alpine AS build-env
FROM golang:1.16-alpine AS build-env
WORKDIR /go/src/tailscale
@@ -48,6 +48,10 @@ RUN go mod download
COPY . .
# see build_docker.sh
ARG goflags_arg=""
ENV GOFLAGS=$goflags_arg
RUN go install -v ./cmd/...
FROM alpine:3.11

46
LICENSE
View File

@@ -1,27 +1,29 @@
Copyright (c) 2020 Tailscale & AUTHORS. All rights reserved.
BSD 3-Clause License
Copyright (c) 2020 Tailscale & AUTHORS.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Tailscale Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -43,7 +43,7 @@ If your distro has conventions that preclude the use of
distro's way, so that bug reports contain useful version information.
We only guarantee to support the latest Go release and any Go beta or
release candidate builds (currently Go 1.15) in module mode. It might
release candidate builds (currently Go 1.16) in module mode. It might
work in earlier Go versions or in GOPATH mode, but we're making no
effort to keep those working.

35
TODO Normal file
View File

@@ -0,0 +1,35 @@
TODO for Tailscale Mac Homebrew
This is a grab bag of ideas, issues, tasks, bugs and upgrades.
Ordered starting roughly with the most important or ideally next.
Not rigorous. Not limited to it. Subjective. Revised as we go.
-----------------------------------------------------------------
( ) in formula set version tag value properly
( ) in formula test block, assert versions reported by running installed tailscale{d} match expected
( ) in formula after built or installed, calc "shasum -a256" of the tailscale{d} and print it. helpful for security conscious folks afterward to ensure the binaries have not changed when unexpected (since they provide a VPN and run as root)
( ) for the tarball variants (most especially for tb-github), define the tarball's expected sha256 (the "want") in the proper way; dl-tarball-sha.sh uses only a placeholder technique for WIP
( ) correct or suppress this (default and automatic?) message by brew during install, because sudo required:
To have launchd start tailscale now and restart at login:
brew services start tailscale
( ) make scripts learn what tun value was picked by tailscaled (likely utun<NUM>)
(X) added correct message via formula caveat:
To have launchd start tailscale now and restart at boot:
sudo brew services start tailscale
( ) write/confirm test after reboot
( ) README: make it clear this is not an official/company Tailscale package
( ) retest the tarball versions, make sure they work right; esp the version stuff
( ) tailcfg Hostinfo.Package: control/controlclient/direct.go packageType(): either inject an ENVVAR, or have it decide by exe/path?
( ) fix that 1.5-vs-1.4.4 FIXME bug in the "global, tb-github" test case in install* (after log msg "checking exe location and diffs...")
( ) shrink the gap to ideal minimum (via refactors anywhere) between our formulae plists and Brad's daemon install plist, the latter being in: cmd/tailscaled/install_darwin.go
( ) ideally eliminate all TODOs from formula and scripts and README; or, just remove/satisfy as many as you can soon in upcoming sessions
( ) can pass extra args at brew install time, and inside your formula they are readable from ARGV.value. BEST/ONLY if they become stable/perm -- embedded in the installed-for-launchd-plist version
( ) print log location by formula when brew installed (ideally also the state file, sock and utun); via caveats method?
( ) discuss paths in README
( ) document or automate-away the $PWD path assumption deviance issue in make-test-source-tarball.sh
( ) stop script: TODO the installed plist is gone?
( ) stop script: TODO wipeout brew git checkout cache and/or add test for with-vs-without
( ) has the master-vs-main bug come back? since I twiddle the tag/branch/head stuff in formula to fix that other bug
( ) add test variants for other brew tool versions?
( ) in future: think about documenting the plist EnvironmentVariables key, as a way for user to set Tailscale-specific ENVVARS for tailscaled to read (like for debugging or testing)
( ) in future: maybe maybe play with the plist's ProcessType key, as a way to tune cpu/io and battery use

View File

@@ -1 +1 @@
1.3.0
1.5.0

62
api.md
View File

@@ -5,7 +5,28 @@ The Tailscale API is a (mostly) RESTful API. Typically, POST bodies should be JS
# Authentication
Currently based on {some authentication method}. Visit the [admin panel](https://api.tailscale.com/admin) and navigate to the `Keys` page. Generate an API Key and keep it safe. Provide the key as the user key in basic auth when making calls to Tailscale API endpoints.
# APIS
# APIs
* **[Devices](#device)**
- [GET device](#device-get)
- [DELETE device](#device-delete)
- Routes
- [GET device routes](#device-routes-get)
- [POST device routes](#device-routes-post)
* **[Tailnets](#tailnet)**
- ACLs
- [GET tailnet ACL](#tailnet-acl-get)
- [POST tailnet ACL](#tailnet-acl-post): set ACL for a tailnet
- [POST tailnet ACL preview](#tailnet-acl-preview-post): preview rule matches on an ACL for a resource
- [Devices](#tailnet-devices)
- [GET tailnet devices](#tailnet-devices-get)
- [DNS](#tailnet-dns)
- [GET tailnet DNS nameservers](#tailnet-dns-nameservers-get)
- [POST tailnet DNS nameservers](#tailnet-dns-nameservers-post)
- [GET tailnet DNS preferences](#tailnet-dns-preferences-get)
- [POST tailnet DNS preferences](#tailnet-dns-preferences-post)
- [GET tailnet DNS searchpaths](#tailnet-dns-searchpaths-get)
- [POST tailnet DNS searchpaths](#tailnet-dns-searchpaths-post)
## Device
<!-- TODO: description about what devices are -->
@@ -16,6 +37,8 @@ To find the deviceID of a particular device, you can use the ["GET /devices"](#g
Find the device you're looking for and get the "id" field.
This is your deviceID.
<a name=device-get></div>
#### `GET /api/v2/device/:deviceid` - lists the details for a device
Returns the details for the specified device.
Supply the device of interest in the path using its ID.
@@ -103,6 +126,7 @@ Response
}
```
<a name=device-delete></div>
#### `DELETE /api/v2/device/:deviceID` - deletes the device from its tailnet
Deletes the provided device from its tailnet.
@@ -139,6 +163,8 @@ If the device is not owned by your tailnet:
```
<a name=device-routes-get></div>
#### `GET /api/v2/device/:deviceID/routes` - fetch subnet routes that are advertised and enabled for a device
Retrieves the list of subnet routes that a device is advertising, as well as those that are enabled for it. Enabled routes are not necessarily advertised (e.g. for pre-enabling), and likewise, advertised routes are not necessarily enabled.
@@ -166,6 +192,8 @@ Response
}
```
<a name=device-routes-post></div>
#### `POST /api/v2/device/:deviceID/routes` - set the subnet routes that are enabled for a device
Sets which subnet routes are enabled to be routed by a device by replacing the existing list of subnet routes with the supplied parameters. Routes can be enabled without a device advertising them (e.g. for preauth). Returns a list of enabled subnet routes and a list of advertised subnet routes for a device.
@@ -210,7 +238,8 @@ A tailnet is the name of your Tailscale network.
You can find it in the top left corner of the [Admin Panel](https://login.tailscale.com/admin) beside the Tailscale logo.
"alice@example.com" belongs to the "example.com" tailnet and would use the following format for API calls:
`alice@example.com` belongs to the `example.com` tailnet and would use the following format for API calls:
```
GET /api/v2/tailnet/example.com/...
curl https://api.tailscale.com/api/v2/tailnet/example.com/...
@@ -218,21 +247,21 @@ curl https://api.tailscale.com/api/v2/tailnet/example.com/...
For solo plans, the tailnet is the email you signed up with.
So "alice@gmail.com" has the tailnet "alice@gmail.com" since @gmail.com is a shared email host.
So `alice@gmail.com` has the tailnet `alice@gmail.com` since `@gmail.com` is a shared email host.
Her API calls would have the following format:
```
GET /api/v2/tailnet/alice@gmail.com/...
curl https://api.tailscale.com/api/v2/tailnet/alice@gmail.com/...
```
Tailnets are a top level resource. ACL is an example of a resource that is tied to a top level tailnet.
Tailnets are a top-level resource. ACL is an example of a resource that is tied to a top-level tailnet.
For more information on Tailscale networks/tailnets, click [here](https://tailscale.com/kb/1064/invite-team-members).
### ACL
<a name=tailnet-acl-get></a>
#### `GET /api/v2/tailnet/:tailnet/acl` - fetch ACL for a tailnet
Retrieves the ACL that is currently set for the given tailnet. Supply the tailnet of interest in the path. This endpoint can send back either the HuJSON of the ACL or a parsed JSON, depending on the `Accept` header.
@@ -334,6 +363,8 @@ Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
}
```
<a name=tailnet-acl-post></a>
#### `POST /api/v2/tailnet/:tailnet/acl` - set ACL for a tailnet
Sets the ACL for the given tailnet. HuJSON and JSON are both accepted inputs. An `If-Match` header can be set to avoid missed updates.
@@ -405,6 +436,8 @@ Response
}
```
<a name=tailnet-acl-preview-post></a>
#### `POST /api/v2/tailnet/:tailnet/acl/preview` - preview rule matches on an ACL for a resource
Determines what rules match for a user on an ACL without saving the ACL to the server.
@@ -449,8 +482,12 @@ Response
{"matches":[{"users":["*"],"ports":["*:*"],"lineNumber":19}],"user":"user1@example.com"}
```
<a name=tailnet-devices></a>
### Devices
<a name=tailnet-devices-get></a>
#### <a name="getdevices"></a> `GET /api/v2/tailnet/:tailnet/devices` - list the devices for a tailnet
Lists the devices in a tailnet.
Supply the tailnet of interest in the path.
@@ -531,9 +568,12 @@ Response
}
```
<a name=tailnet-dns></a>
### DNS
<a name=tailnet-dns-nameservers-get></a>
#### `GET /api/v2/tailnet/:tailnet/dns/nameservers` - list the DNS nameservers for a tailnet
Lists the DNS nameservers for a tailnet.
Supply the tailnet of interest in the path.
@@ -556,6 +596,8 @@ Response
}
```
<a name=tailnet-dns-nameservers-post></a>
#### `POST /api/v2/tailnet/:tailnet/dns/nameservers` - replaces the list of DNS nameservers for a tailnet
Replaces the list of DNS nameservers for the given tailnet with the list supplied by the user.
Supply the tailnet of interest in the path.
@@ -608,6 +650,8 @@ Response:
}
```
<a name=tailnet-dns-preferences-get></a>
#### `GET /api/v2/tailnet/:tailnet/dns/preferences` - retrieves the DNS preferences for a tailnet
Retrieves the DNS preferences that are currently set for the given tailnet.
Supply the tailnet of interest in the path.
@@ -629,6 +673,8 @@ Response:
}
```
<a name=tailnet-dns-preferences-post></a>
#### `POST /api/v2/tailnet/:tailnet/dns/preferences` - replaces the DNS preferences for a tailnet
Replaces the DNS preferences for a tailnet, specifically, the MagicDNS setting.
Note that MagicDNS is dependent on DNS servers.
@@ -673,6 +719,8 @@ If there are DNS servers:
}
```
<a name=tailnet-dns-searchpaths-get></a>
#### `GET /api/v2/tailnet/:tailnet/dns/searchpaths` - retrieves the search paths for a tailnet
Retrieves the list of search paths that is currently set for the given tailnet.
Supply the tailnet of interest in the path.
@@ -695,6 +743,8 @@ Response:
}
```
<a name=tailnet-dns-searchpaths-post></a>
#### `POST /api/v2/tailnet/:tailnet/dns/searchpaths` - replaces the search paths for a tailnet
Replaces the list of searchpaths with the list supplied by the user and returns an error otherwise.

1
brew/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
local/

102
brew/README.md Normal file
View File

@@ -0,0 +1,102 @@
# Tailscale Mac Homebrew Maintainer Guide
***********
*** WIP ***
***********
Homebrew packaging of the non-IPNExtension, unsandboxed tailscale{d} releases for macOS/darwin.
So the public can "brew install" tailscale and start it as a global boot daemon via brew services.
## Platforms Supported
These platform versions and permutations have been tested and are known to work well:
| role | brew | golang | os | arch | repo |
| ----------- | --------------------------- | --------------------- | ---------------------- | ------------------ | ---------- |
| maintainer: | Homebrew 3.0.1-120-g5ca03ab | go1.15.8 darwin/amd64 | macOS Catalina 10.15.3 | Intel 64-bit | |
| | w/ ruby 2.6.3p62 | | macOS Big Sur 11.x (?) | Apple M1 ARM64 (?) | |
| | | | | | |
| | | | | | |
| pkg target: | Homebrew 3.0.1-120-g5ca03ab | go1.15.8 darwin/amd64 | macOS Catalina 10.15.3 | Intel 64-bit | tailscale: |
| | w/ ruby 2.6.3p62 | | macOS Big Sur 11.x (?) | Apple M1 ARM64 (?) | 188bb14269 |
| | | | | | HEAD |
| | | | | | likely 1.5 |
## Directory Contents
| type | name | purpose |
| ------------------ | ---------------------------- | -------------------------------------------------------------- |
| formulae | tailscale.rb | The default packaging "formula" in Ruby, based on a |
| | | a pinned commit from the tailscale/tailscale GitHub repo |
| | | |
| | tailscale.*.rb | Alternate formulae (mainly used for testing now, |
| | | and includes some variants based on tagged release source |
| | | tarballs, and local test standin candidates) |
| | | |
| maintainer scripts | vars.sh | role for brew like version/version.sh for build_dist.sh |
| | generate-formulae.sh | regenerates all formulae (default, plus some alternates) |
| | generate-formula.sh | generates formula from an embedded template, to stdout |
| | make-test-source-tarball.sh | |
| | serve-tarball.sh | |
| | dl-tarball-sha.sh | |
| | test.sh | tests all supported permutations |
| | install-start-with-checks.sh | install test run for maintainer |
| | up.sh | |
| | status.sh | |
| | stop-uninstall-wipe.sh | closing counterpart to the install*.sh script |
| | search-interesting.sh | nice to keep these on maintainer's radar |
| | | |
| subdirs | brew/local/ | .gitignored tree created and used during brew maintenance work |
## Use Cases
For the ultimate endusers (ONLY ONCE READY & AVAIL)...
```
brew install tailscale
sudo brew services start tailscale
```
TODO(mkramlich): add context, details, variants to the above
For an early adopter who wants to try an unofficial sneak preview of a local brew install of the WIP formula:
```
git clone https://github.com/mkramlich/tailscale
cd tailscale
git switch mkramlich/macos-brew2
brew install --formula brew/tailscale.rb # default formula draws from a recent good commit from tailscale/tailscale
sudo brew services start tailscale
# tailscaled is now running and registered as a global boot daemon (via launchctl/launchd)
brew/up.sh # then do your auth
# tailscaled is now authorized and providing normal service to your VPN
```
For maintainers...
All scripts assume you are in the cloned repo root, just above brew/
(EXCEPT make-test-source-tarball.sh, TODO)
To edit the formula source, regen and retest:
```
# modify the template fragments inside generate-formula.sh
# and/or the var values in brew/vars.sh or brew/generate-formulae.sh
brew/generate-formulae.sh
brew/serve-tarball.sh # ensure running in background; only needed for the local source tarball test in test.sh
brew/test.sh # this is very WIP, but generally the goal is if it exits 0 then the tests are green
```
A brew audit of a formula candidate can fail and its possible for it to 'brew install' and tailscale start successfully. A clean brew audit is only required (or strongly recommended) for submission to the Homebrew Core repository.
A "clean" brew doctor on the maintainer's host is not required, but its helpful to ensure no unnecessary problems arise with brew testing downstream. And helps ensure that no quirks of the maintainer's host cause the formula to "works on my box!" but then fail on other's machines. Majority of issues it reports will have no impact, but ideally it should be kept silent and exit 0.
A quirk of brew is that it likes to autoupdate under the hood, in reaction to (and not strictly needed to carry out) the user's express commands. This can cause unexpected delays (especially if bandwidth is your bottleneck) during package maintainer testing workflows. And makes it a challenge to achieve perfectly strict idempotency and deterministic installs. It appears to be a known trade-off call made by the Homebrew tool devs.
TODO(mkramlich): flesh out much further; topics: formula vs bottle, sudo vs not, prefix diffs, local vs public ts tap vs core, testing, submission and bumping

10
brew/dl-tarball-sha.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/usr/bin/env sh
set -eu
eval $(brew/vars.sh)
#URLBASE=https://github.com
URLBASE=http://localhost
TARBALL=v$TS_VER.tar.gz
curl -OL $URLBASE/tailscale/tailscale/archive/$TARBALL
shasum -a256 ./$TARBALL # TODO(mkramlich): not the proper point in lifecycle to calc this

132
brew/generate-formula.sh Executable file
View File

@@ -0,0 +1,132 @@
#!/usr/bin/env sh
#set -u # TODO(mkramlich): recreate
# TODO(mkramlich): in form where the url is a .git, the url can also have a tag arg, like: tag: "v1.5.0"
cat <<TEMPLATE
# typed: false
# frozen_string_literal: true
# Homebrew formula for Tailscale
class Tailscale < Formula
desc "Easiest, secure, crossplatform WireGuard Go-based VPN w/oauth2, 2FA/SSO"
homepage "https://www.tailscale.com"
TEMPLATE
if [ "$FORMULA_TYPE" = "tarball" ]; then
cat <<TEMPLATE2
url "$URL"
sha256 "$SHA256"
TEMPLATE2
elif [ "$FORMULA_TYPE" = "commit" ]; then
cat <<TEMPLATE3
url "$URL",
revision: "$REVISION"
version "$VERSION" # TODO(mkramlich): WIP, so not necessarily; brew required a version
TEMPLATE3
else
exit 1
fi
cat <<TEMPLATE4
license "BSD-3-Clause"
head "$HEAD",
branch: "$BRANCH"
depends_on "go" => :build
def install
ENV["GOPATH"] = buildpath
tailscale_path = buildpath/"src/github.com/tailscale/tailscale"
tailscale_path.install buildpath.children
cd tailscale_path do
# build the exes with version strings equiv to the tailscale repo's build_dist.sh:
ldflags = prepare_ldflags
system "go", "build", "-o", ".", "-tags", "xversion", "-ldflags", ldflags, "tailscale.com/cmd/tailscale"
system "go", "build", "-o", ".", "-tags", "xversion", "-ldflags", ldflags, "tailscale.com/cmd/tailscaled"
bin.install "tailscale"
bin.install "tailscaled"
end
end
def prepare_ldflags
ver_props = prepare_ver_props
vl = ver_props["VERSION_LONG"]
vs = ver_props["VERSION_SHORT"]
vgh = ver_props["VERSION_GIT_HASH"]
vl = "tailscale.com/version.Long=#{vl}"
vs = "tailscale.com/version.Short=#{vs}"
vgh = "tailscale.com/version.GitCommit=#{vgh}"
"-X #{vl} -X #{vs} -X #{vgh}"
end
def prepare_ver_props
distvers = Utils.safe_popen_read("./version/version.sh")
lines = distvers.split("\n")
ver_props = {}
lines.each do |line|
parts = line.split("=")
key = parts.at(0)
val = parts.at(1).delete('"') # cuz version.sh emits each prop with double-quotes enclosing each val
# system "echo adding to ver_props for go builds: key #{key}, val #{val}"
ver_props[key] = val
end
ver_props
end
def post_install
(var/"run/tailscale").mkpath
(var/"lib/tailscale").mkpath
end
def caveats
<<~EOS
To have launchd start tailscale now and restart at boot:
sudo brew services start tailscale
NOTE: The caveat message below with 'restart at login' is incorrect, but we can't suppress it. Requires sudo.
EOS
end
plist_options manual: "sudo tailscaled --socket=#{HOMEBREW_PREFIX}/run/tailscale/tailscaled.sock --state=#{HOMEBREW_PREFIX}/lib/tailscale/tailscaled.state"
def plist
<<~EOS
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
<key>NetworkState</key>
<true/>
</dict>
<key>Label</key>
<string>#{plist_name}</string>
<key>ProgramArguments</key>
<array>
<string>#{opt_bin}/tailscaled</string>
<string>--socket=#{var}/run/tailscale/tailscaled.sock</string>
<string>--state=#{var}/lib/tailscale/tailscaled.state</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>WorkingDirectory</key>
<string>#{var}/lib/tailscale</string>
<key>StandardErrorPath</key>
<string>#{var}/log/tailscale/tailscaled-stderr.log</string>
<key>StandardOutPath</key>
<string>#{var}/log/tailscale/tailscaled-stdout.log</string>
</dict>
</plist>
EOS
end
test do
system bin/"tailscale", "version"
system bin/"tailscaled", "-version"
system bin/"tailscale", "netcheck"
end
end
TEMPLATE4

19
brew/generate-formulae.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env sh
set -eu
eval $(brew/vars.sh)
GIT_URL=https://github.com/tailscale/tailscale.git
BRANCH=main
VERSION=$TS_VER # TODO(mkramlich): WIP. do this right
# default formula (pinned against a commmit in the Tailscale GitHub repo)
FORMULA_TYPE=commit URL=$GIT_URL BRANCH=$BRANCH REVISION=$TS_COMMIT_PIN HEAD=$GIT_URL BRANCH=$BRANCH VERSION=$VERSION brew/generate-formula.sh > brew/tailscale.commit-pin.rb
# alt formula using local HTTP source tarball of a release standin/mock
FORMULA_TYPE=tarball URL="http://localhost:$TS_TARBALL_PORT/tailscale/tailscale/archive/v1.5.0.tar.gz" SHA256="a62caf2eb5c84d2d1775cd8a17eeb0e8ad3b8dbc9453399862d908227e0af721" HEAD=$GIT_URL BRANCH=$BRANCH VERSION=$VERSION brew/generate-formula.sh > brew/tailscale.tb-local.rb
# alt formula using Tailscale GitHub HTTPS source tarball of a release tag -- the LATEST tag
FORMULA_TYPE=tarball URL="https://github.com/tailscale/tailscale/archive/v1.4.4.tar.gz" SHA256="5312c6d075a32049912e0932a89269869def9ac8ea9d0fdccc6b41db60fc2d4c" HEAD=$GIT_URL BRANCH=$BRANCH VERSION=$VERSION brew/generate-formula.sh > brew/tailscale.tb-github.rb
cp brew/tailscale{.commit-pin,}.rb

100
brew/install-start-with-checks.sh Executable file
View File

@@ -0,0 +1,100 @@
#!/usr/bin/env sh
set -eu
eval $(brew/vars.sh)
echo
echo SUDO is $SUDO
echo
echo versions...
brew --version
brew config
go version
echo maintainer VERSION.txt: `cat VERSION.TXT`
echo maintainer script version/version.sh exec:
version/version.sh # we don't truly use this (yet?), just print here as helpful info for later analysis
# TODO(mkramlich) make distinction clear between maintainer script source ver and target package vers
echo
echo brew doctor...
set +e # to turn -e off (relax this only for time being)
brew doctor # TODO(mkramlich): for brew maintainer, best if have no findings and exit 0
set -e # to turn back on
echo
echo audit formula...
# requires Internet access for some audit checks:
brew audit --strict --online --formula $TS_FORMULA # keep this run exit 0 (NOTE: Homebrew can be noisy if updates)
echo
echo installing...
#brew install -v --build-from-source $TS_FORMULA
brew install -v --formula $TS_FORMULA
echo
echo checking exe location and diffs...
diff $TS_BIN/tailscale $TS_CELLAR/$TS_VER/bin/tailscale # TODO(mkramlich): FIXME in test case global,tb-github; want 1.5.0, got 1.4.4, SHOULD want 1.4.4 cuz 1.4.4 correct
diff $TS_BIN/tailscaled $TS_CELLAR/$TS_VER/bin/tailscaled
echo
echo check if brew has formula...
brew list tailscale
echo
echo check if installed formula matches orig...
diff $TS_FORMULA $TS_CELLAR/$TS_VER/.$TS_FORMULA
echo
echo lint of the generated launchctl plist for homebrew tailscale...
# should print "<filepath>: OK" and exit 0; brew services (or launchctl) below
# might reject (or silently allow) any given !0 case so better to catch an issue
# early/wider than not, and automate extra clues for maintainer
plutil -lint $TS_CELLAR/$TS_VER/$TS_PLIST
echo
echo brew test before starting...
brew test $TS_FORMULA
echo
echo starting...
$SUDO brew services start tailscale
echo
echo pausing as tailscaled starts...
sleep 8
echo
echo checking that our registered Global Daemons plist matches expected...
diff $TS_CELLAR/$TS_VER/$TS_PLIST $INSTALLED_PLIST_DIR/$TS_PLIST
echo
echo brew service shows tailscale as started...
$SUDO brew services list | grep tailscale
echo
echo launchctl lists tailscale...
$SUDO launchctl list | grep tailscale
echo
echo tailscale version...
$TS_BIN/tailscale --version # should be found, executable and exit 0
echo
echo tailscale netcheck...
$TS_BIN/tailscale netcheck # should be found, executable and exit 0
echo
echo tailscale up \(and wait for human to do the auth dance if needed\)...
brew/up.sh # this will block, displaying the auth url, if it needs to auth. then/or proceed once happy
echo
echo list all tailscale files installed or generated...
$SUDO find $BREW | grep -i tailscale
echo
echo pausing to give tailscaled enough time to finish post-auth setup...
sleep 15 # otherwise some of my early status pings sometimes fail (esp hello.ipn.dev; then work on all subsequent attempts)
echo
echo status...
brew/status.sh

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env sh
set -eu
# does not create an official release source tarball per GitHub. creates a standin for it, for brew test purposes only
eval $(tailscale/brew/vars.sh) # TODO(mkramlich): doc the path deviance or automate away
TARBALL_ARCHIVE=tailscale/brew/local/tarball-serve-root/tailscale/tailscale/archive
mkdir -p $TARBALL_ARCHIVE
TARBALL=$TARBALL_ARCHIVE/v$TS_VER.tar.gz
# assume a repo is cloned at that dir tree location, and its file state is what we want
echo making $TARBALL
tar -czf $TARBALL tailscale-$TS_VER/.gitignore tailscale-$TS_VER/.github/* tailscale-$TS_VER/.gitattributes tailscale-$TS_VER/*
shasum -a256 ./$TARBALL

6
brew/search-interesting.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env sh
brew search tailscale
brew search wireguard
brew search vpn
brew search golang

14
brew/serve-tarball.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env sh
set -eu
# only for Tailscale brew maintainer, to local-only serve a source tarball standin, during brew testing
eval $(brew/vars.sh)
python3 -m http.server --directory brew/local/tarball-serve-root --bind localhost $TS_TARBALL_PORT
# why the above? during one formula/release test brew may expect to fetch a tarball from (w/version varying):
# http://localhost:$TS_TARBALL_PORT/tailscale/tailscale/archive/v$TS_VER.tar.gz
# where $TS_VER is like 1.5.0
# therefore under tarball-serve-root/ that file should be in:
# tailscale/tailscale/archive/

83
brew/status.sh Executable file
View File

@@ -0,0 +1,83 @@
#!/usr/bin/env sh
set -u
eval $(brew/vars.sh)
# everything you see below is not expected to be rigorous, or decisive or even always relevant,
# but gives useful data and a nice smoke test of a candidate install for the brew maintainer
echo file checks \(exes, state, socket\)...
ls -la $TS_BIN/tailscale*
ls -l $TS_STATE
ls -l $TS_SOCK
echo
$TS_BIN/tailscale version
echo
$TS_BIN/tailscale netcheck
echo
echo tailscale status:
sudo $TS_BIN/tailscale -socket=$TS_SOCK status
echo
ifconfig -r en0
# TODO(mkramlich): learn the utun<NUM> it picked and handle tun/gateway right everywhere below
echo
ifconfig -r utun5
echo Routing\\n
echo \(Destination Gateway Flags Netif Expire\):
netstat -r -n -f inet | grep utun
echo IPv4 routes:
netstat -r -n -f inet -ll
echo IPv4 route stats:
netstat -r -n -f inet -s
echo IPv6 routes:
netstat -r -n -f inet6 -ll
echo IPv6 route stats:
netstat -r -n -f inet6 -s
echo
echo checking if host IP forwarding is enabled...
sysctl -a | grep forward
echo
echo pinging the TS login/coord server...
ping -c1 login.tailscale.com # only to help rule out other issues during a brew test
echo
echo pinging the TS log server...
ping -c1 log.tailscale.io
echo
echo pinging the common hello node by name...
ping -c1 hello.ipn.dev # a nice smoke test
echo ... by tailscale ping DNS...
sudo $TS_BIN/tailscale -socket=$TS_SOCK ping -c 1 hello.ipn.dev
echo ... by ping IPv4...
ping -c1 100.101.102.103
echo ... by tailscale ping IPv4...
sudo $TS_BIN/tailscale -socket=$TS_SOCK ping -c 1 100.101.102.103
echo ... by ping6 IPv6...
ping6 -c1 fd7a:115c:a1e0:ab12:4843:cd96:6265:6667 # TODO(mkramlich): do right or drop
# TODO(mkramlich): below is personal node Ts addr, so either drop or make overridable by maintainer
echo
echo pinging my android node...
ping -c1 100.119.147.125
sudo $TS_BIN/tailscale -socket=$TS_SOCK ping -c 1 100.119.147.125
ping6 -c1 fd7a:115c:a1e0:ab12:4843:cd96:6277:937d
echo
echo check stderr log for any recent error, warn, fail, missing or weird...
# NOTE we dont do a 'set -e' style fail assert on this error log tail here because this is for human consumption,
# plus, not quite sure/ready about what specifc messages are scary enough to potentially fail a test
#sudo grep -i -e error -e warn -e fail -e missing -e weird $TS_LOG_DIR/tailscaled-stderr.log
sudo tail -100 $TS_LOG_DIR/tailscaled-stderr.log | grep -i -e error -e warn -e fail -e missing -e weird

33
brew/stop-uninstall-wipe.sh Executable file
View File

@@ -0,0 +1,33 @@
#!/usr/bin/env sh
set -u # also -e?
eval $(brew/vars.sh)
echo
echo SUDO is $SUDO
echo
echo stopping...
$SUDO brew services stop tailscale
$SUDO brew services list | grep tailscale
ps -ef | grep tailscaled | grep -v "grep tailscaled" # TODO(mkramlich): do better
# TODO(mkramlich): the installed plist is gone?
echo
echo uninstalling...
brew uninstall --force tailscale
$SUDO brew services list | grep tailscale
echo
echo deleting...
$SUDO rm -rf $TS_CELLAR/$TS_VER
$SUDO rm -f $BREW/var/lib/tailscale/*
$SUDO rm -f $TS_LOG_DIR/*
rmdir $BREW/var/lib/tailscale
rmdir $TS_LOG_DIR
rmdir $BREW/var/run/tailscale
rm $BREW/opt/tailscale # was symlink to $TS_CELLAR/$TS_VER
rmdir $TS_CELLAR
find $BREW | grep -i tailscale
find /opt | grep -i tailscale
# TODO(mkramlich): wipeout brew git checkout cache and/or add test for with-vs-without

View File

@@ -0,0 +1,110 @@
# typed: false
# frozen_string_literal: true
# Homebrew formula for Tailscale
class Tailscale < Formula
desc "Easiest, secure, crossplatform WireGuard Go-based VPN w/oauth2, 2FA/SSO"
homepage "https://www.tailscale.com"
url "https://github.com/tailscale/tailscale.git",
revision: "188bb142691c8b97673a9cb2aebd9f1521e9bb12"
version "1.5.0" # TODO(mkramlich): WIP, so not necessarily; brew required a version
license "BSD-3-Clause"
head "https://github.com/tailscale/tailscale.git",
branch: "main"
depends_on "go" => :build
def install
ENV["GOPATH"] = buildpath
tailscale_path = buildpath/"src/github.com/tailscale/tailscale"
tailscale_path.install buildpath.children
cd tailscale_path do
# build the exes with version strings equiv to the tailscale repo's build_dist.sh:
ldflags = prepare_ldflags
system "go", "build", "-o", ".", "-tags", "xversion", "-ldflags", ldflags, "tailscale.com/cmd/tailscale"
system "go", "build", "-o", ".", "-tags", "xversion", "-ldflags", ldflags, "tailscale.com/cmd/tailscaled"
bin.install "tailscale"
bin.install "tailscaled"
end
end
def prepare_ldflags
ver_props = prepare_ver_props
vl = ver_props["VERSION_LONG"]
vs = ver_props["VERSION_SHORT"]
vgh = ver_props["VERSION_GIT_HASH"]
vl = "tailscale.com/version.Long=#{vl}"
vs = "tailscale.com/version.Short=#{vs}"
vgh = "tailscale.com/version.GitCommit=#{vgh}"
"-X #{vl} -X #{vs} -X #{vgh}"
end
def prepare_ver_props
distvers = Utils.safe_popen_read("./version/version.sh")
lines = distvers.split("\n")
ver_props = {}
lines.each do |line|
parts = line.split("=")
key = parts.at(0)
val = parts.at(1).delete('"') # cuz version.sh emits each prop with double-quotes enclosing each val
# system "echo adding to ver_props for go builds: key #{key}, val #{val}"
ver_props[key] = val
end
ver_props
end
def post_install
(var/"run/tailscale").mkpath
(var/"lib/tailscale").mkpath
end
def caveats
<<~EOS
To have launchd start tailscale now and restart at boot:
sudo brew services start tailscale
NOTE: The caveat message below with 'restart at login' is incorrect, but we can't suppress it. Requires sudo.
EOS
end
plist_options manual: "sudo tailscaled --socket=#{HOMEBREW_PREFIX}/run/tailscale/tailscaled.sock --state=#{HOMEBREW_PREFIX}/lib/tailscale/tailscaled.state"
def plist
<<~EOS
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
<key>NetworkState</key>
<true/>
</dict>
<key>Label</key>
<string>#{plist_name}</string>
<key>ProgramArguments</key>
<array>
<string>#{opt_bin}/tailscaled</string>
<string>--socket=#{var}/run/tailscale/tailscaled.sock</string>
<string>--state=#{var}/lib/tailscale/tailscaled.state</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>WorkingDirectory</key>
<string>#{var}/lib/tailscale</string>
<key>StandardErrorPath</key>
<string>#{var}/log/tailscale/tailscaled-stderr.log</string>
<key>StandardOutPath</key>
<string>#{var}/log/tailscale/tailscaled-stdout.log</string>
</dict>
</plist>
EOS
end
test do
system bin/"tailscale", "version"
system bin/"tailscaled", "-version"
system bin/"tailscale", "netcheck"
end
end

110
brew/tailscale.rb Normal file
View File

@@ -0,0 +1,110 @@
# typed: false
# frozen_string_literal: true
# Homebrew formula for Tailscale
class Tailscale < Formula
desc "Easiest, secure, crossplatform WireGuard Go-based VPN w/oauth2, 2FA/SSO"
homepage "https://www.tailscale.com"
url "https://github.com/tailscale/tailscale.git",
revision: "188bb142691c8b97673a9cb2aebd9f1521e9bb12"
version "1.5.0" # TODO(mkramlich): WIP, so not necessarily; brew required a version
license "BSD-3-Clause"
head "https://github.com/tailscale/tailscale.git",
branch: "main"
depends_on "go" => :build
def install
ENV["GOPATH"] = buildpath
tailscale_path = buildpath/"src/github.com/tailscale/tailscale"
tailscale_path.install buildpath.children
cd tailscale_path do
# build the exes with version strings equiv to the tailscale repo's build_dist.sh:
ldflags = prepare_ldflags
system "go", "build", "-o", ".", "-tags", "xversion", "-ldflags", ldflags, "tailscale.com/cmd/tailscale"
system "go", "build", "-o", ".", "-tags", "xversion", "-ldflags", ldflags, "tailscale.com/cmd/tailscaled"
bin.install "tailscale"
bin.install "tailscaled"
end
end
def prepare_ldflags
ver_props = prepare_ver_props
vl = ver_props["VERSION_LONG"]
vs = ver_props["VERSION_SHORT"]
vgh = ver_props["VERSION_GIT_HASH"]
vl = "tailscale.com/version.Long=#{vl}"
vs = "tailscale.com/version.Short=#{vs}"
vgh = "tailscale.com/version.GitCommit=#{vgh}"
"-X #{vl} -X #{vs} -X #{vgh}"
end
def prepare_ver_props
distvers = Utils.safe_popen_read("./version/version.sh")
lines = distvers.split("\n")
ver_props = {}
lines.each do |line|
parts = line.split("=")
key = parts.at(0)
val = parts.at(1).delete('"') # cuz version.sh emits each prop with double-quotes enclosing each val
# system "echo adding to ver_props for go builds: key #{key}, val #{val}"
ver_props[key] = val
end
ver_props
end
def post_install
(var/"run/tailscale").mkpath
(var/"lib/tailscale").mkpath
end
def caveats
<<~EOS
To have launchd start tailscale now and restart at boot:
sudo brew services start tailscale
NOTE: The caveat message below with 'restart at login' is incorrect, but we can't suppress it. Requires sudo.
EOS
end
plist_options manual: "sudo tailscaled --socket=#{HOMEBREW_PREFIX}/run/tailscale/tailscaled.sock --state=#{HOMEBREW_PREFIX}/lib/tailscale/tailscaled.state"
def plist
<<~EOS
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
<key>NetworkState</key>
<true/>
</dict>
<key>Label</key>
<string>#{plist_name}</string>
<key>ProgramArguments</key>
<array>
<string>#{opt_bin}/tailscaled</string>
<string>--socket=#{var}/run/tailscale/tailscaled.sock</string>
<string>--state=#{var}/lib/tailscale/tailscaled.state</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>WorkingDirectory</key>
<string>#{var}/lib/tailscale</string>
<key>StandardErrorPath</key>
<string>#{var}/log/tailscale/tailscaled-stderr.log</string>
<key>StandardOutPath</key>
<string>#{var}/log/tailscale/tailscaled-stdout.log</string>
</dict>
</plist>
EOS
end
test do
system bin/"tailscale", "version"
system bin/"tailscaled", "-version"
system bin/"tailscale", "netcheck"
end
end

109
brew/tailscale.tb-github.rb Normal file
View File

@@ -0,0 +1,109 @@
# typed: false
# frozen_string_literal: true
# Homebrew formula for Tailscale
class Tailscale < Formula
desc "Easiest, secure, crossplatform WireGuard Go-based VPN w/oauth2, 2FA/SSO"
homepage "https://www.tailscale.com"
url "https://github.com/tailscale/tailscale/archive/v1.4.4.tar.gz"
sha256 "5312c6d075a32049912e0932a89269869def9ac8ea9d0fdccc6b41db60fc2d4c"
license "BSD-3-Clause"
head "https://github.com/tailscale/tailscale.git",
branch: "main"
depends_on "go" => :build
def install
ENV["GOPATH"] = buildpath
tailscale_path = buildpath/"src/github.com/tailscale/tailscale"
tailscale_path.install buildpath.children
cd tailscale_path do
# build the exes with version strings equiv to the tailscale repo's build_dist.sh:
ldflags = prepare_ldflags
system "go", "build", "-o", ".", "-tags", "xversion", "-ldflags", ldflags, "tailscale.com/cmd/tailscale"
system "go", "build", "-o", ".", "-tags", "xversion", "-ldflags", ldflags, "tailscale.com/cmd/tailscaled"
bin.install "tailscale"
bin.install "tailscaled"
end
end
def prepare_ldflags
ver_props = prepare_ver_props
vl = ver_props["VERSION_LONG"]
vs = ver_props["VERSION_SHORT"]
vgh = ver_props["VERSION_GIT_HASH"]
vl = "tailscale.com/version.Long=#{vl}"
vs = "tailscale.com/version.Short=#{vs}"
vgh = "tailscale.com/version.GitCommit=#{vgh}"
"-X #{vl} -X #{vs} -X #{vgh}"
end
def prepare_ver_props
distvers = Utils.safe_popen_read("./version/version.sh")
lines = distvers.split("\n")
ver_props = {}
lines.each do |line|
parts = line.split("=")
key = parts.at(0)
val = parts.at(1).delete('"') # cuz version.sh emits each prop with double-quotes enclosing each val
# system "echo adding to ver_props for go builds: key #{key}, val #{val}"
ver_props[key] = val
end
ver_props
end
def post_install
(var/"run/tailscale").mkpath
(var/"lib/tailscale").mkpath
end
def caveats
<<~EOS
To have launchd start tailscale now and restart at boot:
sudo brew services start tailscale
NOTE: The caveat message below with 'restart at login' is incorrect, but we can't suppress it. Requires sudo.
EOS
end
plist_options manual: "sudo tailscaled --socket=#{HOMEBREW_PREFIX}/run/tailscale/tailscaled.sock --state=#{HOMEBREW_PREFIX}/lib/tailscale/tailscaled.state"
def plist
<<~EOS
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
<key>NetworkState</key>
<true/>
</dict>
<key>Label</key>
<string>#{plist_name}</string>
<key>ProgramArguments</key>
<array>
<string>#{opt_bin}/tailscaled</string>
<string>--socket=#{var}/run/tailscale/tailscaled.sock</string>
<string>--state=#{var}/lib/tailscale/tailscaled.state</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>WorkingDirectory</key>
<string>#{var}/lib/tailscale</string>
<key>StandardErrorPath</key>
<string>#{var}/log/tailscale/tailscaled-stderr.log</string>
<key>StandardOutPath</key>
<string>#{var}/log/tailscale/tailscaled-stdout.log</string>
</dict>
</plist>
EOS
end
test do
system bin/"tailscale", "version"
system bin/"tailscaled", "-version"
system bin/"tailscale", "netcheck"
end
end

109
brew/tailscale.tb-local.rb Normal file
View File

@@ -0,0 +1,109 @@
# typed: false
# frozen_string_literal: true
# Homebrew formula for Tailscale
class Tailscale < Formula
desc "Easiest, secure, crossplatform WireGuard Go-based VPN w/oauth2, 2FA/SSO"
homepage "https://www.tailscale.com"
url "http://localhost:8901/tailscale/tailscale/archive/v1.5.0.tar.gz"
sha256 "a62caf2eb5c84d2d1775cd8a17eeb0e8ad3b8dbc9453399862d908227e0af721"
license "BSD-3-Clause"
head "https://github.com/tailscale/tailscale.git",
branch: "main"
depends_on "go" => :build
def install
ENV["GOPATH"] = buildpath
tailscale_path = buildpath/"src/github.com/tailscale/tailscale"
tailscale_path.install buildpath.children
cd tailscale_path do
# build the exes with version strings equiv to the tailscale repo's build_dist.sh:
ldflags = prepare_ldflags
system "go", "build", "-o", ".", "-tags", "xversion", "-ldflags", ldflags, "tailscale.com/cmd/tailscale"
system "go", "build", "-o", ".", "-tags", "xversion", "-ldflags", ldflags, "tailscale.com/cmd/tailscaled"
bin.install "tailscale"
bin.install "tailscaled"
end
end
def prepare_ldflags
ver_props = prepare_ver_props
vl = ver_props["VERSION_LONG"]
vs = ver_props["VERSION_SHORT"]
vgh = ver_props["VERSION_GIT_HASH"]
vl = "tailscale.com/version.Long=#{vl}"
vs = "tailscale.com/version.Short=#{vs}"
vgh = "tailscale.com/version.GitCommit=#{vgh}"
"-X #{vl} -X #{vs} -X #{vgh}"
end
def prepare_ver_props
distvers = Utils.safe_popen_read("./version/version.sh")
lines = distvers.split("\n")
ver_props = {}
lines.each do |line|
parts = line.split("=")
key = parts.at(0)
val = parts.at(1).delete('"') # cuz version.sh emits each prop with double-quotes enclosing each val
# system "echo adding to ver_props for go builds: key #{key}, val #{val}"
ver_props[key] = val
end
ver_props
end
def post_install
(var/"run/tailscale").mkpath
(var/"lib/tailscale").mkpath
end
def caveats
<<~EOS
To have launchd start tailscale now and restart at boot:
sudo brew services start tailscale
NOTE: The caveat message below with 'restart at login' is incorrect, but we can't suppress it. Requires sudo.
EOS
end
plist_options manual: "sudo tailscaled --socket=#{HOMEBREW_PREFIX}/run/tailscale/tailscaled.sock --state=#{HOMEBREW_PREFIX}/lib/tailscale/tailscaled.state"
def plist
<<~EOS
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key>
<false/>
<key>NetworkState</key>
<true/>
</dict>
<key>Label</key>
<string>#{plist_name}</string>
<key>ProgramArguments</key>
<array>
<string>#{opt_bin}/tailscaled</string>
<string>--socket=#{var}/run/tailscale/tailscaled.sock</string>
<string>--state=#{var}/lib/tailscale/tailscaled.state</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>WorkingDirectory</key>
<string>#{var}/lib/tailscale</string>
<key>StandardErrorPath</key>
<string>#{var}/log/tailscale/tailscaled-stderr.log</string>
<key>StandardOutPath</key>
<string>#{var}/log/tailscale/tailscaled-stdout.log</string>
</dict>
</plist>
EOS
end
test do
system bin/"tailscale", "version"
system bin/"tailscaled", "-version"
system bin/"tailscale", "netcheck"
end
end

39
brew/test.sh Executable file
View File

@@ -0,0 +1,39 @@
#!/usr/bin/env sh
set -eu
# TODO(mkramlich):
# dl-tarball-sha.sh
# make-test-source-tarball.sh
echo test variant: global, tb-local
#TODO(mkramlich): for the tb-local tests, server-tarball.sh should be running, with tarball in place
cp brew/tailscale{.tb-local,}.rb
# brew services (via launchd) start as a global boot daemon (in /Library/LaunchDaemons), running as root
# TODO(mkramlich): confirm at reboot starts & works
SUDO="sudo" INSTALLED_PLIST_DIR=/Library/LaunchDaemons brew/install-start-with-checks.sh
SUDO="sudo" INSTALLED_PLIST_DIR=/Library/LaunchDaemons brew/stop-uninstall-wipe.sh
echo test variant: global, tb-github
cp brew/tailscale{.tb-github,}.rb
# brew services (via launchd) start as a global boot daemon (in /Library/LaunchDaemons), running as root
# TODO(mkramlich): confirm at reboot starts & works
SUDO="sudo" INSTALLED_PLIST_DIR=/Library/LaunchDaemons brew/install-start-with-checks.sh
SUDO="sudo" INSTALLED_PLIST_DIR=/Library/LaunchDaemons brew/stop-uninstall-wipe.sh
echo test variant: global, commit-pin
cp brew/tailscale{.commit-pin,}.rb
# brew services (via launchd) start as a global boot daemon (in /Library/LaunchDaemons), running as root
# TODO(mkramlich): confirm at reboot starts & works
SUDO="sudo" INSTALLED_PLIST_DIR=/Library/LaunchDaemons brew/install-start-with-checks.sh
SUDO="sudo" INSTALLED_PLIST_DIR=/Library/LaunchDaemons brew/stop-uninstall-wipe.sh
#####################################
# brew services (via launchd) start as a user login agent (in ~/Library/LaunchAgents), BUT runs as sudo
# TODO(mkramlich): just a WIP POC, much lower priority than boot perm, might drop
#SUDO="" INSTALLED_PLIST_DIR=~/Library/LaunchAgents brew/install-start-with-checks.sh
#SUDO="" INSTALLED_PLIST_DIR=~/Library/LaunchAgents brew/stop-uninstall-wipe.sh

6
brew/up.sh Executable file
View File

@@ -0,0 +1,6 @@
#!/usr/bin/env sh
set -eu
eval $(brew/vars.sh)
sudo $TS_BIN/tailscale -socket=$TS_SOCK up

23
brew/vars.sh Executable file
View File

@@ -0,0 +1,23 @@
#!/usr/bin/env sh
set -eu
# TODO(mkramlich): elim redundancy with or otherwise maybe take advantage of version/version.sh
BREW=`brew --prefix` # because varies; /usr/local on Intel, /opt/homebrew on ARM64/M1/AppleSilicon
# TODO(mkramlich): Cellar
cat <<EOF
TS_VER=1.5.0
TS_COMMIT_PIN=188bb142691c8b97673a9cb2aebd9f1521e9bb12
TS_BIN=$BREW/bin
TS_SOCK=$BREW/var/run/tailscale/tailscaled.sock
TS_STATE=$BREW/var/lib/tailscale/tailscaled.state
TS_LOG_DIR=$BREW/var/log/tailscale
TS_CELLAR=$BREW/Cellar/tailscale
TS_PLIST=homebrew.mxcl.tailscale.plist
TS_TARBALL_PORT=8901
TS_FORMULA=brew/tailscale.rb
BREW=$BREW
INSTALLED_PLIST_DIR=/Library/LaunchDaemons
EOF

View File

@@ -8,6 +8,9 @@
# If you're packaging Tailscale for a distro, please consider using
# this script, or executing equivalent commands in your
# distro-specific build system.
#
# Homebrew: if the version embed mechanism changes here, please
# also change it in brew/generate-formula.sh, to keep identical.
set -eu

31
build_docker.sh Executable file
View File

@@ -0,0 +1,31 @@
#!/usr/bin/env sh
#
# Runs `go build` with flags configured for docker distribution. All
# it does differently from `go build` is burn git commit and version
# information into the binaries inside docker, so that we can track down user
# issues.
#
############################################################################
#
# WARNING: Tailscale is not yet officially supported in Docker,
# Kubernetes, etc.
#
# It might work, but we don't regularly test it, and it's not as polished as
# our currently supported platforms. This is provided for people who know
# how Tailscale works and what they're doing.
#
# Our tracking bug for officially support container use cases is:
# https://github.com/tailscale/tailscale/issues/504
#
# Also, see the various bugs tagged "containers":
# https://github.com/tailscale/tailscale/labels/containers
#
############################################################################
set -eu
eval $(./version/version.sh)
GOFLAGS='-tags xversion -ldflags '"-X tailscale.com/version.Long=${VERSION_LONG} -X tailscale.com/version.Short=${VERSION_SHORT} -X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}"
docker build --build-arg goflags_arg="'""${GOFLAGS}""'" -t tailscale:tailscale .

View File

@@ -5,6 +5,7 @@
package main
import (
"context"
"errors"
"fmt"
"log"
@@ -40,6 +41,6 @@ func startMeshWithHost(s *derp.Server, host string) error {
c.MeshKey = s.MeshKey()
add := func(k key.Public) { s.AddPacketForwarder(k, c) }
remove := func(k key.Public) { s.RemovePacketForwarder(k, c) }
go c.RunWatchConnectionLoop(s.PublicKey(), add, remove)
go c.RunWatchConnectionLoop(context.Background(), s.PublicKey(), logf, add, remove)
return nil
}

208
cmd/hello/hello.go Normal file
View File

@@ -0,0 +1,208 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// The hello binary runs hello.ipn.dev.
package main // import "tailscale.com/cmd/hello"
import (
"context"
"encoding/json"
"flag"
"fmt"
"html/template"
"io/ioutil"
"log"
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"tailscale.com/safesocket"
"tailscale.com/tailcfg"
)
var (
httpAddr = flag.String("http", ":80", "address to run an HTTP server on, or empty for none")
httpsAddr = flag.String("https", ":443", "address to run an HTTPS server on, or empty for none")
testIP = flag.String("test-ip", "", "if non-empty, look up IP and exit before running a server")
)
func main() {
flag.Parse()
if *testIP != "" {
res, err := whoIs(*testIP)
if err != nil {
log.Fatal(err)
}
e := json.NewEncoder(os.Stdout)
e.SetIndent("", "\t")
e.Encode(res)
return
}
if !devMode() {
tmpl = template.Must(template.New("home").Parse(slurpHTML()))
}
http.HandleFunc("/", root)
log.Printf("Starting hello server.")
errc := make(chan error, 1)
if *httpAddr != "" {
log.Printf("running HTTP server on %s", *httpAddr)
go func() {
errc <- http.ListenAndServe(*httpAddr, nil)
}()
}
if *httpsAddr != "" {
log.Printf("running HTTPS server on %s", *httpsAddr)
go func() {
errc <- http.ListenAndServeTLS(*httpsAddr,
"/etc/hello/hello.ipn.dev.crt",
"/etc/hello/hello.ipn.dev.key",
nil,
)
}()
}
log.Fatal(<-errc)
}
func slurpHTML() string {
slurp, err := ioutil.ReadFile("hello.tmpl.html")
if err != nil {
log.Fatal(err)
}
return string(slurp)
}
func devMode() bool { return *httpsAddr == "" && *httpAddr != "" }
func getTmpl() (*template.Template, error) {
if devMode() {
return template.New("home").Parse(slurpHTML())
}
return tmpl, nil
}
var tmpl *template.Template // not used in dev mode, initialized by main after flag parse
type tmplData struct {
DisplayName string // "Foo Barberson"
LoginName string // "foo@bar.com"
ProfilePicURL string // "https://..."
MachineName string // "imac5k"
MachineOS string // "Linux"
IP string // "100.2.3.4"
}
func root(w http.ResponseWriter, r *http.Request) {
if r.TLS == nil && *httpsAddr != "" {
host := r.Host
if strings.Contains(r.Host, "100.101.102.103") {
host = "hello.ipn.dev"
}
http.Redirect(w, r, "https://"+host, http.StatusFound)
return
}
if r.RequestURI != "/" {
http.Redirect(w, r, "/", http.StatusFound)
return
}
ip, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
http.Error(w, "no remote addr", 500)
return
}
tmpl, err := getTmpl()
if err != nil {
w.Header().Set("Content-Type", "text/plain")
http.Error(w, "template error: "+err.Error(), 500)
return
}
who, err := whoIs(ip)
var data tmplData
if err != nil {
if devMode() {
log.Printf("warning: using fake data in dev mode due to whois lookup error: %v", err)
data = tmplData{
DisplayName: "Taily Scalerson",
LoginName: "taily@scaler.son",
ProfilePicURL: "https://placekitten.com/200/200",
MachineName: "scaled",
MachineOS: "Linux",
IP: "100.1.2.3",
}
} else {
log.Printf("whois(%q) error: %v", ip, err)
http.Error(w, "Your Tailscale works, but we failed to look you up.", 500)
return
}
} else {
data = tmplData{
DisplayName: who.UserProfile.DisplayName,
LoginName: who.UserProfile.LoginName,
ProfilePicURL: who.UserProfile.ProfilePicURL,
MachineName: firstLabel(who.Node.ComputedName),
MachineOS: who.Node.Hostinfo.OS,
IP: ip,
}
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl.Execute(w, data)
}
// firstLabel s up until the first period, if any.
func firstLabel(s string) string {
if i := strings.Index(s, "."); i != -1 {
return s[:i]
}
return s
}
// tsSockClient does HTTP requests to the local Tailscale daemon.
// The hostname in the HTTP request is ignored.
var tsSockClient = &http.Client{
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
// a TCP server on a random port, find the random port. For HTTP connections,
// we don't send the token. It gets added in an HTTP Basic-Auth header.
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
var d net.Dialer
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
}
return safesocket.ConnectDefault()
},
},
}
func whoIs(ip string) (*tailcfg.WhoIsResponse, error) {
ctx := context.Background()
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/whois?ip="+url.QueryEscape(ip), nil)
if err != nil {
return nil, err
}
if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil {
req.SetBasicAuth("", token)
}
res, err := tsSockClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
slurp, _ := ioutil.ReadAll(res.Body)
if res.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %s: %s", res.Status, slurp)
}
r := new(tailcfg.WhoIsResponse)
if err := json.Unmarshal(slurp, r); err != nil {
if max := 200; len(slurp) > max {
slurp = slurp[:max]
}
return nil, fmt.Errorf("failed to parse JSON WhoIsResponse from %q", slurp)
}
return r, nil
}

436
cmd/hello/hello.tmpl.html Normal file
View File

@@ -0,0 +1,436 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0">
<title>Hello from Tailscale</title>
<style>
html,
body {
margin: 0;
padding: 0;
}
body {
font-family: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 100%;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html,
body,
main {
height: 100%;
}
*,
::before,
::after {
box-sizing: border-box;
border-width: 0;
border-style: solid;
border-color: #dad6d5;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 0;
font-size: 1rem;
font-weight: inherit;
}
a {
color: inherit;
}
p {
margin: 0;
}
main {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
max-width: 24rem;
width: 95%;
margin-left: auto;
margin-right: auto;
}
.p-2 {
padding: 0.5rem;
}
.p-4 {
padding: 1rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.pl-3 {
padding-left: 0.75rem;
}
.pr-3 {
padding-right: 0.75rem;
}
.pt-4 {
padding-top: 1rem;
}
.mr-2 {
margin-right: 0.5rem;
;
}
.mb-1 {
margin-bottom: 0.25rem;
}
.mb-2 {
margin-bottom: 0.5rem;
}
.mb-4 {
margin-bottom: 1rem;
}
.mb-6 {
margin-bottom: 1.5rem;
}
.mb-8 {
margin-bottom: 2rem;
}
.mb-12 {
margin-bottom: 3rem;
}
.width-full {
width: 100%;
}
.min-width-0 {
min-width: 0;
}
.rounded-lg {
border-radius: 0.5rem;
}
.relative {
position: relative;
}
.flex {
display: flex;
}
.justify-between {
justify-content: space-between;
}
.items-center {
align-items: center;
}
.border {
border-width: 1px;
}
.border-t-1 {
border-top-width: 1px;
}
.border-gray-100 {
border-color: #f7f5f4;
}
.border-gray-200 {
border-color: #eeebea;
}
.border-gray-300 {
border-color: #dad6d5;
}
.bg-white {
background-color: white;
}
.bg-gray-0 {
background-color: #faf9f8;
}
.bg-gray-100 {
background-color: #f7f5f4;
}
.text-green-600 {
color: #0d4b3b;
}
.text-blue-600 {
color: #3f5db3;
}
.hover\:text-blue-800:hover {
color: #253570;
}
.text-gray-600 {
color: #444342;
}
.text-gray-700 {
color: #2e2d2d;
}
.text-gray-800 {
color: #232222;
}
.text-center {
text-align: center;
}
.text-sm {
font-size: 0.875rem;
}
.font-title {
font-size: 1.25rem;
letter-spacing: -0.025em;
}
.font-semibold {
font-weight: 600;
}
.font-medium {
font-weight: 500;
}
.font-regular {
font-weight: 400;
}
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.overflow-hidden {
overflow: hidden;
}
.profile-pic {
width: 2.5rem;
height: 2.5rem;
border-radius: 9999px;
background-size: cover;
margin-right: 0.5rem;
flex-shrink: 0;
}
.panel {
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.animate .panel {
transform: translateY(10%);
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.0), 0 10px 10px -5px rgba(0, 0, 0, 0.0);
transition: transform 1200ms ease, opacity 1200ms ease, box-shadow 1200ms ease;
}
.animate .panel-interior {
opacity: 0.0;
transition: opacity 1200ms ease;
}
.animate .logo {
transform: translateY(2rem);
opacity: 0.0;
transition: transform 1200ms ease, opacity 1200ms ease;
}
.animate .header-title {
transform: translateY(1.6rem);
opacity: 0.0;
transition: transform 1200ms ease, opacity 1200ms ease;
}
.animate .header-text {
transform: translateY(1.2rem);
opacity: 0.0;
transition: transform 1200ms ease, opacity 1200ms ease;
}
.animate .footer {
transform: translateY(-0.5rem);
opacity: 0.0;
transition: transform 1200ms ease, opacity 1200ms ease;
}
.animating .panel {
transform: translateY(0);
opacity: 1.0;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
}
.animating .panel-interior {
opacity: 1.0;
}
.animating .spinner {
opacity: 0.0;
}
.animating .logo,
.animating .header-title,
.animating .header-text,
.animating .footer {
transform: translateY(0);
opacity: 1.0;
}
.spinner {
display: inline-flex;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
align-items: center;
transition: opacity 200ms ease;
}
.spinner span {
display: inline-block;
background-color: currentColor;
border-radius: 9999px;
animation-name: loading-dots-blink;
animation-duration: 1.4s;
animation-iteration-count: infinite;
animation-fill-mode: both;
width: 0.35em;
height: 0.35em;
margin: 0 0.15em;
}
.spinner span:nth-child(2) {
animation-delay: 200ms;
}
.spinner span:nth-child(3) {
animation-delay: 400ms;
}
.spinner {
display: none;
}
.animate .spinner {
display: inline-flex;
}
@keyframes loading-dots-blink {
0% {
opacity: 0.2;
}
20% {
opacity: 1;
}
100% {
opacity: 0.2;
}
}
@media (prefers-reduced-motion) {
* {
animation-duration: 0ms !important;
transition-duration: 0ms !important;
transition-delay: 0ms !important;
}
}
</style>
</head>
<body class="bg-gray-100">
<script>
(function() {
var lastSeen = localStorage.getItem("lastSeen");
if (!lastSeen) {
document.body.classList.add("animate");
window.addEventListener("load", function () {
setTimeout(function () {
document.body.classList.add("animating");
localStorage.setItem("lastSeen", Date.now());
}, 100);
});
}
})();
</script>
<main class="text-gray-800">
<svg class="logo mb-6" width="28" height="28" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle opacity="0.2" cx="3.4" cy="3.25" r="2.7" fill="currentColor" />
<circle cx="3.4" cy="11.3" r="2.7" fill="currentColor" />
<circle opacity="0.2" cx="3.4" cy="19.5" r="2.7" fill="currentColor" />
<circle cx="11.5" cy="11.3" r="2.7" fill="currentColor" />
<circle cx="11.5" cy="19.5" r="2.7" fill="currentColor" />
<circle opacity="0.2" cx="11.5" cy="3.25" r="2.7" fill="currentColor" />
<circle opacity="0.2" cx="19.5" cy="3.25" r="2.7" fill="currentColor" />
<circle cx="19.5" cy="11.3" r="2.7" fill="currentColor" />
<circle opacity="0.2" cx="19.5" cy="19.5" r="2.7" fill="currentColor" />
</svg>
<header class="mb-8 text-center">
<h1 class="header-title font-title font-semibold mb-2">You're connected over Tailscale!</h1>
<p class="header-text">This device is signed in as…</p>
</header>
<div class="panel relative bg-white rounded-lg width-full shadow-xl mb-8 p-4">
<div class="spinner text-gray-600">
<span></span>
<span></span>
<span></span>
</div>
<div class="panel-interior flex items-center width-full min-width-0 p-2 mb-4">
<div class="profile-pic bg-gray-100" style="background-image: url({{.ProfilePicURL}});"></div>
<div class="overflow-hidden">
{{ with .DisplayName }}
<h4 class="font-semibold truncate">{{.}}</h4>
{{ end }}
<h5 class="text-gray-600 truncate">{{.LoginName}}</h5>
</div>
</div>
<div
class="panel-interior border border-gray-200 bg-gray-0 rounded-lg p-2 pl-3 pr-3 mb-2 width-full flex justify-between items-center">
<div class="flex items-center min-width-0">
<svg class="text-gray-600 mr-2" xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
<line x1="6" y1="6" x2="6.01" y2="6"></line>
<line x1="6" y1="18" x2="6.01" y2="18"></line>
</svg>
<h4 class="font-semibold truncate mr-2">{{.MachineName}}</h4>
</div>
<h5>{{.IP}}</h5>
</div>
</div>
<footer class="footer text-gray-600 text-center mb-12">
<p>Read about <a href="https://tailscale.com/kb/1017/install#advanced-features" class="text-blue-600 hover:text-blue-800"
target="_blank">what you can do next &rarr;</a></p>
</footer>
</main>
</body>
</html>

View File

@@ -68,11 +68,6 @@ change in the future.
Exec: func(context.Context, []string) error { return flag.ErrHelp },
}
// Don't advertise the debug command, but it exists.
if strSliceContains(args, "debug") {
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
}
if err := rootCmd.Parse(args); err != nil {
return err
}
@@ -134,12 +129,3 @@ func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) {
bc.GotNotifyMsg(msg)
}
}
func strSliceContains(ss []string, s string) bool {
for _, v := range ss {
if v == s {
return true
}
}
return false
}

View File

@@ -18,6 +18,7 @@ import (
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/derp/derpmap"
"tailscale.com/net/netcheck"
"tailscale.com/net/portmapper"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
)
@@ -43,7 +44,10 @@ var netcheckArgs struct {
}
func runNetcheck(ctx context.Context, args []string) error {
c := &netcheck.Client{}
c := &netcheck.Client{
UDPBindAddr: os.Getenv("TS_DEBUG_NETCHECK_UDP_BIND"),
PortMapper: portmapper.NewClient(logger.WithPrefix(log.Printf, "portmap: ")),
}
if netcheckArgs.verbose {
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
c.Verbose = true

View File

@@ -64,34 +64,63 @@ func runPing(ctx context.Context, args []string) error {
c, bc, ctx, cancel := connect(ctx)
defer cancel()
if len(args) != 1 {
if len(args) != 1 || args[0] == "" {
return errors.New("usage: ping <hostname-or-IP>")
}
hostOrIP := args[0]
var ip string
var res net.Resolver
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
return fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
} else if len(addrs) == 0 {
return fmt.Errorf("no IPs found for %q", hostOrIP)
} else {
ip = addrs[0]
}
if pingArgs.verbose && ip != hostOrIP {
log.Printf("lookup %q => %q", hostOrIP, ip)
}
ch := make(chan *ipnstate.PingResult, 1)
prc := make(chan *ipnstate.PingResult, 1)
stc := make(chan *ipnstate.Status, 1)
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.ErrMessage != nil {
log.Fatal(*n.ErrMessage)
}
if pr := n.PingResult; pr != nil && pr.IP == ip {
ch <- pr
prc <- pr
}
if n.Status != nil {
stc <- n.Status
}
})
go pump(ctx, bc, c)
hostOrIP := args[0]
// If the argument is an IP address, use it directly without any resolution.
if net.ParseIP(hostOrIP) != nil {
ip = hostOrIP
}
// Otherwise, try to resolve it first from the network peer list.
if ip == "" {
bc.RequestStatus()
select {
case st := <-stc:
for _, ps := range st.Peer {
if hostOrIP == dnsOrQuoteHostname(st, ps) || hostOrIP == ps.DNSName {
ip = ps.TailAddr
break
}
}
case <-ctx.Done():
return ctx.Err()
}
}
// Finally, use DNS.
if ip == "" {
var res net.Resolver
if addrs, err := res.LookupHost(ctx, hostOrIP); err != nil {
return fmt.Errorf("error looking up IP of %q: %v", hostOrIP, err)
} else if len(addrs) == 0 {
return fmt.Errorf("no IPs found for %q", hostOrIP)
} else {
ip = addrs[0]
}
}
if pingArgs.verbose && ip != hostOrIP {
log.Printf("lookup %q => %q", hostOrIP, ip)
}
n := 0
anyPong := false
for {
@@ -101,7 +130,7 @@ func runPing(ctx context.Context, args []string) error {
select {
case <-timer.C:
fmt.Printf("timeout waiting for ping reply\n")
case pr := <-ch:
case pr := <-prc:
timer.Stop()
if pr.Err != "" {
return errors.New(pr.Err)

View File

@@ -14,7 +14,6 @@ import (
"net"
"net/http"
"os"
"sort"
"strings"
"time"
@@ -66,7 +65,17 @@ func runStatus(ctx context.Context, args []string) error {
log.Fatal(*n.ErrMessage)
}
if n.Status != nil {
ch <- n.Status
select {
case ch <- n.Status:
default:
// A status update from somebody else's request.
// Ignoring this matters mostly for "tailscale status -web"
// mode, otherwise the channel send would block forever
// and pump would stop reading from tailscaled, which
// previously caused tailscaled to block (while holding
// a mutex), backing up unrelated clients.
// See https://github.com/tailscale/tailscale/issues/1234
}
}
})
go pump(ctx, bc, c)
@@ -150,13 +159,18 @@ func runStatus(ctx context.Context, args []string) error {
relay := ps.Relay
anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
if !active {
if anyTraffic {
if ps.ExitNode {
f("idle; exit node")
} else if anyTraffic {
f("idle")
} else {
f("-")
}
} else {
f("active; ")
if ps.ExitNode {
f("exit node; ")
}
if relay != "" && ps.CurAddr == "" {
f("relay %q", relay)
} else if ps.CurAddr != "" {
@@ -181,7 +195,7 @@ func runStatus(ctx context.Context, args []string) error {
}
peers = append(peers, ps)
}
sort.Slice(peers, func(i, j int) bool { return sortKey(peers[i]) < sortKey(peers[j]) })
ipnstate.SortPeers(peers)
for _, ps := range peers {
active := peerActive(ps)
if statusArgs.active && !active {
@@ -202,23 +216,11 @@ func peerActive(ps *ipnstate.PeerStatus) bool {
}
func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
if i := strings.Index(ps.DNSName, "."); i != -1 && dnsname.HasSuffix(ps.DNSName, st.MagicDNSSuffix) {
return ps.DNSName[:i]
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
if baseName != "" {
return baseName
}
if ps.DNSName != "" {
return strings.TrimRight(ps.DNSName, ".")
}
return fmt.Sprintf("(%q)", strings.ReplaceAll(ps.SimpleHostName(), " ", "_"))
}
func sortKey(ps *ipnstate.PeerStatus) string {
if ps.DNSName != "" {
return ps.DNSName
}
if ps.HostName != "" {
return ps.HostName
}
return ps.TailAddr
return fmt.Sprintf("(%q)", dnsname.SanitizeHostname(ps.HostName))
}
func ownerLogin(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {

View File

@@ -22,9 +22,9 @@ import (
"inet.af/netaddr"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/types/preftype"
"tailscale.com/version"
"tailscale.com/version/distro"
"tailscale.com/wgengine/router"
)
var upCmd = &ffcli.Command{
@@ -45,6 +45,7 @@ specify any flags, options are reset to their default.
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "install host routes to other Tailscale nodes")
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale IP of the exit node for internet traffic")
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication")
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "ACL tags to request (comma-separated, e.g. eng,montreal,ssh)")
@@ -74,6 +75,7 @@ var upArgs struct {
acceptRoutes bool
acceptDNS bool
singleRoutes bool
exitNodeIP string
shieldsUp bool
forceReauth bool
advertiseRoutes string
@@ -120,6 +122,11 @@ func checkIPForwarding() {
}
}
var (
ipv4default = netaddr.MustParseIPPrefix("0.0.0.0/0")
ipv6default = netaddr.MustParseIPPrefix("::/0")
)
func runUp(ctx context.Context, args []string) error {
if len(args) > 0 {
log.Fatalf("too many non-flag arguments: %q", args)
@@ -133,12 +140,16 @@ func runUp(ctx context.Context, args []string) error {
if upArgs.acceptRoutes {
return errors.New("--accept-routes is " + notSupported)
}
if upArgs.exitNodeIP != "" {
return errors.New("--exit-node is " + notSupported)
}
if upArgs.netfilterMode != "off" {
return errors.New("--netfilter-mode values besides \"off\" " + notSupported)
}
}
var routes []netaddr.IPPrefix
var default4, default6 bool
if upArgs.advertiseRoutes != "" {
advroutes := strings.Split(upArgs.advertiseRoutes, ",")
for _, s := range advroutes {
@@ -149,11 +160,30 @@ func runUp(ctx context.Context, args []string) error {
if ipp != ipp.Masked() {
fatalf("%s has non-address bits set; expected %s", ipp, ipp.Masked())
}
if ipp == ipv4default {
default4 = true
} else if ipp == ipv6default {
default6 = true
}
routes = append(routes, ipp)
}
if default4 && !default6 {
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv4default, ipv6default)
} else if default6 && !default4 {
fatalf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv6default, ipv4default)
}
checkIPForwarding()
}
var exitNodeIP netaddr.IP
if upArgs.exitNodeIP != "" {
var err error
exitNodeIP, err = netaddr.ParseIP(upArgs.exitNodeIP)
if err != nil {
fatalf("invalid IP address %q for --exit-node: %v", upArgs.exitNodeIP, err)
}
}
var tags []string
if upArgs.advertiseTags != "" {
tags = strings.Split(upArgs.advertiseTags, ",")
@@ -169,11 +199,11 @@ func runUp(ctx context.Context, args []string) error {
fatalf("hostname too long: %d bytes (max 256)", len(upArgs.hostname))
}
// TODO(apenwarr): fix different semantics between prefs and uflags
prefs := ipn.NewPrefs()
prefs.ControlURL = upArgs.server
prefs.WantRunning = true
prefs.RouteAll = upArgs.acceptRoutes
prefs.ExitNodeIP = exitNodeIP
prefs.CorpDNS = upArgs.acceptDNS
prefs.AllowSingleHosts = upArgs.singleRoutes
prefs.ShieldsUp = upArgs.shieldsUp
@@ -186,12 +216,12 @@ func runUp(ctx context.Context, args []string) error {
if runtime.GOOS == "linux" {
switch upArgs.netfilterMode {
case "on":
prefs.NetfilterMode = router.NetfilterOn
prefs.NetfilterMode = preftype.NetfilterOn
case "nodivert":
prefs.NetfilterMode = router.NetfilterNoDivert
prefs.NetfilterMode = preftype.NetfilterNoDivert
warnf("netfilter=nodivert; add iptables calls to ts-* chains manually.")
case "off":
prefs.NetfilterMode = router.NetfilterOff
prefs.NetfilterMode = preftype.NetfilterOff
warnf("netfilter=off; configure iptables yourself.")
default:
fatalf("invalid value --netfilter-mode: %q", upArgs.netfilterMode)
@@ -212,7 +242,16 @@ func runUp(ctx context.Context, args []string) error {
AuthKey: upArgs.authKey,
Notify: func(n ipn.Notify) {
if n.ErrMessage != nil {
fatalf("backend error: %v\n", *n.ErrMessage)
msg := *n.ErrMessage
if msg == ipn.ErrMsgPermissionDenied {
switch runtime.GOOS {
case "windows":
msg += " (Tailscale service in use by other user?)"
default:
msg += " (try 'sudo tailscale up [...]')"
}
}
fatalf("backend error: %v\n", msg)
}
if s := n.State; s != nil {
switch *s {

View File

@@ -4,122 +4,84 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscale
W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
LW github.com/go-multierror/multierror from tailscale.com/wgengine/router
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
L 💣 github.com/godbus/dbus/v5 from tailscale.com/wgengine/router/dns
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
github.com/peterbourgon/ff/v2 from github.com/peterbourgon/ff/v2/ffcli
github.com/peterbourgon/ff/v2/ffcli from tailscale.com/cmd/tailscale/cli
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
W 💣 github.com/tailscale/wireguard-go/ipc/winpipe from github.com/tailscale/wireguard-go/ipc
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
github.com/tailscale/wireguard-go/replay from github.com/tailscale/wireguard-go/device
github.com/tailscale/wireguard-go/rwcancel from github.com/tailscale/wireguard-go/device+
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device+
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun+
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/device+
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
💣 go4.org/intern from inet.af/netaddr
💣 go4.org/mem from tailscale.com/control/controlclient+
💣 go4.org/mem from tailscale.com/derp+
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
rsc.io/goversion/version from tailscale.com/version
tailscale.com/atomicfile from tailscale.com/ipn+
tailscale.com/atomicfile from tailscale.com/ipn
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
tailscale.com/control/controlclient from tailscale.com/ipn+
tailscale.com/derp from tailscale.com/derp/derphttp+
tailscale.com/derp/derphttp from tailscale.com/cmd/tailscale/cli+
tailscale.com/derp from tailscale.com/derp/derphttp
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
tailscale.com/derp/derpmap from tailscale.com/cmd/tailscale/cli
tailscale.com/disco from tailscale.com/derp+
tailscale.com/internal/deepprint from tailscale.com/ipn+
tailscale.com/disco from tailscale.com/derp
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
tailscale.com/ipn/policy from tailscale.com/ipn
tailscale.com/log/logheap from tailscale.com/control/controlclient
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
tailscale.com/metrics from tailscale.com/derp
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
tailscale.com/net/dnscache from tailscale.com/derp/derphttp
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli+
tailscale.com/net/netns from tailscale.com/control/controlclient+
tailscale.com/net/packet from tailscale.com/wgengine+
tailscale.com/net/stun from tailscale.com/net/netcheck+
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
tailscale.com/net/tsaddr from tailscale.com/ipn+
💣 tailscale.com/net/tshttpproxy from tailscale.com/cmd/tailscale/cli+
tailscale.com/paths from tailscale.com/cmd/tailscale/cli
tailscale.com/portlist from tailscale.com/ipn
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
tailscale.com/net/netns from tailscale.com/derp/derphttp+
tailscale.com/net/packet from tailscale.com/wgengine/filter
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
tailscale.com/net/stun from tailscale.com/net/netcheck
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
tailscale.com/net/tsaddr from tailscale.com/net/interfaces
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
tailscale.com/syncs from tailscale.com/net/interfaces+
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/types/empty from tailscale.com/control/controlclient+
tailscale.com/types/key from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/empty from tailscale.com/ipn
tailscale.com/types/key from tailscale.com/derp+
tailscale.com/types/logger from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
tailscale.com/types/opt from tailscale.com/control/controlclient+
tailscale.com/types/netmap from tailscale.com/ipn
tailscale.com/types/opt from tailscale.com/net/netcheck+
tailscale.com/types/persist from tailscale.com/ipn
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
tailscale.com/types/strbuilder from tailscale.com/net/packet
tailscale.com/types/structs from tailscale.com/control/controlclient+
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
tailscale.com/types/structs from tailscale.com/ipn+
tailscale.com/types/wgkey from tailscale.com/types/netmap+
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
LW tailscale.com/util/endian from tailscale.com/net/netns+
tailscale.com/util/lineread from tailscale.com/control/controlclient+
tailscale.com/util/systemd from tailscale.com/control/controlclient+
W tailscale.com/util/endian from tailscale.com/net/netns
tailscale.com/util/lineread from tailscale.com/net/interfaces
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
tailscale.com/wgengine from tailscale.com/ipn
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
tailscale.com/wgengine/magicsock from tailscale.com/wgengine
💣 tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscale/cli+
tailscale.com/wgengine/router from tailscale.com/cmd/tailscale/cli+
tailscale.com/wgengine/router/dns from tailscale.com/ipn+
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
tailscale.com/wgengine/tstun from tailscale.com/wgengine
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli
tailscale.com/wgengine/filter from tailscale.com/types/netmap
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
golang.org/x/crypto/curve25519 from crypto/tls+
golang.org/x/crypto/hkdf from crypto/tls
golang.org/x/crypto/nacl/box from tailscale.com/control/controlclient+
golang.org/x/crypto/nacl/box from tailscale.com/derp
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/poly1305 from github.com/tailscale/wireguard-go/device+
golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305+
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/context/ctxhttp from golang.org/x/oauth2/internal
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/dns/dnsmessage from net
golang.org/x/net/http/httpguts from net/http
golang.org/x/net/http/httpproxy from net/http
golang.org/x/net/http2/hpack from net/http
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net
golang.org/x/oauth2 from tailscale.com/control/controlclient+
D golang.org/x/net/route from net+
golang.org/x/oauth2 from tailscale.com/ipn+
golang.org/x/oauth2/internal from golang.org/x/oauth2
golang.org/x/sync/errgroup from tailscale.com/derp
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
LD golang.org/x/sys/unix from tailscale.com/net/netns+
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
@@ -128,7 +90,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
bufio from compress/flate+
bytes from bufio+
compress/flate from compress/gzip+
compress/gzip from net/http+
compress/gzip from net/http
compress/zlib from debug/elf+
container/list from crypto/tls+
context from crypto/tls+
@@ -156,7 +118,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
debug/elf from rsc.io/goversion/version
debug/macho from rsc.io/goversion/version
debug/pe from rsc.io/goversion/version
encoding from encoding/json+
encoding from encoding/json
encoding/asn1 from crypto/x509+
encoding/base64 from encoding/json+
encoding/binary from compress/gzip+
@@ -170,16 +132,16 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
hash from compress/zlib+
hash/adler32 from compress/zlib
hash/crc32 from compress/gzip+
hash/fnv from tailscale.com/wgengine/magicsock
hash/maphash from go4.org/mem
html from tailscale.com/ipn/ipnstate
io from bufio+
io/ioutil from crypto/tls+
io/fs from crypto/rand+
io/ioutil from golang.org/x/oauth2/internal+
log from expvar+
math from compress/flate+
math/big from crypto/dsa+
math/bits from compress/flate+
math/rand from github.com/mdlayher/netlink+
math/rand from math/big+
mime from golang.org/x/oauth2/internal+
mime/multipart from net/http
mime/quotedprintable from mime/multipart
@@ -190,23 +152,21 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
net/textproto from golang.org/x/net/http/httpguts+
net/url from crypto/x509+
os from crypto/rand+
os/exec from github.com/coreos/go-iptables/iptables+
os/exec from github.com/toqueteos/webbrowser+
os/signal from tailscale.com/cmd/tailscale/cli
L os/user from github.com/godbus/dbus/v5
path from debug/dwarf+
path/filepath from crypto/x509+
reflect from crypto/x509+
regexp from github.com/coreos/go-iptables/iptables+
regexp from rsc.io/goversion/version
regexp/syntax from regexp
runtime/debug from golang.org/x/sync/singleflight
runtime/pprof from tailscale.com/log/logheap+
sort from compress/flate+
strconv from compress/flate+
strings from bufio+
sync from compress/flate+
sync/atomic from context+
syscall from crypto/rand+
text/tabwriter from github.com/peterbourgon/ff/v2/ffcli+
text/tabwriter from github.com/peterbourgon/ff/v2/ffcli
time from compress/gzip+
unicode from bytes+
unicode/utf16 from encoding/asn1+

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
package main
import (
"context"
@@ -18,7 +18,6 @@ import (
"os"
"time"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/derp/derphttp"
"tailscale.com/derp/derpmap"
"tailscale.com/net/interfaces"
@@ -28,28 +27,26 @@ import (
"tailscale.com/wgengine/monitor"
)
var debugCmd = &ffcli.Command{
Name: "debug",
Exec: runDebug,
FlagSet: (func() *flag.FlagSet {
fs := flag.NewFlagSet("debug", flag.ExitOnError)
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
return fs
})(),
}
var debugArgs struct {
monitor bool
getURL string
derpCheck string
}
func runDebug(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("unknown arguments")
var debugModeFunc = debugMode // so it can be addressable
func debugMode(args []string) error {
fs := flag.NewFlagSet("debug", flag.ExitOnError)
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
if err := fs.Parse(args); err != nil {
return err
}
if len(fs.Args()) > 0 {
return errors.New("unknown non-flag debug subcommand arguments")
}
ctx := context.Background()
if debugArgs.derpCheck != "" {
return checkDerp(ctx, debugArgs.derpCheck)
}

View File

@@ -2,14 +2,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/apenwarr/fixconsole from tailscale.com/cmd/tailscaled
W 💣 github.com/apenwarr/w32 from github.com/apenwarr/fixconsole
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
LW github.com/go-multierror/multierror from tailscale.com/wgengine/router
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
L 💣 github.com/godbus/dbus/v5 from tailscale.com/wgengine/router/dns
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header+
L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/wgengine/monitor
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
@@ -22,7 +21,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
💣 github.com/tailscale/wireguard-go/conn from github.com/tailscale/wireguard-go/device+
💣 github.com/tailscale/wireguard-go/device from tailscale.com/wgengine+
github.com/tailscale/wireguard-go/device/tokenbucket from github.com/tailscale/wireguard-go/device
💣 github.com/tailscale/wireguard-go/ipc from github.com/tailscale/wireguard-go/device
W 💣 github.com/tailscale/wireguard-go/ipc/winpipe from github.com/tailscale/wireguard-go/ipc
github.com/tailscale/wireguard-go/ratelimiter from github.com/tailscale/wireguard-go/device
@@ -31,7 +29,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/tailscale/wireguard-go/tai64n from github.com/tailscale/wireguard-go/device+
💣 github.com/tailscale/wireguard-go/tun from github.com/tailscale/wireguard-go/device+
W 💣 github.com/tailscale/wireguard-go/tun/wintun from github.com/tailscale/wireguard-go/tun+
github.com/tailscale/wireguard-go/wgcfg from github.com/tailscale/wireguard-go/device+
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
💣 go4.org/intern from inet.af/netaddr
💣 go4.org/mem from tailscale.com/control/controlclient+
@@ -67,17 +64,22 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
gvisor.dev/gvisor/pkg/tcpip/transport/udp from gvisor.dev/gvisor/pkg/tcpip/adapters/gonet+
gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/tcpip+
inet.af/netaddr from tailscale.com/control/controlclient+
inet.af/peercred from tailscale.com/ipn/ipnserver
rsc.io/goversion/version from tailscale.com/version
tailscale.com/atomicfile from tailscale.com/ipn+
tailscale.com/control/controlclient from tailscale.com/ipn+
tailscale.com/control/controlclient from tailscale.com/ipn/ipnlocal+
tailscale.com/derp from tailscale.com/derp/derphttp+
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
tailscale.com/derp/derpmap from tailscale.com/cmd/tailscaled
tailscale.com/disco from tailscale.com/derp+
tailscale.com/internal/deepprint from tailscale.com/ipn+
tailscale.com/ipn from tailscale.com/ipn/ipnserver
tailscale.com/health from tailscale.com/control/controlclient+
tailscale.com/internal/deepprint from tailscale.com/ipn/ipnlocal+
tailscale.com/ipn from tailscale.com/ipn/ipnserver+
tailscale.com/ipn/ipnlocal from tailscale.com/ipn/ipnserver+
tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled
tailscale.com/ipn/ipnstate from tailscale.com/ipn+
tailscale.com/ipn/policy from tailscale.com/ipn
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver
tailscale.com/log/logheap from tailscale.com/control/controlclient
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled
@@ -87,32 +89,38 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/metrics from tailscale.com/derp
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
💣 tailscale.com/net/interfaces from tailscale.com/ipn+
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscaled+
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
tailscale.com/net/netns from tailscale.com/control/controlclient+
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver
tailscale.com/net/packet from tailscale.com/wgengine+
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
tailscale.com/net/stun from tailscale.com/net/netcheck+
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
tailscale.com/net/tsaddr from tailscale.com/ipn+
tailscale.com/net/tsaddr from tailscale.com/ipn/ipnlocal+
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
tailscale.com/paths from tailscale.com/cmd/tailscaled+
tailscale.com/portlist from tailscale.com/ipn
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
tailscale.com/safesocket from tailscale.com/ipn/ipnserver
tailscale.com/smallzstd from tailscale.com/ipn/ipnserver+
💣 tailscale.com/syncs from tailscale.com/net/interfaces+
tailscale.com/syncs from tailscale.com/net/interfaces+
tailscale.com/tailcfg from tailscale.com/control/controlclient+
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/tstime from tailscale.com/wgengine/magicsock
tailscale.com/types/empty from tailscale.com/control/controlclient+
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
tailscale.com/types/key from tailscale.com/derp+
tailscale.com/types/logger from tailscale.com/cmd/tailscaled+
tailscale.com/types/netmap from tailscale.com/control/controlclient+
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
tailscale.com/types/opt from tailscale.com/control/controlclient+
tailscale.com/types/pad32 from tailscale.com/wgengine/magicsock
tailscale.com/types/persist from tailscale.com/control/controlclient+
tailscale.com/types/preftype from tailscale.com/ipn+
tailscale.com/types/strbuilder from tailscale.com/net/packet
tailscale.com/types/structs from tailscale.com/control/controlclient+
tailscale.com/types/wgkey from tailscale.com/control/controlclient+
tailscale.com/util/dnsname from tailscale.com/control/controlclient+
tailscale.com/util/dnsname from tailscale.com/wgengine/tsdns+
LW tailscale.com/util/endian from tailscale.com/net/netns+
tailscale.com/util/lineread from tailscale.com/control/controlclient+
tailscale.com/util/pidowner from tailscale.com/ipn/ipnserver
@@ -123,12 +131,15 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/wgengine from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
tailscale.com/wgengine/magicsock from tailscale.com/cmd/tailscaled+
💣 tailscale.com/wgengine/monitor from tailscale.com/wgengine
💣 tailscale.com/wgengine/monitor from tailscale.com/wgengine+
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
tailscale.com/wgengine/router/dns from tailscale.com/ipn+
tailscale.com/wgengine/tsdns from tailscale.com/ipn+
tailscale.com/wgengine/router/dns from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/tsdns from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/tstun from tailscale.com/wgengine+
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
tailscale.com/wgengine/wglog from tailscale.com/wgengine
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
@@ -152,15 +163,16 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/net/ipv4 from github.com/tailscale/wireguard-go/device
golang.org/x/net/ipv6 from github.com/tailscale/wireguard-go/device+
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net
D golang.org/x/net/route from net+
golang.org/x/oauth2 from tailscale.com/control/controlclient+
golang.org/x/oauth2/internal from golang.org/x/oauth2
golang.org/x/sync/errgroup from tailscale.com/derp
golang.org/x/sync/singleflight from tailscale.com/net/dnscache
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
W golang.org/x/sys/windows from github.com/apenwarr/fixconsole+
W golang.org/x/sys/windows from github.com/tailscale/wireguard-go/conn+
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
W golang.org/x/sys/windows/svc from tailscale.com/cmd/tailscaled
golang.org/x/term from tailscale.com/logpolicy
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
@@ -215,10 +227,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
hash/crc32 from compress/gzip+
hash/fnv from tailscale.com/wgengine/magicsock
hash/maphash from go4.org/mem
html from html/template+
html/template from net/http/pprof
html from net/http/pprof+
io from bufio+
io/ioutil from crypto/tls+
io/fs from crypto/rand+
io/ioutil from github.com/godbus/dbus/v5+
log from expvar+
math from compress/flate+
math/big from crypto/dsa+
@@ -253,8 +265,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
sync/atomic from context+
syscall from crypto/rand+
text/tabwriter from runtime/pprof
text/template from html/template
text/template/parse from html/template+
time from compress/gzip+
unicode from bytes+
unicode/utf16 from encoding/asn1+

View File

@@ -0,0 +1,142 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
)
func init() {
installSystemDaemon = installSystemDaemonDarwin
uninstallSystemDaemon = uninstallSystemDaemonDarwin
}
// darwinLaunchdPlist is the launchd.plist that's written to
// /Library/LaunchDaemons/com.tailscale.tailscaled.plist or (in the
// future) a user-specific location.
//
// See man launchd.plist.
const darwinLaunchdPlist = `
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.tailscale.tailscaled</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/tailscaled</string>
</array>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
`
const sysPlist = "/Library/LaunchDaemons/com.tailscale.tailscaled.plist"
const targetBin = "/usr/local/bin/tailscaled"
const service = "com.tailscale.tailscaled"
func uninstallSystemDaemonDarwin(args []string) (ret error) {
if len(args) > 0 {
return errors.New("uninstall subcommand takes no arguments")
}
plist, err := exec.Command("launchctl", "list", "com.tailscale.tailscaled").Output()
_ = plist // parse it? https://github.com/DHowett/go-plist if we need something.
running := err == nil
if running {
out, err := exec.Command("launchctl", "stop", "com.tailscale.tailscaled").CombinedOutput()
if err != nil {
fmt.Printf("launchctl stop com.tailscale.tailscaled: %v, %s\n", err, out)
ret = err
}
out, err = exec.Command("launchctl", "unload", sysPlist).CombinedOutput()
if err != nil {
fmt.Printf("launchctl unload %s: %v, %s\n", sysPlist, err, out)
if ret == nil {
ret = err
}
}
}
err = os.Remove(sysPlist)
if os.IsNotExist(err) {
err = nil
if ret == nil {
ret = err
}
}
return ret
}
func installSystemDaemonDarwin(args []string) (err error) {
if len(args) > 0 {
return errors.New("install subcommand takes no arguments")
}
defer func() {
if err != nil && os.Getuid() != 0 {
err = fmt.Errorf("%w; try running tailscaled with sudo", err)
}
}()
// Copy ourselves to /usr/local/bin/tailscaled.
exe, err := os.Executable()
if err != nil {
return fmt.Errorf("failed to find our own executable path: %w", err)
}
tmpBin := targetBin + ".tmp"
f, err := os.Create(tmpBin)
if err != nil {
return err
}
self, err := os.Open(exe)
if err != nil {
f.Close()
return err
}
_, err = io.Copy(f, self)
self.Close()
if err != nil {
f.Close()
return err
}
if err := f.Close(); err != nil {
return err
}
if err := os.Chmod(tmpBin, 0755); err != nil {
return err
}
if err := os.Rename(tmpBin, targetBin); err != nil {
return err
}
// Best effort:
uninstallSystemDaemonDarwin(nil)
if err := ioutil.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil {
return err
}
if out, err := exec.Command("launchctl", "load", sysPlist).CombinedOutput(); err != nil {
return fmt.Errorf("error running launchctl load %s: %v, %s", sysPlist, err, out)
}
if out, err := exec.Command("launchctl", "start", service).CombinedOutput(); err != nil {
return fmt.Errorf("error running launchctl start %s: %v, %s", service, err, out)
}
return nil
}

View File

@@ -24,7 +24,6 @@ import (
"syscall"
"time"
"github.com/apenwarr/fixconsole"
"tailscale.com/ipn/ipnserver"
"tailscale.com/logpolicy"
"tailscale.com/paths"
@@ -53,6 +52,10 @@ func defaultTunName() string {
return "tun"
case "windows":
return "Tailscale"
case "darwin":
// "utun" is recognized by wireguard-go/tun/tun_darwin.go
// as a magic value that uses/creates any free number.
return "utun"
}
return "tailscale0"
}
@@ -68,6 +71,17 @@ var args struct {
verbose int
}
var (
installSystemDaemon func([]string) error // non-nil on some platforms
uninstallSystemDaemon func([]string) error // non-nil on some platforms
)
var subCommands = map[string]*func([]string) error{
"install-system-daemon": &installSystemDaemon,
"uninstall-system-daemon": &uninstallSystemDaemon,
"debug": &debugModeFunc,
}
func main() {
// We aren't very performance sensitive, and the parts that are
// performance sensitive (wireguard) try hard not to do any memory
@@ -88,9 +102,23 @@ func main() {
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
flag.BoolVar(&printVersion, "version", false, "print version information and exit")
err := fixconsole.FixConsoleIfNeeded()
if err != nil {
log.Fatalf("fixConsoleOutput: %v", err)
if len(os.Args) > 1 {
sub := os.Args[1]
if fp, ok := subCommands[sub]; ok {
if *fp == nil {
log.SetFlags(0)
log.Fatalf("%s not available on %v", sub, runtime.GOOS)
}
if err := (*fp)(os.Args[2:]); err != nil {
log.SetFlags(0)
log.Fatal(err)
}
return
}
}
if beWindowsSubprocess() {
return
}
flag.Parse()
@@ -103,11 +131,13 @@ func main() {
os.Exit(0)
}
if args.statepath == "" {
log.Fatalf("--state is required")
if runtime.GOOS == "darwin" && os.Getuid() != 0 {
log.SetFlags(0)
log.Fatalf("tailscaled requires root; use sudo tailscaled")
}
if args.socketpath == "" && runtime.GOOS != "windows" {
log.SetFlags(0)
log.Fatalf("--socket is required")
}
@@ -129,6 +159,16 @@ func run() error {
pol.Shutdown(ctx)
}()
if isWindowsService() {
// Run the IPN server from the Windows service manager.
log.Printf("Running service...")
if err := runWindowsService(pol); err != nil {
log.Printf("runservice: %v", err)
}
log.Printf("Service ended.")
return nil
}
var logf logger.Logf = log.Printf
if v, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_MEMORY")); v {
logf = logger.RusagePrefixLog(logf)
@@ -140,6 +180,10 @@ func run() error {
return nil
}
if args.statepath == "" {
log.Fatalf("--state is required")
}
var debugMux *http.ServeMux
if args.debug != "" {
debugMux = newDebugMux()

View File

@@ -20,22 +20,5 @@ CacheDirectory=tailscale
CacheDirectoryMode=0750
Type=notify
DeviceAllow=/dev/net/tun
DeviceAllow=/dev/null
DeviceAllow=/dev/random
DeviceAllow=/dev/urandom
DevicePolicy=strict
LockPersonality=true
MemoryDenyWriteExecute=true
PrivateTmp=true
ProtectClock=true
ProtectControlGroups=true
ProtectHome=true
ProtectKernelTunables=true
ProtectSystem=strict
ReadWritePaths=/etc/
RestrictSUIDSGID=true
SystemCallArchitectures=native
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,15 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !windows
package main // import "tailscale.com/cmd/tailscaled"
import "tailscale.com/logpolicy"
func isWindowsService() bool { return false }
func runWindowsService(pol *logpolicy.Policy) error { panic("unreachable") }
func beWindowsSubprocess() bool { return false }

View File

@@ -0,0 +1,180 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package main // import "tailscale.com/cmd/tailscaled"
// TODO: check if administrator, like tswin does.
//
// TODO: try to load wintun.dll early at startup, before wireguard/tun
// does (which panics) and if we'd fail (e.g. due to access
// denied, even if administrator), use 'tasklist /m wintun.dll'
// to see if something else is currently using it and tell user.
//
// TODO: check if Tailscale service is already running, and fail early
// like tswin does.
//
// TODO: on failure, check if on a UNC drive and recommend copying it
// to C:\ to run it, like tswin does.
import (
"context"
"fmt"
"log"
"os"
"time"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/svc"
"tailscale.com/ipn/ipnserver"
"tailscale.com/logpolicy"
"tailscale.com/types/logger"
"tailscale.com/version"
"tailscale.com/wgengine"
)
const serviceName = "Tailscale"
func isWindowsService() bool {
v, err := svc.IsWindowsService()
if err != nil {
log.Fatalf("svc.IsWindowsService failed: %v", err)
}
return v
}
func runWindowsService(pol *logpolicy.Policy) error {
return svc.Run(serviceName, &ipnService{Policy: pol})
}
type ipnService struct {
Policy *logpolicy.Policy
}
// Called by Windows to execute the windows service.
func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, changes chan<- svc.Status) (bool, uint32) {
changes <- svc.Status{State: svc.StartPending}
ctx, cancel := context.WithCancel(context.Background())
doneCh := make(chan struct{})
go func() {
defer close(doneCh)
args := []string{"/subproc", service.Policy.PublicID.String()}
ipnserver.BabysitProc(ctx, args, log.Printf)
}()
changes <- svc.Status{State: svc.Running, Accepts: svc.AcceptStop}
for ctx.Err() == nil {
select {
case <-doneCh:
case cmd := <-r:
switch cmd.Cmd {
case svc.Stop:
cancel()
case svc.Interrogate:
changes <- cmd.CurrentStatus
}
}
}
changes <- svc.Status{State: svc.StopPending}
return false, windows.NO_ERROR
}
func beWindowsSubprocess() bool {
if len(os.Args) != 3 || os.Args[1] != "/subproc" {
return false
}
logid := os.Args[2]
log.Printf("Program starting: v%v: %#v", version.Long, os.Args)
log.Printf("subproc mode: logid=%v", logid)
go func() {
b := make([]byte, 16)
for {
_, err := os.Stdin.Read(b)
if err != nil {
log.Fatalf("stdin err (parent process died): %v", err)
}
}
}()
err := startIPNServer(context.Background(), logid)
if err != nil {
log.Fatalf("ipnserver: %v", err)
}
return true
}
func startIPNServer(ctx context.Context, logid string) error {
var logf logger.Logf = log.Printf
var eng wgengine.Engine
var err error
getEngine := func() (wgengine.Engine, error) {
eng, err := wgengine.NewUserspaceEngine(logf, "Tailscale", 41641)
if err != nil {
return nil, err
}
return wgengine.NewWatchdog(eng), nil
}
if msg := os.Getenv("TS_DEBUG_WIN_FAIL"); msg != "" {
err = fmt.Errorf("pretending to be a service failure: %v", msg)
} else {
// We have a bunch of bug reports of wgengine.NewUserspaceEngine returning a few different errors,
// all intermittently. A few times I (Brad) have also seen sporadic failures that simply
// restarting fixed. So try a few times.
for try := 1; try <= 5; try++ {
if try > 1 {
// Only sleep a bit. Don't do some massive backoff because
// the frontend GUI has a 30 second timeout on connecting to us,
// but even 5 seconds is too long for them to get any results.
// 5 tries * 1 second each seems fine.
time.Sleep(time.Second)
}
eng, err = getEngine()
if err != nil {
logf("wgengine.NewUserspaceEngine: (try %v) %v", try, err)
continue
}
if try > 1 {
logf("wgengine.NewUserspaceEngine: ended up working on try %v", try)
}
break
}
}
if err != nil {
// Log the error, but don't fatalf. We want to
// propagate the error message to the UI frontend. So
// we continue and tell the ipnserver to return that
// in a Notify message.
logf("wgengine.NewUserspaceEngine: %v", err)
}
opts := ipnserver.Options{
Port: 41112,
SurviveDisconnects: false,
StatePath: args.statepath,
}
if err != nil {
// Return nicer errors to users, annotated with logids, which helps
// when they file bugs.
rawGetEngine := getEngine // raw == without verbose logid-containing error
getEngine = func() (wgengine.Engine, error) {
eng, err := rawGetEngine()
if err != nil {
return nil, fmt.Errorf("wgengine.NewUserspaceEngine: %v\n\nlogid: %v", err, logid)
}
return eng, nil
}
} else {
getEngine = ipnserver.FixedEngine(eng)
}
err = ipnserver.Run(ctx, logf, logid, getEngine, opts)
if err != nil {
logf("ipnserver.Run: %v", err)
}
return err
}

View File

@@ -32,7 +32,9 @@ import (
"github.com/gliderlabs/ssh"
"github.com/kr/pty"
gossh "golang.org/x/crypto/ssh"
"inet.af/netaddr"
"tailscale.com/net/interfaces"
"tailscale.com/net/tsaddr"
)
var (
@@ -96,7 +98,13 @@ func handleSSH(s ssh.Session) {
s.Exit(1)
return
}
if !interfaces.IsTailscaleIP(ta.IP) {
tanetaddr, ok := netaddr.FromStdIP(ta.IP)
if !ok {
log.Printf("tsshd: rejecting unparseable addr %v", ta.IP)
s.Exit(1)
return
}
if !tsaddr.IsTailscaleIP(tanetaddr) {
log.Printf("tsshd: rejecting non-Tailscale addr %v", ta.IP)
s.Exit(1)
return

View File

@@ -18,10 +18,13 @@ import (
"time"
"golang.org/x/oauth2"
"tailscale.com/health"
"tailscale.com/logtail/backoff"
"tailscale.com/tailcfg"
"tailscale.com/types/empty"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/types/persist"
"tailscale.com/types/structs"
"tailscale.com/types/wgkey"
)
@@ -68,9 +71,9 @@ type Status struct {
LoginFinished *empty.Message
Err string
URL string
Persist *Persist // locally persisted configuration
NetMap *NetworkMap // server-pushed configuration
Hostinfo *tailcfg.Hostinfo // current Hostinfo data
Persist *persist.Persist // locally persisted configuration
NetMap *netmap.NetworkMap // server-pushed configuration
Hostinfo *tailcfg.Hostinfo // current Hostinfo data
State State
}
@@ -114,6 +117,8 @@ type Client struct {
closed bool
newMapCh chan struct{} // readable when we must restart a map request
unregisterHealthWatch func()
mu sync.Mutex // mutex guards the following fields
statusFunc func(Status) // called to update Client status
@@ -169,7 +174,14 @@ func NewNoStart(opts Options) (*Client, error) {
}
c.authCtx, c.authCancel = context.WithCancel(context.Background())
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
c.unregisterHealthWatch = health.RegisterWatcher(c.onHealthChange)
return c, nil
}
func (c *Client) onHealthChange(key string, err error) {
c.logf("controlclient: restarting map request for %q health change to new state: %v", key, err)
c.cancelMapSafely()
}
// SetPaused controls whether HTTP activity should be paused.
@@ -213,7 +225,7 @@ func (c *Client) sendNewMapRequest() {
// If we're not already streaming a netmap, or if we're already stuck
// in a lite update, then tear down everything and start a new stream
// (which starts by sending a new map request)
if !c.inPollNetMap || c.inLiteMapUpdate {
if !c.inPollNetMap || c.inLiteMapUpdate || !c.loggedIn {
c.mu.Unlock()
c.cancelMapSafely()
return
@@ -509,7 +521,7 @@ func (c *Client) mapRoutine() {
c.inPollNetMap = false
c.mu.Unlock()
err := c.direct.PollNetMap(ctx, -1, func(nm *NetworkMap) {
err := c.direct.PollNetMap(ctx, -1, func(nm *netmap.NetworkMap) {
c.mu.Lock()
select {
@@ -606,7 +618,7 @@ func (c *Client) SetNetInfo(ni *tailcfg.NetInfo) {
c.sendNewMapRequest()
}
func (c *Client) sendStatus(who string, err error, url string, nm *NetworkMap) {
func (c *Client) sendStatus(who string, err error, url string, nm *netmap.NetworkMap) {
c.mu.Lock()
state := c.state
loggedIn := c.loggedIn
@@ -618,7 +630,7 @@ func (c *Client) sendStatus(who string, err error, url string, nm *NetworkMap) {
c.logf("[v1] sendStatus: %s: %v", who, state)
var p *Persist
var p *persist.Persist
var fin *empty.Message
if state == StateAuthenticated {
fin = new(empty.Message)
@@ -698,6 +710,7 @@ func (c *Client) Shutdown() {
c.logf("client.Shutdown: inSendStatus=%v", inSendStatus)
if !closed {
c.unregisterHealthWatch()
close(c.quit)
c.cancelAuth()
<-c.authDone

View File

@@ -4,8 +4,6 @@
package controlclient
//go:generate go run tailscale.com/cmd/cloner -type=Persist -output=direct_clone.go
import (
"bytes"
"context"
@@ -22,6 +20,7 @@ import (
"net/url"
"os"
"os/exec"
"path/filepath"
"reflect"
"runtime"
"sort"
@@ -34,6 +33,7 @@ import (
"golang.org/x/crypto/nacl/box"
"golang.org/x/oauth2"
"inet.af/netaddr"
"tailscale.com/health"
"tailscale.com/log/logheap"
"tailscale.com/net/dnscache"
"tailscale.com/net/netns"
@@ -41,70 +41,15 @@ import (
"tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/types/opt"
"tailscale.com/types/structs"
"tailscale.com/types/persist"
"tailscale.com/types/wgkey"
"tailscale.com/util/systemd"
"tailscale.com/version"
"tailscale.com/wgengine/filter"
)
type Persist struct {
_ structs.Incomparable
// LegacyFrontendPrivateMachineKey is here temporarily
// (starting 2020-09-28) during migration of Windows users'
// machine keys from frontend storage to the backend. On the
// first LocalBackend.Start call, the backend will initialize
// the real (backend-owned) machine key from the frontend's
// provided value (if non-zero), picking a new random one if
// needed. This field should be considered read-only from GUI
// frontends. The real value should not be written back in
// this field, lest the frontend persist it to disk.
LegacyFrontendPrivateMachineKey wgkey.Private `json:"PrivateMachineKey"`
PrivateNodeKey wgkey.Private
OldPrivateNodeKey wgkey.Private // needed to request key rotation
Provider string
LoginName string
}
func (p *Persist) Equals(p2 *Persist) bool {
if p == nil && p2 == nil {
return true
}
if p == nil || p2 == nil {
return false
}
return p.LegacyFrontendPrivateMachineKey.Equal(p2.LegacyFrontendPrivateMachineKey) &&
p.PrivateNodeKey.Equal(p2.PrivateNodeKey) &&
p.OldPrivateNodeKey.Equal(p2.OldPrivateNodeKey) &&
p.Provider == p2.Provider &&
p.LoginName == p2.LoginName
}
func (p *Persist) Pretty() string {
var mk, ok, nk wgkey.Key
if !p.LegacyFrontendPrivateMachineKey.IsZero() {
mk = p.LegacyFrontendPrivateMachineKey.Public()
}
if !p.OldPrivateNodeKey.IsZero() {
ok = p.OldPrivateNodeKey.Public()
}
if !p.PrivateNodeKey.IsZero() {
nk = p.PrivateNodeKey.Public()
}
ss := func(k wgkey.Key) string {
if k.IsZero() {
return ""
}
return k.ShortString()
}
return fmt.Sprintf("Persist{lm=%v, o=%v, n=%v u=%#v}",
ss(mk), ss(ok), ss(nk), p.LoginName)
}
// Direct is the client that connects to a tailcontrol server for a node.
type Direct struct {
httpc *http.Client // HTTP client used to talk to tailcontrol
@@ -121,7 +66,7 @@ type Direct struct {
mu sync.Mutex // mutex guards the following fields
serverKey wgkey.Key
persist Persist
persist persist.Persist
authKey string
tryingNewKey wgkey.Private
expiry *time.Time
@@ -133,7 +78,7 @@ type Direct struct {
}
type Options struct {
Persist Persist // initial persistent data
Persist persist.Persist // initial persistent data
MachinePrivateKey wgkey.Private // the machine key to use
ServerURL string // URL of the tailcontrol server
AuthKey string // optional node auth key for auto registration
@@ -229,10 +174,25 @@ func NewHostinfo() *tailcfg.Hostinfo {
Hostname: hostname,
OS: version.OS(),
OSVersion: osv,
Package: packageType(),
GoArch: runtime.GOARCH,
}
}
func packageType() string {
switch runtime.GOOS {
case "windows":
if _, err := os.Stat(`C:\ProgramData\chocolatey\lib\tailscale`); err == nil {
return "choco"
}
case "darwin":
// Using tailscaled or IPNExtension?
exe, _ := os.Executable() // TODO(mkramlich): for tailscaled, distinguish if brew
return filepath.Base(exe)
}
return ""
}
// SetHostinfo clones the provided Hostinfo and remembers it for the
// next update. It reports whether the Hostinfo has changed.
func (c *Direct) SetHostinfo(hi *tailcfg.Hostinfo) bool {
@@ -271,7 +231,7 @@ func (c *Direct) SetNetInfo(ni *tailcfg.NetInfo) bool {
return true
}
func (c *Direct) GetPersist() Persist {
func (c *Direct) GetPersist() persist.Persist {
c.mu.Lock()
defer c.mu.Unlock()
return c.persist
@@ -294,7 +254,7 @@ func (c *Direct) TryLogout(ctx context.Context) error {
// immediately invalidated.
//if !c.persist.PrivateNodeKey.IsZero() {
//}
c.persist = Persist{}
c.persist = persist.Persist{}
return nil
}
@@ -526,7 +486,7 @@ func inTest() bool { return flag.Lookup("test.v") != nil }
//
// maxPolls is how many network maps to download; common values are 1
// or -1 (to keep a long-poll query open to the server).
func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*NetworkMap)) error {
func (c *Direct) PollNetMap(ctx context.Context, maxPolls int, cb func(*netmap.NetworkMap)) error {
return c.sendMapRequest(ctx, maxPolls, cb)
}
@@ -538,7 +498,7 @@ func (c *Direct) SendLiteMapUpdate(ctx context.Context) error {
}
// cb nil means to omit peers.
func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*NetworkMap)) error {
func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netmap.NetworkMap)) error {
c.mu.Lock()
persist := c.persist
serverURL := c.serverURL
@@ -550,6 +510,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
everEndpoints := c.everEndpoints
c.mu.Unlock()
if persist.PrivateNodeKey.IsZero() {
return errors.New("privateNodeKey is zero")
}
if backendLogID == "" {
return errors.New("hostinfo: BackendLogID missing")
}
@@ -575,9 +538,16 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
DebugFlags: c.debugFlags,
OmitPeers: cb == nil,
}
var extraDebugFlags []string
if hostinfo != nil && ipForwardingBroken(hostinfo.RoutableIPs) {
extraDebugFlags = append(extraDebugFlags, "warn-ip-forwarding-off")
}
if health.RouterHealth() != nil {
extraDebugFlags = append(extraDebugFlags, "warn-router-unhealthy")
}
if len(extraDebugFlags) > 0 {
old := request.DebugFlags
request.DebugFlags = append(old[:len(old):len(old)], "warn-ip-forwarding-off")
request.DebugFlags = append(old[:len(old):len(old)], extraDebugFlags...)
}
if c.newDecompressor != nil {
request.Compress = "zstd"
@@ -744,6 +714,14 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
}
resp.Peers = filtered
}
if Debug.StripEndpoints {
for _, p := range resp.Peers {
// We need at least one endpoint here for now else
// other code doesn't even create the discoEndpoint.
// TODO(bradfitz): fix that and then just nil this out.
p.Endpoints = []string{"127.9.9.9:456"}
}
}
if pf := resp.PacketFilter; pf != nil {
lastParsedPacketFilter = c.parsePacketFilter(pf)
@@ -761,7 +739,8 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
localPort = c.localPort
c.mu.Unlock()
nm := &NetworkMap{
nm := &netmap.NetworkMap{
SelfNode: resp.Node,
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
PrivateKey: persist.PrivateNodeKey,
MachineKey: machinePubKey,
@@ -790,7 +769,10 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*Netw
}
}
addUserProfile(nm.User)
magicDNSSuffix := nm.MagicDNSSuffix()
nm.SelfNode.InitDisplayNames(magicDNSSuffix)
for _, peer := range resp.Peers {
peer.InitDisplayNames(magicDNSSuffix)
if !peer.Sharer.IsZero() {
if c.keepSharerAndUserSplit {
addUserProfile(peer.Sharer)
@@ -972,19 +954,21 @@ func loadServerKey(ctx context.Context, httpc *http.Client, serverURL string) (w
var Debug = initDebug()
type debug struct {
NetMap bool
ProxyDNS bool
OnlyDisco bool
Disco bool
NetMap bool
ProxyDNS bool
OnlyDisco bool
Disco bool
StripEndpoints bool // strip endpoints from control (only use disco messages)
}
func initDebug() debug {
use := os.Getenv("TS_DEBUG_USE_DISCO")
return debug{
NetMap: envBool("TS_DEBUG_NETMAP"),
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
OnlyDisco: use == "only",
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
NetMap: envBool("TS_DEBUG_NETMAP"),
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
StripEndpoints: envBool("TS_DEBUG_STRIP_ENDPOINTS"),
OnlyDisco: use == "only",
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
}
}
@@ -1065,6 +1049,24 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
}
}
sortNodes(newFull)
if mapRes.PeerSeenChange != nil {
peerByID := make(map[tailcfg.NodeID]*tailcfg.Node, len(newFull))
for _, n := range newFull {
peerByID[n.ID] = n
}
now := time.Now()
for nodeID, seen := range mapRes.PeerSeenChange {
if n, ok := peerByID[nodeID]; ok {
if seen {
n.LastSeen = &now
} else {
n.LastSeen = nil
}
}
}
}
mapRes.Peers = newFull
mapRes.PeersChanged = nil
mapRes.PeersRemoved = nil

View File

@@ -1,20 +0,0 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// 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

@@ -5,6 +5,7 @@
package controlclient
import (
"encoding/json"
"fmt"
"reflect"
"strings"
@@ -156,3 +157,15 @@ func TestNewDirect(t *testing.T) {
t.Errorf("c.newEndpoints(13) want true got %v", changed)
}
}
func TestNewHostinfo(t *testing.T) {
hi := NewHostinfo()
if hi == nil {
t.Fatal("no Hostinfo")
}
j, err := json.MarshalIndent(hi, " ", "")
if err != nil {
t.Fatal(err)
}
t.Logf("Got: %s", j)
}

View File

@@ -709,10 +709,19 @@ func (c *Client) RecvDetail() (m derp.ReceivedMessage, connGen int, err error) {
m, err = client.Recv()
if err != nil {
c.closeForReconnect(client)
if c.isClosed() {
err = ErrClientClosed
}
}
return m, connGen, err
}
func (c *Client) isClosed() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.closed
}
// Close closes the client. It will not automatically reconnect after
// being closed.
func (c *Client) Close() error {

View File

@@ -5,20 +5,32 @@
package derphttp
import (
"context"
"sync"
"time"
"tailscale.com/derp"
"tailscale.com/types/key"
"tailscale.com/types/logger"
)
// RunWatchConnectionLoop loops forever, sending WatchConnectionChanges and subscribing to
// RunWatchConnectionLoop loops until ctx is done, sending WatchConnectionChanges and subscribing to
// connection changes.
//
// If the server's public key is ignoreServerKey, RunWatchConnectionLoop returns.
//
// Otherwise, the add and remove funcs are called as clients come & go.
func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove func(key.Public)) {
//
// infoLogf, if non-nil, is the logger to write periodic status
// updates about how many peers are on the server. Error log output is
// set to the c's logger, regardless of infoLogf's value.
//
// To force RunWatchConnectionLoop to return quickly, its ctx needs to
// be closed, and c itself needs to be closed.
func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key.Public, infoLogf logger.Logf, add, remove func(key.Public)) {
if infoLogf == nil {
infoLogf = logger.Discard
}
logf := c.logf
const retryInterval = 5 * time.Second
const statusInterval = 10 * time.Second
@@ -45,7 +57,7 @@ func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove
if loggedConnected {
return
}
logf("connected; %d peers", len(present))
infoLogf("connected; %d peers", len(present))
loggedConnected = true
}
@@ -79,12 +91,21 @@ func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove
}
}
for {
sleep := func(d time.Duration) {
t := time.NewTimer(d)
select {
case <-ctx.Done():
t.Stop()
case <-t.C:
}
}
for ctx.Err() == nil {
err := c.WatchConnectionChanges()
if err != nil {
clear()
logf("WatchConnectionChanges: %v", err)
time.Sleep(retryInterval)
sleep(retryInterval)
continue
}
@@ -97,7 +118,7 @@ func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove
if err != nil {
clear()
logf("Recv: %v", err)
time.Sleep(retryInterval)
sleep(retryInterval)
break
}
if connGen != lastConnGen {
@@ -114,9 +135,8 @@ func (c *Client) RunWatchConnectionLoop(ignoreServerKey key.Public, add, remove
}
if now := time.Now(); now.Sub(lastStatus) > statusInterval {
lastStatus = now
logf("%d peers", len(present))
infoLogf("%d peers", len(present))
}
}
}
}

View File

@@ -70,7 +70,7 @@ func Parse(p []byte) (Message, error) {
case TypePong:
return parsePong(ver, p)
case TypeCallMeMaybe:
return CallMeMaybe{}, nil
return parseCallMeMaybe(ver, p)
default:
return nil, fmt.Errorf("unknown message type 0x%02x", byte(t))
}
@@ -122,13 +122,57 @@ func parsePing(ver uint8, p []byte) (m *Ping, err error) {
//
// The recipient may choose to not open a path back, if it's already
// happy with its path. But usually it will.
type CallMeMaybe struct{}
type CallMeMaybe struct {
// MyNumber is what the peer believes its endpoints are.
//
// Prior to Tailscale 1.4, the endpoints were exchanged purely
// between nodes and the control server.
//
// Starting with Tailscale 1.4, clients advertise their endpoints.
// Older clients won't use this, but newer clients should
// use any endpoints in here that aren't included from control.
//
// Control might have sent stale endpoints if the client was idle
// before contacting us. In that case, the client likely did a STUN
// request immediately before sending the CallMeMaybe to recreate
// their NAT port mapping, and that new good endpoint is included
// in this field, but might not yet be in control's endpoints.
// (And in the future, control will stop distributing endpoints
// when clients are suitably new.)
MyNumber []netaddr.IPPort
}
func (CallMeMaybe) AppendMarshal(b []byte) []byte {
ret, _ := appendMsgHeader(b, TypeCallMeMaybe, v0, 0)
const epLength = 16 + 2 // 16 byte IP address + 2 byte port
func (m *CallMeMaybe) AppendMarshal(b []byte) []byte {
ret, p := appendMsgHeader(b, TypeCallMeMaybe, v0, epLength*len(m.MyNumber))
for _, ipp := range m.MyNumber {
a := ipp.IP.As16()
copy(p[:], a[:])
binary.BigEndian.PutUint16(p[16:], ipp.Port)
p = p[epLength:]
}
return ret
}
func parseCallMeMaybe(ver uint8, p []byte) (m *CallMeMaybe, err error) {
m = new(CallMeMaybe)
if len(p)%epLength != 0 || ver != 0 || len(p) == 0 {
return m, nil
}
m.MyNumber = make([]netaddr.IPPort, 0, len(p)/epLength)
for len(p) > 0 {
var a [16]byte
copy(a[:], p)
m.MyNumber = append(m.MyNumber, netaddr.IPPort{
IP: netaddr.IPFrom16(a),
Port: binary.BigEndian.Uint16(p[16:18]),
})
p = p[epLength:]
}
return m, nil
}
// Pong is a response a Ping.
//
// It includes the sender's source IP + port, so it's effectively a
@@ -171,7 +215,7 @@ func MessageSummary(m Message) string {
return fmt.Sprintf("ping tx=%x", m.TxID[:6])
case *Pong:
return fmt.Sprintf("pong tx=%x", m.TxID[:6])
case CallMeMaybe:
case *CallMeMaybe:
return "call-me-maybe"
default:
return fmt.Sprintf("%#v", m)

View File

@@ -44,9 +44,19 @@ func TestMarshalAndParse(t *testing.T) {
},
{
name: "call_me_maybe",
m: CallMeMaybe{},
m: &CallMeMaybe{},
want: "03 00",
},
{
name: "call_me_maybe_endpoints",
m: &CallMeMaybe{
MyNumber: []netaddr.IPPort{
netaddr.MustParseIPPort("1.2.3.4:567"),
netaddr.MustParseIPPort("[2001::3456]:789"),
},
},
want: "03 00 00 00 00 00 00 00 00 00 00 00 ff ff 01 02 03 04 02 37 20 01 00 00 00 00 00 00 00 00 00 00 00 00 34 56 03 15",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {

19
go.mod
View File

@@ -1,6 +1,6 @@
module tailscale.com
go 1.15
go 1.16
require (
github.com/alexbrainman/sspi v0.0.0-20180613141037-e580b900e9f5
@@ -15,30 +15,31 @@ require (
github.com/golang/protobuf v1.4.2 // indirect
github.com/google/go-cmp v0.5.4
github.com/goreleaser/nfpm v1.1.10
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b
github.com/klauspost/compress v1.10.10
github.com/kr/pty v1.1.4-0.20190131011033-7dc38fb350b1
github.com/mdlayher/netlink v1.2.0
github.com/kr/pty v1.1.8
github.com/mdlayher/netlink v1.3.2
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a
github.com/miekg/dns v1.1.30
github.com/pborman/getopt v0.0.0-20190409184431-ee0cd42419d3
github.com/peterbourgon/ff/v2 v2.0.0
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d
github.com/tailscale/wireguard-go v0.0.0-20210210202228-3cc76ed5f222
github.com/tcnksm/go-httpstat v0.2.0
github.com/toqueteos/webbrowser v1.2.0
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392
golang.org/x/net v0.0.0-20201216054612-986b41b23924
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
golang.org/x/tools v0.0.0-20201211185031-d93e913c1a58
golang.zx2c4.com/wireguard/windows v0.1.2-0.20201113162609-9b85be97fdf8
gvisor.dev/gvisor v0.0.0-20210111185822-3ff3110fcdd6
honnef.co/go/tools v0.1.0
inet.af/netaddr v0.0.0-20210105212526-648fbc18a69d
inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44
inet.af/peercred v0.0.0-20210216231719-993aa01eacaa
rsc.io/goversion v1.2.0
)

73
go.sum
View File

@@ -68,6 +68,8 @@ github.com/coreos/go-systemd/v22 v22.0.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+
github.com/coreos/go-systemd/v22 v22.1.0/go.mod h1:xO0FLkIi5MaZafQlIrOotqXZ90ih+1atmu1JpKERPPk=
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/creack/pty v1.1.7 h1:6pwm8kMQKCmgUg0ZHTm5+/YvRK0s3THD/28+T6/kk4A=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -121,7 +123,6 @@ github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfb
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -140,9 +141,7 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.3-0.20201020212313-ab46b8bd0abd/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -179,12 +178,16 @@ github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJ
github.com/imdario/mergo v0.3.8 h1:CGgOkSJeqMRmt0D9XLWExdT4m4F1vd3FV3VPt+0VxkQ=
github.com/imdario/mergo v0.3.8/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 h1:uhL5Gw7BINiiPAo24A2sxkcDI0Jt/sqp1v5xQCniEFA=
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4 h1:nwOc1YaOrYJ37sEBrtWZrdqzK22hiJs3GpDmP3sR2Yw=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391 h1:Dqu/4JhMV1vpXHDjzQCuDCEsjNi0xfuSmQlMOyqayKA=
github.com/jsimonetti/rtnetlink v0.0.0-20201216134343-bde56ed16391/go.mod h1:cR77jAZG3Y3bsb8hF6fHJbFoyFukLFOkQ98S0pQz3xw=
github.com/jsimonetti/rtnetlink v0.0.0-20201220180245-69540ac93943/go.mod h1:z4c53zj6Eex712ROyh8WI0ihysb5j2ROyV42iNogmAs=
github.com/jsimonetti/rtnetlink v0.0.0-20210122163228-8d122574c736/go.mod h1:ZXpIyOK59ZnN7J0BV99cZUPmsqDRZ3eq5X+st7u/oSA=
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b h1:c3NTyLNozICy8B4mlMXemD3z/gXgQzVXZS/HqT+i3do=
github.com/jsimonetti/rtnetlink v0.0.0-20210212075122-66c871082f2b/go.mod h1:8w9Rh8m+aHZIG69YPGGem1i5VzoyRC8nw2kA8B+ik5U=
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
@@ -198,10 +201,10 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxv
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.4-0.20190131011033-7dc38fb350b1 h1:zc0R6cOw98cMengLA0fvU55mqbnN7sd/tBMLzSejp+M=
github.com/kr/pty v1.1.4-0.20190131011033-7dc38fb350b1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lxn/walk v0.0.0-20201110160827-18ea5e372cdb/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
@@ -210,13 +213,20 @@ github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN
github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0=
github.com/mattn/go-zglob v0.0.1 h1:xsEx/XUoVlI6yXjqBK062zYhRTZltCNmYPx6v+8DNaY=
github.com/mattn/go-zglob v0.0.1/go.mod h1:9fxibJccNxU2cnpIKLRRFA7zX7qhkJIQWBb449FYHOo=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43 h1:WgyLFv10Ov49JAQI/ZLUkCZ7VJS3r74hwFIGXJsgZlY=
github.com/mdlayher/ethtool v0.0.0-20210210192532-2b88debcdd43/go.mod h1:+t7E0lkKfbBsebllff1xdTmyJt8lH37niI6kwFk9OTo=
github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0=
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
github.com/mdlayher/netlink v1.1.0 h1:mpdLgm+brq10nI9zM1BpX1kpDbh3NLl3RSnVq6ZSkfg=
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
github.com/mdlayher/netlink v1.2.0 h1:zPolhRjfuabdf8ofZsl56eoU+92cvSlAn13lw4veCZ0=
github.com/mdlayher/netlink v1.2.0/go.mod h1:kwVW1io0AZy9A1E2YYgaD4Cj+C+GPkU6klXCMzIJ9p8=
github.com/mdlayher/netlink v1.2.1/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
github.com/mdlayher/netlink v1.2.2-0.20210123213345-5cc92139ae3e/go.mod h1:bacnNlfhqHqqLo4WsYeXSqfyXkInQ9JneWI68v1KwSU=
github.com/mdlayher/netlink v1.3.0/go.mod h1:xK/BssKuwcRXHrtN04UBkwQ6dY9VviGGuriDdoPSWys=
github.com/mdlayher/netlink v1.3.2 h1:fMZOU2/M7PRMzGM3br5l1N2fu6bPSHtRytmQ338a9iA=
github.com/mdlayher/netlink v1.3.2/go.mod h1:dRJi5IABcZpBD2A3D0Mv/AiX8I9uDEu5oGkAVrekmf8=
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a h1:wMv2mvcHRH4jqIxaVL5t6gSq1hjPiaWH7TOcA0Z+uNo=
github.com/mdlayher/sdnotify v0.0.0-20200625151349-e4a4f32afc4a/go.mod h1:HtjVsQfsrBm1GDcDTUFn4ZXhftxTwO/hxrvEiRc61U4=
github.com/miekg/dns v1.1.30 h1:Qww6FseFn8PRfw07jueqIXqodm0JKiiKuK0DeXSqfyo=
@@ -251,7 +261,6 @@ github.com/peterbourgon/ff/v2 v2.0.0 h1:lx0oYI5qr/FU1xnpNhQ+EZM04gKgn46jyYvGEEqB
github.com/peterbourgon/ff/v2 v2.0.0/go.mod h1:xjwr+t+SjWm4L46fcj/D+Ap+6ME7+HqFzaP22pP5Ggk=
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 h1:+/+DxvQaYifJ+grD4klzrS5y+KJXldn/2YTl5JG+vZ8=
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7/go.mod h1:zO8QMzTeZd5cpnIkz/Gn6iK0jDfGicM1nynOkkPIl28=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -286,14 +295,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww=
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027 h1:lK99QQdH3yBWY6aGilF+IRlQIdmhzLrsEmF6JgN+Ryw=
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
github.com/tailscale/wireguard-go v0.0.0-20210109012254-dc30a1b9415e h1:ZXbXfVJOhSq4/Gt7TnqwXBPCctzYXkWXo3oQS7LZ40I=
github.com/tailscale/wireguard-go v0.0.0-20210109012254-dc30a1b9415e/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
github.com/tailscale/wireguard-go v0.0.0-20210113223737-a6213b5eaf98 h1:khwYPK1eT+4pmEFyCjpf6Br/0JWjdVT3uQ+ILFJPTRo=
github.com/tailscale/wireguard-go v0.0.0-20210113223737-a6213b5eaf98/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
github.com/tailscale/wireguard-go v0.0.0-20210114205708-a1377e83f551 h1:hjBVxvVa145kVflAFkVcTr/zwUzBO4SqfSS6xhbcMv8=
github.com/tailscale/wireguard-go v0.0.0-20210114205708-a1377e83f551/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d h1:8GcGtZ4Ui+lzHm6gOq7s2Oe4ksxkbUYtS/JuoJ2Nce8=
github.com/tailscale/wireguard-go v0.0.0-20210116013233-4cd297ed5a7d/go.mod h1:K/wyv4+3PcdVVTV7szyoiEjJ1nVHonM8cJ2mQwG5Fl8=
github.com/tailscale/wireguard-go v0.0.0-20210210202228-3cc76ed5f222 h1:VzTS7LIwCH8jlxwrZguU0TsCLV/MDOunoNIDJdFajyM=
github.com/tailscale/wireguard-go v0.0.0-20210210202228-3cc76ed5f222/go.mod h1:6t0OVdJwFOKFnvaHaVMKG6GznWaHqkmiR2n3kH0t924=
github.com/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=
@@ -312,15 +315,12 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go4.org/intern v0.0.0-20201223054237-ef8cbcb8edd7 h1:yeDrXaQ3VRXbTN7lHj70DxW4LdPow83MVwPPRjpP70U=
go4.org/intern v0.0.0-20201223054237-ef8cbcb8edd7/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
go4.org/intern v0.0.0-20201223061701-969c7e87e7cb h1:yuqO0E4bHRsTPUocDpRKXfLE40lwWplVxENQ2WOV7Gc=
go4.org/intern v0.0.0-20201223061701-969c7e87e7cb/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
go4.org/intern v0.0.0-20210101010959-7cab76ca296a h1:28p852HIWWaOS019DYK/A3yTmpm1HJaUce63pvll4C8=
go4.org/intern v0.0.0-20210101010959-7cab76ca296a/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg=
go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc=
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174 h1:vSug/WNOi2+4jrKdivxayTN/zd8EA1UrStjpWvvo1jk=
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e h1:ExUmGi0ZsQmiVo9giDQqXkr7vreeXPMkOGIusfsfbzI=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc=
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
@@ -334,8 +334,8 @@ golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 h1:xYJJ3S178yv++9zXV/hnr29plCAGO9vAFG9dorqaFQc=
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -382,10 +382,12 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY=
golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d h1:1aflnvSoWWLI2k/dMUAl5lvU1YO4Mb4hz0gh+1rjcxU=
golang.org/x/net v0.0.0-20210220033124-5f55cee0dc0d/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -438,9 +440,15 @@ golang.org/x/sys v0.0.0-20201107080550-4d91cf3a1aaf/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201118182958-a01c418693c7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742 h1:+CBz4km/0KPU3RGTwARGh/noP3bEwtHcq+0YcBQM2JQ=
golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210105210732-16f7687f5001/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210110051926-789bb1bd4061/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210123111255-9b0068b26619/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210216224549-f992740a1bac/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 h1:SgQ6LNaYJU0JIuEHv9+s6EbhSCwYeAf5Yvj6lpYlqAE=
golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201207232118-ee85cb95a76b h1:a0ErnNnPKmhDyIXQvdZr+Lq8dc8xpMeqkF8y5PgQU4Q=
@@ -538,7 +546,6 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -552,10 +559,12 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.1.0 h1:AWNL1W1i7f0wNZ8VwOKNJ0sliKvOF/adn0EHenfUh+c=
honnef.co/go/tools v0.1.0/go.mod h1:XtegFAyX/PfluP4921rXU5IkjkqBCDnUq4W8VCIoKvM=
inet.af/netaddr v0.0.0-20201228234250-33d0a924ebbf h1:0eHZ8v6j5wIiOVyoYPd70ueZ/RPEQtRlzi60uneDbRU=
inet.af/netaddr v0.0.0-20201228234250-33d0a924ebbf/go.mod h1:9NdhtHLglxJliAZB6aC5ws3mfnUArdAzHG/iJq7cB/o=
inet.af/netaddr v0.0.0-20210105212526-648fbc18a69d h1:6f0242aW/6x2enQBOSKgDS8KQNw6Tp7IVR8eG3x0Jc8=
inet.af/netaddr v0.0.0-20210105212526-648fbc18a69d/go.mod h1:jPZo7Jy4nke2cCgISa4fKJKa5T7+EO8k5fWwWghzneg=
inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44 h1:p7fX77zWzZMuNdJUhniBsmN1OvFOrW9SOtvgnzqUZX4=
inet.af/netaddr v0.0.0-20210222205655-a1ec2b7b8c44/go.mod h1:I2i9ONCXRZDnG1+7O8fSuYzjcPxHQXrIfzD/IkR87x4=
inet.af/peercred v0.0.0-20210216231719-993aa01eacaa h1:6qseJO2iNDHl+MLL2BkO5oURJR4A9pLmRz11Yf7KdGM=
inet.af/peercred v0.0.0-20210216231719-993aa01eacaa/go.mod h1:VZeNdG7cRIUqKl9DWoFX86AHyfYwdb4RextAw1CAEO4=
k8s.io/api v0.16.13/go.mod h1:QWu8UWSTiuQZMMeYjwLs6ILu5O74qKSJ0c+4vrchDxs=
k8s.io/apimachinery v0.16.13/go.mod h1:4HMHS3mDHtVttspuuhrJ1GGr/0S9B6iWYWZ57KnnZqQ=
k8s.io/apimachinery v0.16.14-rc.0/go.mod h1:4HMHS3mDHtVttspuuhrJ1GGr/0S9B6iWYWZ57KnnZqQ=

71
health/health.go Normal file
View File

@@ -0,0 +1,71 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package health is a registry for other packages to report & check
// overall health status of the node.
package health
import (
"sync"
)
var (
mu sync.Mutex
m = map[string]error{} // error key => err (or nil for no error)
watchers = map[*watchHandle]func(string, error){} // opt func to run if error state changes
)
type watchHandle byte
// RegisterWatcher adds a function that will be called if an
// error changes state either to unhealthy or from unhealthy. It is
// not called on transition from unknown to healthy. It must be non-nil
// and is run in its own goroutine. The returned func unregisters it.
func RegisterWatcher(cb func(errKey string, err error)) (unregister func()) {
mu.Lock()
defer mu.Unlock()
handle := new(watchHandle)
watchers[handle] = cb
return func() {
mu.Lock()
defer mu.Unlock()
delete(watchers, handle)
}
}
// SetRouter sets the state of the wgengine/router.Router.
func SetRouterHealth(err error) { set("router", err) }
// RouterHealth returns the wgengine/router.Router error state.
func RouterHealth() error { return get("router") }
func get(key string) error {
mu.Lock()
defer mu.Unlock()
return m[key]
}
func set(key string, err error) {
mu.Lock()
defer mu.Unlock()
old, ok := m[key]
if !ok && err == nil {
// Initial happy path.
m[key] = nil
return
}
if ok && (old == nil) == (err == nil) {
// No change in overall error status (nil-vs-not), so
// don't run callbacks, but exact error might've
// changed, so note it.
if err != nil {
m[key] = err
}
return
}
m[key] = err
for _, cb := range watchers {
go cb(key, err)
}
}

View File

@@ -8,10 +8,10 @@ import (
"bytes"
"testing"
"github.com/tailscale/wireguard-go/wgcfg"
"inet.af/netaddr"
"tailscale.com/wgengine/router"
"tailscale.com/wgengine/router/dns"
"tailscale.com/wgengine/wgcfg"
)
func TestDeepPrint(t *testing.T) {

View File

@@ -9,12 +9,11 @@ import (
"time"
"golang.org/x/oauth2"
"tailscale.com/control/controlclient"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/types/empty"
"tailscale.com/types/netmap"
"tailscale.com/types/structs"
"tailscale.com/wgengine"
)
type State int
@@ -46,10 +45,10 @@ func (s State) String() string {
// EngineStatus contains WireGuard engine stats.
type EngineStatus struct {
RBytes, WBytes wgengine.ByteCount
RBytes, WBytes int64
NumLive int
LiveDERPs int // number of active DERP connections
LivePeers map[tailcfg.NodeKey]wgengine.PeerStatus
LivePeers map[tailcfg.NodeKey]ipnstate.PeerStatusLite
}
// Notify is a communication from a backend (e.g. tailscaled) to a frontend
@@ -59,16 +58,16 @@ type EngineStatus struct {
// They are JSON-encoded on the wire, despite the lack of struct tags.
type Notify struct {
_ structs.Incomparable
Version string // version number of IPN backend
ErrMessage *string // critical error message, if any; for InUseOtherUser, the details
LoginFinished *empty.Message // event: non-nil when login process succeeded
State *State // current IPN state has changed
Prefs *Prefs // preferences were changed
NetMap *controlclient.NetworkMap // new netmap received
Engine *EngineStatus // wireguard engine stats
Status *ipnstate.Status // full status
BrowseToURL *string // UI should open a browser right now
BackendLogID *string // public logtail id used by backend
Version string // version number of IPN backend
ErrMessage *string // critical error message, if any; for InUseOtherUser, the details
LoginFinished *empty.Message // event: non-nil when login process succeeded
State *State // current IPN state has changed
Prefs *Prefs // preferences were changed
NetMap *netmap.NetworkMap // new netmap received
Engine *EngineStatus // wireguard engine stats
Status *ipnstate.Status // full status
BrowseToURL *string // UI should open a browser right now
BackendLogID *string // public logtail id used by backend
PingResult *ipnstate.PingResult
// LocalTCPPort, if non-nil, informs the UI frontend which

View File

@@ -9,8 +9,8 @@ import (
"time"
"golang.org/x/oauth2"
"tailscale.com/control/controlclient"
"tailscale.com/ipn/ipnstate"
"tailscale.com/types/netmap"
)
type FakeBackend struct {
@@ -54,7 +54,7 @@ func (b *FakeBackend) login() {
b.newState(NeedsMachineAuth)
b.newState(Stopped)
// TODO(apenwarr): Fill in a more interesting netmap here.
b.notify(Notify{NetMap: &controlclient.NetworkMap{}})
b.notify(Notify{NetMap: &netmap.NetworkMap{}})
b.newState(Starting)
// TODO(apenwarr): Fill in a more interesting status.
b.notify(Notify{Engine: &EngineStatus{}})
@@ -92,7 +92,7 @@ func (b *FakeBackend) RequestStatus() {
}
func (b *FakeBackend) FakeExpireAfter(x time.Duration) {
b.notify(Notify{NetMap: &controlclient.NetworkMap{}})
b.notify(Notify{NetMap: &netmap.NetworkMap{}})
}
func (b *FakeBackend) Ping(ip string) {

View File

@@ -10,8 +10,8 @@ import (
"golang.org/x/oauth2"
"inet.af/netaddr"
"tailscale.com/control/controlclient"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
)
type Handle struct {
@@ -22,7 +22,7 @@ type Handle struct {
// Mutex protects everything below
mu sync.Mutex
netmapCache *controlclient.NetworkMap
netmapCache *netmap.NetworkMap
engineStatusCache EngineStatus
stateCache State
prefsCache *Prefs
@@ -129,7 +129,7 @@ func (h *Handle) LocalAddrs() []netaddr.IPPrefix {
return []netaddr.IPPrefix{}
}
func (h *Handle) NetMap() *controlclient.NetworkMap {
func (h *Handle) NetMap() *netmap.NetworkMap {
h.mu.Lock()
defer h.mu.Unlock()

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ipn
package ipnlocal
import (
"bytes"
@@ -15,11 +15,11 @@ import (
"sync"
"time"
"github.com/tailscale/wireguard-go/wgcfg"
"golang.org/x/oauth2"
"inet.af/netaddr"
"tailscale.com/control/controlclient"
"tailscale.com/internal/deepprint"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/ipn/policy"
"tailscale.com/net/interfaces"
@@ -29,6 +29,8 @@ import (
"tailscale.com/types/empty"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/types/persist"
"tailscale.com/types/wgkey"
"tailscale.com/util/systemd"
"tailscale.com/version"
@@ -37,6 +39,8 @@ import (
"tailscale.com/wgengine/router"
"tailscale.com/wgengine/router/dns"
"tailscale.com/wgengine/tsdns"
"tailscale.com/wgengine/wgcfg"
"tailscale.com/wgengine/wgcfg/nmcfg"
)
var controlDebugFlags = getControlDebugFlags()
@@ -66,7 +70,7 @@ type LocalBackend struct {
keyLogf logger.Logf // for printing list of peers on change
statsLogf logger.Logf // for printing peers stats on change
e wgengine.Engine
store StateStore
store ipn.StateStore
backendLogID string
portpoll *portlist.Poller // may be nil
portpollOnce sync.Once // guards starting readPoller
@@ -78,20 +82,21 @@ type LocalBackend struct {
// The mutex protects the following elements.
mu sync.Mutex
notify func(Notify)
notify func(ipn.Notify)
c *controlclient.Client
stateKey StateKey // computed in part from user-provided value
userID string // current controlling user ID (for Windows, primarily)
prefs *Prefs
stateKey ipn.StateKey // computed in part from user-provided value
userID string // current controlling user ID (for Windows, primarily)
prefs *ipn.Prefs
inServerMode bool
machinePrivKey wgkey.Private
state State
state ipn.State
// hostinfo is mutated in-place while mu is held.
hostinfo *tailcfg.Hostinfo
// netMap is not mutated in-place once set.
netMap *controlclient.NetworkMap
netMap *netmap.NetworkMap
nodeByAddr map[netaddr.IP]*tailcfg.Node
activeLogin string // last logged LoginName from netMap
engineStatus EngineStatus
engineStatus ipn.EngineStatus
endpoints []string
blocked bool
authURL string
@@ -106,7 +111,7 @@ type LocalBackend struct {
// NewLocalBackend returns a new LocalBackend that is ready to run,
// but is not actually running.
func NewLocalBackend(logf logger.Logf, logid string, store StateStore, e wgengine.Engine) (*LocalBackend, error) {
func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wgengine.Engine) (*LocalBackend, error) {
if e == nil {
panic("ipn.NewLocalBackend: wgengine must not be nil")
}
@@ -129,7 +134,7 @@ func NewLocalBackend(logf logger.Logf, logid string, store StateStore, e wgengin
e: e,
store: store,
backendLogID: logid,
state: NoState,
state: ipn.NoState,
portpoll: portpoll,
gotPortPollRes: make(chan struct{}),
}
@@ -150,7 +155,7 @@ func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
networkUp := ifst.AnyInterfaceUp()
if b.c != nil {
go b.c.SetPaused(b.state == Stopped || !networkUp)
go b.c.SetPaused(b.state == ipn.Stopped || !networkUp)
}
// If the PAC-ness of the network changed, reconfig wireguard+route to
@@ -158,7 +163,7 @@ func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
if hadPAC != ifst.HasPAC() {
b.logf("linkChange: in state %v; PAC changed from %v->%v", b.state, hadPAC, ifst.HasPAC())
switch b.state {
case NoState, Stopped:
case ipn.NoState, ipn.Stopped:
// Do nothing.
default:
go b.authReconfig()
@@ -231,10 +236,26 @@ func (b *LocalBackend) UpdateStatus(sb *ipnstate.StatusBuilder) {
Created: p.Created,
LastSeen: lastSeen,
ShareeNode: p.Hostinfo.ShareeNode,
ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID,
})
}
}
}
// WhoIs reports the node and user who owns the node with the given IP.
// If ok == true, n and u are valid.
func (b *LocalBackend) WhoIs(ip netaddr.IP) (n *tailcfg.Node, u tailcfg.UserProfile, ok bool) {
b.mu.Lock()
defer b.mu.Unlock()
n, ok = b.nodeByAddr[ip]
if !ok {
return nil, u, false
}
u, ok = b.netMap.UserProfiles[n.User]
if !ok {
return nil, u, false
}
return n, u, true
}
// SetDecompressor sets a decompression function, which must be a zstd
@@ -264,7 +285,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
// Auth completed, unblock the engine
b.blockEngineUpdates(false)
b.authReconfig()
b.send(Notify{LoginFinished: &empty.Message{}})
b.send(ipn.Notify{LoginFinished: &empty.Message{}})
}
prefsChanged := false
@@ -289,13 +310,15 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
prefsChanged = true
}
if st.NetMap != nil {
if b.keepOneExitNodeLocked(st.NetMap) {
prefsChanged = true
}
b.setNetMapLocked(st.NetMap)
}
if st.URL != "" {
b.authURL = st.URL
}
if b.state == NeedsLogin {
if b.state == ipn.NeedsLogin {
if !b.prefs.WantRunning {
prefsChanged = true
}
@@ -315,7 +338,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
b.logf("Failed to save new controlclient state: %v", err)
}
}
b.send(Notify{Prefs: prefs})
b.send(ipn.Notify{Prefs: prefs})
}
if st.NetMap != nil {
if netMap != nil {
@@ -334,7 +357,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
}
b.e.SetDERPMap(st.NetMap.DERPMap)
b.send(Notify{NetMap: st.NetMap})
b.send(ipn.Notify{NetMap: st.NetMap})
}
if st.URL != "" {
b.logf("Received auth URL: %.20v...", st.URL)
@@ -348,6 +371,53 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
b.authReconfig()
}
// keepOneExitNodeLocked edits nm to retain only the default
// routes provided by the exit node specified in b.prefs. It returns
// whether prefs was mutated as part of the process, due to an exit
// node IP being converted into a node ID.
func (b *LocalBackend) keepOneExitNodeLocked(nm *netmap.NetworkMap) (prefsChanged bool) {
// If we have a desired IP on file, try to find the corresponding
// node.
if !b.prefs.ExitNodeIP.IsZero() {
// IP takes precedence over ID, so if both are set, clear ID.
if b.prefs.ExitNodeID != "" {
b.prefs.ExitNodeID = ""
prefsChanged = true
}
peerLoop:
for _, peer := range nm.Peers {
for _, addr := range peer.Addresses {
if !addr.IsSingleIP() || addr.IP != b.prefs.ExitNodeIP {
continue
}
// Found the node being referenced, upgrade prefs to
// reference it directly for next time.
b.prefs.ExitNodeID = peer.StableID
b.prefs.ExitNodeIP = netaddr.IP{}
prefsChanged = true
break peerLoop
}
}
}
// At this point, we have a node ID if the requested node is in
// the netmap. If not, the ID will be empty, and we'll strip out
// all default routes.
for _, peer := range nm.Peers {
out := peer.AllowedIPs[:0]
for _, allowedIP := range peer.AllowedIPs {
if allowedIP.Bits == 0 && peer.StableID != b.prefs.ExitNodeID {
continue
}
out = append(out, allowedIP)
}
peer.AllowedIPs = out
}
return prefsChanged
}
// setWgengineStatus is the callback by the wireguard engine whenever it posts a new status.
// This updates the endpoints both in the backend and in the control client.
func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) {
@@ -376,7 +446,7 @@ func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) {
b.statusChanged.Broadcast()
b.statusLock.Unlock()
b.send(Notify{Engine: &es})
b.send(ipn.Notify{Engine: &es})
}
// Start applies the configuration specified in opts, and starts the
@@ -389,7 +459,7 @@ func (b *LocalBackend) setWgengineStatus(s *wgengine.Status, err error) {
// guarantee that switching from one user's state to another is
// actually a supported operation (it should be, but it's very unclear
// from the following whether or not that is a safe transition).
func (b *LocalBackend) Start(opts Options) error {
func (b *LocalBackend) Start(opts ipn.Options) error {
if opts.Prefs == nil && opts.StateKey == "" {
return errors.New("no state key or prefs provided")
}
@@ -422,7 +492,7 @@ func (b *LocalBackend) Start(opts Options) error {
hostinfo.NetInfo = b.hostinfo.NetInfo
}
b.hostinfo = hostinfo
b.state = NoState
b.state = ipn.NoState
if err := b.loadStateLocked(opts.StateKey, opts.Prefs, opts.LegacyConfigPath); err != nil {
b.mu.Unlock()
@@ -440,7 +510,7 @@ func (b *LocalBackend) Start(opts Options) error {
b.notify = opts.Notify
b.setNetMapLocked(nil)
persist := b.prefs.Persist
persistv := b.prefs.Persist
machinePrivKey := b.machinePrivKey
b.mu.Unlock()
@@ -473,14 +543,14 @@ func (b *LocalBackend) Start(opts Options) error {
}
var err error
if persist == nil {
if persistv == nil {
// let controlclient initialize it
persist = &controlclient.Persist{}
persistv = &persist.Persist{}
}
cli, err := controlclient.New(controlclient.Options{
MachinePrivateKey: machinePrivKey,
Logf: logger.WithPrefix(b.logf, "control: "),
Persist: *persist,
Persist: *persistv,
ServerURL: b.serverURL,
AuthKey: opts.AuthKey,
Hostinfo: hostinfo,
@@ -519,8 +589,8 @@ func (b *LocalBackend) Start(opts Options) error {
blid := b.backendLogID
b.logf("Backend: logs: be:%v fe:%v", blid, opts.FrontendLogID)
b.send(Notify{BackendLogID: &blid})
b.send(Notify{Prefs: prefs})
b.send(ipn.Notify{BackendLogID: &blid})
b.send(ipn.Notify{Prefs: prefs})
cli.Login(nil, controlclient.LoginDefault)
return nil
@@ -528,7 +598,7 @@ 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, prefs *Prefs) {
func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.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
@@ -538,18 +608,26 @@ func (b *LocalBackend) updateFilter(netMap *controlclient.NetworkMap, prefs *Pre
haveNetmap = netMap != nil
addrs []netaddr.IPPrefix
packetFilter []filter.Match
advRoutes []netaddr.IPPrefix
localNetsB netaddr.IPSetBuilder
shieldsUp = prefs == nil || prefs.ShieldsUp // Be conservative when not ready
)
if haveNetmap {
addrs = netMap.Addresses
for _, p := range addrs {
localNetsB.AddPrefix(p)
}
packetFilter = netMap.PacketFilter
}
if prefs != nil {
advRoutes = prefs.AdvertiseRoutes
for _, r := range prefs.AdvertiseRoutes {
// TODO: when advertising default routes, trim out local
// nets.
localNetsB.AddPrefix(r)
}
}
localNets := localNetsB.IPSet()
changed := deepprint.UpdateHash(&b.filterHash, haveNetmap, addrs, packetFilter, advRoutes, shieldsUp)
changed := deepprint.UpdateHash(&b.filterHash, haveNetmap, addrs, packetFilter, localNets.Ranges(), shieldsUp)
if !changed {
return
}
@@ -560,14 +638,13 @@ func (b *LocalBackend) updateFilter(netMap *controlclient.NetworkMap, prefs *Pre
return
}
localNets := unmapIPPrefixes(netMap.Addresses, advRoutes)
oldFilter := b.e.GetFilter()
if shieldsUp {
b.logf("netmap packet filter: (shields up)")
b.e.SetFilter(filter.NewShieldsUpFilter(b.logf))
b.e.SetFilter(filter.NewShieldsUpFilter(localNets, oldFilter, b.logf))
} else {
b.logf("netmap packet filter: %v", packetFilter)
b.e.SetFilter(filter.New(packetFilter, localNets, b.e.GetFilter(), b.logf))
b.e.SetFilter(filter.New(packetFilter, localNets, oldFilter, b.logf))
}
}
@@ -586,7 +663,7 @@ func dnsCIDRsEqual(newAddr, oldAddr []netaddr.IPPrefix) bool {
// 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 {
func dnsMapsEqual(new, old *netmap.NetworkMap) bool {
if (old == nil) != (new == nil) {
return false
}
@@ -620,7 +697,7 @@ func dnsMapsEqual(new, old *controlclient.NetworkMap) bool {
// 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) {
func (b *LocalBackend) updateDNSMap(netMap *netmap.NetworkMap) {
if netMap == nil {
b.logf("dns map: (not ready)")
return
@@ -684,7 +761,7 @@ func (b *LocalBackend) readPoller() {
// send delivers n to the connected frontend. If no frontend is
// connected, the notification is dropped without being delivered.
func (b *LocalBackend) send(n Notify) {
func (b *LocalBackend) send(n ipn.Notify) {
b.mu.Lock()
notify := b.notify
b.mu.Unlock()
@@ -710,9 +787,9 @@ func (b *LocalBackend) popBrowserAuthNow() {
b.blockEngineUpdates(true)
b.stopEngineAndWait()
b.send(Notify{BrowseToURL: &url})
if b.State() == Running {
b.enterState(Starting)
b.send(ipn.Notify{BrowseToURL: &url})
if b.State() == ipn.Running {
b.enterState(ipn.Starting)
}
}
@@ -743,21 +820,21 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
legacyMachineKey = b.prefs.Persist.LegacyFrontendPrivateMachineKey
}
keyText, err := b.store.ReadState(MachineKeyStateKey)
keyText, err := b.store.ReadState(ipn.MachineKeyStateKey)
if err == nil {
if err := b.machinePrivKey.UnmarshalText(keyText); err != nil {
return fmt.Errorf("invalid key in %s key of %v: %w", MachineKeyStateKey, b.store, err)
return fmt.Errorf("invalid key in %s key of %v: %w", ipn.MachineKeyStateKey, b.store, err)
}
if b.machinePrivKey.IsZero() {
return fmt.Errorf("invalid zero key stored in %v key of %v", MachineKeyStateKey, b.store)
return fmt.Errorf("invalid zero key stored in %v key of %v", ipn.MachineKeyStateKey, b.store)
}
if !legacyMachineKey.IsZero() && !bytes.Equal(legacyMachineKey[:], b.machinePrivKey[:]) {
b.logf("frontend-provided legacy machine key ignored; used value from server state")
}
return nil
}
if err != ErrStateNotExist {
return fmt.Errorf("error reading %v key of %v: %w", MachineKeyStateKey, b.store, err)
if err != ipn.ErrStateNotExist {
return fmt.Errorf("error reading %v key of %v: %w", ipn.MachineKeyStateKey, b.store, err)
}
// If we didn't find one already on disk and the prefs already
@@ -780,7 +857,7 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
}
keyText, _ = b.machinePrivKey.MarshalText()
if err := b.store.WriteState(MachineKeyStateKey, keyText); err != nil {
if err := b.store.WriteState(ipn.MachineKeyStateKey, keyText); err != nil {
b.logf("error writing machine key to store: %v", err)
return err
}
@@ -793,14 +870,14 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
// user and prefs. If userID is blank or prefs is blank, no work is done.
//
// b.mu may either be held or not.
func (b *LocalBackend) writeServerModeStartState(userID string, prefs *Prefs) {
func (b *LocalBackend) writeServerModeStartState(userID string, prefs *ipn.Prefs) {
if userID == "" || prefs == nil {
return
}
if prefs.ForceDaemon {
stateKey := StateKey("user-" + userID)
if err := b.store.WriteState(ServerModeStartKey, []byte(stateKey)); err != nil {
stateKey := ipn.StateKey("user-" + userID)
if err := b.store.WriteState(ipn.ServerModeStartKey, []byte(stateKey)); err != nil {
b.logf("WriteState error: %v", err)
}
// It's important we do this here too, even if it looks
@@ -812,7 +889,7 @@ func (b *LocalBackend) writeServerModeStartState(userID string, prefs *Prefs) {
b.logf("WriteState error: %v", err)
}
} else {
if err := b.store.WriteState(ServerModeStartKey, nil); err != nil {
if err := b.store.WriteState(ipn.ServerModeStartKey, nil); err != nil {
b.logf("WriteState error: %v", err)
}
}
@@ -821,7 +898,7 @@ func (b *LocalBackend) writeServerModeStartState(userID string, prefs *Prefs) {
// loadStateLocked sets b.prefs and b.stateKey based on a complex
// combination of key, prefs, and legacyPath. b.mu must be held when
// calling.
func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath string) (err error) {
func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs, legacyPath string) (err error) {
if prefs == nil && key == "" {
panic("state key and prefs are both unset")
}
@@ -863,19 +940,19 @@ func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath st
b.logf("using backend prefs")
bs, err := b.store.ReadState(key)
if err != nil {
if errors.Is(err, ErrStateNotExist) {
if errors.Is(err, ipn.ErrStateNotExist) {
if legacyPath != "" {
b.prefs, err = LoadPrefs(legacyPath)
b.prefs, err = ipn.LoadPrefs(legacyPath)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
b.logf("failed to load legacy prefs: %v", err)
}
b.prefs = NewPrefs()
b.prefs = ipn.NewPrefs()
} else {
b.logf("imported prefs from relaynode for %q: %v", key, b.prefs.Pretty())
}
} else {
b.prefs = NewPrefs()
b.prefs = ipn.NewPrefs()
b.logf("created empty state for %q: %s", key, b.prefs.Pretty())
}
if err := b.initMachineKeyLocked(); err != nil {
@@ -885,7 +962,7 @@ func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath st
}
return fmt.Errorf("store.ReadState(%q): %v", key, err)
}
b.prefs, err = PrefsFromBytes(bs, false)
b.prefs, err = ipn.PrefsFromBytes(bs, false)
if err != nil {
return fmt.Errorf("PrefsFromBytes: %v", err)
}
@@ -897,7 +974,7 @@ func (b *LocalBackend) loadStateLocked(key StateKey, prefs *Prefs, legacyPath st
}
// State returns the backend state machine's current state.
func (b *LocalBackend) State() State {
func (b *LocalBackend) State() ipn.State {
b.mu.Lock()
defer b.mu.Unlock()
@@ -913,7 +990,7 @@ func (b *LocalBackend) InServerMode() bool {
// getEngineStatus returns a copy of b.engineStatus.
//
// TODO(bradfitz): remove this and use Status() throughout.
func (b *LocalBackend) getEngineStatus() EngineStatus {
func (b *LocalBackend) getEngineStatus() ipn.EngineStatus {
b.mu.Lock()
defer b.mu.Unlock()
@@ -969,7 +1046,7 @@ func (b *LocalBackend) FakeExpireAfter(x time.Duration) {
mapCopy.Expiry = time.Now().Add(x)
}
b.setNetMapLocked(&mapCopy)
b.send(Notify{NetMap: b.netMap})
b.send(ipn.Notify{NetMap: b.netMap})
}
func (b *LocalBackend) Ping(ipStr string) {
@@ -979,7 +1056,7 @@ func (b *LocalBackend) Ping(ipStr string) {
return
}
b.e.Ping(ip, func(pr *ipnstate.PingResult) {
b.send(Notify{PingResult: pr})
b.send(ipn.Notify{PingResult: pr})
})
}
@@ -988,11 +1065,11 @@ func (b *LocalBackend) Ping(ipStr string) {
// b.mu must be held; mostly because the caller is about to anyway, and doing so
// gives us slightly better guarantees about the two peers stats lines not
// being intermixed if there are concurrent calls to our caller.
func (b *LocalBackend) parseWgStatusLocked(s *wgengine.Status) (ret EngineStatus) {
func (b *LocalBackend) parseWgStatusLocked(s *wgengine.Status) (ret ipn.EngineStatus) {
var peerStats, peerKeys strings.Builder
ret.LiveDERPs = s.DERPs
ret.LivePeers = map[tailcfg.NodeKey]wgengine.PeerStatus{}
ret.LivePeers = map[tailcfg.NodeKey]ipnstate.PeerStatusLite{}
for _, p := range s.Peers {
if !p.LastHandshake.IsZero() {
fmt.Fprintf(&peerStats, "%d/%d ", p.RxBytes, p.TxBytes)
@@ -1048,7 +1125,7 @@ func (b *LocalBackend) SetWantRunning(wantRunning bool) {
// SetPrefs saves new user preferences and propagates them throughout
// the system. Implements Backend.
func (b *LocalBackend) SetPrefs(newp *Prefs) {
func (b *LocalBackend) SetPrefs(newp *ipn.Prefs) {
if newp == nil {
panic("SetPrefs got nil prefs")
}
@@ -1115,7 +1192,7 @@ func (b *LocalBackend) SetPrefs(newp *Prefs) {
b.authReconfig()
}
b.send(Notify{Prefs: newp})
b.send(ipn.Notify{Prefs: newp})
}
// doSetHostinfoFilterServices calls SetHostinfo on the controlclient,
@@ -1141,7 +1218,7 @@ func (b *LocalBackend) doSetHostinfoFilterServices(hi *tailcfg.Hostinfo) {
// NetMap returns the latest cached network map received from
// controlclient, or nil if no network map was received yet.
func (b *LocalBackend) NetMap() *controlclient.NetworkMap {
func (b *LocalBackend) NetMap() *netmap.NetworkMap {
b.mu.Lock()
defer b.mu.Unlock()
return b.netMap
@@ -1183,23 +1260,21 @@ func (b *LocalBackend) authReconfig() {
return
}
var flags controlclient.WGConfigFlags
var flags netmap.WGConfigFlags
if uc.RouteAll {
flags |= controlclient.AllowDefaultRoute
// TODO(apenwarr): Make subnet routes a different pref?
flags |= controlclient.AllowSubnetRoutes
flags |= netmap.AllowSubnetRoutes
}
if uc.AllowSingleHosts {
flags |= controlclient.AllowSingleHosts
flags |= netmap.AllowSingleHosts
}
if hasPAC && disableSubnetsIfPAC {
if flags&controlclient.AllowSubnetRoutes != 0 {
if flags&netmap.AllowSubnetRoutes != 0 {
b.logf("authReconfig: have PAC; disabling subnet routes")
flags &^= controlclient.AllowSubnetRoutes
flags &^= netmap.AllowSubnetRoutes
}
}
cfg, err := nm.WGCfg(b.logf, flags)
cfg, err := nmcfg.WGCfg(nm, b.logf, flags)
if err != nil {
b.logf("wgcfg: %v", err)
return
@@ -1231,15 +1306,20 @@ func (b *LocalBackend) authReconfig() {
// magicDNSRootDomains returns the subset of nm.DNS.Domains that are the search domains for MagicDNS.
// Each entry has a trailing period.
func magicDNSRootDomains(nm *controlclient.NetworkMap) []string {
func magicDNSRootDomains(nm *netmap.NetworkMap) []string {
if v := nm.MagicDNSSuffix(); v != "" {
return []string{strings.Trim(v, ".") + "."}
}
return nil
}
var (
ipv4Default = netaddr.MustParseIPPrefix("0.0.0.0/0")
ipv6Default = netaddr.MustParseIPPrefix("::/0")
)
// routerConfig produces a router.Config from a wireguard config and IPN prefs.
func routerConfig(cfg *wgcfg.Config, prefs *Prefs) *router.Config {
func routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router.Config {
rs := &router.Config{
LocalAddrs: unmapIPPrefixes(cfg.Addresses),
SubnetRoutes: unmapIPPrefixes(prefs.AdvertiseRoutes),
@@ -1251,6 +1331,32 @@ func routerConfig(cfg *wgcfg.Config, prefs *Prefs) *router.Config {
rs.Routes = append(rs.Routes, unmapIPPrefixes(peer.AllowedIPs)...)
}
// Sanity check: we expect the control server to program both a v4
// and a v6 default route, if default routing is on. Fill in
// blackhole routes appropriately if we're missing some. This is
// likely to break some functionality, but if the user expressed a
// preference for routing remotely, we want to avoid leaking
// traffic at the expense of functionality.
if prefs.ExitNodeID != "" || !prefs.ExitNodeIP.IsZero() {
var default4, default6 bool
for _, route := range rs.Routes {
if route == ipv4Default {
default4 = true
} else if route == ipv6Default {
default6 = true
}
if default4 && default6 {
break
}
}
if !default4 {
rs.Routes = append(rs.Routes, ipv4Default)
}
if !default6 {
rs.Routes = append(rs.Routes, ipv6Default)
}
}
rs.Routes = append(rs.Routes, netaddr.IPPrefix{
IP: tsaddr.TailscaleServiceIP(),
Bits: 32,
@@ -1268,7 +1374,7 @@ func unmapIPPrefixes(ippsList ...[]netaddr.IPPrefix) (ret []netaddr.IPPrefix) {
return ret
}
func applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *Prefs) {
func applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *ipn.Prefs) {
if h := prefs.Hostname; h != "" {
hi.Hostname = h
}
@@ -1288,7 +1394,7 @@ func applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *Prefs) {
// places twiddle IPN internal state without going through here, so
// really this is more "one of several places in which random things
// happen".
func (b *LocalBackend) enterState(newState State) {
func (b *LocalBackend) enterState(newState ipn.State) {
b.mu.Lock()
state := b.state
b.state = newState
@@ -1306,19 +1412,19 @@ func (b *LocalBackend) enterState(newState State) {
b.logf("Switching ipn state %v -> %v (WantRunning=%v)",
state, newState, prefs.WantRunning)
if notify != nil {
b.send(Notify{State: &newState})
b.send(ipn.Notify{State: &newState})
}
if bc != nil {
bc.SetPaused(newState == Stopped || !networkUp)
bc.SetPaused(newState == ipn.Stopped || !networkUp)
}
switch newState {
case NeedsLogin:
case ipn.NeedsLogin:
systemd.Status("Needs login: %s", authURL)
b.blockEngineUpdates(true)
fallthrough
case Stopped:
case ipn.Stopped:
err := b.e.Reconfig(&wgcfg.Config{}, &router.Config{})
if err != nil {
b.logf("Reconfig(down): %v", err)
@@ -1327,11 +1433,11 @@ func (b *LocalBackend) enterState(newState State) {
if authURL == "" {
systemd.Status("Stopped; run 'tailscale up' to log in")
}
case Starting, NeedsMachineAuth:
case ipn.Starting, ipn.NeedsMachineAuth:
b.authReconfig()
// Needed so that UpdateEndpoints can run
b.e.RequestStatus()
case Running:
case ipn.Running:
var addrs []string
for _, addr := range b.netMap.Addresses {
addrs = append(addrs, addr.IP.String())
@@ -1345,7 +1451,7 @@ func (b *LocalBackend) enterState(newState State) {
// nextState returns the state the backend seems to be in, based on
// its internal state.
func (b *LocalBackend) nextState() State {
func (b *LocalBackend) nextState() ipn.State {
b.mu.Lock()
b.assertClientLocked()
var (
@@ -1361,31 +1467,31 @@ func (b *LocalBackend) nextState() State {
if c.AuthCantContinue() {
// Auth was interrupted or waiting for URL visit,
// so it won't proceed without human help.
return NeedsLogin
return ipn.NeedsLogin
} else {
// Auth or map request needs to finish
return state
}
case !wantRunning:
return Stopped
return ipn.Stopped
case !netMap.Expiry.IsZero() && time.Until(netMap.Expiry) <= 0:
return NeedsLogin
return ipn.NeedsLogin
case netMap.MachineStatus != tailcfg.MachineAuthorized:
// TODO(crawshaw): handle tailcfg.MachineInvalid
return NeedsMachineAuth
case state == NeedsMachineAuth:
return ipn.NeedsMachineAuth
case state == ipn.NeedsMachineAuth:
// (if we get here, we know MachineAuthorized == true)
return Starting
case state == Starting:
return ipn.Starting
case state == ipn.Starting:
if st := b.getEngineStatus(); st.NumLive > 0 || st.LiveDERPs > 0 {
return Running
return ipn.Running
} else {
return state
}
case state == Running:
return Running
case state == ipn.Running:
return ipn.Running
default:
return Starting
return ipn.Starting
}
}
@@ -1397,7 +1503,7 @@ func (b *LocalBackend) RequestEngineStatus() {
// RequestStatus implements Backend.
func (b *LocalBackend) RequestStatus() {
st := b.Status()
b.send(Notify{Status: st})
b.send(ipn.Notify{Status: st})
}
// stateMachine updates the state machine state based on other things
@@ -1493,7 +1599,7 @@ func (b *LocalBackend) setNetInfo(ni *tailcfg.NetInfo) {
c.SetNetInfo(ni)
}
func (b *LocalBackend) setNetMapLocked(nm *controlclient.NetworkMap) {
func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
var login string
if nm != nil {
login = nm.UserProfiles[nm.User].LoginName
@@ -1506,6 +1612,39 @@ func (b *LocalBackend) setNetMapLocked(nm *controlclient.NetworkMap) {
b.logf("active login: %v", login)
b.activeLogin = login
}
if nm == nil {
b.nodeByAddr = nil
return
}
// Update the nodeByAddr index.
if b.nodeByAddr == nil {
b.nodeByAddr = map[netaddr.IP]*tailcfg.Node{}
}
// First pass, mark everything unwanted.
for k := range b.nodeByAddr {
b.nodeByAddr[k] = nil
}
addNode := func(n *tailcfg.Node) {
for _, ipp := range n.Addresses {
if ipp.IsSingleIP() {
b.nodeByAddr[ipp.IP] = n
}
}
}
if nm.SelfNode != nil {
addNode(nm.SelfNode)
}
for _, p := range nm.Peers {
addNode(p)
}
// Third pass, actually delete the unwanted items.
for k, v := range b.nodeByAddr {
if v == nil {
delete(b.nodeByAddr, k)
}
}
}
// TestOnlyPublicKeys returns the current machine and node public

View File

@@ -2,13 +2,14 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ipn
package ipnlocal
import (
"inet.af/netaddr"
"tailscale.com/control/controlclient"
"tailscale.com/tailcfg"
"testing"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/netmap"
)
func TestNetworkMapCompare(t *testing.T) {
@@ -26,7 +27,7 @@ func TestNetworkMapCompare(t *testing.T) {
tests := []struct {
name string
a, b *controlclient.NetworkMap
a, b *netmap.NetworkMap
want bool
}{
{
@@ -37,76 +38,76 @@ func TestNetworkMapCompare(t *testing.T) {
},
{
"b nil",
&controlclient.NetworkMap{},
&netmap.NetworkMap{},
nil,
false,
},
{
"a nil",
nil,
&controlclient.NetworkMap{},
&netmap.NetworkMap{},
false,
},
{
"both default",
&controlclient.NetworkMap{},
&controlclient.NetworkMap{},
&netmap.NetworkMap{},
&netmap.NetworkMap{},
true,
},
{
"names identical",
&controlclient.NetworkMap{Name: "map1"},
&controlclient.NetworkMap{Name: "map1"},
&netmap.NetworkMap{Name: "map1"},
&netmap.NetworkMap{Name: "map1"},
true,
},
{
"names differ",
&controlclient.NetworkMap{Name: "map1"},
&controlclient.NetworkMap{Name: "map2"},
&netmap.NetworkMap{Name: "map1"},
&netmap.NetworkMap{Name: "map2"},
false,
},
{
"Peers identical",
&controlclient.NetworkMap{Peers: []*tailcfg.Node{}},
&controlclient.NetworkMap{Peers: []*tailcfg.Node{}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{}},
true,
},
{
"Peer list length",
// length of Peers list differs
&controlclient.NetworkMap{Peers: []*tailcfg.Node{{}}},
&controlclient.NetworkMap{Peers: []*tailcfg.Node{}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{{}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{}},
false,
},
{
"Node names identical",
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
true,
},
{
"Node names differ",
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "B"}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "B"}}},
false,
},
{
"Node lists identical",
&controlclient.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
&controlclient.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
true,
},
{
"Node lists differ",
&controlclient.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
&controlclient.NetworkMap{Peers: []*tailcfg.Node{node1, node2}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node1}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{node1, node2}},
false,
},
{
"Node Users differ",
// User field is not checked.
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 0}}},
&controlclient.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 1}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 0}}},
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 1}}},
true,
},
}

View File

@@ -2,18 +2,20 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package ipn
package ipnlocal
import (
"reflect"
"testing"
"time"
"tailscale.com/control/controlclient"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/logtail"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/types/key"
"tailscale.com/types/persist"
"tailscale.com/wgengine"
)
@@ -38,9 +40,7 @@ func TestLocalLogLines(t *testing.T) {
idA := logid(0xaa)
// set up a LocalBackend, super bare bones. No functional data.
store := &MemoryStore{
cache: make(map[StateKey][]byte),
}
store := &ipn.MemoryStore{}
e, err := wgengine.NewFakeUserspaceEngine(logListen.Logf, 0, nil)
if err != nil {
t.Fatal(err)
@@ -53,7 +53,7 @@ func TestLocalLogLines(t *testing.T) {
defer lb.Shutdown()
// custom adjustments for required non-nil fields
lb.prefs = NewPrefs()
lb.prefs = ipn.NewPrefs()
lb.hostinfo = &tailcfg.Hostinfo{}
// hacky manual override of the usual log-on-change behaviour of keylogf
lb.keyLogf = logListen.Logf
@@ -67,8 +67,8 @@ func TestLocalLogLines(t *testing.T) {
}
// log prefs line
persist := &controlclient.Persist{}
prefs := NewPrefs()
persist := &persist.Persist{}
prefs := ipn.NewPrefs()
prefs.Persist = persist
lb.SetPrefs(prefs)
@@ -76,7 +76,7 @@ func TestLocalLogLines(t *testing.T) {
// log peers, peer keys
status := &wgengine.Status{
Peers: []wgengine.PeerStatus{wgengine.PeerStatus{
Peers: []ipnstate.PeerStatusLite{{
TxBytes: 10,
RxBytes: 10,
LastHandshake: time.Now(),

View File

@@ -1,49 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build linux
package ipnserver
import (
"net"
"golang.org/x/sys/unix"
"tailscale.com/types/logger"
)
func isReadonlyConn(c net.Conn, logf logger.Logf) (ro bool) {
ro = true // conservative default for naked returns below
uc, ok := c.(*net.UnixConn)
if !ok {
logf("unexpected connection type %T", c)
return
}
raw, err := uc.SyscallConn()
if err != nil {
logf("SyscallConn: %v", err)
return
}
var cred *unix.Ucred
cerr := raw.Control(func(fd uintptr) {
cred, err = unix.GetsockoptUcred(int(fd),
unix.SOL_SOCKET,
unix.SO_PEERCRED)
})
if cerr != nil {
logf("raw.Control: %v", err)
return
}
if err != nil {
logf("raw.Control: %v", err)
return
}
if cred.Uid == 0 {
// root is not read-only.
return false
}
logf("non-root connection from %v (read-only)", cred.Uid)
return true
}

View File

@@ -1,27 +0,0 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !linux
package ipnserver
import (
"net"
"tailscale.com/types/logger"
)
func isReadonlyConn(c net.Conn, logf logger.Logf) bool {
// Windows doesn't need/use this mechanism, at least yet. It
// has a different last-user-wins auth model.
// And on Darwin, we're not using it yet, as the Darwin
// tailscaled port isn't yet done, and unix.Ucred and
// unix.GetsockoptUcred aren't in x/sys/unix.
// TODO(bradfitz): OpenBSD and FreeBSD should implement this too.
// But their x/sys/unix package is different than Linux, so
// I didn't include it for now.
return false
}

View File

@@ -21,12 +21,17 @@ import (
"runtime"
"strings"
"sync"
"sync/atomic"
"syscall"
"time"
"go4.org/mem"
"inet.af/netaddr"
"inet.af/peercred"
"tailscale.com/control/controlclient"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/localapi"
"tailscale.com/log/filelogger"
"tailscale.com/logtail/backoff"
"tailscale.com/net/netstat"
@@ -91,7 +96,7 @@ type Options struct {
// server is an IPN backend and its set of 0 or more active connections
// talking to an IPN backend.
type server struct {
b *ipn.LocalBackend
b *ipnlocal.LocalBackend
logf logger.Logf
// resetOnZero is whether to call bs.Reset on transition from
// 1->0 connections. That is, this is whether the backend is
@@ -113,10 +118,11 @@ type server struct {
// connIdentity represents the owner of a localhost TCP connection.
type connIdentity struct {
Unknown bool
Pid int
UserID string
User *user.User
Unknown bool
Pid int
UserID string
User *user.User
IsUnixSock bool
}
// getConnIdentity returns the localhost TCP connection's identity information
@@ -125,7 +131,9 @@ type connIdentity struct {
// to be able to map it and couldn't.
func (s *server) getConnIdentity(c net.Conn) (ci connIdentity, err error) {
if runtime.GOOS != "windows" { // for now; TODO: expand to other OSes
return connIdentity{Unknown: true}, nil
ci = connIdentity{Unknown: true}
_, ci.IsUnixSock = c.(*net.UnixConn)
return ci, nil
}
la, err := netaddr.ParseIPPort(c.LocalAddr().String())
if err != nil {
@@ -216,13 +224,22 @@ func (s *server) blockWhileInUse(conn io.Reader, ci connIdentity) {
}
}
// bufferHasHTTPRequest reports whether br looks like it has an HTTP
// request in it, without reading any bytes from it.
func bufferHasHTTPRequest(br *bufio.Reader) bool {
peek, _ := br.Peek(br.Buffered())
return mem.HasPrefix(mem.B(peek), mem.S("GET ")) ||
mem.HasPrefix(mem.B(peek), mem.S("POST ")) ||
mem.Contains(mem.B(peek), mem.S(" HTTP/"))
}
func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
// First see if it's an HTTP request.
br := bufio.NewReader(c)
c.SetReadDeadline(time.Now().Add(time.Second))
peek, _ := br.Peek(4)
br.Peek(4)
c.SetReadDeadline(time.Time{})
isHTTPReq := string(peek) == "GET "
isHTTPReq := bufferHasHTTPRequest(br)
ci, err := s.addConn(c, isHTTPReq)
if err != nil {
@@ -249,7 +266,7 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
s.b.SetCurrentUserID(ci.UserID)
if isHTTPReq {
httpServer := http.Server{
httpServer := &http.Server{
// Localhost connections are cheap; so only do
// keep-alives for a short period of time, as these
// active connections lock the server into only serving
@@ -294,6 +311,75 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
}
}
func isReadonlyConn(c net.Conn, logf logger.Logf) bool {
if runtime.GOOS == "windows" {
// Windows doesn't need/use this mechanism, at least yet. It
// has a different last-user-wins auth model.
return false
}
const ro = true
const rw = false
creds, err := peercred.Get(c)
if err != nil {
logf("connection from unknown peer; read-only")
return ro
}
uid, ok := creds.UserID()
if !ok {
logf("connection from peer with unknown userid; read-only")
return ro
}
if uid == "0" {
logf("connection from userid %v; root has access", uid)
return rw
}
var adminGroupID string
switch runtime.GOOS {
case "darwin":
adminGroupID = darwinAdminGroupID()
default:
logf("connection from userid %v; read-only", uid)
return ro
}
if adminGroupID == "" {
logf("connection from userid %v; no system admin group found, read-only", uid)
return ro
}
u, err := user.LookupId(uid)
if err != nil {
logf("connection from userid %v; failed to look up user; read-only", uid)
return ro
}
gids, err := u.GroupIds()
if err != nil {
logf("connection from userid %v; failed to look up groups; read-only", uid)
return ro
}
for _, gid := range gids {
if gid == adminGroupID {
logf("connection from userid %v; is local admin, has access", uid)
return rw
}
}
logf("connection from userid %v; read-only", uid)
return ro
}
var darwinAdminGroupIDCache atomic.Value // of string
func darwinAdminGroupID() string {
s, _ := darwinAdminGroupIDCache.Load().(string)
if s != "" {
return s
}
g, err := user.LookupGroup("admin")
if err != nil {
return ""
}
darwinAdminGroupIDCache.Store(g.Gid)
return g.Gid
}
// inUseOtherUserError is the error type for when the server is in use
// by a different local user.
type inUseOtherUserError struct{ error }
@@ -503,41 +589,6 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
}()
logf("Listening on %v", listen.Addr())
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
eng, err := getEngine()
if err != nil {
logf("ipnserver: initial getEngine call: %v", err)
for i := 1; ctx.Err() == nil; i++ {
c, err := listen.Accept()
if err != nil {
logf("%d: Accept: %v", i, err)
bo.BackOff(ctx, err)
continue
}
logf("ipnserver: try%d: trying getEngine again...", i)
eng, err = getEngine()
if err == nil {
logf("%d: GetEngine worked; exiting failure loop", i)
unservedConn = c
break
}
logf("ipnserver%d: getEngine failed again: %v", i, err)
errMsg := err.Error()
go func() {
defer c.Close()
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
bs := ipn.NewBackendServer(logf, nil, serverToClient)
bs.SendErrorMessage(errMsg)
time.Sleep(time.Second)
}()
}
if err := ctx.Err(); err != nil {
return err
}
}
var store ipn.StateStore
if opts.StatePath != "" {
store, err = ipn.NewFileStore(opts.StatePath)
@@ -566,7 +617,83 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
store = &ipn.MemoryStore{}
}
b, err := ipn.NewLocalBackend(logf, logid, store, eng)
bo := backoff.NewBackoff("ipnserver", logf, 30*time.Second)
var unservedConn net.Conn // if non-nil, accepted, but hasn't served yet
eng, err := getEngine()
if err != nil {
logf("ipnserver: initial getEngine call: %v", err)
// Issue 1187: on Windows, in unattended mode,
// sometimes we try 5 times and fail to create the
// engine before the system's ready. Hack until the
// bug if fixed properly: if we're running in
// unattended mode on Windows, keep trying forever,
// waiting for the machine to be ready (networking to
// come up?) and then dial our own safesocket TCP
// listener to wake up the usual mechanism that lets
// us surface getEngine errors to UI clients. (We
// don't want to just call getEngine in a loop without
// the listener.Accept, as we do want to handle client
// connections so we can tell them about errors)
bootRaceWaitForEngine, bootRaceWaitForEngineCancel := context.WithTimeout(context.Background(), time.Minute)
if runtime.GOOS == "windows" && opts.AutostartStateKey != "" {
logf("ipnserver: in unattended mode, waiting for engine availability")
getEngine = getEngineUntilItWorksWrapper(getEngine)
// Wait for it to be ready.
go func() {
defer bootRaceWaitForEngineCancel()
t0 := time.Now()
for {
time.Sleep(10 * time.Second)
if _, err := getEngine(); err != nil {
logf("ipnserver: unattended mode engine load: %v", err)
continue
}
c, err := net.Dial("tcp", listen.Addr().String())
logf("ipnserver: engine created after %v; waking up Accept: Dial error: %v", time.Since(t0).Round(time.Second), err)
if err == nil {
c.Close()
}
break
}
}()
} else {
bootRaceWaitForEngineCancel()
}
for i := 1; ctx.Err() == nil; i++ {
c, err := listen.Accept()
if err != nil {
logf("%d: Accept: %v", i, err)
bo.BackOff(ctx, err)
continue
}
<-bootRaceWaitForEngine.Done()
logf("ipnserver: try%d: trying getEngine again...", i)
eng, err = getEngine()
if err == nil {
logf("%d: GetEngine worked; exiting failure loop", i)
unservedConn = c
break
}
logf("ipnserver%d: getEngine failed again: %v", i, err)
errMsg := err.Error()
go func() {
defer c.Close()
serverToClient := func(b []byte) { ipn.WriteMsg(c, b) }
bs := ipn.NewBackendServer(logf, nil, serverToClient)
bs.SendErrorMessage(errMsg)
time.Sleep(time.Second)
}()
}
if err := ctx.Err(); err != nil {
return err
}
}
b, err := ipnlocal.NewLocalBackend(logf, logid, store, eng)
if err != nil {
return fmt.Errorf("NewLocalBackend: %v", err)
}
@@ -579,6 +706,9 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
opts.DebugMux.HandleFunc("/debug/ipn", func(w http.ResponseWriter, r *http.Request) {
serveHTMLStatus(w, b)
})
h := localapi.NewHandler(b)
h.PermitRead = true
opts.DebugMux.Handle("/localapi/", h)
}
server.b = b
@@ -756,6 +886,27 @@ func FixedEngine(eng wgengine.Engine) func() (wgengine.Engine, error) {
return func() (wgengine.Engine, error) { return eng, nil }
}
// getEngineUntilItWorksWrapper returns a getEngine wrapper that does
// not call getEngine concurrently and stops calling getEngine once
// it's returned a working engine.
func getEngineUntilItWorksWrapper(getEngine func() (wgengine.Engine, error)) func() (wgengine.Engine, error) {
var mu sync.Mutex
var engGood wgengine.Engine
return func() (wgengine.Engine, error) {
mu.Lock()
defer mu.Unlock()
if engGood != nil {
return engGood, nil
}
e, err := getEngine()
if err != nil {
return nil, err
}
engGood = e
return e, nil
}
}
type dummyAddr string
type oneConnListener struct {
conn net.Conn
@@ -798,6 +949,13 @@ func (psc *protoSwitchConn) Close() error {
func (s *server) localhostHandler(ci connIdentity) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if ci.IsUnixSock && strings.HasPrefix(r.URL.Path, "/localapi/") {
h := localapi.NewHandler(s.b)
h.PermitRead = true
h.PermitWrite = false // TODO: flesh out connIdentity on more platforms then set this
h.ServeHTTP(w, r)
return
}
if ci.Unknown {
io.WriteString(w, "<html><title>Tailscale</title><body><h1>Tailscale</h1>This is the local Tailscale daemon.")
return
@@ -806,7 +964,7 @@ func (s *server) localhostHandler(ci connIdentity) http.Handler {
})
}
func serveHTMLStatus(w http.ResponseWriter, b *ipn.LocalBackend) {
func serveHTMLStatus(w http.ResponseWriter, b *ipnlocal.LocalBackend) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
st := b.Status()
// TODO(bradfitz): add LogID and opts to st?

View File

@@ -21,14 +21,21 @@ import (
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/util/dnsname"
)
// Status represents the entire state of the IPN network.
type Status struct {
BackendState string
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
Self *PeerStatus
MagicDNSSuffix string // e.g. "userfoo.tailscale.net" (no surrounding dots)
BackendState string
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
Self *PeerStatus
// MagicDNSSuffix is the network's MagicDNS suffix for nodes
// in the network such as "userfoo.tailscale.net".
// There are no surrounding dots.
// MagicDNSSuffix should be populated regardless of whether a domain
// has MagicDNS enabled.
MagicDNSSuffix string
Peer map[key.Public]*PeerStatus
User map[tailcfg.UserID]tailcfg.UserProfile
@@ -43,6 +50,12 @@ func (s *Status) Peers() []key.Public {
return kk
}
type PeerStatusLite struct {
TxBytes, RxBytes int64
LastHandshake time.Time
NodeKey tailcfg.NodeKey
}
type PeerStatus struct {
PublicKey key.Public
HostName string // HostInfo's Hostname (not a DNS name or necessarily unique)
@@ -64,6 +77,7 @@ type PeerStatus struct {
LastSeen time.Time // last seen to tailcontrol
LastHandshake time.Time // with local wireguard
KeepAlive bool
ExitNode bool // true if this is the currently selected exit node.
// ShareeNode indicates this node exists in the netmap because
// it's owned by a shared-to user and that node might connect
@@ -84,14 +98,6 @@ type PeerStatus struct {
InEngine bool
}
// SimpleHostName returns a potentially simplified version of ps.HostName for display purposes.
func (ps *PeerStatus) SimpleHostName() string {
n := ps.HostName
n = strings.TrimSuffix(n, ".local")
n = strings.TrimSuffix(n, ".localdomain")
return n
}
type StatusBuilder struct {
mu sync.Mutex
locked bool
@@ -231,6 +237,9 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
if st.KeepAlive {
e.KeepAlive = true
}
if st.ExitNode {
e.ExitNode = true
}
if st.ShareeNode {
e.ShareeNode = true
}
@@ -274,13 +283,22 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
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("<tr><th>Peer</th><th>OS</th><th>Node</th><th>Owner</th><th>Rx</th><th>Tx</th><th>Activity</th><th>Connection</th></tr>\n")
f("</thead>\n<tbody>\n")
now := time.Now()
var peers []*PeerStatus
for _, peer := range st.Peers() {
ps := st.Peer[peer]
if ps.ShareeNode {
continue
}
peers = append(peers, ps)
}
SortPeers(peers)
for _, ps := range peers {
var actAgo string
if !ps.LastWrite.IsZero() {
ago := now.Sub(ps.LastWrite)
@@ -296,40 +314,41 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
owner = owner[:i]
}
}
f("<tr><td>%s</td><td>%s %s<br><span class=\"tailaddr\">%s</span></td><td class=\"acenter owner\">%s</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td>",
peer.ShortString(),
html.EscapeString(ps.SimpleHostName()),
hostName := dnsname.SanitizeHostname(ps.HostName)
dnsName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
if strings.EqualFold(dnsName, hostName) || ps.UserID != st.Self.UserID {
hostName = ""
}
var hostNameHTML string
if hostName != "" {
hostNameHTML = "<br>" + html.EscapeString(hostName)
}
f("<tr><td>%s</td><td class=acenter>%s</td>"+
"<td><b>%s</b>%s<div class=\"tailaddr\">%s</div></td><td class=\"acenter owner\">%s</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td><td class=\"aright\">%v</td>",
ps.PublicKey.ShortString(),
osEmoji(ps.OS),
html.EscapeString(dnsName),
hostNameHTML,
ps.TailAddr,
html.EscapeString(owner),
ps.RxBytes,
ps.TxBytes,
actAgo,
)
f("<td class=\"aright\">")
f("<td>")
// TODO: let server report this active bool instead
active := !ps.LastWrite.IsZero() && time.Since(ps.LastWrite) < 2*time.Minute
relay := ps.Relay
if relay != "" {
if active && ps.CurAddr == "" {
f("🔗 <b>derp-%v</b><br>", html.EscapeString(relay))
} else {
f("derp-%v<br>", html.EscapeString(relay))
if active {
if ps.Relay != "" && ps.CurAddr == "" {
f("relay <b>%s</b>", html.EscapeString(ps.Relay))
} else if ps.CurAddr != "" {
f("direct <b>%s</b>", html.EscapeString(ps.CurAddr))
}
}
match := false
for _, addr := range ps.Addrs {
if addr == ps.CurAddr {
match = true
f("🔗 <b>%s</b><br>", addr)
} else {
f("%s<br>", addr)
}
}
if ps.CurAddr != "" && !match {
f("<b>%s</b> \xf0\x9f\xa7\xb3<br>", ps.CurAddr)
}
f("</td>") // end Addrs
f("</tr>\n")
@@ -375,3 +394,17 @@ type PingResult struct {
// TODO(bradfitz): details like whether port mapping was used on either side? (Once supported)
}
func SortPeers(peers []*PeerStatus) {
sort.Slice(peers, func(i, j int) bool { return sortKey(peers[i]) < sortKey(peers[j]) })
}
func sortKey(ps *PeerStatus) string {
if ps.DNSName != "" {
return ps.DNSName
}
if ps.HostName != "" {
return ps.HostName
}
return ps.TailAddr
}

95
ipn/localapi/localapi.go Normal file
View File

@@ -0,0 +1,95 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package localapi contains the HTTP server handlers for tailscaled's API server.
package localapi
import (
"encoding/json"
"io"
"net/http"
"inet.af/netaddr"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/tailcfg"
)
func NewHandler(b *ipnlocal.LocalBackend) *Handler {
return &Handler{b: b}
}
type Handler struct {
// RequiredPassword, if non-empty, forces all HTTP
// requests to have HTTP basic auth with this password.
// It's used by the sandboxed macOS sameuserproof GUI auth mechanism.
RequiredPassword string
// PermitRead is whether read-only HTTP handlers are allowed.
PermitRead bool
// PermitWrite is whether mutating HTTP handlers are allowed.
PermitWrite bool
b *ipnlocal.LocalBackend
}
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if h.b == nil {
http.Error(w, "server has no local backend", http.StatusInternalServerError)
return
}
if h.RequiredPassword != "" {
_, pass, ok := r.BasicAuth()
if !ok {
http.Error(w, "auth required", http.StatusUnauthorized)
return
}
if pass != h.RequiredPassword {
http.Error(w, "bad password", http.StatusForbidden)
return
}
}
switch r.URL.Path {
case "/localapi/v0/whois":
h.serveWhoIs(w, r)
default:
io.WriteString(w, "tailscaled\n")
}
}
func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead {
http.Error(w, "whois access denied", http.StatusForbidden)
return
}
b := h.b
var ip netaddr.IP
if v := r.FormValue("ip"); v != "" {
var err error
ip, err = netaddr.ParseIP(r.FormValue("ip"))
if err != nil {
http.Error(w, "invalid 'ip' parameter", 400)
return
}
} else {
http.Error(w, "missing 'ip' parameter", 400)
return
}
n, u, ok := b.WhoIs(ip)
if !ok {
http.Error(w, "no match for IP", 404)
return
}
res := &tailcfg.WhoIsResponse{
Node: n,
UserProfile: &u,
}
j, err := json.MarshalIndent(res, "", "\t")
if err != nil {
http.Error(w, "JSON encoding error", 500)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(j)
}

View File

@@ -146,6 +146,10 @@ func (bs *BackendServer) GotFakeCommand(ctx context.Context, cmd *Command) error
return bs.GotCommand(ctx, cmd)
}
// ErrMsgPermissionDenied is the Notify.ErrMessage value used an
// operation was done from a user/context that didn't have permission.
const ErrMsgPermissionDenied = "permission denied"
func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error {
if cmd.Version != version.Long && !cmd.AllowVersionSkew {
vs := fmt.Sprintf("GotCommand: Version mismatch! frontend=%#v backend=%#v",
@@ -178,7 +182,7 @@ func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error {
}
if IsReadonlyContext(ctx) {
msg := "permission denied"
msg := ErrMsgPermissionDenied
bs.send(Notify{ErrMessage: &msg})
return nil
}

View File

@@ -16,9 +16,7 @@ import (
func TestReadWrite(t *testing.T) {
tstest.PanicOnLog()
rc := tstest.NewResourceCheck()
defer rc.Assert(t)
tstest.ResourceCheck(t)
buf := bytes.Buffer{}
err := WriteMsg(&buf, []byte("Test string1"))
@@ -64,9 +62,7 @@ func TestReadWrite(t *testing.T) {
func TestClientServer(t *testing.T) {
tstest.PanicOnLog()
rc := tstest.NewResourceCheck()
defer rc.Assert(t)
tstest.ResourceCheck(t)
b := &FakeBackend{}
var bs *BackendServer

View File

@@ -17,8 +17,9 @@ import (
"inet.af/netaddr"
"tailscale.com/atomicfile"
"tailscale.com/control/controlclient"
"tailscale.com/wgengine/router"
"tailscale.com/tailcfg"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
)
//go:generate go run tailscale.com/cmd/cloner -type=Prefs -output=prefs_clone.go
@@ -28,8 +29,10 @@ type Prefs struct {
// ControlURL is the URL of the control server to use.
ControlURL string
// RouteAll specifies whether to accept subnet and default routes
// advertised by other nodes on the Tailscale network.
// RouteAll specifies whether to accept subnets advertised by
// other nodes on the Tailscale network. Note that this does not
// include default routes (0.0.0.0/0 and ::/0), those are
// controlled by ExitNodeID/IP below.
RouteAll bool
// AllowSingleHosts specifies whether to install routes for each
@@ -44,6 +47,24 @@ type Prefs struct {
// packets stop flowing. What's up with that?
AllowSingleHosts bool
// ExitNodeID and ExitNodeIP specify the node that should be used
// as an exit node for internet traffic. At most one of these
// should be non-zero.
//
// The preferred way to express the chosen node is ExitNodeID, but
// in some cases it's not possible to use that ID (e.g. in the
// linux CLI, before tailscaled has a netmap). For those
// situations, we allow specifying the exit node by IP, and
// ipnlocal.LocalBackend will translate the IP into an ID when the
// node is found in the netmap.
//
// If the selected exit node doesn't exist (e.g. it's not part of
// the current tailnet), or it doesn't offer exit node services, a
// blackhole route will be installed on the local system to
// prevent any traffic escaping to the local network.
ExitNodeID tailcfg.StableNodeID
ExitNodeIP netaddr.IP
// CorpDNS specifies whether to install the Tailscale network's
// DNS configuration, if it exists.
CorpDNS bool
@@ -116,14 +137,14 @@ type Prefs struct {
// NetfilterMode specifies how much to manage netfilter rules for
// Tailscale, if at all.
NetfilterMode router.NetfilterMode
NetfilterMode preftype.NetfilterMode
// The Persist field is named 'Config' in the file for backward
// compatibility with earlier versions.
// TODO(apenwarr): We should move this out of here, it's not a pref.
// We can maybe do that once we're sure which module should persist
// it (backend or frontend?)
Persist *controlclient.Persist `json:"Config"`
Persist *persist.Persist `json:"Config"`
}
// IsEmpty reports whether p is nil or pointing to a Prefs zero value.
@@ -191,6 +212,8 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
p.ControlURL == p2.ControlURL &&
p.RouteAll == p2.RouteAll &&
p.AllowSingleHosts == p2.AllowSingleHosts &&
p.ExitNodeID == p2.ExitNodeID &&
p.ExitNodeIP == p2.ExitNodeIP &&
p.CorpDNS == p2.CorpDNS &&
p.WantRunning == p2.WantRunning &&
p.NotepadURLs == p2.NotepadURLs &&
@@ -240,7 +263,7 @@ func NewPrefs() *Prefs {
AllowSingleHosts: true,
CorpDNS: true,
WantRunning: true,
NetfilterMode: router.NetfilterOn,
NetfilterMode: preftype.NetfilterOn,
}
}
@@ -252,7 +275,7 @@ func PrefsFromBytes(b []byte, enforceDefaults bool) (*Prefs, error) {
if len(b) == 0 {
return p, nil
}
persist := &controlclient.Persist{}
persist := &persist.Persist{}
err := json.Unmarshal(b, persist)
if err == nil && (persist.Provider != "" || persist.LoginName != "") {
// old-style relaynode config; import it

View File

@@ -8,8 +8,9 @@ package ipn
import (
"inet.af/netaddr"
"tailscale.com/control/controlclient"
"tailscale.com/wgengine/router"
"tailscale.com/tailcfg"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
)
// Clone makes a deep copy of Prefs.
@@ -23,7 +24,7 @@ func (src *Prefs) Clone() *Prefs {
dst.AdvertiseTags = append(src.AdvertiseTags[:0:0], src.AdvertiseTags...)
dst.AdvertiseRoutes = append(src.AdvertiseRoutes[:0:0], src.AdvertiseRoutes...)
if dst.Persist != nil {
dst.Persist = new(controlclient.Persist)
dst.Persist = new(persist.Persist)
*dst.Persist = *src.Persist
}
return dst
@@ -35,6 +36,8 @@ var _PrefsNeedsRegeneration = Prefs(struct {
ControlURL string
RouteAll bool
AllowSingleHosts bool
ExitNodeID tailcfg.StableNodeID
ExitNodeIP netaddr.IP
CorpDNS bool
WantRunning bool
ShieldsUp bool
@@ -46,6 +49,6 @@ var _PrefsNeedsRegeneration = Prefs(struct {
ForceDaemon bool
AdvertiseRoutes []netaddr.IPPrefix
NoSNAT bool
NetfilterMode router.NetfilterMode
Persist *controlclient.Persist
NetfilterMode preftype.NetfilterMode
Persist *persist.Persist
}{})

View File

@@ -14,10 +14,10 @@ import (
"time"
"inet.af/netaddr"
"tailscale.com/control/controlclient"
"tailscale.com/tstest"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
"tailscale.com/types/wgkey"
"tailscale.com/wgengine/router"
)
func fieldsOf(t reflect.Type) (fields []string) {
@@ -30,7 +30,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
func TestPrefsEqual(t *testing.T) {
tstest.PanicOnLog()
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"}
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "ExitNodeID", "ExitNodeIP", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"}
if have := fieldsOf(reflect.TypeOf(Prefs{})); !reflect.DeepEqual(have, prefsHandles) {
t.Errorf("Prefs.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
have, prefsHandles)
@@ -99,6 +99,28 @@ func TestPrefsEqual(t *testing.T) {
true,
},
{
&Prefs{ExitNodeID: "n1234"},
&Prefs{},
false,
},
{
&Prefs{ExitNodeID: "n1234"},
&Prefs{ExitNodeID: "n1234"},
true,
},
{
&Prefs{ExitNodeIP: netaddr.MustParseIP("1.2.3.4")},
&Prefs{},
false,
},
{
&Prefs{ExitNodeIP: netaddr.MustParseIP("1.2.3.4")},
&Prefs{ExitNodeIP: netaddr.MustParseIP("1.2.3.4")},
true,
},
{
&Prefs{CorpDNS: true},
&Prefs{CorpDNS: false},
@@ -192,24 +214,24 @@ func TestPrefsEqual(t *testing.T) {
},
{
&Prefs{NetfilterMode: router.NetfilterOff},
&Prefs{NetfilterMode: router.NetfilterOn},
&Prefs{NetfilterMode: preftype.NetfilterOff},
&Prefs{NetfilterMode: preftype.NetfilterOn},
false,
},
{
&Prefs{NetfilterMode: router.NetfilterOn},
&Prefs{NetfilterMode: router.NetfilterOn},
&Prefs{NetfilterMode: preftype.NetfilterOn},
&Prefs{NetfilterMode: preftype.NetfilterOn},
true,
},
{
&Prefs{Persist: &controlclient.Persist{}},
&Prefs{Persist: &controlclient.Persist{LoginName: "dave"}},
&Prefs{Persist: &persist.Persist{}},
&Prefs{Persist: &persist.Persist{LoginName: "dave"}},
false,
},
{
&Prefs{Persist: &controlclient.Persist{LoginName: "dave"}},
&Prefs{Persist: &controlclient.Persist{LoginName: "dave"}},
&Prefs{Persist: &persist.Persist{LoginName: "dave"}},
&Prefs{Persist: &persist.Persist{LoginName: "dave"}},
true,
},
}
@@ -274,7 +296,7 @@ func TestBasicPrefs(t *testing.T) {
func TestPrefsPersist(t *testing.T) {
tstest.PanicOnLog()
c := controlclient.Persist{
c := persist.Persist{
LoginName: "test@example.com",
}
p := Prefs{
@@ -340,14 +362,14 @@ func TestPrefsPretty(t *testing.T) {
},
{
Prefs{
Persist: &controlclient.Persist{},
Persist: &persist.Persist{},
},
"linux",
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist{lm=, o=, n= u=""}}`,
},
{
Prefs{
Persist: &controlclient.Persist{
Persist: &persist.Persist{
PrivateNodeKey: wgkey.Private{1: 1},
},
},

View File

@@ -17,6 +17,7 @@ import (
"log"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
@@ -338,6 +339,18 @@ func New(collection string) *Policy {
tryFixLogStateLocation(dir, cmdName)
cfgPath := filepath.Join(dir, fmt.Sprintf("%s.log.conf", cmdName))
// The Windows service previously ran as tailscale-ipn.exe, so
// let's keep using that log base name if it exists.
if runtime.GOOS == "windows" && cmdName == "tailscaled" {
const oldCmdName = "tailscale-ipn"
oldPath := filepath.Join(dir, oldCmdName+".log.conf")
if fi, err := os.Stat(oldPath); err == nil && fi.Mode().IsRegular() {
cfgPath = oldPath
cmdName = oldCmdName
}
}
var oldc *Config
data, err := ioutil.ReadFile(cfgPath)
if err != nil {
@@ -387,6 +400,13 @@ func New(collection string) *Policy {
HTTPC: &http.Client{Transport: newLogtailTransport(logtail.DefaultHost)},
}
if val, ok := os.LookupEnv("TS_LOG_TARGET"); ok {
log.Println("You have enabled a non-default log target. Doing without being told to by Tailscale staff or your network administrator will make getting support difficult.")
c.BaseURL = val
u, _ := url.Parse(val)
c.HTTPC = &http.Client{Transport: newLogtailTransport(u.Host)}
}
filchBuf, filchErr := filch.New(filepath.Join(dir, cmdName), filch.Options{})
if filchBuf != nil {
c.Buffer = filchBuf

View File

@@ -10,6 +10,7 @@ import (
"net"
"net/http"
"reflect"
"runtime"
"sort"
"strings"
@@ -39,8 +40,11 @@ func Tailscale() (net.IP, *net.Interface, error) {
continue
}
for _, a := range addrs {
if ipnet, ok := a.(*net.IPNet); ok && IsTailscaleIP(ipnet.IP) {
return ipnet.IP, &iface, nil
if ipnet, ok := a.(*net.IPNet); ok {
nip, ok := netaddr.FromStdIP(ipnet.IP)
if ok && tsaddr.IsTailscaleIP(nip) {
return ipnet.IP, &iface, nil
}
}
}
}
@@ -57,16 +61,21 @@ func maybeTailscaleInterfaceName(s string) bool {
strings.HasPrefix(s, "utun")
}
// IsTailscaleIP reports whether ip is an IP in a range used by
// Tailscale virtual network interfaces.
func IsTailscaleIP(ip net.IP) bool {
nip, _ := netaddr.FromStdIP(ip) // TODO: push this up to caller, change func signature
return tsaddr.IsTailscaleIP(nip)
}
func isUp(nif *net.Interface) bool { return nif.Flags&net.FlagUp != 0 }
func isLoopback(nif *net.Interface) bool { return nif.Flags&net.FlagLoopback != 0 }
func isProblematicInterface(nif *net.Interface) bool {
name := nif.Name
// Don't try to send disco/etc packets over zerotier; they effectively
// DoS each other by doing traffic amplification, both of them
// preferring/trying to use each other for transport. See:
// https://github.com/tailscale/tailscale/issues/1208
if strings.HasPrefix(name, "zt") || (runtime.GOOS == "windows" && strings.Contains(name, "ZeroTier")) {
return true
}
return false
}
// LocalAddresses returns the machine's IP addresses, separated by
// whether they're loopback addresses.
func LocalAddresses() (regular, loopback []string, err error) {
@@ -77,8 +86,10 @@ func LocalAddresses() (regular, loopback []string, err error) {
}
for i := range ifaces {
iface := &ifaces[i]
if !isUp(iface) {
// Down interfaces don't count
if !isUp(iface) || isProblematicInterface(iface) {
// Skip down interfaces and ones that are
// problematic that we don't want to try to
// send Tailscale traffic over.
continue
}
ifcIsLoopback := isLoopback(iface)
@@ -186,10 +197,9 @@ func (s *State) String() string {
fmt.Fprintf(&sb, "interfaces.State{defaultRoute=%v ifs={", s.DefaultRouteInterface)
ifs := make([]string, 0, len(s.InterfaceUp))
for k := range s.InterfaceUp {
if allLoopbackIPs(s.InterfaceIPs[k]) {
continue
if anyInterestingIP(s.InterfaceIPs[k]) {
ifs = append(ifs, k)
}
ifs = append(ifs, k)
}
sort.Slice(ifs, func(i, j int) bool {
upi, upj := s.InterfaceUp[ifs[i]], s.InterfaceUp[ifs[j]]
@@ -207,7 +217,7 @@ func (s *State) String() string {
fmt.Fprintf(&sb, "%s:[", ifName)
needSpace := false
for _, ip := range s.InterfaceIPs[ifName] {
if ip.IsLinkLocalUnicast() {
if !isInterestingIP(ip) {
continue
}
if needSpace {
@@ -392,14 +402,23 @@ var (
v6Global1 = mustCIDR("2000::/3")
)
func allLoopbackIPs(ips []netaddr.IP) bool {
if len(ips) == 0 {
return false
}
// anyInterestingIP reports ips contains any IP that matches
// isInterestingIP.
func anyInterestingIP(ips []netaddr.IP) bool {
for _, ip := range ips {
if !ip.IsLoopback() {
return false
if isInterestingIP(ip) {
return true
}
}
return false
}
// isInterestingIP reports whether ip is an interesting IP that we
// should log in interfaces.State logging. We don't need to show
// localhost or link-local addresses.
func isInterestingIP(ip netaddr.IP) bool {
if ip.IsLoopback() || ip.IsLinkLocalUnicast() {
return false
}
return true
}

View File

@@ -15,7 +15,7 @@ package interfaces
// privateGatewayIPFromRoute returns the private gateway ip address from rtm, if it exists.
// Otherwise, it returns 0.
int privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
uint32_t privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
{
// sockaddrs are after the message header
struct sockaddr* dst_sa = (struct sockaddr *)(rtm + 1);
@@ -38,7 +38,7 @@ int privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
return 0; // gateway not IPv4
struct sockaddr_in* gateway_si= (struct sockaddr_in *)gateway_sa;
int ip;
uint32_t ip;
ip = gateway_si->sin_addr.s_addr;
unsigned char a, b;
@@ -62,7 +62,7 @@ int privateGatewayIPFromRoute(struct rt_msghdr2 *rtm)
// If no private gateway IP address was found, it returns 0.
// On an error, it returns an error code in (0, 255].
// Any private gateway IP address is > 255.
int privateGatewayIP()
uint32_t privateGatewayIP()
{
size_t needed;
int mib[6];
@@ -90,7 +90,7 @@ int privateGatewayIP()
struct rt_msghdr2 *rtm;
for (next = buf; next < lim; next += rtm->rtm_msglen) {
rtm = (struct rt_msghdr2 *)next;
int ip;
uint32_t ip;
ip = privateGatewayIPFromRoute(rtm);
if (ip) {
free(buf);

View File

@@ -0,0 +1,81 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin,!redo,!ios
// (Exclude redo, because we don't want this code in the App Store
// version's sandbox, where it won't work, and also don't want it on
// iOS. This is just for utun-using non-sandboxed cmd/tailscaled on macOS.
package interfaces
import (
"errors"
"fmt"
"net"
"syscall"
"golang.org/x/net/route"
)
func DefaultRouteInterface() (string, error) {
idx, err := DefaultRouteInterfaceIndex()
if err != nil {
return "", err
}
iface, err := net.InterfaceByIndex(idx)
if err != nil {
return "", err
}
return iface.Name, nil
}
func DefaultRouteInterfaceIndex() (int, error) {
// $ netstat -nr
// Routing tables
// Internet:
// Destination Gateway Flags Netif Expire
// default 10.0.0.1 UGSc en0 <-- want this one
// default 10.0.0.1 UGScI en1
// From man netstat:
// U RTF_UP Route usable
// G RTF_GATEWAY Destination requires forwarding by intermediary
// S RTF_STATIC Manually added
// c RTF_PRCLONING Protocol-specified generate new routes on use
// I RTF_IFSCOPE Route is associated with an interface scope
rib, err := route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0)
if err != nil {
return 0, fmt.Errorf("route.FetchRIB: %w", err)
}
msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
if err != nil {
return 0, fmt.Errorf("route.ParseRIB: %w", err)
}
indexSeen := map[int]int{} // index => count
for _, m := range msgs {
rm, ok := m.(*route.RouteMessage)
if !ok {
continue
}
const RTF_GATEWAY = 0x2
const RTF_IFSCOPE = 0x1000000
if rm.Flags&RTF_GATEWAY == 0 {
continue
}
if rm.Flags&RTF_IFSCOPE != 0 {
continue
}
indexSeen[rm.Index]++
}
if len(indexSeen) == 0 {
return 0, errors.New("no gateway index found")
}
if len(indexSeen) == 1 {
for idx := range indexSeen {
return idx, nil
}
}
return 0, fmt.Errorf("ambiguous gateway interfaces found: %v", indexSeen)
}

View File

@@ -0,0 +1,17 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build linux darwin,!redo
package interfaces
import "testing"
func TestDefaultRouteInterface(t *testing.T) {
v, err := DefaultRouteInterface()
if err != nil {
t.Fatal(err)
}
t.Logf("got %q", v)
}

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !linux,!windows
// +build !linux,!windows,!darwin darwin,redo
package interfaces

View File

@@ -5,30 +5,9 @@
package interfaces
import (
"net"
"testing"
)
func TestIsTailscaleIP(t *testing.T) {
tests := []struct {
ip string
want bool
}{
{"100.81.251.94", true},
{"8.8.8.8", false},
}
for _, tt := range tests {
ip := net.ParseIP(tt.ip)
if ip == nil {
t.Fatalf("failed to parse IP %q", tt.ip)
}
got := IsTailscaleIP(ip)
if got != tt.want {
t.Errorf("F(%q) = %v; want %v", tt.ip, got, tt.want)
}
}
}
func TestGetState(t *testing.T) {
st, err := GetState()
if err != nil {

View File

@@ -8,9 +8,7 @@ package netcheck
import (
"bufio"
"context"
"crypto/rand"
"crypto/tls"
"encoding/binary"
"errors"
"fmt"
"io"
@@ -25,11 +23,11 @@ import (
"time"
"github.com/tcnksm/go-httpstat"
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/derp/derphttp"
"tailscale.com/net/interfaces"
"tailscale.com/net/netns"
"tailscale.com/net/portmapper"
"tailscale.com/net/stun"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
@@ -68,12 +66,6 @@ const (
// 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 {
@@ -160,6 +152,10 @@ type Client struct {
// It defaults to ":0".
UDPBindAddr string
// PortMapper, if non-nil, is used for portmap queries.
// If nil, portmap discovery is not done.
PortMapper *portmapper.Client // lazily initialized on first use
mu sync.Mutex // guards following
nextFull bool // do a full region scan, even if last != nil
prev map[time.Time]*Report // some previous reports
@@ -219,8 +215,8 @@ func (c *Client) handleHairSTUNLocked(pkt []byte, src netaddr.IPPort) bool {
// (non-incremental) probe of all DERP regions.
func (c *Client) MakeNextReportFull() {
c.mu.Lock()
defer c.mu.Unlock()
c.nextFull = true
c.mu.Unlock()
}
func (c *Client) ReceiveSTUNPacket(pkt []byte, src netaddr.IPPort) {
@@ -680,104 +676,20 @@ func (rs *reportState) setOptBool(b *opt.Bool, v bool) {
func (rs *reportState) probePortMapServices() {
defer rs.waitPortMap.Done()
gw, myIP, ok := interfaces.LikelyHomeRouterIP()
if !ok {
return
}
rs.setOptBool(&rs.report.UPnP, false)
rs.setOptBool(&rs.report.PMP, false)
rs.setOptBool(&rs.report.PCP, false)
port1900 := netaddr.IPPort{IP: gw, Port: 1900}.UDPAddr()
port5351 := netaddr.IPPort{IP: gw, Port: 5351}.UDPAddr()
rs.c.logf("[v1] probePortMapServices: me %v -> gw %v", myIP, gw)
// Create a UDP4 socket used just for querying for UPnP, NAT-PMP, and PCP.
uc, err := netns.Listener().ListenPacket(context.Background(), "udp4", ":0")
res, err := rs.c.PortMapper.Probe(context.Background())
if err != nil {
rs.c.logf("probePortMapServices: %v", err)
return
}
defer uc.Close()
tempPort := uc.LocalAddr().(*net.UDPAddr).Port
uc.SetReadDeadline(time.Now().Add(portMapServiceProbeTimeout))
// Send request packets for all three protocols.
uc.WriteTo(uPnPPacket, port1900)
uc.WriteTo(pmpPacket, port5351)
uc.WriteTo(pcpPacket(myIP, tempPort, false), port5351)
res := make([]byte, 1500)
sentPCPDelete := false
for {
n, addr, err := uc.ReadFrom(res)
if err != nil {
return
}
switch addr.(*net.UDPAddr).Port {
case 1900:
if mem.Contains(mem.B(res[:n]), mem.S(":InternetGatewayDevice:")) {
rs.setOptBool(&rs.report.UPnP, true)
}
case 5351:
if n == 12 && res[0] == 0x00 { // right length and version 0
rs.setOptBool(&rs.report.PMP, true)
}
if n == 60 && res[0] == 0x02 { // right length and version 2
rs.setOptBool(&rs.report.PCP, true)
if !sentPCPDelete {
sentPCPDelete = true
// And now delete the mapping.
// (PCP is the only protocol of the three that requires
// we cause a side effect to detect whether it's present,
// so we need to redo that side effect now.)
uc.WriteTo(pcpPacket(myIP, tempPort, true), port5351)
}
}
}
}
}
var pmpPacket = []byte{0, 0} // version 0, opcode 0 = "Public address request"
var uPnPPacket = []byte("M-SEARCH * HTTP/1.1\r\n" +
"HOST: 239.255.255.250:1900\r\n" +
"ST: ssdp:all\r\n" +
"MAN: \"ssdp:discover\"\r\n" +
"MX: 2\r\n\r\n")
var v4unspec, _ = netaddr.ParseIP("0.0.0.0")
// pcpPacket generates a PCP packet with a MAP opcode.
func pcpPacket(myIP netaddr.IP, mapToLocalPort int, delete bool) []byte {
const udpProtoNumber = 17
lifetimeSeconds := uint32(1)
if delete {
lifetimeSeconds = 0
}
const opMap = 1
// 24 byte header + 36 byte map opcode
pkt := make([]byte, (32+32+128)/8+(96+8+24+16+16+128)/8)
// The header (https://tools.ietf.org/html/rfc6887#section-7.1)
pkt[0] = 2 // version
pkt[1] = opMap
binary.BigEndian.PutUint32(pkt[4:8], lifetimeSeconds)
myIP16 := myIP.As16()
copy(pkt[8:], myIP16[:])
// The map opcode body (https://tools.ietf.org/html/rfc6887#section-11.1)
mapOp := pkt[24:]
rand.Read(mapOp[:12]) // 96 bit mappping nonce
mapOp[12] = udpProtoNumber
binary.BigEndian.PutUint16(mapOp[16:], uint16(mapToLocalPort))
v4unspec16 := v4unspec.As16()
copy(mapOp[20:], v4unspec16[:])
return pkt
rs.setOptBool(&rs.report.UPnP, res.UPnP)
rs.setOptBool(&rs.report.PMP, res.PMP)
rs.setOptBool(&rs.report.PCP, res.PCP)
}
func newReport() *Report {
@@ -854,7 +766,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
}
defer rs.pc4Hair.Close()
if !c.SkipExternalNetwork {
if !c.SkipExternalNetwork && c.PortMapper != nil {
rs.waitPortMap.Add(1)
go rs.probePortMapServices()
}
@@ -927,7 +839,7 @@ func (c *Client) GetReport(ctx context.Context, dm *tailcfg.DERPMap) (*Report, e
rs.waitHairCheck(ctx)
c.vlogf("hairCheck done")
if !c.SkipExternalNetwork {
if !c.SkipExternalNetwork && c.PortMapper != nil {
rs.waitPortMap.Wait()
c.vlogf("portMap done")
}

View File

@@ -0,0 +1,52 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build darwin,!redo
package netns
import (
"fmt"
"log"
"strings"
"syscall"
"golang.org/x/sys/unix"
"tailscale.com/net/interfaces"
)
// control marks c as necessary to dial in a separate network namespace.
//
// It's intentionally the same signature as net.Dialer.Control
// and net.ListenConfig.Control.
func control(network, address string, c syscall.RawConn) error {
if strings.HasPrefix(address, "127.") || address == "::1" {
// Don't bind to an interface for localhost connections.
return nil
}
idx, err := interfaces.DefaultRouteInterfaceIndex()
if err != nil {
log.Printf("netns: DefaultRouteInterfaceIndex: %v", err)
return nil
}
v6 := strings.Contains(address, "]:") || strings.HasSuffix(network, "6") // hacky test for v6
proto := unix.IPPROTO_IP
opt := unix.IP_BOUND_IF
if v6 {
proto = unix.IPPROTO_IPV6
opt = unix.IPV6_BOUND_IF
}
var sockErr error
err = c.Control(func(fd uintptr) {
sockErr = unix.SetsockoptInt(int(fd), proto, opt, idx)
})
if err != nil {
return fmt.Errorf("RawConn.Control on %T: %w", c, err)
}
if sockErr != nil {
log.Printf("netns: control(%q, %q), v6=%v, index=%v: %v", network, address, v6, idx, sockErr)
}
return sockErr
}

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !linux,!windows
// +build !linux,!windows,!darwin darwin,redo
package netns

42
net/netns/netns_test.go Normal file
View File

@@ -0,0 +1,42 @@
// 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 netns contains the common code for using the Go net package
// in a logical "network namespace" to avoid routing loops where
// Tailscale-created packets would otherwise loop back through
// Tailscale routes.
//
// Despite the name netns, the exact mechanism used differs by
// operating system, and perhaps even by version of the OS.
//
// The netns package also handles connecting via SOCKS proxies when
// configured by the environment.
package netns
import (
"flag"
"testing"
)
var extNetwork = flag.Bool("use-external-network", false, "use the external network in tests")
func TestDial(t *testing.T) {
if !*extNetwork {
t.Skip("skipping test without --use-external-network")
}
d := NewDialer()
c, err := d.Dial("tcp", "google.com:80")
if err != nil {
t.Fatal(err)
}
defer c.Close()
t.Logf("got addr %v", c.RemoteAddr())
c, err = d.Dial("tcp4", "google.com:80")
if err != nil {
t.Fatal(err)
}
defer c.Close()
t.Logf("got addr %v", c.RemoteAddr())
}

View File

@@ -23,12 +23,14 @@ import (
// Tailscale node has rejected the connection from another. Unlike a
// TCP RST, this includes a reason.
//
// On the wire, after the IP header, it's currently 7 bytes:
// On the wire, after the IP header, it's currently 7 or 8 bytes:
// * '!'
// * IPProto byte (IANA protocol number: TCP or UDP)
// * 'A' or 'S' (RejectedDueToACLs, RejectedDueToShieldsUp)
// * srcPort big endian uint16
// * dstPort big endian uint16
// * [optional] byte of flag bits:
// lowest bit (0x1): MaybeBroken
//
// In the future it might also accept 16 byte IP flow src/dst IPs
// after the header, if they're different than the IP-level ones.
@@ -39,8 +41,21 @@ type TailscaleRejectedHeader struct {
Dst netaddr.IPPort // rejected flow's dst
Proto IPProto // proto that was rejected (TCP or UDP)
Reason TailscaleRejectReason // why the connection was rejected
// MaybeBroken is whether the rejection is non-terminal (the
// client should not fail immediately). This is sent by a
// target when it's not sure whether it's totally broken, but
// it might be. For example, the target tailscaled might think
// its host firewall or IP forwarding aren't configured
// properly, but tailscaled might be wrong (not having enough
// visibility into what the OS is doing). When true, the
// message is simply an FYI as a potential reason to use for
// later when the pendopen connection tracking timer expires.
MaybeBroken bool
}
const rejectFlagBitMaybeBroken = 0x1
func (rh TailscaleRejectedHeader) Flow() flowtrack.Tuple {
return flowtrack.Tuple{Src: rh.Src, Dst: rh.Dst}
}
@@ -52,14 +67,32 @@ func (rh TailscaleRejectedHeader) String() string {
type TSMPType uint8
const (
// TSMPTypeRejectedConn is the type byte for a TailscaleRejectedHeader.
TSMPTypeRejectedConn TSMPType = '!'
)
type TailscaleRejectReason byte
// IsZero reports whether r is the zero value, representing no rejection.
func (r TailscaleRejectReason) IsZero() bool { return r == TailscaleRejectReasonNone }
const (
RejectedDueToACLs TailscaleRejectReason = 'A'
// TailscaleRejectReasonNone is the TailscaleRejectReason zero value.
TailscaleRejectReasonNone TailscaleRejectReason = 0
// RejectedDueToACLs means that the host rejected the connection due to ACLs.
RejectedDueToACLs TailscaleRejectReason = 'A'
// RejectedDueToShieldsUp means that the host rejected the connection due to shields being up.
RejectedDueToShieldsUp TailscaleRejectReason = 'S'
// RejectedDueToIPForwarding means that the relay node's IP
// forwarding is disabled.
RejectedDueToIPForwarding TailscaleRejectReason = 'F'
// RejectedDueToHostFirewall means that the target host's
// firewall is blocking the traffic.
RejectedDueToHostFirewall TailscaleRejectReason = 'W'
)
func (r TailscaleRejectReason) String() string {
@@ -68,22 +101,32 @@ func (r TailscaleRejectReason) String() string {
return "acl"
case RejectedDueToShieldsUp:
return "shields"
case RejectedDueToIPForwarding:
return "host-ip-forwarding-unavailable"
case RejectedDueToHostFirewall:
return "host-firewall"
}
return fmt.Sprintf("0x%02x", byte(r))
}
func (h TailscaleRejectedHeader) hasFlags() bool {
return h.MaybeBroken // the only one currently
}
func (h TailscaleRejectedHeader) Len() int {
var ipHeaderLen int
if h.IPSrc.Is4() {
ipHeaderLen = ip4HeaderLength
} else if h.IPSrc.Is6() {
ipHeaderLen = ip6HeaderLength
}
return ipHeaderLen +
1 + // TSMPType byte
v := 1 + // TSMPType byte
1 + // IPProto byte
1 + // TailscaleRejectReason byte
2*2 // 2 uint16 ports
if h.IPSrc.Is4() {
v += ip4HeaderLength
} else if h.IPSrc.Is6() {
v += ip6HeaderLength
}
if h.hasFlags() {
v++
}
return v
}
func (h TailscaleRejectedHeader) Marshal(buf []byte) error {
@@ -117,6 +160,14 @@ func (h TailscaleRejectedHeader) Marshal(buf []byte) error {
buf[2] = byte(h.Reason)
binary.BigEndian.PutUint16(buf[3:5], h.Src.Port)
binary.BigEndian.PutUint16(buf[5:7], h.Dst.Port)
if h.hasFlags() {
var flags byte
if h.MaybeBroken {
flags |= rejectFlagBitMaybeBroken
}
buf[7] = flags
}
return nil
}
@@ -129,12 +180,17 @@ func (pp *Parsed) AsTailscaleRejectedHeader() (h TailscaleRejectedHeader, ok boo
if len(p) < 7 || p[0] != byte(TSMPTypeRejectedConn) {
return
}
return TailscaleRejectedHeader{
h = TailscaleRejectedHeader{
Proto: IPProto(p[1]),
Reason: TailscaleRejectReason(p[2]),
IPSrc: pp.Src.IP,
IPDst: pp.Dst.IP,
Src: netaddr.IPPort{IP: pp.Dst.IP, Port: binary.BigEndian.Uint16(p[3:5])},
Dst: netaddr.IPPort{IP: pp.Src.IP, Port: binary.BigEndian.Uint16(p[5:7])},
}, true
}
if len(p) > 7 {
flags := p[7]
h.MaybeBroken = (flags & rejectFlagBitMaybeBroken) != 0
}
return h, true
}

View File

@@ -37,6 +37,18 @@ func TestTailscaleRejectedHeader(t *testing.T) {
},
wantStr: "TSMP-reject-flow{UDP [1::1]:567 > [2::2]:443}: shields",
},
{
h: TailscaleRejectedHeader{
IPSrc: netaddr.MustParseIP("2::2"),
IPDst: netaddr.MustParseIP("1::1"),
Src: netaddr.MustParseIPPort("[1::1]:567"),
Dst: netaddr.MustParseIPPort("[2::2]:443"),
Proto: UDP,
Reason: RejectedDueToIPForwarding,
MaybeBroken: true,
},
wantStr: "TSMP-reject-flow{UDP [1::1]:567 > [2::2]:443}: host-ip-forwarding-unavailable",
},
}
for i, tt := range tests {
gotStr := tt.h.String()

View File

@@ -0,0 +1,611 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package portmapper is a UDP port mapping client. It currently only does
// NAT-PMP, but will likely do UPnP and perhaps PCP later.
package portmapper
import (
"context"
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"io"
"net"
"sync"
"time"
"go4.org/mem"
"inet.af/netaddr"
"tailscale.com/net/interfaces"
"tailscale.com/net/netns"
"tailscale.com/types/logger"
)
// References:
//
// NAT-PMP: https://tools.ietf.org/html/rfc6886
// PCP: https://tools.ietf.org/html/rfc6887
// portMapServiceTimeout 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.
const portMapServiceTimeout = 250 * time.Millisecond
// trustServiceStillAvailableDuration is how often we re-verify a port
// mapping service is available.
const trustServiceStillAvailableDuration = 10 * time.Minute
// Client is a port mapping client.
type Client struct {
logf logger.Logf
mu sync.Mutex // guards following, and all fields thereof
lastMyIP netaddr.IP
lastGW netaddr.IP
closed bool
lastProbe time.Time
pmpPubIP netaddr.IP // non-zero if known
pmpPubIPTime time.Time // time pmpPubIP last verified
pmpLastEpoch uint32
pcpSawTime time.Time // time we last saw PCP was available
uPnPSawTime time.Time // time we last saw UPnP was available
localPort uint16
pmpMapping *pmpMapping // non-nil if we have a PMP mapping
}
// pmpMapping is an already-created PMP mapping.
//
// All fields are immutable once created.
type pmpMapping struct {
gw netaddr.IP
external netaddr.IPPort
internal netaddr.IPPort
useUntil time.Time // the mapping's lifetime minus renewal interval
epoch uint32
}
// externalValid reports whether m.external is valid, with both its IP and Port populated.
func (m *pmpMapping) externalValid() bool {
return !m.external.IP.IsZero() && m.external.Port != 0
}
// release does a best effort fire-and-forget release of the PMP mapping m.
func (m *pmpMapping) release() {
uc, err := netns.Listener().ListenPacket(context.Background(), "udp4", ":0")
if err != nil {
return
}
defer uc.Close()
pkt := buildPMPRequestMappingPacket(m.internal.Port, m.external.Port, pmpMapLifetimeDelete)
uc.WriteTo(pkt, netaddr.IPPort{IP: m.gw, Port: pmpPort}.UDPAddr())
}
// NewClient returns a new portmapping client.
func NewClient(logf logger.Logf) *Client {
return &Client{
logf: logf,
}
}
// NoteNetworkDown should be called when the network has transitioned to a down state.
// It's too late to release port mappings at this point (the user might've just turned off
// their wifi), but we can make sure we invalidate mappings for later when the network
// comes back.
func (c *Client) NoteNetworkDown() {
c.mu.Lock()
defer c.mu.Unlock()
c.invalidateMappingsLocked(false)
}
func (c *Client) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
if c.closed {
return nil
}
c.closed = true
c.invalidateMappingsLocked(true)
// TODO: close some future ever-listening UDP socket(s),
// waiting for multicast announcements from router.
return nil
}
// SetLocalPort updates the local port number to which we want to port
// map UDP traffic.
func (c *Client) SetLocalPort(localPort uint16) {
c.mu.Lock()
defer c.mu.Unlock()
if c.localPort == localPort {
return
}
c.localPort = localPort
c.invalidateMappingsLocked(true)
}
func (c *Client) gatewayAndSelfIP() (gw, myIP netaddr.IP, ok bool) {
gw, myIP, ok = interfaces.LikelyHomeRouterIP()
if !ok {
gw = netaddr.IP{}
myIP = netaddr.IP{}
}
c.mu.Lock()
defer c.mu.Unlock()
if gw != c.lastGW || myIP != c.lastMyIP || !ok {
c.lastMyIP = myIP
c.lastGW = gw
c.invalidateMappingsLocked(true)
}
return
}
func (c *Client) invalidateMappingsLocked(releaseOld bool) {
if c.pmpMapping != nil {
if releaseOld {
c.pmpMapping.release()
}
c.pmpMapping = nil
}
c.pmpPubIP = netaddr.IP{}
c.pmpPubIPTime = time.Time{}
c.pcpSawTime = time.Time{}
c.uPnPSawTime = time.Time{}
}
func (c *Client) sawPMPRecently() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.sawPMPRecentlyLocked()
}
func (c *Client) sawPMPRecentlyLocked() bool {
return !c.pmpPubIP.IsZero() && c.pmpPubIPTime.After(time.Now().Add(-trustServiceStillAvailableDuration))
}
func (c *Client) sawPCPRecently() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.pcpSawTime.After(time.Now().Add(-trustServiceStillAvailableDuration))
}
func (c *Client) sawUPnPRecently() bool {
c.mu.Lock()
defer c.mu.Unlock()
return c.uPnPSawTime.After(time.Now().Add(-trustServiceStillAvailableDuration))
}
// closeCloserOnContextDone starts a new goroutine to call c.Close
// if/when ctx becomes done.
// To stop the goroutine, call the returned stop func.
func closeCloserOnContextDone(ctx context.Context, c io.Closer) (stop func()) {
// Close uc on ctx being done.
ctxDone := ctx.Done()
if ctxDone == nil {
return func() {}
}
stopWaitDone := make(chan struct{})
go func() {
select {
case <-stopWaitDone:
case <-ctxDone:
c.Close()
}
}()
return func() { close(stopWaitDone) }
}
// NoMappingError is returned by CreateOrGetMapping when no NAT
// mapping could be returned.
type NoMappingError struct {
err error
}
func (nme NoMappingError) Unwrap() error { return nme.err }
func (nme NoMappingError) Error() string { return fmt.Sprintf("no NAT mapping available: %v", nme.err) }
// IsNoMappingError reports whether err is of type NoMappingError.
func IsNoMappingError(err error) bool {
_, ok := err.(NoMappingError)
return ok
}
var (
ErrNoPortMappingServices = errors.New("no port mapping services were found")
ErrGatewayNotFound = errors.New("failed to look gateway address")
)
// CreateOrGetMapping either creates a new mapping or returns a cached
// valid one.
//
// If no mapping is available, the error will be of type
// NoMappingError; see IsNoMappingError.
func (c *Client) CreateOrGetMapping(ctx context.Context) (external netaddr.IPPort, err error) {
gw, myIP, ok := c.gatewayAndSelfIP()
if !ok {
return netaddr.IPPort{}, NoMappingError{ErrGatewayNotFound}
}
c.mu.Lock()
localPort := c.localPort
m := &pmpMapping{
gw: gw,
internal: netaddr.IPPort{IP: myIP, Port: localPort},
}
// prevPort is the port we had most previously, if any. We try
// to ask for the same port. 0 means to give us any port.
var prevPort uint16
// Do we have an existing mapping that's valid?
now := time.Now()
if m := c.pmpMapping; m != nil {
if now.Before(m.useUntil) {
defer c.mu.Unlock()
return m.external, nil
}
// The mapping might still be valid, so just try to renew it.
prevPort = m.external.Port
}
// If we just did a Probe (e.g. via netchecker) but didn't
// find a PMP service, bail out early rather than probing
// again. Cuts down latency for most clients.
haveRecentPMP := c.sawPMPRecentlyLocked()
if haveRecentPMP {
m.external.IP = c.pmpPubIP
}
if c.lastProbe.After(now.Add(-5*time.Second)) && !haveRecentPMP {
c.mu.Unlock()
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
}
c.mu.Unlock()
uc, err := netns.Listener().ListenPacket(ctx, "udp4", ":0")
if err != nil {
return netaddr.IPPort{}, err
}
defer uc.Close()
uc.SetReadDeadline(time.Now().Add(portMapServiceTimeout))
defer closeCloserOnContextDone(ctx, uc)()
pmpAddr := netaddr.IPPort{IP: gw, Port: pmpPort}
pmpAddru := pmpAddr.UDPAddr()
// Ask for our external address if needed.
if m.external.IP.IsZero() {
if _, err := uc.WriteTo(pmpReqExternalAddrPacket, pmpAddru); err != nil {
return netaddr.IPPort{}, err
}
}
// And ask for a mapping.
pmpReqMapping := buildPMPRequestMappingPacket(localPort, prevPort, pmpMapLifetimeSec)
if _, err := uc.WriteTo(pmpReqMapping, pmpAddru); err != nil {
return netaddr.IPPort{}, err
}
res := make([]byte, 1500)
for {
n, srci, err := uc.ReadFrom(res)
if err != nil {
if ctx.Err() == context.Canceled {
return netaddr.IPPort{}, err
}
return netaddr.IPPort{}, NoMappingError{ErrNoPortMappingServices}
}
srcu := srci.(*net.UDPAddr)
src, ok := netaddr.FromStdAddr(srcu.IP, srcu.Port, srcu.Zone)
if !ok {
continue
}
if src == pmpAddr {
pres, ok := parsePMPResponse(res[:n])
if !ok {
c.logf("unexpected PMP response: % 02x", res[:n])
continue
}
if pres.ResultCode != 0 {
return netaddr.IPPort{}, NoMappingError{fmt.Errorf("PMP response Op=0x%x,Res=0x%x", pres.OpCode, pres.ResultCode)}
}
if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr {
m.external.IP = pres.PublicAddr
}
if pres.OpCode == pmpOpReply|pmpOpMapUDP {
m.external.Port = pres.ExternalPort
d := time.Duration(pres.MappingValidSeconds) * time.Second
d /= 2 // renew in half the time
m.useUntil = time.Now().Add(d)
m.epoch = pres.SecondsSinceEpoch
}
}
if m.externalValid() {
c.mu.Lock()
defer c.mu.Unlock()
c.pmpMapping = m
return m.external, nil
}
}
}
type pmpResultCode uint16
// NAT-PMP constants.
const (
pmpPort = 5351
pmpMapLifetimeSec = 7200 // RFC recommended 2 hour map duration
pmpMapLifetimeDelete = 0 // 0 second lifetime deletes
pmpOpMapPublicAddr = 0
pmpOpMapUDP = 1
pmpOpReply = 0x80 // OR'd into request's op code on response
pmpCodeOK pmpResultCode = 0
pmpCodeUnsupportedVersion pmpResultCode = 1
pmpCodeNotAuthorized pmpResultCode = 2 // "e.g., box supports mapping, but user has turned feature off"
pmpCodeNetworkFailure pmpResultCode = 3 // "e.g., NAT box itself has not obtained a DHCP lease"
pmpCodeOutOfResources pmpResultCode = 4
pmpCodeUnsupportedOpcode pmpResultCode = 5
)
func buildPMPRequestMappingPacket(localPort, prevPort uint16, lifetimeSec uint32) (pkt []byte) {
pkt = make([]byte, 12)
pkt[1] = pmpOpMapUDP
binary.BigEndian.PutUint16(pkt[4:], localPort)
binary.BigEndian.PutUint16(pkt[6:], prevPort)
binary.BigEndian.PutUint32(pkt[8:], lifetimeSec)
return pkt
}
type pmpResponse struct {
OpCode uint8
ResultCode pmpResultCode
SecondsSinceEpoch uint32
// For Map ops:
MappingValidSeconds uint32
InternalPort uint16
ExternalPort uint16
// For public addr ops:
PublicAddr netaddr.IP
}
func parsePMPResponse(pkt []byte) (res pmpResponse, ok bool) {
if len(pkt) < 12 {
return
}
ver := pkt[0]
if ver != 0 {
return
}
res.OpCode = pkt[1]
res.ResultCode = pmpResultCode(binary.BigEndian.Uint16(pkt[2:]))
res.SecondsSinceEpoch = binary.BigEndian.Uint32(pkt[4:])
if res.OpCode == pmpOpReply|pmpOpMapUDP {
if len(pkt) != 16 {
return res, false
}
res.InternalPort = binary.BigEndian.Uint16(pkt[8:])
res.ExternalPort = binary.BigEndian.Uint16(pkt[10:])
res.MappingValidSeconds = binary.BigEndian.Uint32(pkt[12:])
}
if res.OpCode == pmpOpReply|pmpOpMapPublicAddr {
if len(pkt) != 12 {
return res, false
}
res.PublicAddr = netaddr.IPv4(pkt[8], pkt[9], pkt[10], pkt[11])
}
return res, true
}
type ProbeResult struct {
PCP bool
PMP bool
UPnP bool
}
// Probe returns a summary of which port mapping services are
// available on the network.
//
// If a probe has run recently and there haven't been any network changes since,
// the returned result might be server from the Client's cache, without
// sending any network traffic.
func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
gw, myIP, ok := c.gatewayAndSelfIP()
if !ok {
return res, ErrGatewayNotFound
}
defer func() {
if err == nil {
c.mu.Lock()
defer c.mu.Unlock()
c.lastProbe = time.Now()
}
}()
uc, err := netns.Listener().ListenPacket(context.Background(), "udp4", ":0")
if err != nil {
c.logf("ProbePCP: %v", err)
return res, err
}
defer uc.Close()
ctx, cancel := context.WithTimeout(ctx, 250*time.Millisecond)
defer cancel()
defer closeCloserOnContextDone(ctx, uc)()
pcpAddr := netaddr.IPPort{IP: gw, Port: pcpPort}.UDPAddr()
pmpAddr := netaddr.IPPort{IP: gw, Port: pmpPort}.UDPAddr()
upnpAddr := netaddr.IPPort{IP: gw, Port: upnpPort}.UDPAddr()
// Don't send probes to services that we recently learned (for
// the same gw/myIP) are available. See
// https://github.com/tailscale/tailscale/issues/1001
if c.sawPMPRecently() {
res.PMP = true
} else {
uc.WriteTo(pmpReqExternalAddrPacket, pmpAddr)
}
if c.sawPCPRecently() {
res.PCP = true
} else {
uc.WriteTo(pcpAnnounceRequest(myIP), pcpAddr)
}
if c.sawUPnPRecently() {
res.UPnP = true
} else {
uc.WriteTo(uPnPPacket, upnpAddr)
}
buf := make([]byte, 1500)
for {
if res.PCP && res.PMP && res.UPnP {
// Nothing more to discover.
return res, nil
}
n, addr, err := uc.ReadFrom(buf)
if err != nil {
if ctx.Err() == context.DeadlineExceeded {
err = nil
}
return res, err
}
port := addr.(*net.UDPAddr).Port
switch port {
case upnpPort:
if mem.Contains(mem.B(buf[:n]), mem.S(":InternetGatewayDevice:")) {
res.UPnP = true
c.mu.Lock()
c.uPnPSawTime = time.Now()
c.mu.Unlock()
}
case pcpPort: // same as pmpPort
if pres, ok := parsePCPResponse(buf[:n]); ok {
if pres.OpCode == pcpOpReply|pcpOpAnnounce && pres.ResultCode == pcpCodeOK {
c.logf("Got PCP response: epoch: %v", pres.Epoch)
res.PCP = true
c.mu.Lock()
c.pcpSawTime = time.Now()
c.mu.Unlock()
continue
}
c.logf("unexpected PCP probe response: %+v", pres)
}
if pres, ok := parsePMPResponse(buf[:n]); ok {
if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr && pres.ResultCode == pmpCodeOK {
c.logf("Got PMP response; IP: %v, epoch: %v", pres.PublicAddr, pres.SecondsSinceEpoch)
res.PMP = true
c.mu.Lock()
c.pmpPubIP = pres.PublicAddr
c.pmpPubIPTime = time.Now()
c.pmpLastEpoch = pres.SecondsSinceEpoch
c.mu.Unlock()
continue
}
c.logf("unexpected PMP probe response: %+v", pres)
}
}
}
}
const (
pcpVersion = 2
pcpPort = 5351
pcpCodeOK = 0
pcpOpReply = 0x80 // OR'd into request's op code on response
pcpOpAnnounce = 0
pcpOpMap = 1
)
// pcpAnnounceRequest generates a PCP packet with an ANNOUNCE opcode.
func pcpAnnounceRequest(myIP netaddr.IP) []byte {
// See https://tools.ietf.org/html/rfc6887#section-7.1
pkt := make([]byte, 24)
pkt[0] = pcpVersion // version
pkt[1] = pcpOpAnnounce
myIP16 := myIP.As16()
copy(pkt[8:], myIP16[:])
return pkt
}
//lint:ignore U1000 moved this code from netcheck's old PCP probing; will be needed when we add PCP mapping
// pcpMapRequest generates a PCP packet with a MAP opcode.
func pcpMapRequest(myIP netaddr.IP, mapToLocalPort int, delete bool) []byte {
const udpProtoNumber = 17
lifetimeSeconds := uint32(1)
if delete {
lifetimeSeconds = 0
}
const opMap = 1
// 24 byte header + 36 byte map opcode
pkt := make([]byte, (32+32+128)/8+(96+8+24+16+16+128)/8)
// The header (https://tools.ietf.org/html/rfc6887#section-7.1)
pkt[0] = 2 // version
pkt[1] = opMap
binary.BigEndian.PutUint32(pkt[4:8], lifetimeSeconds)
myIP16 := myIP.As16()
copy(pkt[8:], myIP16[:])
// The map opcode body (https://tools.ietf.org/html/rfc6887#section-11.1)
mapOp := pkt[24:]
rand.Read(mapOp[:12]) // 96 bit mappping nonce
mapOp[12] = udpProtoNumber
binary.BigEndian.PutUint16(mapOp[16:], uint16(mapToLocalPort))
v4unspec := netaddr.MustParseIP("0.0.0.0")
v4unspec16 := v4unspec.As16()
copy(mapOp[20:], v4unspec16[:])
return pkt
}
type pcpResponse struct {
OpCode uint8
ResultCode uint8
Lifetime uint32
Epoch uint32
}
func parsePCPResponse(b []byte) (res pcpResponse, ok bool) {
if len(b) < 24 || b[0] != pcpVersion {
return
}
res.OpCode = b[1]
res.ResultCode = b[3]
res.Lifetime = binary.BigEndian.Uint32(b[4:])
res.Epoch = binary.BigEndian.Uint32(b[8:])
return res, true
}
const (
upnpPort = 1900
)
var uPnPPacket = []byte("M-SEARCH * HTTP/1.1\r\n" +
"HOST: 239.255.255.250:1900\r\n" +
"ST: ssdp:all\r\n" +
"MAN: \"ssdp:discover\"\r\n" +
"MX: 2\r\n\r\n")
var pmpReqExternalAddrPacket = []byte{0, 0} // version 0, opcode 0 = "Public address request"

View File

@@ -0,0 +1,54 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package portmapper
import (
"context"
"os"
"strconv"
"testing"
"time"
)
func TestCreateOrGetMapping(t *testing.T) {
if v, _ := strconv.ParseBool(os.Getenv("HIT_NETWORK")); !v {
t.Skip("skipping test without HIT_NETWORK=1")
}
c := NewClient(t.Logf)
c.SetLocalPort(1234)
for i := 0; i < 2; i++ {
if i > 0 {
time.Sleep(100 * time.Millisecond)
}
ext, err := c.CreateOrGetMapping(context.Background())
t.Logf("Got: %v, %v", ext, err)
}
}
func TestClientProbe(t *testing.T) {
if v, _ := strconv.ParseBool(os.Getenv("HIT_NETWORK")); !v {
t.Skip("skipping test without HIT_NETWORK=1")
}
c := NewClient(t.Logf)
for i := 0; i < 2; i++ {
if i > 0 {
time.Sleep(100 * time.Millisecond)
}
res, err := c.Probe(context.Background())
t.Logf("Got: %+v, %v", res, err)
}
}
func TestClientProbeThenMap(t *testing.T) {
if v, _ := strconv.ParseBool(os.Getenv("HIT_NETWORK")); !v {
t.Skip("skipping test without HIT_NETWORK=1")
}
c := NewClient(t.Logf)
c.SetLocalPort(1234)
res, err := c.Probe(context.Background())
t.Logf("Probe: %+v, %v", res, err)
ext, err := c.CreateOrGetMapping(context.Background())
t.Logf("CreateOrGetMapping: %v, %v", ext, err)
}

View File

@@ -59,6 +59,7 @@ func runSTUN(t *testing.T, pc net.PacketConn, stats *stunStats, done chan<- stru
for {
n, addr, err := pc.ReadFrom(buf[:])
if err != nil {
// TODO: when we switch to Go 1.16, replace this with errors.Is(err, net.ErrClosed)
if strings.Contains(err.Error(), "closed network connection") {
t.Logf("STUN server shutdown")
return

View File

@@ -71,6 +71,16 @@ func GetAuthHeader(u *url.URL) (string, error) {
if fake := os.Getenv("TS_DEBUG_FAKE_PROXY_AUTH"); fake != "" {
return fake, nil
}
if user := u.User.Username(); user != "" {
pass, ok := u.User.Password()
if !ok {
return "", nil
}
req := &http.Request{Header: make(http.Header)}
req.SetBasicAuth(user, pass)
return req.Header.Get("Authorization"), nil
}
if sysAuthHeader != nil {
return sysAuthHeader(u)
}

View File

@@ -0,0 +1,53 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package tshttpproxy
import (
"net/url"
"runtime"
"strings"
"testing"
)
func TestGetAuthHeaderNoResult(t *testing.T) {
const proxyURL = "http://127.0.0.1:38274"
u, err := url.Parse(proxyURL)
if err != nil {
t.Fatalf("can't parse %q: %v", proxyURL, err)
}
got, err := GetAuthHeader(u)
if err != nil {
t.Fatalf("can't get auth header value: %v", err)
}
if runtime.GOOS == "windows" && strings.HasPrefix(got, "Negotiate") {
t.Logf("didn't get empty result, but got acceptable Windows Negotiate header")
return
}
if got != "" {
t.Fatalf("GetAuthHeader(%q) = %q; want empty string", proxyURL, got)
}
}
func TestGetAuthHeaderBasicAuth(t *testing.T) {
const proxyURL = "http://user:password@127.0.0.1:38274"
const want = "Basic dXNlcjpwYXNzd29yZA=="
u, err := url.Parse(proxyURL)
if err != nil {
t.Fatalf("can't parse %q: %v", proxyURL, err)
}
got, err := GetAuthHeader(u)
if err != nil {
t.Fatalf("can't get auth header value: %v", err)
}
if got != want {
t.Fatalf("GetAuthHeader(%q) = %q; want %q", proxyURL, got, want)
}
}

Some files were not shown because too many files have changed in this diff Show More