Compare commits

...

126 Commits

Author SHA1 Message Date
Brad Fitzpatrick
ea0ca7e721 ipn{,/ipnlocal}: in direct file receive mode, don't rename partial file
Let caller (macOS) do it so Finder progress bar can be dismissed
without races.

Updates tailscale/corp#1575

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-16 12:34:20 -07:00
Naman Sood
b85d80b37f net/tsaddr: add new IP range for ephemeral nodes in Tailscale ULA (#1715)
Signed-off-by: Naman Sood <mail@nsood.in>
2021-04-16 14:47:55 -04:00
Brad Fitzpatrick
b993d9802a ipn/ipnlocal, etc: require file sharing capability to send/recv files
tailscale/corp#1582

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-16 10:58:19 -07:00
Brad Fitzpatrick
2f422434aa cmd/tailscale/cli: wait on the right contexts in up
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-16 10:27:56 -07:00
Brad Fitzpatrick
6da812b4cf cmd/tailscale/cli: avoid a spammy log message on SIGINT
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-16 08:00:31 -07:00
Brad Fitzpatrick
670838c45f tailcfg, control/controlclient: (mapver 16) add Node.Online, MapResponse.OnlineChange
And fix PeerSeenChange bug where it was ignored unless there were
other peer changes.

Updates tailscale/corp#1574

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-15 20:07:24 -07:00
David Anderson
7055f870f8 control/controlclient: only use a single DNS label as the hostname.
Fixes #971

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-15 17:08:58 -07:00
Maisem Ali
4f3203556d wgengine/router: add the Tailscale ULA route on darwin.
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-04-15 17:07:50 -07:00
Ross Zurowski
c748c20fba cmd/tailscale: fix command descriptions (#1710)
Signed-off-by: Ross Zurowski <ross@rosszurowski.com>
2021-04-15 18:33:23 -04:00
Brad Fitzpatrick
b34fbb24e8 logtail: reduce PublicID.UnmarshalText from 2 allocs to 0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-15 10:42:12 -07:00
David Anderson
bb0710d51d net/dns: add debugging traces to DNS manager selection on linux.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-14 15:52:41 -07:00
David Anderson
4b70c7b717 net/dns: fix inverted test for NetworkManager.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-14 15:52:22 -07:00
David Anderson
4849a4d3c8 net/dns: error out on linux if /etc/resolv.conf can't be read.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-14 15:35:32 -07:00
David Anderson
1f9b73a531 net/dns: fix freebsd DNS manager selection.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-14 15:34:59 -07:00
Naman Sood
5ea53891fe cmd/tailscaled: populate netstack variable to use dialer in SOCKS5
Signed-off-by: Naman Sood <mail@nsood.in>
2021-04-14 13:13:10 -04:00
Brad Fitzpatrick
d6a95d807a ipn/ipnlocal: advertise netstack to control server
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-14 09:38:33 -07:00
AdamKorcz
2243bb48c2 stun fuzzer: Small fix
Signed-off-by: AdamKorcz <adam@adalogics.com>
2021-04-14 08:17:46 -07:00
Brad Fitzpatrick
75b99555f3 cmd/tailscale/cli: let ip take a peername
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-14 08:13:16 -07:00
Brad Fitzpatrick
762180595d ipn/ipnstate: add PeerStatus.TailscaleIPs slice, deprecate TailAddr
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-14 08:12:31 -07:00
Brad Fitzpatrick
c2ca2ac8c4 net/dns: fix FreeBSD build
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-13 17:38:17 -07:00
David Anderson
84bd50329a net/dns: fix staticheck.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-13 17:28:37 -07:00
David Anderson
d6bb11b5bf net/dns: implement correct manager detection on linux.
Part of #953.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-13 17:19:00 -07:00
David Anderson
9ef932517b net/dns: fix NM's GetBaseConfig when no configs exist.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-13 17:19:00 -07:00
David Anderson
fe3b1ab747 net/dns: refactor dbus connection setup in resolved manager.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-13 17:19:00 -07:00
Brad Fitzpatrick
2df6372b67 portlist: de-dup services on same (proto, port) on both IPv4/IPv6
Fixes #1703

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-13 14:40:33 -07:00
Brad Fitzpatrick
a8d95a18b2 cmd/tailscale/cli: add up --unattended for Windows
RELNOTE=Windows CLI behavior change: ForceDaemon now off by default

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-13 11:40:31 -07:00
Brad Fitzpatrick
34d2f5a3d9 tailcfg: add Endpoint, EndpointType, MapRequest.EndpointType
Track endpoints internally with a new tailcfg.Endpoint type that
includes a typed netaddr.IPPort (instead of just a string) and
includes a type for how that endpoint was discovered (STUN, local,
etc).

Use []tailcfg.Endpoint instead of []string internally.

At the last second, send it to the control server as the existing
[]string for endpoints, but also include a new parallel
MapRequest.EndpointType []tailcfg.EndpointType, so the control server
can start filtering out less-important endpoint changes from
new-enough clients. Notably, STUN-discovered endpoints can be filtered
out from 1.6+ clients, as they can discover them amongst each other
via CallMeMaybe disco exchanges started over DERP. And STUN endpoints
change a lot, causing a lot of MapResposne updates. But portmapped
endpoints are worth keeping for now, as they they work right away
without requiring the firewall traversal extra RTT dance.

End result will be less control->client bandwidth. (despite negligible
increase in client->control bandwidth)

Updates tailscale/corp#1543

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-13 10:12:14 -07:00
Brad Fitzpatrick
b91f3c4191 ipn/ipnlocal: fix peerapi printf arg mismatch
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-13 08:56:26 -07:00
Brad Fitzpatrick
a08d978476 cmd/tailscale/cli: make push get peerapi base via localapi, not TSMP ping
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-13 08:50:16 -07:00
Brad Fitzpatrick
1dc2cf4835 cmd/tailscale/cli: add push --targets to list possible targets
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-13 08:36:14 -07:00
Brad Fitzpatrick
1f4cf1a4f4 ipn/localapi: only require read access to list file targets
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-13 08:35:16 -07:00
Brad Fitzpatrick
d17f96b586 cmd/tailscale/cli: restore SIGINT/SIGTERM on context cancel
This fixes Ctrl-C not interrupting "tailscale push".

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-13 08:34:42 -07:00
Brad Fitzpatrick
db5e269463 client/tailscale/apitype: move local API types to new apitype package
They were scattered/duplicated in misc places before.

It can't be in the client package itself for circular dep reasons.

This new package is basically tailcfg but for localhost
communications, instead of to control.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-13 08:13:46 -07:00
Maisem Ali
1b9d8771dc ipn/ipnlocal,wgengine/router,cmd/tailscale: add flag to allow local lan access when routing traffic via an exit node.
For #1527

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-04-12 17:29:01 -07:00
David Anderson
854d5d36a1 net/dns: return error from NewOSManager, use it to initialize NM.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-12 15:51:37 -07:00
Maisem Ali
4d142ebe06 derp: handle net.ErrClosed in TestSendFreeze
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-04-12 15:23:36 -07:00
Brad Fitzpatrick
8e75c8504c ipn/ipnlocal: in direct file mode, don't readdir
And don't even allow attempts at Open/Delete.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 15:12:47 -07:00
Brad Fitzpatrick
9972c02b60 cmd/tailscale/cli: don't let up change prefs based on implicit flag values
This changes the behavior of "tailscale up".

Previously "tailscale up" always did a new Start and reset all the settings.

Now "tailscale up" with no flags just brings the world [back] up.
(The opposite of "tailscale down").

But with flags, "tailscale up" now only is allowed to change
preferences if they're explicitly named in the flags. Otherwise it's
an error. Or you need to use --reset to explicitly nuke everything.

RELNOTE=tailscale up change

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 14:39:47 -07:00
David Anderson
9aa33b43e6 net/dns: support split and unsplit DNS in NetworkManager.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-12 14:34:08 -07:00
Brad Fitzpatrick
f5742b0647 ipn/ipnlocal: add LocalBackend.SetDirectFileRoot
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 14:29:45 -07:00
Brad Fitzpatrick
64c80129f1 types/netmap: add some docs/warning to NetworkMap
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 12:49:25 -07:00
Brad Fitzpatrick
ccb322db04 tailcfg, control/controlclient: make nil MapResponse.DNSConfig mean unchanged (mapver15)
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 12:46:32 -07:00
Brad Fitzpatrick
a3113a793a ipn: add hostname to Prefs.Pretty output
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 10:45:33 -07:00
Brad Fitzpatrick
4c3f7c06fc ipn/ipnlocal: be consistent in not logging when no notify registered
Some paths already didn't. And in the future I hope to shut all the
notify funcs down end-to-end when nothing is connected (as in the
common case in tailscaled).  Then we can save some JSON encoding work.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 10:38:51 -07:00
Brad Fitzpatrick
7c0e58c537 ipn/ipnlocal: remove redundant notify nil check
send does it.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 10:38:51 -07:00
Brad Fitzpatrick
d9ee9a0d3f ipn: set BackendServer's notify earlier; don't require Start
We've been slowly making Start less special and making IPN a
multi-connection "watch" bus of changes, but this Start specialness
had remained.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 10:38:51 -07:00
Brad Fitzpatrick
8e4d1e3f2c ipn: include err in Notify decode fatal path
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 10:38:51 -07:00
Brad Fitzpatrick
d5d70ae9ea wgengine/monitor: reduce Linux log spam on down
Fixes #1689

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 10:38:51 -07:00
Brad Fitzpatrick
c0befee188 portlist: use windows OpenCurrentProcessToken, not GetCurrentProcessToken
The latter only works on Windows 8+.

Also add a TODO to get do this all more efficiently.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 09:23:52 -07:00
Brad Fitzpatrick
e619296ece portlist: filter out all of 127.0.0.0/8, not just 127.0.0.1/32
Per user private bug report.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 09:17:39 -07:00
Brad Fitzpatrick
f325aa7e38 portlist: exclude services bound to IPv6 loopback address
Fixes #1683

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-12 09:07:16 -07:00
David Anderson
87eb8384f5 net/dns: fix up NetworkManager configurator a bit.
Clear LLMNR and mdns flags, update reasoning for our settings,
and set our override priority harder than before when we want
to be primary resolver.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-11 23:19:50 -07:00
Brad Fitzpatrick
303805a389 ipn/localapi: require write access to PATCH prefs
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-11 21:31:20 -07:00
David Anderson
3d81e6260b net/dns: set resolved DefaultRoute setting according to split-dns mode.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-11 20:53:28 -07:00
David Anderson
cca230cc23 net/dns: fix staticcheck errors.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-11 20:53:28 -07:00
Brad Fitzpatrick
79109f4965 ipn/ipnlocal: use PATCH for EditPrefs, not POST
Addendum to earlier 00d641d9fc.

Reserve POST for SetPrefs in the future.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-11 20:49:07 -07:00
Brad Fitzpatrick
4b47393e0c net/dns: pacify staticcheck for now
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-11 20:43:15 -07:00
David Anderson
a7340c2015 net/dns: support split DNS in systemd-resolved.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-11 18:14:23 -07:00
Brad Fitzpatrick
00d641d9fc ipn/localapi: move EditPrefs to localapi
Follow-up/revision to recent 53cfff109b which
added EditPrefs.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-11 16:11:43 -07:00
David Anderson
84430cdfa1 net/dns: improve NetworkManager detection, using more DBus.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-11 15:22:06 -07:00
David Anderson
9a48bac8ad net/dns: rename resolvconf.go to debian_resolvconf.go.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-10 23:31:14 -07:00
David Anderson
9831f1b183 net/dns: also include 'tail' and 'base' files when fixing up resolv.conf.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-10 23:01:11 -07:00
David Anderson
e43afe9140 net/dns: implement prior config reading for debian resolvconf.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-10 22:37:13 -07:00
David Anderson
143e5dd087 net/dns: rename script variable.
Debian resolvconf is not legacy, it's alive and well,
just historically before the other implementations.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-10 21:28:27 -07:00
David Anderson
55b39fa945 net/dns: add documentation to openresolv's config fetch.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-10 20:21:05 -07:00
David Anderson
61b361bac0 net/dns: teach the openresolv manager to read DNS config.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-10 19:37:11 -07:00
David Anderson
19eca34f47 wgengine/router: fix FreeBSD configuration failure on the v6 /48.
On FreeBSD, we add the interface IP as a /48 to work around a kernel
bug, so we mustn't then try to add a /48 route to the Tailscale ULA,
since that will fail as a dupe.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-10 19:36:26 -07:00
David Anderson
58760f7b82 net/dns: split resolvconfManager into a debian and an openresolv manager.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-10 18:55:05 -07:00
David Anderson
5480189313 net/dns: implement a DNS override workaround for legacy resolvconf.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-10 17:58:13 -07:00
David Anderson
1a371b93be util/dnsname: add FQDN type, use throughout codebase.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-10 17:58:13 -07:00
AdamKorcz
7a1813fd24 Added 2 fuzzers
Signed-off-by: AdamKorcz <adam@adalogics.com>
2021-04-10 11:40:10 -07:00
Daniel Chung
5e90037f1a api.md: clarify response behaviour for ACL POST endpoint
Signed-off-by: Daniel Chung <daniel@tailscale.com>
2021-04-10 11:38:12 -07:00
Simon Kirillov
a64b57e2fb control/controlclient: create tls client config with server hostname instead of serverURL.Host
Signed-off-by: Simon Kirillov <svkirillov3@gmail.com>
2021-04-10 11:20:10 -07:00
Brad Fitzpatrick
958782c737 cmd/{tailscale,tailscaled}: use netstack for subnet routing on Synology
Updates #707
Fixes #451
Fixes tailscale/tailscale-synology#52 (just make it work by default)

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-09 18:44:33 -07:00
Brad Fitzpatrick
3b451509dd cmd/tailscale/cli: make advertise-{routes,exit-node} available unconditionally
It was only Linux and BSDs before, but now with netstack mode, it also works on
Windows and darwin. It's not worth limiting it to certain platforms.

Tailscaled itself can complain/fail if it doesn't like the settings
for the mode/OS it's operating under.

Updates #707

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-09 18:37:24 -07:00
Brad Fitzpatrick
83402e2753 cmd/tailscale/cli: show nicer status output when logged out
Also nicer output when running "down".

Fixes #1680

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-09 18:25:22 -07:00
Brad Fitzpatrick
5c5acadb2a portlist: unexport SameInodes method
Signed-off-by: Brad Fitzpatrick <brad@danga.com>
2021-04-09 15:16:36 -07:00
Brad Fitzpatrick
3167e55ddf ipn/{ipnlocal,localapi}, cmd/tailscale: add logout command
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-09 13:26:35 -07:00
Brad Fitzpatrick
11127666b2 ipn/ipnlocal: fix deadlock from 227f73284
Sigh.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-09 13:23:34 -07:00
Brad Fitzpatrick
227f73284f ipn/ipnlocal: eagerly announce peerapi service(s) on change
We were previously only doing it as a side effect of the port poller,
which doesn't run on e.g. iOS.

Updates tailscale/corp#1559
2021-04-09 12:10:52 -07:00
Brad Fitzpatrick
fe23506471 ipn/ipnlocal: avoid unneeded initPeerAPIListener work if no changes
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-09 11:13:46 -07:00
Brad Fitzpatrick
20e7646b8d ipn/ipnlocal: always set Notify.FilesWaiting, set IncomingFiles non-nil when empty 2021-04-09 07:59:36 -07:00
Brad Fitzpatrick
b0af15ff5c portlist: remove some old TODOs
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-09 07:50:00 -07:00
David Anderson
e638a4d86b net/dns: make directManager support split DNS, and work in sandboxes.
Fixes #1495, #683.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-09 02:52:21 -07:00
David Anderson
2685260ba1 net/dns: add temporary fallback to quad-9 resolver for split-DNS testing.
This allows split-DNS configurations to not break clients on OSes that
haven't yet been ported to understand split DNS, by falling back to quad-9
as a global resolver when handed an "impossible to implement"
split-DNS config.

Part of #953. Needs to be removed before shipping 1.8.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-08 23:26:22 -07:00
David Anderson
b9e194c14b net/dns: add missing FQDN qualification.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-08 23:07:16 -07:00
Brad Fitzpatrick
c50c3f0313 tailcfg: document new RegisterRequest.Expiry behavior
Deployed to control server.

For upcoming "logout" command and fixes.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-08 22:27:14 -07:00
David Anderson
b74a8994ca net/dns: make FQDN dot style consistent in more places.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-08 22:03:32 -07:00
Brad Fitzpatrick
6d01d3bece ipn/ipnlocal: provide IPN bus updates as files arrive
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-08 20:09:52 -07:00
David Anderson
2f398106e2 ipn/ipnlocal: allow setting MagicDNS without DefaultResolvers.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-08 16:23:33 -07:00
David Anderson
fad21af01c tailcfg: add DNS routes and advanced resolver config.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-08 15:29:08 -07:00
Brad Fitzpatrick
6a7912e37a cmd/tailscale/cli: add debug mode to push slowly for testing
Also set Content-Length when known, and fail explicitly on sending
directories for now.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-08 15:01:12 -07:00
Brad Fitzpatrick
a9a3d3b4c1 ipn/ipnlocal: don't filter by time in FileTargets
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-08 14:02:07 -07:00
David Anderson
6def647514 net/dns/resolver: don't avoid tailscale routes for DNS forwarding.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-08 12:20:42 -07:00
Brad Fitzpatrick
597c19ff4e control/controlclient: refactor some internals
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-07 21:20:17 -07:00
Brad Fitzpatrick
71432c6449 ipn/ipnlocal: some more variable renames
Missed in earlier commit.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-07 21:17:33 -07:00
Brad Fitzpatrick
e86b7752ef ipn/ipnlocal: rename some variables to be consistent
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-07 21:12:28 -07:00
David Anderson
4a64d2a603 net/dns: some post-review cleanups.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-07 15:40:31 -07:00
David Anderson
720c1ad0f0 net/dns: insert OS base config when emulating split DNS.
Part of #953.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-07 15:40:31 -07:00
David Anderson
e560be6443 net/dns: sort matchDomains to avoid test flake.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-07 15:40:31 -07:00
David Anderson
68f76e9aa1 net/dns: add GetBaseConfig to OSConfigurator interface.
Part of #953, required to make split DNS work on more basic
platforms.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-07 15:40:31 -07:00
David Anderson
fe9cd61d71 net/dns: add tests for DNS config generation.
Part of #953.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-07 15:40:31 -07:00
David Anderson
0ba6d03768 net/dns/resolver: add a test helper to get at the resolver config.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-07 15:40:31 -07:00
David Anderson
da4cc8bbb4 net/dns: handle all possible translations of high-level DNS config.
With this change, all OSes can sort-of do split DNS, except that the
default upstream is hardcoded to 8.8.8.8 pending further plumbing.
Additionally, Windows 8-10 can do split DNS fully correctly, without
the 8.8.8.8 hack.

Part of #953.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-07 15:40:31 -07:00
Brad Fitzpatrick
939861773d net/tstun: accept peerapi connections through the filter
Fixes tailscale/corp#1545

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-07 12:29:20 -07:00
Brad Fitzpatrick
950fc28887 ipn, paths, cmd/tailscaled: remove LegacyConfigPath, relaynode migration
It is time.

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-07 10:15:45 -07:00
Brad Fitzpatrick
d581ee2536 ipn: remove Options.HTTPTestClient, move to LocalBackend
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-07 09:20:51 -07:00
Brad Fitzpatrick
50b309c1eb ipn/localapi, cmd/tailscale: add API to get prefs, CLI debug command to show
Updates #1436

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-07 08:28:11 -07:00
Brad Fitzpatrick
03be116997 client/tailscale: factor out some helpers to reduce boilerplate
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-07 08:19:36 -07:00
Brad Fitzpatrick
d4b609e138 cmd/tailscale/cli: fix bug in earlier tailscale debug --local-creds addition
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-07 08:01:04 -07:00
Adrian Dewhurst
3f456ba2e7 control/controlclient: return correct certificate
When searching for the matching client identity, the returned
certificate chain was accidentally set to that of the last identity
returned by the certificate store instead of the one corresponding to
the selected identity.

Also, add some extra error checking for invalid certificate chains, just
in case.

Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2021-04-07 09:42:13 -04:00
Brad Fitzpatrick
799973a68d ipn: move Options.Notify to its own method
We already had SetNotifyCallback elsewhere on controlclient, so use
that name.

Baby steps towards some CLI refactor work.

Updates tailscale/tailscale#1436
2021-04-06 22:12:40 -07:00
Brad Fitzpatrick
d488678fdc cmd/tailscaled, wgengine{,/netstack}: add netstack hybrid mode, add to Windows
For #707

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-06 21:37:28 -07:00
Brad Fitzpatrick
1f99f889e1 ipn/{ipnlocal,localapi}: add localapi handler to dial/proxy file PUTs
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-06 21:31:50 -07:00
Denton Gentry
3089081349 monitor/polling: reduce Cloud Run polling interval.
Cloud Run's routes never change at runtime. Don't poll it for
route changes very often.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-04-06 17:21:16 -07:00
Denton Gentry
224e60cef2 hostifo: update LXC, add Cloud Run.
Recent LXC support no longer has "lxc" in /proc/1/cgroup:
    # cat /proc/1/cgroup
    12:freezer:/
    11:rdma:/
    10:cpuset:/
    9:pids:/
    8:blkio:/
    7:devices:/
    6:perf_event:/
    5:net_cls,net_prio:/
    4:memory:/
    3:hugetlb:/
    2:cpu,cpuacct:/
    1:name=systemd:/init.scope
    0::/init.scope

Look for fuse.lxcfs in /proc.mounts in addition:
    # grep lxc /proc/mounts
    lxcfs /proc/cpuinfo fuse.lxcfs ...
    lxcfs /proc/diskstats fuse.lxcfs ...
    lxcfs /proc/loadavg fuse.lxcfs ...
    lxcfs /proc/meminfo fuse.lxcfs ...
    lxcfs /proc/stat fuse.lxcfs ...
    lxcfs /proc/swaps fuse.lxcfs ...
    lxcfs /proc/uptime fuse.lxcfs ...
    lxcfs /sys/devices/system/cpu/online fuse.lxcfs ...

Add Knative detection by looking for the environment variables
which are part of its container contract.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-04-06 17:21:16 -07:00
Maisem Ali
57756ef673 net/nettest: make nettest.NewConn pass x/net/nettest.TestConn.
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-04-06 15:34:29 -07:00
David Anderson
e0e677a8f6 net/dns: split out search domains and match domains in OSConfig.
It seems that all the setups that support split DNS understand
this distinction, and it's an important one when translating
high-level configuration.

Part of #953.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-06 15:27:09 -07:00
David Anderson
a8dcda9c9a net/dns: start of compat hacks for Windows 7.
Correctly reports that Win7 cannot do split DNS, and has a helper to
discover the "base" resolvers for the system.

Part of #953

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-06 15:27:09 -07:00
Brad Fitzpatrick
ea9e68280d cmd/tailscale/cli: add debug command to print localapi curl command 2021-04-06 14:05:49 -07:00
Brad Fitzpatrick
d717499ac4 ipn/localapi: add API for getting file targets
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-04-06 11:01:30 -07:00
David Anderson
3e915ac783 net/dns: implement OS-level split DNS for Windows.
Part of #953.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-05 22:53:10 -07:00
David Anderson
c16a926bf2 net/dns: set OSConfig.Primary.
OS implementations are going to support split DNS soon.
Until they're all in place, hardcode Primary=true to get
the old behavior.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-05 22:53:10 -07:00
David Anderson
bc4381447f net/tstun: return the real interface name at device creation.
This is usually the same as the requested interface, but on some
unixes can vary based on device number allocation, and on Windows
it's the GUID instead of the pretty name, since everything relating
to configuration wants the GUID.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-05 22:53:10 -07:00
David Crawshaw
d2f838c058 ipn/localapi: 404 on bad endpoints
Confused us for a while!

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2021-04-05 15:09:54 -07:00
David Anderson
de6dc4c510 net/dns: add a Primary field to OSConfig.
Currently ignored.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-04-05 13:05:47 -07:00
108 changed files with 6079 additions and 1735 deletions

39
api.md
View File

@@ -367,10 +367,11 @@ Etag: "e0b2816b418b3f266309d94426ac7668ab3c1fa87798785bf82f1085cc2f6d9c"
#### `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.
Sets the ACL for the given domain.
HuJSON and JSON are both accepted inputs.
An `If-Match` header can be set to avoid missed updates.
Returns error for invalid ACLs.
Returns error if using an `If-Match` header and the ETag does not match.
Returns the updated ACL in JSON or HuJSON according to the `Accept` header on success. Otherwise, errors are returned for incorrectly defined ACLs, ACLs with failing tests on attempted updates, and mismatched `If-Match` header and ETag.
##### Parameters
@@ -380,7 +381,17 @@ Returns error if using an `If-Match` header and the ETag does not match.
`Accept` - Sets the return type of the updated ACL. Response is parsed `JSON` if `application/json` is explicitly named, otherwise HuJSON will be returned.
###### POST Body
ACL JSON or HuJSON (see https://tailscale.com/kb/1018/acls)
The POST body should be a JSON or [HuJSON](https://github.com/tailscale/hujson#hujson---human-json) formatted JSON object.
An ACL policy may contain the following top-level properties:
* `Groups` - Static groups of users which can be used for ACL rules.
* `Hosts` - Hostname aliases to use in place of IP addresses or subnets.
* `ACLs` - Access control lists.
* `TagOwners` - Defines who is allowed to use which tags.
* `Tests` - Run on ACL updates to check correct functionality of defined ACLs.
See https://tailscale.com/kb/1018/acls for more information on those properties.
##### Example
```
@@ -411,7 +422,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl' \
}'
```
Response
Response:
```
// Example/default ACLs for unrestricted connections.
{
@@ -436,9 +447,25 @@ Response
}
```
Failed test error response:
```
{
"message": "test(s) failed",
"data": [
{
"user": "user1@example.com",
"errors": [
"address \"user2@example.com:400\": want: Accept, got: Drop"
]
}
]
}
```
<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.
##### Parameters
@@ -477,7 +504,7 @@ curl 'https://api.tailscale.com/api/v2/tailnet/example.com/acl?user=user1@exampl
}'
```
Response
Response:
```
{"matches":[{"users":["*"],"ports":["*:*"],"lineNumber":19}],"user":"user1@example.com"}
```

View File

@@ -0,0 +1,29 @@
// 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 apitype contains types for the Tailscale local API.
package apitype
import "tailscale.com/tailcfg"
// WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler.
type WhoIsResponse struct {
Node *tailcfg.Node
UserProfile *tailcfg.UserProfile
}
// FileTarget is a node to which files can be sent, and the PeerAPI
// URL base to do so via.
type FileTarget struct {
Node *tailcfg.Node
// PeerAPI is the http://ip:port URL base of the node's peer API,
// without any path (not even a single slash).
PeerAPIURL string
}
type WaitingFile struct {
Name string
Size int64
}

View File

@@ -6,6 +6,7 @@
package tailscale
import (
"bytes"
"context"
"encoding/json"
"errors"
@@ -18,10 +19,11 @@ import (
"strconv"
"strings"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/tailcfg"
)
// TailscaledSocket is the tailscaled Unix socket.
@@ -64,9 +66,22 @@ func DoLocalRequest(req *http.Request) (*http.Response, error) {
return tsClient.Do(req)
}
// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port.
func WhoIs(ctx context.Context, remoteAddr string) (*tailcfg.WhoIsResponse, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr), nil)
type errorJSON struct {
Error string
}
// bestError returns either err, or if body contains a valid JSON
// object of type errorJSON, its non-empty error body.
func bestError(err error, body []byte) error {
var j errorJSON
if err := json.Unmarshal(body, &j); err == nil && j.Error != "" {
return errors.New(j.Error)
}
return err
}
func send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, method, "http://local-tailscaled.sock"+path, body)
if err != nil {
return nil, err
}
@@ -75,60 +90,48 @@ func WhoIs(ctx context.Context, remoteAddr string) (*tailcfg.WhoIsResponse, erro
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)
slurp, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
r := new(tailcfg.WhoIsResponse)
if err := json.Unmarshal(slurp, r); err != nil {
if max := 200; len(slurp) > max {
slurp = slurp[:max]
if res.StatusCode != wantStatus {
err := fmt.Errorf("HTTP %s: %s (expected %v)", res.Status, slurp, wantStatus)
return nil, bestError(err, slurp)
}
return slurp, nil
}
func get200(ctx context.Context, path string) ([]byte, error) {
return send(ctx, "GET", path, 200, nil)
}
// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port.
func WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
body, err := get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr))
if err != nil {
return nil, err
}
r := new(apitype.WhoIsResponse)
if err := json.Unmarshal(body, r); err != nil {
if max := 200; len(body) > max {
body = append(body[:max], "..."...)
}
return nil, fmt.Errorf("failed to parse JSON WhoIsResponse from %q", slurp)
return nil, fmt.Errorf("failed to parse JSON WhoIsResponse from %q", body)
}
return r, nil
}
// Goroutines returns a dump of the Tailscale daemon's current goroutines.
func Goroutines(ctx context.Context) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/goroutines", nil)
if err != nil {
return nil, err
}
res, err := DoLocalRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
return nil, fmt.Errorf("HTTP %s: %s", res.Status, body)
}
return body, nil
return get200(ctx, "/localapi/v0/goroutines")
}
// BugReport logs and returns a log marker that can be shared by the user with support.
func BugReport(ctx context.Context, note string) (string, error) {
u := fmt.Sprintf("http://local-tailscaled.sock/localapi/v0/bugreport?note=%s", url.QueryEscape(note))
req, err := http.NewRequestWithContext(ctx, "POST", u, nil)
body, err := send(ctx, "POST", "/localapi/v0/bugreport?note="+url.QueryEscape(note), 200, nil)
if err != nil {
return "", err
}
res, err := DoLocalRequest(req)
if err != nil {
return "", err
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return "", err
}
if res.StatusCode != 200 {
return "", fmt.Errorf("HTTP %s: %s", res.Status, body)
}
return strings.TrimSpace(string(body)), nil
}
@@ -143,67 +146,32 @@ func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
}
func status(ctx context.Context, queryString string) (*ipnstate.Status, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/status"+queryString, nil)
body, err := get200(ctx, "/localapi/v0/status"+queryString)
if err != nil {
return nil, err
}
res, err := DoLocalRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != 200 {
body, _ := ioutil.ReadAll(res.Body)
return nil, fmt.Errorf("HTTP %s: %s", res.Status, body)
}
st := new(ipnstate.Status)
if err := json.NewDecoder(res.Body).Decode(st); err != nil {
if err := json.Unmarshal(body, st); err != nil {
return nil, err
}
return st, nil
}
type WaitingFile struct {
Name string
Size int64
}
func WaitingFiles(ctx context.Context) ([]WaitingFile, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/files/", nil)
func WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
body, err := get200(ctx, "/localapi/v0/files/")
if err != nil {
return nil, err
}
res, err := DoLocalRequest(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != 200 {
body, _ := ioutil.ReadAll(res.Body)
return nil, fmt.Errorf("HTTP %s: %s", res.Status, body)
}
var wfs []WaitingFile
if err := json.NewDecoder(res.Body).Decode(&wfs); err != nil {
var wfs []apitype.WaitingFile
if err := json.Unmarshal(body, &wfs); err != nil {
return nil, err
}
return wfs, nil
}
func DeleteWaitingFile(ctx context.Context, baseName string) error {
req, err := http.NewRequestWithContext(ctx, "DELETE", "http://local-tailscaled.sock/localapi/v0/files/"+url.PathEscape(baseName), nil)
if err != nil {
return err
}
res, err := DoLocalRequest(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != http.StatusNoContent {
body, _ := ioutil.ReadAll(res.Body)
return fmt.Errorf("expected 204 No Content; got HTTP %s: %s", res.Status, body)
}
return nil
_, err := send(ctx, "DELETE", "/localapi/v0/files/"+url.PathEscape(baseName), http.StatusNoContent, nil)
return err
}
func GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, size int64, err error) {
@@ -227,25 +195,27 @@ func GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, siz
return res.Body, res.ContentLength, nil
}
func FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
body, err := get200(ctx, "/localapi/v0/file-targets")
if err != nil {
return nil, err
}
var fts []apitype.FileTarget
if err := json.Unmarshal(body, &fts); err != nil {
return nil, fmt.Errorf("invalid JSON: %w", err)
}
return fts, nil
}
func CheckIPForwarding(ctx context.Context) error {
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/check-ip-forwarding", nil)
body, err := get200(ctx, "/localapi/v0/check-ip-forwarding")
if err != nil {
return err
}
res, err := DoLocalRequest(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode != 200 {
body, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
return fmt.Errorf("HTTP %s: %s", res.Status, body)
}
var jres struct {
Warning string
}
if err := json.NewDecoder(res.Body).Decode(&jres); err != nil {
if err := json.Unmarshal(body, &jres); err != nil {
return fmt.Errorf("invalid JSON from check-ip-forwarding: %w", err)
}
if jres.Warning != "" {
@@ -253,3 +223,36 @@ func CheckIPForwarding(ctx context.Context) error {
}
return nil
}
func GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
body, err := get200(ctx, "/localapi/v0/prefs")
if err != nil {
return nil, err
}
var p ipn.Prefs
if err := json.Unmarshal(body, &p); err != nil {
return nil, fmt.Errorf("invalid prefs JSON: %w", err)
}
return &p, nil
}
func EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
mpj, err := json.Marshal(mp)
if err != nil {
return nil, err
}
body, err := send(ctx, "PATCH", "/localapi/v0/prefs", http.StatusOK, bytes.NewReader(mpj))
if err != nil {
return nil, err
}
var p ipn.Prefs
if err := json.Unmarshal(body, &p); err != nil {
return nil, fmt.Errorf("invalid prefs JSON: %w", err)
}
return &p, nil
}
func Logout(ctx context.Context) error {
_, err := send(ctx, "POST", "/localapi/v0/logout", http.StatusNoContent, nil)
return err
}

View File

@@ -18,7 +18,7 @@ import (
"strings"
"tailscale.com/client/tailscale"
"tailscale.com/tailcfg"
"tailscale.com/client/tailscale/apitype"
)
var (
@@ -107,7 +107,7 @@ type tmplData struct {
IP string // "100.2.3.4"
}
func tailscaleIP(who *tailcfg.WhoIsResponse) string {
func tailscaleIP(who *apitype.WhoIsResponse) string {
if who == nil {
return ""
}

View File

@@ -24,6 +24,7 @@ import (
"tailscale.com/ipn"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/syncs"
)
// ActLikeCLI reports whether a GUI application should act like the
@@ -64,6 +65,7 @@ change in the future.
Subcommands: []*ffcli.Command{
upCmd,
downCmd,
logoutCmd,
netcheckCmd,
ipCmd,
statusCmd,
@@ -108,6 +110,8 @@ var rootArgs struct {
socket string
}
var gotSignal syncs.AtomicBool
func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) {
c, err := safesocket.Connect(rootArgs.socket, 41112)
if err != nil {
@@ -125,7 +129,14 @@ func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context
go func() {
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
<-interrupt
select {
case <-interrupt:
case <-ctx.Done():
// Context canceled elsewhere.
signal.Reset(syscall.SIGINT, syscall.SIGTERM)
return
}
gotSignal.Set(true)
c.Close()
cancel()
}()
@@ -143,7 +154,9 @@ func pump(ctx context.Context, bc *ipn.BackendClient, conn net.Conn) {
if ctx.Err() != nil {
return
}
log.Printf("ReadMsg: %v\n", err)
if !gotSignal.Get() {
log.Printf("ReadMsg: %v\n", err)
}
break
}
bc.GotNotifyMsg(msg)

View File

@@ -0,0 +1,117 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package cli
import (
"flag"
"testing"
"tailscale.com/ipn"
)
// Test that checkForAccidentalSettingReverts's updateMaskedPrefsFromUpFlag can handle
// all flags. This will panic if a new flag creeps in that's unhandled.
func TestUpdateMaskedPrefsFromUpFlag(t *testing.T) {
mp := new(ipn.MaskedPrefs)
upFlagSet.VisitAll(func(f *flag.Flag) {
updateMaskedPrefsFromUpFlag(mp, f.Name)
})
}
func TestCheckForAccidentalSettingReverts(t *testing.T) {
f := func(flags ...string) map[string]bool {
m := make(map[string]bool)
for _, f := range flags {
m[f] = true
}
return m
}
tests := []struct {
name string
flagSet map[string]bool
curPrefs *ipn.Prefs
mp *ipn.MaskedPrefs
want string
}{
{
name: "bare_up_means_up",
flagSet: f(),
curPrefs: &ipn.Prefs{
WantRunning: false,
Hostname: "foo",
},
mp: &ipn.MaskedPrefs{
Prefs: ipn.Prefs{
WantRunning: true,
},
WantRunningSet: true,
},
want: "",
},
{
name: "losing_hostname",
flagSet: f("accept-dns"),
curPrefs: &ipn.Prefs{
WantRunning: false,
Hostname: "foo",
CorpDNS: true,
},
mp: &ipn.MaskedPrefs{
Prefs: ipn.Prefs{
WantRunning: true,
CorpDNS: true,
},
WantRunningSet: true,
CorpDNSSet: true,
},
want: `'tailscale up' without --reset requires all preferences with changing values to be explicitly mentioned; --hostname is not specified but its default value of "" differs from current value "foo"`,
},
{
name: "hostname_changing_explicitly",
flagSet: f("hostname"),
curPrefs: &ipn.Prefs{
WantRunning: false,
Hostname: "foo",
},
mp: &ipn.MaskedPrefs{
Prefs: ipn.Prefs{
WantRunning: true,
Hostname: "bar",
},
WantRunningSet: true,
HostnameSet: true,
},
want: "",
},
{
name: "hostname_changing_empty_explicitly",
flagSet: f("hostname"),
curPrefs: &ipn.Prefs{
WantRunning: false,
Hostname: "foo",
},
mp: &ipn.MaskedPrefs{
Prefs: ipn.Prefs{
WantRunning: true,
Hostname: "",
},
WantRunningSet: true,
HostnameSet: true,
},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var got string
if err := checkForAccidentalSettingReverts(tt.flagSet, tt.curPrefs, tt.mp); err != nil {
got = err.Error()
}
if got != tt.want {
t.Errorf("unexpected result\n got: %s\nwant: %s\n", got, tt.want)
}
})
}
}

View File

@@ -13,11 +13,14 @@ import (
"io"
"log"
"os"
"runtime"
"strings"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/paths"
"tailscale.com/safesocket"
)
var debugCmd = &ffcli.Command{
@@ -27,23 +30,55 @@ var debugCmd = &ffcli.Command{
fs := flag.NewFlagSet("debug", flag.ExitOnError)
fs.BoolVar(&debugArgs.goroutines, "daemon-goroutines", false, "If true, dump the tailscaled daemon's goroutines")
fs.BoolVar(&debugArgs.ipn, "ipn", false, "If true, subscribe to IPN notifications")
fs.BoolVar(&debugArgs.prefs, "prefs", false, "If true, dump active prefs")
fs.BoolVar(&debugArgs.pretty, "pretty", false, "If true, pretty-print output (for --prefs)")
fs.BoolVar(&debugArgs.netMap, "netmap", true, "whether to include netmap in --ipn mode")
fs.BoolVar(&debugArgs.localCreds, "local-creds", false, "print how to connect to local tailscaled")
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
return fs
})(),
}
var debugArgs struct {
localCreds bool
goroutines bool
ipn bool
netMap bool
file string
prefs bool
pretty bool
}
func runDebug(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("unknown arguments")
}
if debugArgs.localCreds {
port, token, err := safesocket.LocalTCPPortAndToken()
if err == nil {
fmt.Printf("curl -u:%s http://localhost:%d/localapi/v0/status\n", token, port)
return nil
}
if runtime.GOOS == "windows" {
fmt.Printf("curl http://localhost:41112/localapi/v0/status\n")
return nil
}
fmt.Printf("curl --unix-socket %s http://foo/localapi/v0/status\n", paths.DefaultTailscaledSocket())
return nil
}
if debugArgs.prefs {
prefs, err := tailscale.GetPrefs(ctx)
if err != nil {
return err
}
if debugArgs.pretty {
fmt.Println(prefs.Pretty())
} else {
j, _ := json.MarshalIndent(prefs, "", "\t")
fmt.Println(string(j))
}
return nil
}
if debugArgs.goroutines {
goroutines, err := tailscale.Goroutines(ctx)
if err != nil {

View File

@@ -8,7 +8,7 @@ import (
"context"
"fmt"
"log"
"time"
"os"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/client/tailscale"
@@ -33,39 +33,14 @@ func runDown(ctx context.Context, args []string) error {
return fmt.Errorf("error fetching current status: %w", err)
}
if st.BackendState == "Stopped" {
log.Printf("already stopped")
fmt.Fprintf(os.Stderr, "Tailscale was already stopped.\n")
return nil
}
log.Printf("was in state %q", st.BackendState)
c, bc, ctx, cancel := connect(ctx)
defer cancel()
timer := time.AfterFunc(5*time.Second, func() {
log.Fatalf("timeout running stop")
})
defer timer.Stop()
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.ErrMessage != nil {
log.Fatal(*n.ErrMessage)
}
if n.State != nil {
log.Printf("now in state %q", *n.State)
if *n.State == ipn.Stopped {
cancel()
}
return
}
})
bc.EditPrefs(&ipn.MaskedPrefs{
_, err = tailscale.EditPrefs(ctx, &ipn.MaskedPrefs{
Prefs: ipn.Prefs{
WantRunning: false,
},
WantRunningSet: true,
})
pump(ctx, bc, c)
return nil
return err
}

View File

@@ -11,13 +11,16 @@ import (
"fmt"
"github.com/peterbourgon/ff/v2/ffcli"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/ipn/ipnstate"
)
var ipCmd = &ffcli.Command{
Name: "ip",
ShortUsage: "ip [-4] [-6]",
ShortHelp: "Show this machine's current Tailscale IP address(es)",
ShortUsage: "ip [-4] [-6] [peername]",
ShortHelp: "Show current Tailscale IP address(es)",
LongHelp: "Shows the Tailscale IP address of the current machine without an argument. With an argument, it shows the IP of a named peer.",
Exec: runIP,
FlagSet: (func() *flag.FlagSet {
fs := flag.NewFlagSet("ip", flag.ExitOnError)
@@ -33,9 +36,14 @@ var ipArgs struct {
}
func runIP(ctx context.Context, args []string) error {
if len(args) > 0 {
if len(args) > 1 {
return errors.New("unknown arguments")
}
var of string
if len(args) == 1 {
of = args[0]
}
v4, v6 := ipArgs.want4, ipArgs.want6
if v4 && v6 {
return errors.New("tailscale up -4 and -6 are mutually exclusive")
@@ -47,11 +55,24 @@ func runIP(ctx context.Context, args []string) error {
if err != nil {
return err
}
if len(st.TailscaleIPs) == 0 {
ips := st.TailscaleIPs
if of != "" {
ip, err := tailscaleIPFromArg(ctx, of)
if err != nil {
return err
}
peer, ok := peerMatchingIP(st, ip)
if !ok {
return fmt.Errorf("no peer found with IP %v", ip)
}
ips = peer.TailscaleIPs
}
if len(ips) == 0 {
return fmt.Errorf("no current Tailscale IPs; state: %v", st.BackendState)
}
match := false
for _, ip := range st.TailscaleIPs {
for _, ip := range ips {
if ip.Is4() && v4 || ip.Is6() && v6 {
match = true
fmt.Println(ip)
@@ -67,3 +88,18 @@ func runIP(ctx context.Context, args []string) error {
}
return nil
}
func peerMatchingIP(st *ipnstate.Status, ipStr string) (ps *ipnstate.PeerStatus, ok bool) {
ip, err := netaddr.ParseIP(ipStr)
if err != nil {
return
}
for _, ps = range st.Peer {
for _, pip := range ps.TailscaleIPs {
if ip == pip {
return ps, true
}
}
}
return nil, false
}

View File

@@ -0,0 +1,34 @@
// 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 cli
import (
"context"
"log"
"strings"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/client/tailscale"
)
var logoutCmd = &ffcli.Command{
Name: "logout",
ShortUsage: "logout [flags]",
ShortHelp: "Disconnect from Tailscale and expire current node key",
LongHelp: strings.TrimSpace(`
"tailscale logout" brings the network down and invalidates
the current node key, forcing a future use of it to cause
a reauthentication.
`),
Exec: runLogout,
}
func runLogout(ctx context.Context, args []string) error {
if len(args) > 0 {
log.Fatalf("too many non-flag arguments: %q", args)
}
return tailscale.Logout(ctx)
}

View File

@@ -154,7 +154,10 @@ func tailscaleIPFromArg(ctx context.Context, hostOrIP string) (ip string, err er
}
for _, ps := range st.Peer {
if hostOrIP == dnsOrQuoteHostname(st, ps) || hostOrIP == ps.DNSName {
return ps.TailAddr, nil
if len(ps.TailscaleIPs) == 0 {
return "", errors.New("node found but lacks an IP")
}
return ps.TailscaleIPs[0].String(), nil
}
}

View File

@@ -13,16 +13,17 @@ import (
"io"
"log"
"mime"
"net"
"net/http"
"net/url"
"os"
"strconv"
"time"
"unicode/utf8"
"github.com/peterbourgon/ff/v2/ffcli"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"golang.org/x/time/rate"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
)
var pushCmd = &ffcli.Command{
@@ -34,6 +35,7 @@ var pushCmd = &ffcli.Command{
fs := flag.NewFlagSet("push", flag.ExitOnError)
fs.StringVar(&pushArgs.name, "name", "", "alternate filename to use, especially useful when <file> is \"-\" (stdin)")
fs.BoolVar(&pushArgs.verbose, "verbose", false, "verbose output")
fs.BoolVar(&pushArgs.targets, "targets", false, "list possible push targets")
return fs
})(),
}
@@ -41,11 +43,15 @@ var pushCmd = &ffcli.Command{
var pushArgs struct {
name string
verbose bool
targets bool
}
func runPush(ctx context.Context, args []string) error {
if pushArgs.targets {
return runPushTargets(ctx, args)
}
if len(args) != 2 || args[0] == "" {
return errors.New("usage: push <hostname-or-IP> <file>")
return errors.New("usage: push <hostname-or-IP> <file>\n push --targets")
}
var ip string
@@ -55,13 +61,18 @@ func runPush(ctx context.Context, args []string) error {
return err
}
peerAPIPort, err := discoverPeerAPIPort(ctx, ip)
peerAPIBase, lastSeen, err := discoverPeerAPIBase(ctx, ip)
if err != nil {
return err
}
if !lastSeen.IsZero() && time.Since(lastSeen) > lastSeenOld {
fmt.Fprintf(os.Stderr, "# warning: %s last seen %v ago\n", hostOrIP, time.Since(lastSeen).Round(time.Minute))
}
var fileContents io.Reader
var name = pushArgs.name
var contentLength int64 = -1
if fileArg == "-" {
fileContents = os.Stdin
if name == "" {
@@ -76,17 +87,30 @@ func runPush(ctx context.Context, args []string) error {
return err
}
defer f.Close()
fileContents = f
fi, err := f.Stat()
if err != nil {
return err
}
if fi.IsDir() {
return errors.New("directories not supported")
}
contentLength = fi.Size()
fileContents = io.LimitReader(f, contentLength)
if name == "" {
name = fileArg
}
if slow, _ := strconv.ParseBool(os.Getenv("TS_DEBUG_SLOW_PUSH")); slow {
fileContents = &slowReader{r: fileContents}
}
}
dstURL := "http://" + net.JoinHostPort(ip, fmt.Sprint(peerAPIPort)) + "/v0/put/" + url.PathEscape(name)
dstURL := peerAPIBase + "/v0/put/" + url.PathEscape(name)
req, err := http.NewRequestWithContext(ctx, "PUT", dstURL, fileContents)
if err != nil {
return err
}
req.ContentLength = contentLength
if pushArgs.verbose {
log.Printf("sending to %v ...", dstURL)
}
@@ -102,51 +126,28 @@ func runPush(ctx context.Context, args []string) error {
return errors.New(res.Status)
}
func discoverPeerAPIPort(ctx context.Context, ip string) (port uint16, err error) {
c, bc, ctx, cancel := connect(ctx)
defer cancel()
prc := make(chan *ipnstate.PingResult, 2)
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.ErrMessage != nil {
log.Fatal(*n.ErrMessage)
}
if pr := n.PingResult; pr != nil && pr.IP == ip {
prc <- pr
}
})
go pump(ctx, bc, c)
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
discoPings := 0
timer := time.NewTimer(10 * time.Second)
defer timer.Stop()
sendPings := func() {
bc.Ping(ip, false)
bc.Ping(ip, true)
func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, lastSeen time.Time, err error) {
ip, err := netaddr.ParseIP(ipStr)
if err != nil {
return "", time.Time{}, err
}
sendPings()
for {
select {
case <-ticker.C:
sendPings()
case <-timer.C:
return 0, fmt.Errorf("timeout contacting %v; it offline?", ip)
case pr := <-prc:
if p := pr.PeerAPIPort; p != 0 {
return p, nil
fts, err := tailscale.FileTargets(ctx)
if err != nil {
return "", time.Time{}, err
}
for _, ft := range fts {
n := ft.Node
for _, a := range n.Addresses {
if a.IP != ip {
continue
}
discoPings++
if discoPings == 3 {
return 0, fmt.Errorf("%v is online, but seems to be running an old Tailscale version", ip)
if n.LastSeen != nil {
lastSeen = *n.LastSeen
}
case <-ctx.Done():
return 0, ctx.Err()
return ft.PeerAPIURL, lastSeen, nil
}
}
return "", time.Time{}, errors.New("target seems to be running an old Tailscale version")
}
const maxSniff = 4 << 20
@@ -171,3 +172,47 @@ func pickStdinFilename() (name string, r io.Reader, err error) {
}
return "stdin" + ext(sniff), io.MultiReader(bytes.NewReader(sniff), os.Stdin), nil
}
type slowReader struct {
r io.Reader
rl *rate.Limiter
}
func (r *slowReader) Read(p []byte) (n int, err error) {
const burst = 4 << 10
plen := len(p)
if plen > burst {
plen = burst
}
if r.rl == nil {
r.rl = rate.NewLimiter(rate.Limit(1<<10), burst)
}
n, err = r.r.Read(p[:plen])
r.rl.WaitN(context.Background(), n)
return
}
const lastSeenOld = 20 * time.Minute
func runPushTargets(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("invalid arguments with --targets")
}
fts, err := tailscale.FileTargets(ctx)
if err != nil {
return err
}
for _, ft := range fts {
n := ft.Node
var ago string
if n.LastSeen == nil {
ago = "\tnode never seen"
} else {
if d := time.Since(*n.LastSeen); d > lastSeenOld {
ago = fmt.Sprintf("\tlast seen %v ago", d.Round(time.Minute))
}
}
fmt.Printf("%s\t%s%s\n", n.Addresses[0].IP, n.ComputedName, ago)
}
return nil
}

View File

@@ -18,6 +18,7 @@ import (
"github.com/peterbourgon/ff/v2/ffcli"
"github.com/toqueteos/webbrowser"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
@@ -106,9 +107,24 @@ func runStatus(ctx context.Context, args []string) error {
return err
}
if st.BackendState == ipn.Stopped.String() {
switch st.BackendState {
default:
fmt.Fprintf(os.Stderr, "unexpected state: %s\n", st.BackendState)
os.Exit(1)
case ipn.Stopped.String():
fmt.Println("Tailscale is stopped.")
os.Exit(1)
case ipn.NeedsLogin.String():
fmt.Println("Logged out.")
if st.AuthURL != "" {
fmt.Printf("\nLog in at: %s\n", st.AuthURL)
}
os.Exit(1)
case ipn.NeedsMachineAuth.String():
fmt.Println("Machine is not yet authorized by tailnet admin.")
os.Exit(1)
case ipn.Running.String():
// Run below.
}
var buf bytes.Buffer
@@ -116,7 +132,7 @@ func runStatus(ctx context.Context, args []string) error {
printPS := func(ps *ipnstate.PeerStatus) {
active := peerActive(ps)
f("%-15s %-20s %-12s %-7s ",
ps.TailAddr,
firstIPString(ps.TailscaleIPs),
dnsOrQuoteHostname(st, ps),
ownerLogin(st, ps),
ps.OS,
@@ -201,3 +217,10 @@ func ownerLogin(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
}
return u.LoginName
}
func firstIPString(v []netaddr.IP) string {
if len(v) == 0 {
return ""
}
return v[0].String()
}

View File

@@ -9,13 +9,15 @@ import (
"errors"
"flag"
"fmt"
"log"
"os"
"reflect"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"github.com/go-multierror/multierror"
"github.com/peterbourgon/ff/v2/ffcli"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
@@ -28,40 +30,53 @@ import (
var upCmd = &ffcli.Command{
Name: "up",
ShortUsage: "up [flags]",
ShortHelp: "Connect to your Tailscale network",
ShortHelp: "Connect to Tailscale, logging in if needed",
LongHelp: strings.TrimSpace(`
"tailscale up" connects this machine to your Tailscale network,
triggering authentication if necessary.
The flags passed to this command are specific to this machine. If you don't
specify any flags, options are reset to their default.
With no flags, "tailscale up" brings the network online without
changing any settings. (That is, it's the opposite of "tailscale
down").
If flags are specified, the flags must be the complete set of desired
settings. An error is returned if any setting would be changed as a
result of an unspecified flag's default value, unless the --reset
flag is also used.
`),
FlagSet: (func() *flag.FlagSet {
upf := flag.NewFlagSet("up", flag.ExitOnError)
upf.StringVar(&upArgs.server, "login-server", "https://login.tailscale.com", "base URL of control server")
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "install host routes to other Tailscale nodes")
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. \"tag:eng,tag:montreal,tag:ssh\")")
upf.StringVar(&upArgs.authKey, "authkey", "", "node authorization key")
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
if runtime.GOOS == "linux" || isBSD(runtime.GOOS) {
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. \"10.0.0.0/8,192.168.0.0/24\")")
upf.BoolVar(&upArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
}
if runtime.GOOS == "linux" {
upf.BoolVar(&upArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
upf.StringVar(&upArgs.netfilterMode, "netfilter-mode", defaultNetfilterMode(), "netfilter mode (one of on, nodivert, off)")
}
return upf
})(),
Exec: runUp,
FlagSet: upFlagSet,
Exec: runUp,
}
var upFlagSet = (func() *flag.FlagSet {
upf := flag.NewFlagSet("up", flag.ExitOnError)
upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication")
upf.BoolVar(&upArgs.reset, "reset", false, "reset unspecified settings to their default values")
upf.StringVar(&upArgs.server, "login-server", "https://login.tailscale.com", "base URL of control server")
upf.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.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "comma-separated ACL tags to request; each must start with \"tag:\" (e.g. \"tag:eng,tag:montreal,tag:ssh\")")
upf.StringVar(&upArgs.authKey, "authkey", "", "node authorization key")
upf.StringVar(&upArgs.hostname, "hostname", "", "hostname to use instead of the one provided by the OS")
upf.StringVar(&upArgs.advertiseRoutes, "advertise-routes", "", "routes to advertise to other nodes (comma-separated, e.g. \"10.0.0.0/8,192.168.0.0/24\")")
upf.BoolVar(&upArgs.advertiseDefaultRoute, "advertise-exit-node", false, "offer to be an exit node for internet traffic for the tailnet")
if runtime.GOOS == "linux" {
upf.BoolVar(&upArgs.snat, "snat-subnet-routes", true, "source NAT traffic to local routes advertised with --advertise-routes")
upf.StringVar(&upArgs.netfilterMode, "netfilter-mode", defaultNetfilterMode(), "netfilter mode (one of on, nodivert, off)")
}
if runtime.GOOS == "windows" {
upf.BoolVar(&upArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
}
return upf
})()
func defaultNetfilterMode() string {
if distro.Get() == distro.Synology {
return "off"
@@ -70,24 +85,23 @@ func defaultNetfilterMode() string {
}
var upArgs struct {
server string
acceptRoutes bool
acceptDNS bool
singleRoutes bool
exitNodeIP string
shieldsUp bool
forceReauth bool
advertiseRoutes string
advertiseDefaultRoute bool
advertiseTags string
snat bool
netfilterMode string
authKey string
hostname string
}
func isBSD(s string) bool {
return s == "dragonfly" || s == "freebsd" || s == "netbsd" || s == "openbsd"
reset bool
server string
acceptRoutes bool
acceptDNS bool
singleRoutes bool
exitNodeIP string
exitNodeAllowLANAccess bool
shieldsUp bool
forceReauth bool
forceDaemon bool
advertiseRoutes string
advertiseDefaultRoute bool
advertiseTags string
snat bool
netfilterMode string
authKey string
hostname string
}
func warnf(format string, args ...interface{}) {
@@ -101,14 +115,16 @@ var (
func runUp(ctx context.Context, args []string) error {
if len(args) > 0 {
log.Fatalf("too many non-flag arguments: %q", args)
fatalf("too many non-flag arguments: %q", args)
}
st, err := tailscale.Status(ctx)
if err != nil {
fatalf("can't fetch status from tailscaled: %v", err)
}
if distro.Get() == distro.Synology {
notSupported := "not yet supported on Synology; see https://github.com/tailscale/tailscale/issues/451"
if upArgs.advertiseRoutes != "" {
return errors.New("--advertise-routes is " + notSupported)
}
if upArgs.acceptRoutes {
return errors.New("--accept-routes is " + notSupported)
}
@@ -172,6 +188,16 @@ func runUp(ctx context.Context, args []string) error {
if err != nil {
fatalf("invalid IP address %q for --exit-node: %v", upArgs.exitNodeIP, err)
}
} else if upArgs.exitNodeAllowLANAccess {
fatalf("--exit-node-allow-lan-access can only be used with --exit-node")
}
if !exitNodeIP.IsZero() {
for _, ip := range st.TailscaleIPs {
if exitNodeIP == ip {
fatalf("cannot use %s as the exit node as it is a local IP address to this machine, did you mean --advertise-exit-node?", exitNodeIP)
}
}
}
var tags []string
@@ -194,6 +220,7 @@ func runUp(ctx context.Context, args []string) error {
prefs.WantRunning = true
prefs.RouteAll = upArgs.acceptRoutes
prefs.ExitNodeIP = exitNodeIP
prefs.ExitNodeAllowLANAccess = upArgs.exitNodeAllowLANAccess
prefs.CorpDNS = upArgs.acceptDNS
prefs.AllowSingleHosts = upArgs.singleRoutes
prefs.ShieldsUp = upArgs.shieldsUp
@@ -201,7 +228,7 @@ func runUp(ctx context.Context, args []string) error {
prefs.AdvertiseTags = tags
prefs.NoSNAT = !upArgs.snat
prefs.Hostname = upArgs.hostname
prefs.ForceDaemon = (runtime.GOOS == "windows")
prefs.ForceDaemon = upArgs.forceDaemon
if runtime.GOOS == "linux" {
switch upArgs.netfilterMode {
@@ -218,94 +245,290 @@ func runUp(ctx context.Context, args []string) error {
}
}
c, bc, ctx, cancel := connect(ctx)
defer cancel()
curPrefs, err := tailscale.GetPrefs(ctx)
if err != nil {
return err
}
if !prefs.ExitNodeIP.IsZero() {
st, err := tailscale.Status(ctx)
if err != nil {
fatalf("can't fetch status from tailscaled: %v", err)
}
for _, ip := range st.TailscaleIPs {
if prefs.ExitNodeIP == ip {
fatalf("cannot use %s as the exit node as it is a local IP address to this machine, did you mean --advertise-exit-node?", ip)
}
flagSet := map[string]bool{}
mp := new(ipn.MaskedPrefs)
mp.WantRunningSet = true
mp.Prefs = *prefs
upFlagSet.Visit(func(f *flag.Flag) {
updateMaskedPrefsFromUpFlag(mp, f.Name)
flagSet[f.Name] = true
})
if !upArgs.reset {
if err := checkForAccidentalSettingReverts(flagSet, curPrefs, mp); err != nil {
fatalf("%s", err)
}
}
var printed bool
controlURLChanged := curPrefs.ControlURL != prefs.ControlURL
if controlURLChanged && st.BackendState == ipn.Running.String() && !upArgs.forceReauth {
fatalf("can't change --login-server without --force-reauth")
}
// If we're already running and none of the flags require a
// restart, we can just do an EditPrefs call and change the
// prefs at runtime (e.g. changing hostname, changinged
// advertised tags, routes, etc)
justEdit := st.BackendState == ipn.Running.String() &&
!upArgs.forceReauth &&
!upArgs.reset &&
upArgs.authKey == "" &&
!controlURLChanged
if justEdit {
_, err := tailscale.EditPrefs(ctx, mp)
return err
}
// simpleUp is whether we're running a simple "tailscale up"
// to transition to running from a previously-logged-in but
// down state, without changing any settings.
simpleUp := len(flagSet) == 0 && curPrefs.Persist != nil && curPrefs.Persist.LoginName != ""
// At this point we need to subscribe to the IPN bus to watch
// for state transitions and possible need to authenticate.
c, bc, pumpCtx, cancel := connect(ctx)
defer cancel()
startingOrRunning := make(chan bool, 1) // gets value once starting or running
gotEngineUpdate := make(chan bool, 1) // gets value upon an engine update
go pump(pumpCtx, bc, c)
printed := !simpleUp
var loginOnce sync.Once
startLoginInteractive := func() { loginOnce.Do(func() { bc.StartLoginInteractive() }) }
bc.SetPrefs(prefs)
opts := ipn.Options{
StateKey: ipn.GlobalDaemonStateKey,
AuthKey: upArgs.authKey,
Notify: func(n ipn.Notify) {
if n.ErrMessage != nil {
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)
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.Engine != nil {
select {
case gotEngineUpdate <- true:
default:
}
if s := n.State; s != nil {
switch *s {
case ipn.NeedsLogin:
printed = true
startLoginInteractive()
case ipn.NeedsMachineAuth:
printed = true
fmt.Fprintf(os.Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s/admin/machines\n\n", upArgs.server)
case ipn.Starting, ipn.Running:
// Done full authentication process
if printed {
// Only need to print an update if we printed the "please click" message earlier.
fmt.Fprintf(os.Stderr, "Success.\n")
}
cancel()
}
if n.ErrMessage != nil {
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 [...]')"
}
}
if url := n.BrowseToURL; url != nil {
fmt.Fprintf(os.Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
fatalf("backend error: %v\n", msg)
}
if s := n.State; s != nil {
switch *s {
case ipn.NeedsLogin:
printed = true
startLoginInteractive()
case ipn.NeedsMachineAuth:
printed = true
fmt.Fprintf(os.Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s/admin/machines\n\n", upArgs.server)
case ipn.Starting, ipn.Running:
// Done full authentication process
if printed {
// Only need to print an update if we printed the "please click" message earlier.
fmt.Fprintf(os.Stderr, "Success.\n")
}
select {
case startingOrRunning <- true:
default:
}
cancel()
}
},
}
if url := n.BrowseToURL; url != nil {
printed = true
fmt.Fprintf(os.Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
}
})
// Wait for backend client to be connected so we know
// we're subscribed to updates. Otherwise we can miss
// an update upon its transition to running. Do so by causing some traffic
// back to the bus that we then wait on.
bc.RequestEngineStatus()
select {
case <-gotEngineUpdate:
case <-pumpCtx.Done():
return pumpCtx.Err()
}
// On Windows, we still run in mostly the "legacy" way that
// predated the server's StateStore. That is, we send an empty
// StateKey and send the prefs directly. Although the Windows
// supports server mode, though, the transition to StateStore
// is only half complete. Only server mode uses it, and the
// Windows service (~tailscaled) is the one that computes the
// StateKey based on the connection identity. So for now, just
// do as the Windows GUI's always done:
if runtime.GOOS == "windows" {
// The Windows service will set this as needed based
// on our connection's identity.
opts.StateKey = ""
opts.Prefs = prefs
}
// Special case: bare "tailscale up" means to just start
// running, if there's ever been a login.
if simpleUp {
_, err := tailscale.EditPrefs(ctx, &ipn.MaskedPrefs{
Prefs: ipn.Prefs{
WantRunning: true,
},
WantRunningSet: true,
})
if err != nil {
return err
}
} else {
bc.SetPrefs(prefs)
// We still have to Start right now because it's the only way to
// set up notifications and whatnot. This causes a bunch of churn
// every time the CLI touches anything.
//
// TODO(danderson): redo the frontend/backend API to assume
// ephemeral frontends that read/modify/write state, once
// Windows/Mac state is moved into backend.
bc.Start(opts)
if upArgs.forceReauth {
printed = true
opts := ipn.Options{
StateKey: ipn.GlobalDaemonStateKey,
AuthKey: upArgs.authKey,
}
// On Windows, we still run in mostly the "legacy" way that
// predated the server's StateStore. That is, we send an empty
// StateKey and send the prefs directly. Although the Windows
// supports server mode, though, the transition to StateStore
// is only half complete. Only server mode uses it, and the
// Windows service (~tailscaled) is the one that computes the
// StateKey based on the connection identity. So for now, just
// do as the Windows GUI's always done:
if runtime.GOOS == "windows" {
// The Windows service will set this as needed based
// on our connection's identity.
opts.StateKey = ""
opts.Prefs = prefs
}
bc.Start(opts)
startLoginInteractive()
}
pump(ctx, bc, c)
return nil
select {
case <-startingOrRunning:
return nil
case <-pumpCtx.Done():
select {
case <-startingOrRunning:
return nil
default:
}
return pumpCtx.Err()
}
}
var (
flagForPref = map[string]string{} // "ExitNodeIP" => "exit-node"
prefsOfFlag = map[string][]string{}
)
func init() {
addPrefFlagMapping("accept-dns", "CorpDNS")
addPrefFlagMapping("accept-routes", "RouteAll")
addPrefFlagMapping("advertise-routes", "AdvertiseRoutes")
addPrefFlagMapping("advertise-tags", "AdvertiseTags")
addPrefFlagMapping("host-routes", "AllowSingleHosts")
addPrefFlagMapping("hostname", "Hostname")
addPrefFlagMapping("login-server", "ControlURL")
addPrefFlagMapping("netfilter-mode", "NetfilterMode")
addPrefFlagMapping("shields-up", "ShieldsUp")
addPrefFlagMapping("snat-subnet-routes", "NoSNAT")
addPrefFlagMapping("exit-node", "ExitNodeIP", "ExitNodeIP")
addPrefFlagMapping("exit-node-allow-lan-access", "ExitNodeAllowLANAccess")
addPrefFlagMapping("unattended", "ForceDaemon")
}
func addPrefFlagMapping(flagName string, prefNames ...string) {
prefsOfFlag[flagName] = prefNames
prefType := reflect.TypeOf(ipn.Prefs{})
for _, pref := range prefNames {
flagForPref[pref] = flagName
// Crash at runtime if there's a typo in the prefName.
if _, ok := prefType.FieldByName(pref); !ok {
panic(fmt.Sprintf("invalid ipn.Prefs field %q", pref))
}
}
}
func updateMaskedPrefsFromUpFlag(mp *ipn.MaskedPrefs, flagName string) {
if prefs, ok := prefsOfFlag[flagName]; ok {
for _, pref := range prefs {
reflect.ValueOf(mp).Elem().FieldByName(pref + "Set").SetBool(true)
}
return
}
switch flagName {
case "authkey", "force-reauth", "reset":
// Not pref-related flags.
case "advertise-exit-node":
// This pref is a shorthand for advertise-routes.
default:
panic("internal error: unhandled flag " + flagName)
}
}
// checkForAccidentalSettingReverts checks for people running
// "tailscale up" with a subset of the flags they originally ran it
// with.
//
// For example, in Tailscale 1.6 and prior, a user might've advertised
// a tag, but later tried to change just one other setting and forgot
// to mention the tag later and silently wiped it out. We now
// require --reset to change preferences to flag default values when
// the flag is not mentioned on the command line.
//
// curPrefs is what's currently active on the server.
//
// mp is the mask of settings actually set, where mp.Prefs is the new
// preferences to set, including any values set from implicit flags.
func checkForAccidentalSettingReverts(flagSet map[string]bool, curPrefs *ipn.Prefs, mp *ipn.MaskedPrefs) error {
if len(flagSet) == 0 {
// A bare "tailscale up" is a special case to just
// mean bringing the network up without any changes.
return nil
}
curWithExplicitEdits := curPrefs.Clone()
curWithExplicitEdits.ApplyEdits(mp)
prefType := reflect.TypeOf(ipn.Prefs{})
// Explicit values (current + explicit edit):
ev := reflect.ValueOf(curWithExplicitEdits).Elem()
// Implicit values (what we'd get if we replaced everything with flag defaults):
iv := reflect.ValueOf(&mp.Prefs).Elem()
var errs []error
var didExitNodeErr bool
for i := 0; i < prefType.NumField(); i++ {
prefName := prefType.Field(i).Name
if prefName == "Persist" {
continue
}
flagName, hasFlag := flagForPref[prefName]
if hasFlag && flagSet[flagName] {
continue
}
// Get explicit value and implicit value
evi, ivi := ev.Field(i).Interface(), iv.Field(i).Interface()
if reflect.DeepEqual(evi, ivi) {
continue
}
switch flagName {
case "":
errs = append(errs, fmt.Errorf("'tailscale up' without --reset requires all preferences with changing values to be explicitly mentioned; this command would change the value of flagless pref %q", prefName))
case "exit-node":
if !didExitNodeErr {
didExitNodeErr = true
errs = append(errs, errors.New("'tailscale up' without --reset requires all preferences with changing values to be explicitly mentioned; --exit-node is not specified but an exit node is currently configured"))
}
default:
errs = append(errs, fmt.Errorf("'tailscale up' without --reset requires all preferences with changing values to be explicitly mentioned; --%s is not specified but its default value of %v differs from current value %v",
flagName, fmtSettingVal(ivi), fmtSettingVal(evi)))
}
}
return multierror.New(errs)
}
func fmtSettingVal(v interface{}) string {
switch v := v.(type) {
case bool:
return strconv.FormatBool(v)
case string, preftype.NetfilterMode:
return fmt.Sprintf("%q", v)
case []string:
return strings.Join(v, ",")
}
return fmt.Sprint(v)
}

View File

@@ -259,30 +259,30 @@ func tailscaleUp(ctx context.Context) (authURL string, retErr error) {
c, bc, ctx, cancel := connect(ctx)
defer cancel()
bc.SetNotifyCallback(func(n ipn.Notify) {
if n.ErrMessage != nil {
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 [...]')"
}
}
retErr = fmt.Errorf("backend error: %v", msg)
cancel()
} else if url := n.BrowseToURL; url != nil {
authURL = *url
cancel()
}
})
bc.SetPrefs(prefs)
opts := ipn.Options{
bc.Start(ipn.Options{
StateKey: ipn.GlobalDaemonStateKey,
Notify: func(n ipn.Notify) {
if n.ErrMessage != nil {
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 [...]')"
}
}
retErr = fmt.Errorf("backend error: %v", msg)
cancel()
} else if url := n.BrowseToURL; url != nil {
authURL = *url
cancel()
}
},
}
bc.Start(opts)
})
bc.StartLoginInteractive()
pump(ctx, bc, c)

View File

@@ -2,6 +2,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/go-multierror/multierror from tailscale.com/cmd/tailscale/cli
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/tcnksm/go-httpstat from tailscale.com/net/netcheck
@@ -14,12 +15,13 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
rsc.io/goversion/version from tailscale.com/version
tailscale.com/atomicfile from tailscale.com/ipn
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli
tailscale.com/client/tailscale/apitype from tailscale.com/client/tailscale
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
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/ipn from tailscale.com/cmd/tailscale/cli
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli+
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
tailscale.com/metrics from tailscale.com/derp
tailscale.com/net/dnscache from tailscale.com/derp/derphttp

View File

@@ -71,6 +71,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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/client/tailscale/apitype from tailscale.com/ipn/ipnlocal+
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+
@@ -163,7 +164,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
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/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from net/http
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+
@@ -216,6 +217,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
debug/elf from rsc.io/goversion/version
debug/macho from rsc.io/goversion/version
debug/pe from rsc.io/goversion/version
embed from tailscale.com/net/dns
encoding from encoding/json+
encoding/asn1 from crypto/x509+
encoding/base64 from encoding/json+
@@ -247,7 +249,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
net from crypto/tls+
net/http from expvar+
net/http/httptrace from github.com/tcnksm/go-httpstat+
net/http/internal from net/http
net/http/httputil from tailscale.com/ipn/localapi
net/http/internal from net/http+
net/http/pprof from tailscale.com/cmd/tailscaled
net/textproto from golang.org/x/net/http/httpguts+
net/url from crypto/x509+

View File

@@ -230,24 +230,19 @@ func run() error {
}
var ns *netstack.Impl
if useNetstack {
tunDev, magicConn, ok := e.(wgengine.InternalsGetter).GetInternals()
if !ok {
log.Fatalf("%T is not a wgengine.InternalsGetter", e)
}
ns, err = netstack.Create(logf, tunDev, e, magicConn)
if err != nil {
log.Fatalf("netstack.Create: %v", err)
}
if err := ns.Start(); err != nil {
log.Fatalf("failed to start netstack: %v", err)
}
if useNetstack || wrapNetstack {
onlySubnets := wrapNetstack && !useNetstack
ns = mustStartNetstack(logf, e, onlySubnets)
}
if socksListener != nil {
srv := &socks5.Server{
Logf: logger.WithPrefix(logf, "socks5: "),
}
// TODO: also consider wrapNetstack, where dials can go to either Tailscale
// or non-Tailscale targets. But that's also basically what
// https://github.com/tailscale/tailscale/issues/1617 is about, so do them
// both at the same time.
if useNetstack {
srv.Dialer = func(ctx context.Context, network, addr string) (net.Conn, error) {
return ns.DialContextTCP(ctx, addr)
@@ -300,7 +295,6 @@ func run() error {
Port: 41112,
StatePath: args.statepath,
AutostartStateKey: globalStateKey,
LegacyConfigPath: paths.LegacyConfigPath(),
SurviveDisconnects: true,
DebugMux: debugMux,
}
@@ -331,6 +325,28 @@ func createEngine(logf logger.Logf, linkMon *monitor.Mon) (e wgengine.Engine, us
return nil, false, multierror.New(errs)
}
var wrapNetstack = shouldWrapNetstack()
func shouldWrapNetstack() bool {
if e := os.Getenv("TS_DEBUG_WRAP_NETSTACK"); e != "" {
v, err := strconv.ParseBool(e)
if err != nil {
log.Fatalf("invalid TS_DEBUG_WRAP_NETSTACK value: %v", err)
}
return v
}
if distro.Get() == distro.Synology {
return true
}
switch runtime.GOOS {
case "windows", "darwin":
// Enable on Windows and tailscaled-on-macOS (this doesn't
// affect the GUI clients).
return true
}
return false
}
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.Engine, useNetstack bool, err error) {
conf := wgengine.Config{
ListenPort: args.port,
@@ -338,7 +354,7 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
}
useNetstack = name == "userspace-networking"
if !useNetstack {
dev, err := tstun.New(logf, name)
dev, devName, err := tstun.New(logf, name)
if err != nil {
tstun.Diagnose(logf, name)
return nil, false, err
@@ -349,14 +365,15 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
dev.Close()
return nil, false, err
}
conf.Router = r
tunname, err := dev.Name()
d, err := dns.NewOSConfigurator(logf, devName)
if err != nil {
r.Close()
dev.Close()
return nil, false, err
}
conf.DNS = dns.NewOSConfigurator(logf, tunname)
conf.DNS = d
conf.Router = r
if wrapNetstack {
conf.Router = netstack.NewSubnetRouterWrapper(conf.Router)
}
}
e, err = wgengine.NewUserspaceEngine(logf, conf)
if err != nil {
@@ -384,3 +401,18 @@ func runDebugServer(mux *http.ServeMux, addr string) {
log.Fatal(err)
}
}
func mustStartNetstack(logf logger.Logf, e wgengine.Engine, onlySubnets bool) *netstack.Impl {
tunDev, magicConn, ok := e.(wgengine.InternalsGetter).GetInternals()
if !ok {
log.Fatalf("%T is not a wgengine.InternalsGetter", e)
}
ns, err := netstack.Create(logf, tunDev, e, magicConn, onlySubnets)
if err != nil {
log.Fatalf("netstack.Create: %v", err)
}
if err := ns.Start(); err != nil {
log.Fatalf("failed to start netstack: %v", err)
}
return ns
}

View File

@@ -30,11 +30,13 @@ import (
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"tailscale.com/ipn/ipnserver"
"tailscale.com/logpolicy"
"tailscale.com/net/dns"
"tailscale.com/net/tstun"
"tailscale.com/tempfork/wireguard-windows/firewall"
"tailscale.com/types/logger"
"tailscale.com/version"
"tailscale.com/wgengine"
"tailscale.com/wgengine/netstack"
"tailscale.com/wgengine/router"
)
@@ -161,7 +163,7 @@ func startIPNServer(ctx context.Context, logid string) error {
var err error
getEngine := func() (wgengine.Engine, error) {
dev, err := tstun.New(logf, "Tailscale")
dev, devName, err := tstun.New(logf, "Tailscale")
if err != nil {
return nil, err
}
@@ -170,9 +172,19 @@ func startIPNServer(ctx context.Context, logid string) error {
dev.Close()
return nil, err
}
if wrapNetstack {
r = netstack.NewSubnetRouterWrapper(r)
}
d, err := dns.NewOSConfigurator(logf, devName)
if err != nil {
r.Close()
dev.Close()
return nil, err
}
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
Tun: dev,
Router: r,
DNS: d,
ListenPort: 41641,
})
if err != nil {
@@ -180,6 +192,10 @@ func startIPNServer(ctx context.Context, logid string) error {
dev.Close()
return nil, err
}
onlySubnets := true
if wrapNetstack {
mustStartNetstack(logf, eng, onlySubnets)
}
return wgengine.NewWatchdog(eng), nil
}

View File

@@ -100,11 +100,22 @@ func (s Status) String() string {
}
type LoginGoal struct {
_ structs.Incomparable
wantLoggedIn bool // true if we *want* to be logged in
token *tailcfg.Oauth2Token // oauth token to use when logging in
flags LoginFlags // flags to use when logging in
url string // auth url that needs to be visited
_ structs.Incomparable
wantLoggedIn bool // true if we *want* to be logged in
token *tailcfg.Oauth2Token // oauth token to use when logging in
flags LoginFlags // flags to use when logging in
url string // auth url that needs to be visited
loggedOutResult chan<- error
}
func (g *LoginGoal) sendLogoutError(err error) {
if g.loggedOutResult == nil {
return
}
select {
case g.loggedOutResult <- err:
default:
}
}
// Client connects to a tailcontrol server for a node.
@@ -363,6 +374,7 @@ func (c *Client) authRoutine() {
if !goal.wantLoggedIn {
err := c.direct.TryLogout(ctx)
goal.sendLogoutError(err)
if err != nil {
report(err, "TryLogout")
bo.BackOff(ctx, err)
@@ -402,7 +414,8 @@ func (c *Client) authRoutine() {
report(err, f)
bo.BackOff(ctx, err)
continue
} else if url != "" {
}
if url != "" {
if goal.url != "" {
err = fmt.Errorf("[unexpected] server required a new URL?")
report(err, "WaitLoginURL")
@@ -682,18 +695,42 @@ func (c *Client) Login(t *tailcfg.Oauth2Token, flags LoginFlags) {
c.cancelAuth()
}
func (c *Client) Logout() {
c.logf("client.Logout()")
func (c *Client) StartLogout() {
c.logf("client.StartLogout()")
c.mu.Lock()
c.loginGoal = &LoginGoal{
wantLoggedIn: false,
}
c.mu.Unlock()
c.cancelAuth()
}
func (c *Client) Logout(ctx context.Context) error {
c.logf("client.Logout()")
errc := make(chan error, 1)
c.mu.Lock()
c.loginGoal = &LoginGoal{
wantLoggedIn: false,
loggedOutResult: errc,
}
c.mu.Unlock()
c.cancelAuth()
timer := time.NewTimer(10 * time.Second)
defer timer.Stop()
select {
case err := <-errc:
return err
case <-ctx.Done():
return ctx.Err()
case <-timer.C:
return context.DeadlineExceeded
}
}
// UpdateEndpoints sets the client's discovered endpoints and sends
// them to the control server if they've changed.
//
@@ -701,7 +738,7 @@ func (c *Client) Logout() {
//
// The localPort field is unused except for integration tests in
// another repo.
func (c *Client) UpdateEndpoints(localPort uint16, endpoints []string) {
func (c *Client) UpdateEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) {
changed := c.direct.SetEndpoints(localPort, endpoints)
if changed {
c.sendNewMapRequest()

View File

@@ -46,6 +46,7 @@ import (
"tailscale.com/types/opt"
"tailscale.com/types/persist"
"tailscale.com/types/wgkey"
"tailscale.com/util/dnsname"
"tailscale.com/util/systemd"
"tailscale.com/version"
"tailscale.com/wgengine/filter"
@@ -76,7 +77,7 @@ type Direct struct {
expiry *time.Time
// hostinfo is mutated in-place while mu is held.
hostinfo *tailcfg.Hostinfo // always non-nil
endpoints []string
endpoints []tailcfg.Endpoint
everEndpoints bool // whether we've ever had non-empty endpoints
localPort uint16 // or zero to mean auto
}
@@ -144,7 +145,7 @@ func NewDirect(opts Options) (*Direct, error) {
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.Proxy = tshttpproxy.ProxyFromEnvironment
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
tr.TLSClientConfig = tlsdial.Config(serverURL.Host, tr.TLSClientConfig)
tr.TLSClientConfig = tlsdial.Config(serverURL.Hostname(), tr.TLSClientConfig)
tr.DialContext = dnscache.Dialer(dialer.DialContext, dnsCache)
tr.DialTLSContext = dnscache.TLSDialer(dialer.DialContext, dnsCache, tr.TLSClientConfig)
tr.ForceAttemptHTTP2 = true
@@ -179,6 +180,7 @@ var osVersion func() string // non-nil on some platforms
func NewHostinfo() *tailcfg.Hostinfo {
hostname, _ := os.Hostname()
hostname = dnsname.FirstLabel(hostname)
var osv string
if osVersion != nil {
osv = osVersion()
@@ -261,20 +263,19 @@ const (
func (c *Direct) TryLogout(ctx context.Context) error {
c.logf("direct.TryLogout()")
c.mu.Lock()
defer c.mu.Unlock()
mustRegen, newURL, err := c.doLogin(ctx, loginOpt{Logout: true})
c.logf("TryLogout control response: mustRegen=%v, newURL=%v, err=%v", mustRegen, newURL, err)
// TODO(crawshaw): Tell the server. This node key should be
// immediately invalidated.
//if !c.persist.PrivateNodeKey.IsZero() {
//}
c.mu.Lock()
c.persist = persist.Persist{}
return nil
c.mu.Unlock()
return err
}
func (c *Direct) TryLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags LoginFlags) (url string, err error) {
c.logf("direct.TryLogin(token=%v, flags=%v)", t != nil, flags)
return c.doLoginOrRegen(ctx, t, flags, false, "")
return c.doLoginOrRegen(ctx, loginOpt{Token: t, Flags: flags})
}
// WaitLoginURL sits in a long poll waiting for the user to authenticate at url.
@@ -282,22 +283,30 @@ func (c *Direct) TryLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags Log
// On success, newURL and err will both be nil.
func (c *Direct) WaitLoginURL(ctx context.Context, url string) (newURL string, err error) {
c.logf("direct.WaitLoginURL")
return c.doLoginOrRegen(ctx, nil, LoginDefault, false, url)
return c.doLoginOrRegen(ctx, loginOpt{URL: url})
}
func (c *Direct) doLoginOrRegen(ctx context.Context, t *tailcfg.Oauth2Token, flags LoginFlags, regen bool, url string) (newURL string, err error) {
mustregen, url, err := c.doLogin(ctx, t, flags, regen, url)
func (c *Direct) doLoginOrRegen(ctx context.Context, opt loginOpt) (newURL string, err error) {
mustRegen, url, err := c.doLogin(ctx, opt)
if err != nil {
return url, err
}
if mustregen {
_, url, err = c.doLogin(ctx, t, flags, true, url)
if mustRegen {
opt.Regen = true
_, url, err = c.doLogin(ctx, opt)
}
return url, err
}
func (c *Direct) doLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags LoginFlags, regen bool, url string) (mustregen bool, newurl string, err error) {
type loginOpt struct {
Token *tailcfg.Oauth2Token
Flags LoginFlags
Regen bool
URL string
Logout bool
}
func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, newURL string, err error) {
c.mu.Lock()
persist := c.persist
tryingNewKey := c.tryingNewKey
@@ -316,22 +325,27 @@ func (c *Direct) doLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags Logi
return false, "", errors.New("getMachinePrivKey returned zero key")
}
if expired {
c.logf("Old key expired -> regen=true")
systemd.Status("key expired; run 'tailscale up' to authenticate")
regen = true
}
if (flags & LoginInteractive) != 0 {
c.logf("LoginInteractive -> regen=true")
regen = true
regen := opt.Regen
if opt.Logout {
c.logf("logging out...")
} else {
if expired {
c.logf("Old key expired -> regen=true")
systemd.Status("key expired; run 'tailscale up' to authenticate")
regen = true
}
if (opt.Flags & LoginInteractive) != 0 {
c.logf("LoginInteractive -> regen=true")
regen = true
}
}
c.logf("doLogin(regen=%v, hasUrl=%v)", regen, url != "")
c.logf("doLogin(regen=%v, hasUrl=%v)", regen, opt.URL != "")
if serverKey.IsZero() {
var err error
serverKey, err = loadServerKey(ctx, c.httpc, c.serverURL)
if err != nil {
return regen, url, err
return regen, opt.URL, err
}
c.mu.Lock()
@@ -340,17 +354,21 @@ func (c *Direct) doLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags Logi
}
var oldNodeKey wgkey.Key
if url != "" {
} else if regen || persist.PrivateNodeKey.IsZero() {
switch {
case opt.Logout:
tryingNewKey = persist.PrivateNodeKey
case opt.URL != "":
// Nothing.
case regen || persist.PrivateNodeKey.IsZero():
c.logf("Generating a new nodekey.")
persist.OldPrivateNodeKey = persist.PrivateNodeKey
key, err := wgkey.NewPrivate()
if err != nil {
c.logf("login keygen: %v", err)
return regen, url, err
return regen, opt.URL, err
}
tryingNewKey = key
} else {
default:
// Try refreshing the current key first
tryingNewKey = persist.PrivateNodeKey
}
@@ -359,11 +377,14 @@ func (c *Direct) doLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags Logi
}
if tryingNewKey.IsZero() {
if opt.Logout {
return false, "", errors.New("no nodekey to log out")
}
log.Fatalf("tryingNewKey is empty, give up")
}
if backendLogID == "" {
err = errors.New("hostinfo: BackendLogID missing")
return regen, url, err
return regen, opt.URL, err
}
now := time.Now().Round(time.Second)
request := tailcfg.RegisterRequest{
@@ -371,13 +392,16 @@ func (c *Direct) doLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags Logi
OldNodeKey: tailcfg.NodeKey(oldNodeKey),
NodeKey: tailcfg.NodeKey(tryingNewKey.Public()),
Hostinfo: hostinfo,
Followup: url,
Followup: opt.URL,
Timestamp: &now,
}
if opt.Logout {
request.Expiry = time.Unix(123, 0) // far in the past
}
c.logf("RegisterReq: onode=%v node=%v fup=%v",
request.OldNodeKey.ShortString(),
request.NodeKey.ShortString(), url != "")
request.Auth.Oauth2Token = t
request.NodeKey.ShortString(), opt.URL != "")
request.Auth.Oauth2Token = opt.Token
request.Auth.Provider = persist.Provider
request.Auth.LoginName = persist.LoginName
request.Auth.AuthKey = authKey
@@ -395,34 +419,44 @@ func (c *Direct) doLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags Logi
c.logf("RegisterReq sign error: %v", err)
}
}
if debugRegister {
j, _ := json.MarshalIndent(request, "", "\t")
c.logf("RegisterRequest: %s", j)
}
bodyData, err := encode(request, &serverKey, &machinePrivKey)
if err != nil {
return regen, url, err
return regen, opt.URL, err
}
body := bytes.NewReader(bodyData)
u := fmt.Sprintf("%s/machine/%s", c.serverURL, machinePrivKey.Public().HexString())
req, err := http.NewRequest("POST", u, body)
if err != nil {
return regen, url, err
return regen, opt.URL, err
}
req = req.WithContext(ctx)
res, err := c.httpc.Do(req)
if err != nil {
return regen, url, fmt.Errorf("register request: %v", err)
return regen, opt.URL, fmt.Errorf("register request: %v", err)
}
if res.StatusCode != 200 {
msg, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
return regen, url, fmt.Errorf("register request: http %d: %.200s",
return regen, opt.URL, fmt.Errorf("register request: http %d: %.200s",
res.StatusCode, strings.TrimSpace(string(msg)))
}
resp := tailcfg.RegisterResponse{}
if err := decode(res, &resp, &serverKey, &machinePrivKey); err != nil {
c.logf("error decoding RegisterResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
return regen, url, fmt.Errorf("register request: %v", err)
return regen, opt.URL, fmt.Errorf("register request: %v", err)
}
if debugRegister {
j, _ := json.MarshalIndent(resp, "", "\t")
c.logf("RegisterResponse: %s", j)
}
// Log without PII:
c.logf("RegisterReq: got response; nodeKeyExpired=%v, machineAuthorized=%v; authURL=%v",
resp.NodeKeyExpired, resp.MachineAuthorized, resp.AuthURL != "")
@@ -474,7 +508,7 @@ func (c *Direct) doLogin(ctx context.Context, t *tailcfg.Oauth2Token, flags Logi
return false, resp.AuthURL, nil
}
func sameStrings(a, b []string) bool {
func sameEndpoints(a, b []tailcfg.Endpoint) bool {
if len(a) != len(b) {
return false
}
@@ -490,15 +524,19 @@ func sameStrings(a, b []string) bool {
// whether they've changed.
//
// It does not retain the provided slice.
func (c *Direct) newEndpoints(localPort uint16, endpoints []string) (changed bool) {
func (c *Direct) newEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) (changed bool) {
c.mu.Lock()
defer c.mu.Unlock()
// Nothing new?
if c.localPort == localPort && sameStrings(c.endpoints, endpoints) {
if c.localPort == localPort && sameEndpoints(c.endpoints, endpoints) {
return false // unchanged
}
c.logf("client.newEndpoints(%v, %v)", localPort, endpoints)
var epStrs []string
for _, ep := range endpoints {
epStrs = append(epStrs, ep.Addr.String())
}
c.logf("client.newEndpoints(%v, %v)", localPort, epStrs)
c.localPort = localPort
c.endpoints = append(c.endpoints[:0], endpoints...)
if len(endpoints) > 0 {
@@ -510,7 +548,7 @@ func (c *Direct) newEndpoints(localPort uint16, endpoints []string) (changed boo
// SetEndpoints updates the list of locally advertised endpoints.
// It won't be replicated to the server until a *fresh* call to PollNetMap().
// You don't need to restart PollNetMap if we return changed==false.
func (c *Direct) SetEndpoints(localPort uint16, endpoints []string) (changed bool) {
func (c *Direct) SetEndpoints(localPort uint16, endpoints []tailcfg.Endpoint) (changed bool) {
// (no log message on function entry, because it clutters the logs
// if endpoints haven't changed. newEndpoints() will log it.)
return c.newEndpoints(localPort, endpoints)
@@ -543,7 +581,12 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
hostinfo := c.hostinfo.Clone()
backendLogID := hostinfo.BackendLogID
localPort := c.localPort
ep := append([]string(nil), c.endpoints...)
var epStrs []string
var epTypes []tailcfg.EndpointType
for _, ep := range c.endpoints {
epStrs = append(epStrs, ep.Addr.String())
epTypes = append(epTypes, ep.Type)
}
everEndpoints := c.everEndpoints
c.mu.Unlock()
@@ -563,7 +606,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
}
allowStream := maxPolls != 1
c.logf("[v1] PollNetMap: stream=%v :%v ep=%v", allowStream, localPort, ep)
c.logf("[v1] PollNetMap: stream=%v :%v ep=%v", allowStream, localPort, epStrs)
vlogf := logger.Discard
if Debug.NetMap {
@@ -573,15 +616,16 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
}
request := &tailcfg.MapRequest{
Version: tailcfg.CurrentMapRequestVersion,
KeepAlive: c.keepAlive,
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
DiscoKey: c.discoPubKey,
Endpoints: ep,
Stream: allowStream,
Hostinfo: hostinfo,
DebugFlags: c.debugFlags,
OmitPeers: cb == nil,
Version: tailcfg.CurrentMapRequestVersion,
KeepAlive: c.keepAlive,
NodeKey: tailcfg.NodeKey(persist.PrivateNodeKey.Public()),
DiscoKey: c.discoPubKey,
Endpoints: epStrs,
EndpointTypes: epTypes,
Stream: allowStream,
Hostinfo: hostinfo,
DebugFlags: c.debugFlags,
OmitPeers: cb == nil,
}
var extraDebugFlags []string
if hostinfo != nil && c.linkMon != nil && !c.skipIPForwardingCheck &&
@@ -609,7 +653,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
// TODO(bradfitz): we skip this optimization in tests, though,
// because the e2e tests are currently hyperspecific about the
// ordering of things. The e2e tests need love.
if len(ep) == 0 && !everEndpoints && !inTest() {
if len(epStrs) == 0 && !everEndpoints && !inTest() {
request.ReadOnly = true
}
@@ -685,6 +729,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
}
}()
var lastDNSConfig = new(tailcfg.DNSConfig)
var lastDERPMap *tailcfg.DERPMap
var lastUserProfile = map[tailcfg.UserID]tailcfg.UserProfile{}
var lastParsedPacketFilter []filter.Match
@@ -784,10 +829,16 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
p.Endpoints = []string{"127.9.9.9:456"}
}
}
if Debug.StripCaps {
resp.Node.Capabilities = nil
}
if pf := resp.PacketFilter; pf != nil {
lastParsedPacketFilter = c.parsePacketFilter(pf)
}
if c := resp.DNSConfig; c != nil {
lastDNSConfig = c
}
if v, ok := resp.CollectServices.Get(); ok {
collectServices = v
@@ -814,7 +865,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
User: resp.Node.User,
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
Domain: resp.Domain,
DNS: resp.DNSConfig,
DNS: *lastDNSConfig,
Hostinfo: resp.Node.Hostinfo,
PacketFilter: lastParsedPacketFilter,
CollectServices: collectServices,
@@ -894,7 +945,10 @@ func decode(res *http.Response, v interface{}, serverKey *wgkey.Key, mkey *wgkey
return decodeMsg(msg, v, serverKey, mkey)
}
var debugMap, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_MAP"))
var (
debugMap, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_MAP"))
debugRegister, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_REGISTER"))
)
var jsonEscapedZero = []byte(`\u0000`)
@@ -1021,6 +1075,7 @@ type debug struct {
OnlyDisco bool
Disco bool
StripEndpoints bool // strip endpoints from control (only use disco messages)
StripCaps bool // strip all local node's control-provided capabilities
}
func initDebug() debug {
@@ -1029,6 +1084,7 @@ func initDebug() debug {
NetMap: envBool("TS_DEBUG_NETMAP"),
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
StripEndpoints: envBool("TS_DEBUG_STRIP_ENDPOINTS"),
StripCaps: envBool("TS_DEBUG_STRIP_CAPS"),
OnlyDisco: use == "only",
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
}
@@ -1046,8 +1102,13 @@ func envBool(k string) bool {
return v
}
// undeltaPeers updates mapRes.Peers to be complete based on the provided previous peer list
// and the PeersRemoved and PeersChanged fields in mapRes.
var clockNow = time.Now
// undeltaPeers updates mapRes.Peers to be complete based on the
// provided previous peer list and the PeersRemoved and PeersChanged
// fields in mapRes, as well as the PeerSeenChange and OnlineChange
// maps.
//
// It then also nils out the delta fields.
func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
if len(mapRes.Peers) > 0 {
@@ -1068,12 +1129,6 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
}
changed := mapRes.PeersChanged
if len(removed) == 0 && len(changed) == 0 {
// No changes fast path.
mapRes.Peers = prev
return
}
if !nodesSorted(changed) {
log.Printf("netmap: undeltaPeers: MapResponse.PeersChanged not sorted; sorting")
sortNodes(changed)
@@ -1084,40 +1139,43 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
sortNodes(prev)
}
newFull := make([]*tailcfg.Node, 0, len(prev)-len(removed))
for len(prev) > 0 && len(changed) > 0 {
pID := prev[0].ID
cID := changed[0].ID
if removed[pID] {
prev = prev[1:]
continue
newFull := prev
if len(removed) > 0 || len(changed) > 0 {
newFull = make([]*tailcfg.Node, 0, len(prev)-len(removed))
for len(prev) > 0 && len(changed) > 0 {
pID := prev[0].ID
cID := changed[0].ID
if removed[pID] {
prev = prev[1:]
continue
}
switch {
case pID < cID:
newFull = append(newFull, prev[0])
prev = prev[1:]
case pID == cID:
newFull = append(newFull, changed[0])
prev, changed = prev[1:], changed[1:]
case cID < pID:
newFull = append(newFull, changed[0])
changed = changed[1:]
}
}
switch {
case pID < cID:
newFull = append(newFull, prev[0])
prev = prev[1:]
case pID == cID:
newFull = append(newFull, changed[0])
prev, changed = prev[1:], changed[1:]
case cID < pID:
newFull = append(newFull, changed[0])
changed = changed[1:]
newFull = append(newFull, changed...)
for _, n := range prev {
if !removed[n.ID] {
newFull = append(newFull, n)
}
}
sortNodes(newFull)
}
newFull = append(newFull, changed...)
for _, n := range prev {
if !removed[n.ID] {
newFull = append(newFull, n)
}
}
sortNodes(newFull)
if mapRes.PeerSeenChange != nil {
if len(mapRes.PeerSeenChange) != 0 || len(mapRes.OnlineChange) != 0 {
peerByID := make(map[tailcfg.NodeID]*tailcfg.Node, len(newFull))
for _, n := range newFull {
peerByID[n.ID] = n
}
now := time.Now()
now := clockNow()
for nodeID, seen := range mapRes.PeerSeenChange {
if n, ok := peerByID[nodeID]; ok {
if seen {
@@ -1127,6 +1185,12 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
}
}
}
for nodeID, online := range mapRes.OnlineChange {
if n, ok := peerByID[nodeID]; ok {
online := online
n.Online = &online
}
}
}
mapRes.Peers = newFull

View File

@@ -10,21 +10,44 @@ import (
"reflect"
"strings"
"testing"
"time"
"inet.af/netaddr"
"tailscale.com/tailcfg"
"tailscale.com/types/wgkey"
)
func TestUndeltaPeers(t *testing.T) {
n := func(id tailcfg.NodeID, name string) *tailcfg.Node {
return &tailcfg.Node{ID: id, Name: name}
defer func(old func() time.Time) { clockNow = old }(clockNow)
var curTime time.Time
clockNow = func() time.Time {
return curTime
}
online := func(v bool) func(*tailcfg.Node) {
return func(n *tailcfg.Node) {
n.Online = &v
}
}
seenAt := func(t time.Time) func(*tailcfg.Node) {
return func(n *tailcfg.Node) {
n.LastSeen = &t
}
}
n := func(id tailcfg.NodeID, name string, mod ...func(*tailcfg.Node)) *tailcfg.Node {
n := &tailcfg.Node{ID: id, Name: name}
for _, f := range mod {
f(n)
}
return n
}
peers := func(nv ...*tailcfg.Node) []*tailcfg.Node { return nv }
tests := []struct {
name string
mapRes *tailcfg.MapResponse
prev []*tailcfg.Node
want []*tailcfg.Node
name string
mapRes *tailcfg.MapResponse
curTime time.Time
prev []*tailcfg.Node
want []*tailcfg.Node
}{
{
name: "full_peers",
@@ -72,9 +95,54 @@ func TestUndeltaPeers(t *testing.T) {
mapRes: &tailcfg.MapResponse{},
want: peers(n(1, "foo"), n(2, "bar")),
},
{
name: "online_change",
prev: peers(n(1, "foo"), n(2, "bar")),
mapRes: &tailcfg.MapResponse{
OnlineChange: map[tailcfg.NodeID]bool{
1: true,
},
},
want: peers(
n(1, "foo", online(true)),
n(2, "bar"),
),
},
{
name: "online_change_offline",
prev: peers(n(1, "foo"), n(2, "bar")),
mapRes: &tailcfg.MapResponse{
OnlineChange: map[tailcfg.NodeID]bool{
1: false,
2: true,
},
},
want: peers(
n(1, "foo", online(false)),
n(2, "bar", online(true)),
),
},
{
name: "peer_seen_at",
prev: peers(n(1, "foo", seenAt(time.Unix(111, 0))), n(2, "bar")),
curTime: time.Unix(123, 0),
mapRes: &tailcfg.MapResponse{
PeerSeenChange: map[tailcfg.NodeID]bool{
1: false,
2: true,
},
},
want: peers(
n(1, "foo"),
n(2, "bar", seenAt(time.Unix(123, 0))),
),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if !tt.curTime.IsZero() {
curTime = tt.curTime
}
undeltaPeers(tt.mapRes, tt.prev)
if !reflect.DeepEqual(tt.mapRes.Peers, tt.want) {
t.Errorf("wrong results\n got: %s\nwant: %s", formatNodes(tt.mapRes.Peers), formatNodes(tt.want))
@@ -89,7 +157,14 @@ func formatNodes(nodes []*tailcfg.Node) string {
if i > 0 {
sb.WriteString(", ")
}
fmt.Fprintf(&sb, "(%d, %q)", n.ID, n.Name)
var extra string
if n.Online != nil {
extra += fmt.Sprintf(", online=%v", *n.Online)
}
if n.LastSeen != nil {
extra += fmt.Sprintf(", lastSeen=%v", n.LastSeen.Unix())
}
fmt.Fprintf(&sb, "(%d, %q%s)", n.ID, n.Name, extra)
}
return sb.String()
}
@@ -144,7 +219,7 @@ func TestNewDirect(t *testing.T) {
t.Errorf("c.SetHostinfo(hi) want true got %v", changed)
}
endpoints := []string{"1", "2", "3"}
endpoints := fakeEndpoints(1, 2, 3)
changed = c.newEndpoints(12, endpoints)
if !changed {
t.Errorf("c.newEndpoints(12) want true got %v", changed)
@@ -157,13 +232,22 @@ func TestNewDirect(t *testing.T) {
if !changed {
t.Errorf("c.newEndpoints(13) want true got %v", changed)
}
endpoints = []string{"4", "5", "6"}
endpoints = fakeEndpoints(4, 5, 6)
changed = c.newEndpoints(13, endpoints)
if !changed {
t.Errorf("c.newEndpoints(13) want true got %v", changed)
}
}
func fakeEndpoints(ports ...uint16) (ret []tailcfg.Endpoint) {
for _, port := range ports {
ret = append(ret, tailcfg.Endpoint{
Addr: netaddr.IPPort{Port: port},
})
}
return
}
func TestNewHostinfo(t *testing.T) {
hi := NewHostinfo()
if hi == nil {

View File

@@ -11,6 +11,7 @@ import (
"fmt"
"io"
"io/ioutil"
"os"
"strings"
"syscall"
@@ -58,6 +59,9 @@ func osVersionLinux() string {
if inContainer() {
attrBuf.WriteString("; container")
}
if inKnative() {
attrBuf.WriteString("; env=kn")
}
attr := attrBuf.String()
id := m["ID"]
@@ -99,5 +103,21 @@ func inContainer() (ret bool) {
}
return nil
})
lineread.File("/proc/mounts", func(line []byte) error {
if mem.Contains(mem.B(line), mem.S("fuse.lxcfs")) {
ret = true
return io.EOF
}
return nil
})
return
}
func inKnative() bool {
// https://cloud.google.com/run/docs/reference/container-contract#env-vars
if os.Getenv("K_REVISION") != "" && os.Getenv("K_CONFIGURATION") != "" &&
os.Getenv("K_SERVICE") != "" && os.Getenv("PORT") != "" {
return true
}
return false
}

View File

@@ -51,6 +51,46 @@ var (
errBadRequest = errors.New("malformed request")
)
func isSupportedCertificate(cert *x509.Certificate) bool {
return cert.PublicKeyAlgorithm == x509.RSA
}
func isSubjectInChain(subject string, chain []*x509.Certificate) bool {
if len(chain) == 0 || chain[0] == nil {
return false
}
for _, c := range chain {
if c == nil {
continue
}
if c.Subject.String() == subject {
return true
}
}
return false
}
func selectIdentityFromSlice(subject string, ids []certstore.Identity) (certstore.Identity, []*x509.Certificate) {
for _, id := range ids {
chain, err := id.CertificateChain()
if err != nil {
continue
}
if !isSupportedCertificate(chain[0]) {
continue
}
if isSubjectInChain(subject, chain) {
return id, chain
}
}
return nil, nil
}
// findIdentity locates an identity from the Windows or Darwin certificate
// store. It returns the first certificate with a matching Subject anywhere in
// its certificate chain, so it is possible to search for the leaf certificate,
@@ -64,26 +104,7 @@ func findIdentity(subject string, st certstore.Store) (certstore.Identity, []*x5
return nil, nil, err
}
var selected certstore.Identity
var chain []*x509.Certificate
for _, id := range ids {
chain, err = id.CertificateChain()
if err != nil {
continue
}
if chain[0].PublicKeyAlgorithm != x509.RSA {
continue
}
for _, c := range chain {
if c.Subject.String() == subject {
selected = id
break
}
}
}
selected, chain := selectIdentityFromSlice(subject, ids)
for _, id := range ids {
if id != selected {

View File

@@ -409,7 +409,7 @@ func TestSendFreeze(t *testing.T) {
for i := 0; i < cap(errCh); i++ {
err := <-errCh
if err != nil {
if errors.Is(err, io.EOF) {
if errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) {
continue
}
t.Error(err)

18
disco/disco_fuzzer.go Normal file
View File

@@ -0,0 +1,18 @@
// 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 gofuzz
package disco
func Fuzz(data []byte) int {
m, _ := Parse(data)
newBytes := m.AppendMarshal(data)
parsedMarshall, _ := Parse(newBytes)
if m != parsedMarshall {
panic("Parsing error")
}
return 1
}

View File

@@ -5,7 +5,6 @@
package ipn
import (
"net/http"
"time"
"tailscale.com/ipn/ipnstate"
@@ -68,8 +67,18 @@ type Notify struct {
BackendLogID *string // public logtail id used by backend
PingResult *ipnstate.PingResult
// FilesWaiting if non-nil means that files are buffered in
// the Tailscale daemon and ready for local transfer to the
// user's preferred storage location.
FilesWaiting *empty.Message `json:",omitempty"`
// IncomingFiles, if non-nil, specifies which files are in the
// process of being received. A nil IncomingFiles means this
// Notify should not update the state of file transfers. A non-nil
// but empty IncomingFiles means that no files are in the middle
// of being transferred.
IncomingFiles []PartialFile `json:",omitempty"`
// LocalTCPPort, if non-nil, informs the UI frontend which
// (non-zero) localhost TCP port it's listening on.
// This is currently only used by Tailscale when run in the
@@ -79,6 +88,24 @@ type Notify struct {
// type is mirrored in xcode/Shared/IPN.swift
}
// PartialFile represents an in-progress file transfer.
type PartialFile struct {
Name string // e.g. "foo.jpg"
Started time.Time // time transfer started
DeclaredSize int64 // or -1 if unknown
Received int64 // bytes copied thus far
// PartialPath is set non-empty in "direct" file mode to the
// in-progress '*.partial' file's path when the peerapi isn't
// being used; see LocalBackend.SetDirectFileRoot.
PartialPath string `json:",omitempty"`
// Done is set in "direct" mode when the partial file has been
// closed and is ready for the caller to rename away the
// ".partial" suffix.
Done bool `json:",omitempty"`
}
// StateKey is an opaque identifier for a set of LocalBackend state
// (preferences, private keys, etc.).
//
@@ -114,19 +141,6 @@ type Options struct {
// AuthKey is an optional node auth key used to authorize a
// new node key without user interaction.
AuthKey string
// LegacyConfigPath optionally specifies the old-style relaynode
// relay.conf location. If both LegacyConfigPath and StateKey are
// specified and the requested state doesn't exist in the backend
// store, the backend migrates the config from LegacyConfigPath.
//
// TODO(danderson): remove some time after the transition to
// tailscaled is done.
LegacyConfigPath string
// Notify is called when backend events happen.
Notify func(Notify) `json:"-"`
// HTTPTestClient is an optional HTTP client to pass to controlclient
// (for tests only).
HTTPTestClient *http.Client
}
// Backend is the interface between Tailscale frontends
@@ -135,6 +149,9 @@ type Options struct {
// (It has nothing to do with the interface between the backends
// and the cloud control plane.)
type Backend interface {
// SetNotifyCallback sets the callback to be called on updates
// from the backend to the client.
SetNotifyCallback(func(Notify))
// Start starts or restarts the backend, typically when a
// frontend client connects.
Start(Options) error
@@ -151,8 +168,6 @@ type Backend interface {
// WantRunning. This may cause the wireguard engine to
// reconfigure or stop.
SetPrefs(*Prefs)
// EditPrefs is like SetPrefs but only sets the specified fields.
EditPrefs(*MaskedPrefs)
// RequestEngineStatus polls for an update from the wireguard
// engine. Only needed if you want to display byte
// counts. Connection events are emitted automatically without

View File

@@ -5,7 +5,6 @@
package ipn
import (
"log"
"time"
"tailscale.com/ipn/ipnstate"
@@ -21,18 +20,28 @@ type FakeBackend struct {
func (b *FakeBackend) Start(opts Options) error {
b.serverURL = opts.Prefs.ControlURL
if opts.Notify == nil {
log.Fatalf("FakeBackend.Start: opts.Notify is nil\n")
if b.notify == nil {
panic("FakeBackend.Start: SetNotifyCallback not called")
}
b.notify = opts.Notify
b.notify(Notify{Prefs: opts.Prefs})
nl := NeedsLogin
b.notify(Notify{State: &nl})
if b.notify != nil {
b.notify(Notify{Prefs: opts.Prefs})
b.notify(Notify{State: &nl})
}
return nil
}
func (b *FakeBackend) SetNotifyCallback(notify func(Notify)) {
if notify == nil {
panic("FakeBackend.SetNotifyCallback: notify is nil")
}
b.notify = notify
}
func (b *FakeBackend) newState(s State) {
b.notify(Notify{State: &s})
if b.notify != nil {
b.notify(Notify{State: &s})
}
if s == Running {
b.live = true
} else {
@@ -42,7 +51,9 @@ func (b *FakeBackend) newState(s State) {
func (b *FakeBackend) StartLoginInteractive() {
u := b.serverURL + "/this/is/fake"
b.notify(Notify{BrowseToURL: &u})
if b.notify != nil {
b.notify(Notify{BrowseToURL: &u})
}
b.login()
}
@@ -54,10 +65,14 @@ func (b *FakeBackend) login() {
b.newState(NeedsMachineAuth)
b.newState(Stopped)
// TODO(apenwarr): Fill in a more interesting netmap here.
b.notify(Notify{NetMap: &netmap.NetworkMap{}})
if b.notify != nil {
b.notify(Notify{NetMap: &netmap.NetworkMap{}})
}
b.newState(Starting)
// TODO(apenwarr): Fill in a more interesting status.
b.notify(Notify{Engine: &EngineStatus{}})
if b.notify != nil {
b.notify(Notify{Engine: &EngineStatus{}})
}
b.newState(Running)
}
@@ -70,7 +85,9 @@ func (b *FakeBackend) SetPrefs(new *Prefs) {
panic("FakeBackend.SetPrefs got nil prefs")
}
b.notify(Notify{Prefs: new.Clone()})
if b.notify != nil {
b.notify(Notify{Prefs: new.Clone()})
}
if new.WantRunning && !b.live {
b.newState(Starting)
b.newState(Running)
@@ -79,21 +96,20 @@ func (b *FakeBackend) SetPrefs(new *Prefs) {
}
}
func (b *FakeBackend) EditPrefs(mp *MaskedPrefs) {
// This fake implementation only cares about this one pref.
if mp.WantRunningSet {
b.SetPrefs(&mp.Prefs)
func (b *FakeBackend) RequestEngineStatus() {
if b.notify != nil {
b.notify(Notify{Engine: &EngineStatus{}})
}
}
func (b *FakeBackend) RequestEngineStatus() {
b.notify(Notify{Engine: &EngineStatus{}})
}
func (b *FakeBackend) FakeExpireAfter(x time.Duration) {
b.notify(Notify{NetMap: &netmap.NetworkMap{}})
if b.notify != nil {
b.notify(Notify{NetMap: &netmap.NetworkMap{}})
}
}
func (b *FakeBackend) Ping(ip string, useTSMP bool) {
b.notify(Notify{PingResult: &ipnstate.PingResult{}})
if b.notify != nil {
b.notify(Notify{PingResult: &ipnstate.PingResult{}})
}
}

View File

@@ -15,25 +15,26 @@ import (
)
type Handle struct {
frontendLogID string
b Backend
xnotify func(Notify)
logf logger.Logf
b Backend
logf logger.Logf
// Mutex protects everything below
mu sync.Mutex
xnotify func(Notify)
frontendLogID string
netmapCache *netmap.NetworkMap
engineStatusCache EngineStatus
stateCache State
prefsCache *Prefs
}
func NewHandle(b Backend, logf logger.Logf, opts Options) (*Handle, error) {
func NewHandle(b Backend, logf logger.Logf, notify func(Notify), opts Options) (*Handle, error) {
h := &Handle{
b: b,
logf: logf,
}
h.SetNotifyCallback(notify)
err := h.Start(opts)
if err != nil {
return nil, err
@@ -42,18 +43,25 @@ func NewHandle(b Backend, logf logger.Logf, opts Options) (*Handle, error) {
return h, nil
}
func (h *Handle) SetNotifyCallback(notify func(Notify)) {
h.mu.Lock()
h.xnotify = notify
h.mu.Unlock()
h.b.SetNotifyCallback(h.notify)
}
func (h *Handle) Start(opts Options) error {
h.mu.Lock()
h.frontendLogID = opts.FrontendLogID
h.xnotify = opts.Notify
h.netmapCache = nil
h.engineStatusCache = EngineStatus{}
h.stateCache = NoState
if opts.Prefs != nil {
h.prefsCache = opts.Prefs.Clone()
}
xopts := opts
xopts.Notify = h.notify
return h.b.Start(xopts)
h.mu.Unlock()
return h.b.Start(opts)
}
func (h *Handle) Reset() {

File diff suppressed because it is too large Load Diff

View File

@@ -291,3 +291,131 @@ func TestPeerRoutes(t *testing.T) {
}
}
func TestPeerAPIBase(t *testing.T) {
tests := []struct {
name string
nm *netmap.NetworkMap
peer *tailcfg.Node
want string
}{
{
name: "nil_netmap",
peer: new(tailcfg.Node),
want: "",
},
{
name: "nil_peer",
nm: new(netmap.NetworkMap),
want: "",
},
{
name: "self_only_4_them_both",
nm: &netmap.NetworkMap{
Addresses: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("100.64.1.1/32"),
},
},
peer: &tailcfg.Node{
Addresses: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("100.64.1.2/32"),
netaddr.MustParseIPPrefix("fe70::2/128"),
},
Hostinfo: tailcfg.Hostinfo{
Services: []tailcfg.Service{
{Proto: "peerapi4", Port: 444},
{Proto: "peerapi6", Port: 666},
},
},
},
want: "http://100.64.1.2:444",
},
{
name: "self_only_6_them_both",
nm: &netmap.NetworkMap{
Addresses: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("fe70::1/128"),
},
},
peer: &tailcfg.Node{
Addresses: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("100.64.1.2/32"),
netaddr.MustParseIPPrefix("fe70::2/128"),
},
Hostinfo: tailcfg.Hostinfo{
Services: []tailcfg.Service{
{Proto: "peerapi4", Port: 444},
{Proto: "peerapi6", Port: 666},
},
},
},
want: "http://[fe70::2]:666",
},
{
name: "self_both_them_only_4",
nm: &netmap.NetworkMap{
Addresses: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("100.64.1.1/32"),
netaddr.MustParseIPPrefix("fe70::1/128"),
},
},
peer: &tailcfg.Node{
Addresses: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("100.64.1.2/32"),
netaddr.MustParseIPPrefix("fe70::2/128"),
},
Hostinfo: tailcfg.Hostinfo{
Services: []tailcfg.Service{
{Proto: "peerapi4", Port: 444},
},
},
},
want: "http://100.64.1.2:444",
},
{
name: "self_both_them_only_6",
nm: &netmap.NetworkMap{
Addresses: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("100.64.1.1/32"),
netaddr.MustParseIPPrefix("fe70::1/128"),
},
},
peer: &tailcfg.Node{
Addresses: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("100.64.1.2/32"),
netaddr.MustParseIPPrefix("fe70::2/128"),
},
Hostinfo: tailcfg.Hostinfo{
Services: []tailcfg.Service{
{Proto: "peerapi6", Port: 666},
},
},
},
want: "http://[fe70::2]:666",
},
{
name: "self_both_them_no_peerapi_service",
nm: &netmap.NetworkMap{
Addresses: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("100.64.1.1/32"),
netaddr.MustParseIPPrefix("fe70::1/128"),
},
},
peer: &tailcfg.Node{
Addresses: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("100.64.1.2/32"),
netaddr.MustParseIPPrefix("fe70::2/128"),
},
},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := peerAPIBase(tt.nm, tt.peer)
if got != tt.want {
t.Errorf("got %q; want %q", got, tt.want)
}
})
}
}

View File

@@ -82,7 +82,6 @@ func TestLocalLogLines(t *testing.T) {
LastHandshake: time.Now(),
NodeKey: tailcfg.NodeKey(key.NewPrivate()),
}},
LocalAddrs: []string{"idk an address"},
}
lb.mu.Lock()
lb.parseWgStatusLocked(status)

View File

@@ -20,8 +20,11 @@ import (
"runtime"
"strconv"
"strings"
"sync"
"time"
"inet.af/netaddr"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/net/interfaces"
"tailscale.com/syncs"
@@ -37,9 +40,15 @@ type peerAPIServer struct {
tunName string
selfNode *tailcfg.Node
knownEmpty syncs.AtomicBool
// directFileMode is whether we're writing files directly to a
// download directory (as *.partial files), rather than making
// the frontend retrieve it over localapi HTTP and write it
// somewhere itself. This is used on GUI macOS version.
directFileMode bool
}
const partialSuffix = ".tspartial"
const partialSuffix = ".partial"
func (s *peerAPIServer) diskPath(baseName string) (fullPath string, ok bool) {
clean := path.Clean(baseName)
@@ -55,7 +64,7 @@ func (s *peerAPIServer) diskPath(baseName string) (fullPath string, ok bool) {
// hasFilesWaiting reports whether any files are buffered in the
// tailscaled daemon storage.
func (s *peerAPIServer) hasFilesWaiting() bool {
if s.rootDir == "" {
if s.rootDir == "" || s.directFileMode {
return false
}
if s.knownEmpty.Get() {
@@ -91,17 +100,13 @@ func (s *peerAPIServer) hasFilesWaiting() bool {
return false
}
// WaitingFile is a JSON-marshaled struct sent by the localapi to pick
// up queued files.
type WaitingFile struct {
Name string
Size int64
}
func (s *peerAPIServer) WaitingFiles() (ret []WaitingFile, err error) {
func (s *peerAPIServer) WaitingFiles() (ret []apitype.WaitingFile, err error) {
if s.rootDir == "" {
return nil, errors.New("peerapi disabled; no storage configured")
}
if s.directFileMode {
return nil, nil
}
f, err := os.Open(s.rootDir)
if err != nil {
return nil, err
@@ -119,7 +124,7 @@ func (s *peerAPIServer) WaitingFiles() (ret []WaitingFile, err error) {
if err != nil {
continue
}
ret = append(ret, WaitingFile{
ret = append(ret, apitype.WaitingFile{
Name: filepath.Base(name),
Size: fi.Size(),
})
@@ -139,6 +144,9 @@ func (s *peerAPIServer) DeleteFile(baseName string) error {
if s.rootDir == "" {
return errors.New("peerapi disabled; no storage configured")
}
if s.directFileMode {
return errors.New("deletes not allowed in direct mode")
}
path, ok := s.diskPath(baseName)
if !ok {
return errors.New("bad filename")
@@ -154,6 +162,9 @@ func (s *peerAPIServer) OpenFile(baseName string) (rc io.ReadCloser, size int64,
if s.rootDir == "" {
return nil, 0, errors.New("peerapi disabled; no storage configured")
}
if s.directFileMode {
return nil, 0, errors.New("opens not allowed in direct mode")
}
path, ok := s.diskPath(baseName)
if !ok {
return nil, 0, errors.New("bad filename")
@@ -338,11 +349,73 @@ This is my Tailscale device. Your device is %v.
}
}
type incomingFile struct {
name string // "foo.jpg"
started time.Time
size int64 // or -1 if unknown; never 0
w io.Writer // underlying writer
ph *peerAPIHandler
partialPath string // non-empty in direct mode
mu sync.Mutex
copied int64
done bool
lastNotify time.Time
}
func (f *incomingFile) markAndNotifyDone() {
f.mu.Lock()
f.done = true
f.mu.Unlock()
b := f.ph.ps.b
b.sendFileNotify()
}
func (f *incomingFile) Write(p []byte) (n int, err error) {
n, err = f.w.Write(p)
b := f.ph.ps.b
var needNotify bool
defer func() {
if needNotify {
b.sendFileNotify()
}
}()
if n > 0 {
f.mu.Lock()
defer f.mu.Unlock()
f.copied += int64(n)
now := time.Now()
if f.lastNotify.IsZero() || now.Sub(f.lastNotify) > time.Second {
f.lastNotify = now
needNotify = true
}
}
return n, err
}
func (f *incomingFile) PartialFile() ipn.PartialFile {
f.mu.Lock()
defer f.mu.Unlock()
return ipn.PartialFile{
Name: f.name,
Started: f.started,
DeclaredSize: f.size,
Received: f.copied,
PartialPath: f.partialPath,
Done: f.done,
}
}
func (h *peerAPIHandler) put(w http.ResponseWriter, r *http.Request) {
if !h.isSelf {
http.Error(w, "not owner", http.StatusForbidden)
return
}
if !h.ps.b.hasCapFileSharing() {
http.Error(w, "file sharing not enabled by Tailscale admin", http.StatusForbidden)
return
}
if r.Method != "PUT" {
http.Error(w, "not method PUT", http.StatusMethodNotAllowed)
return
@@ -357,6 +430,9 @@ func (h *peerAPIHandler) put(w http.ResponseWriter, r *http.Request) {
http.Error(w, "bad filename", 400)
return
}
if h.ps.directFileMode {
dstFile += partialSuffix
}
f, err := os.Create(dstFile)
if err != nil {
h.logf("put Create error: %v", err)
@@ -369,27 +445,49 @@ func (h *peerAPIHandler) put(w http.ResponseWriter, r *http.Request) {
os.Remove(dstFile)
}
}()
n, err := io.Copy(f, r.Body)
if err != nil {
f.Close()
h.logf("put Copy error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
var finalSize int64
var inFile *incomingFile
if r.ContentLength != 0 {
inFile = &incomingFile{
name: baseName,
started: time.Now(),
size: r.ContentLength,
w: f,
ph: h,
}
if h.ps.directFileMode {
inFile.partialPath = dstFile
}
h.ps.b.registerIncomingFile(inFile, true)
defer h.ps.b.registerIncomingFile(inFile, false)
n, err := io.Copy(inFile, r.Body)
if err != nil {
f.Close()
h.logf("put Copy error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
finalSize = n
}
if err := f.Close(); err != nil {
h.logf("put Close error: %v", err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if h.ps.directFileMode {
if inFile != nil { // non-zero length; TODO: notify even for zero length
inFile.markAndNotifyDone()
}
}
h.logf("put of %s from %v/%v", baseName, approxSize(n), h.remoteAddr.IP, h.peerNode.ComputedName)
h.logf("put of %s from %v/%v", approxSize(finalSize), h.remoteAddr.IP, h.peerNode.ComputedName)
// TODO: set modtime
// TODO: some real response
success = true
io.WriteString(w, "{}\n")
h.ps.knownEmpty.Set(false)
h.ps.b.send(ipn.Notify{}) // it will set FilesWaiting
h.ps.b.sendFileNotify()
}
func approxSize(n int64) string {

View File

@@ -7,6 +7,7 @@
package ipnlocal
import (
"errors"
"fmt"
"log"
"net"
@@ -20,6 +21,7 @@ import (
func init() {
initListenConfig = initListenConfigNetworkExtension
peerDialControlFunc = peerDialControlFuncNetworkExtension
}
// initListenConfigNetworkExtension configures nc for listening on IP
@@ -33,16 +35,7 @@ func initListenConfigNetworkExtension(nc *net.ListenConfig, ip netaddr.IP, st *i
nc.Control = func(network, address string, c syscall.RawConn) error {
var sockErr error
err := c.Control(func(fd uintptr) {
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
}
sockErr = unix.SetsockoptInt(int(fd), proto, opt, tunIf.Index)
sockErr = bindIf(fd, network, address, tunIf.Index)
log.Printf("peerapi: bind(%q, %q) on index %v = %v", network, address, tunIf.Index, sockErr)
})
if err != nil {
@@ -52,3 +45,40 @@ func initListenConfigNetworkExtension(nc *net.ListenConfig, ip netaddr.IP, st *i
}
return nil
}
func bindIf(fd uintptr, network, address string, ifIndex int) error {
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
}
return unix.SetsockoptInt(int(fd), proto, opt, ifIndex)
}
func peerDialControlFuncNetworkExtension(b *LocalBackend) func(network, address string, c syscall.RawConn) error {
b.mu.Lock()
defer b.mu.Unlock()
st := b.prevIfState
pas := b.peerAPIServer
index := -1
if st != nil && pas != nil && pas.tunName != "" {
if tunIf, ok := st.Interface[pas.tunName]; ok {
index = tunIf.Index
}
}
return func(network, address string, c syscall.RawConn) error {
if index == -1 {
return errors.New("failed to find TUN interface to bind to")
}
var sockErr error
err := c.Control(func(fd uintptr) {
sockErr = bindIf(fd, network, address, index)
})
if err != nil {
return err
}
return sockErr
}
}

View File

@@ -63,16 +63,6 @@ type Options struct {
// waits for a frontend to start it.
AutostartStateKey ipn.StateKey
// LegacyConfigPath optionally specifies the old-style relaynode
// relay.conf location. If both LegacyConfigPath and
// AutostartStateKey are specified and the requested state doesn't
// exist in the backend store, the backend migrates the config
// from LegacyConfigPath.
//
// TODO(danderson): remove some time after the transition to
// tailscaled is done.
LegacyConfigPath string
// SurviveDisconnects specifies how the server reacts to its
// frontend disconnecting. If true, the server keeps running on
// its existing state, and accepts new frontend connections. If
@@ -754,10 +744,7 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() (
server.bs.GotCommand(context.TODO(), &ipn.Command{
Version: version.Long,
Start: &ipn.StartArgs{
Opts: ipn.Options{
StateKey: opts.AutostartStateKey,
LegacyConfigPath: opts.LegacyConfigPath,
},
Opts: ipn.Options{StateKey: opts.AutostartStateKey},
},
})
}

View File

@@ -71,7 +71,8 @@ type PeerStatus struct {
OS string // HostInfo.OS
UserID tailcfg.UserID
TailAddr string // Tailscale IP
TailAddrDeprecated string `json:"TailAddr"` // Tailscale IP
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
// Endpoints:
Addrs []string
@@ -87,7 +88,8 @@ type PeerStatus struct {
KeepAlive bool
ExitNode bool // true if this is the currently selected exit node.
PeerAPIURL []string
PeerAPIURL []string
Capabilities []string `json:",omitempty"`
// ShareeNode indicates this node exists in the netmap because
// it's owned by a shared-to user and that node might connect
@@ -213,8 +215,11 @@ func (sb *StatusBuilder) AddPeer(peer key.Public, st *PeerStatus) {
if v := st.UserID; v != 0 {
e.UserID = v
}
if v := st.TailAddr; v != "" {
e.TailAddr = v
if v := st.TailAddrDeprecated; v != "" {
e.TailAddrDeprecated = v
}
if v := st.TailscaleIPs; v != nil {
e.TailscaleIPs = v
}
if v := st.OS; v != "" {
e.OS = st.OS
@@ -343,13 +348,17 @@ table tbody tr:nth-child(even) td { background-color: #f5f5f5; }
hostNameHTML = "<br>" + html.EscapeString(hostName)
}
var tailAddr string
if len(ps.TailscaleIPs) > 0 {
tailAddr = ps.TailscaleIPs[0].String()
}
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,
tailAddr,
html.EscapeString(owner),
ps.RxBytes,
ps.TxBytes,
@@ -437,5 +446,9 @@ func sortKey(ps *PeerStatus) string {
if ps.HostName != "" {
return ps.HostName
}
return ps.TailAddr
// TODO(bradfitz): add PeerStatus.Less and avoid these allocs in a Less func.
if len(ps.TailscaleIPs) > 0 {
return ps.TailscaleIPs[0].String()
}
return string(ps.PublicKey[:])
}

View File

@@ -9,16 +9,23 @@ import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"net/http"
"net/http/httputil"
"net/url"
"reflect"
"runtime"
"strconv"
"strings"
"sync"
"time"
"inet.af/netaddr"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
@@ -72,6 +79,10 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.serveFiles(w, r)
return
}
if strings.HasPrefix(r.URL.Path, "/localapi/v0/file-put/") {
h.serveFilePut(w, r)
return
}
switch r.URL.Path {
case "/localapi/v0/whois":
h.serveWhoIs(w, r)
@@ -79,12 +90,20 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.serveGoroutines(w, r)
case "/localapi/v0/status":
h.serveStatus(w, r)
case "/localapi/v0/logout":
h.serveLogout(w, r)
case "/localapi/v0/prefs":
h.servePrefs(w, r)
case "/localapi/v0/check-ip-forwarding":
h.serveCheckIPForwarding(w, r)
case "/localapi/v0/bugreport":
h.serveBugReport(w, r)
default:
case "/localapi/v0/file-targets":
h.serveFileTargets(w, r)
case "/":
io.WriteString(w, "tailscaled\n")
default:
http.Error(w, "404 not found", 404)
}
}
@@ -126,7 +145,7 @@ func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
http.Error(w, "no match for IP:port", 404)
return
}
res := &tailcfg.WhoIsResponse{
res := &apitype.WhoIsResponse{
Node: n,
UserProfile: &u,
}
@@ -186,6 +205,58 @@ func (h *Handler) serveStatus(w http.ResponseWriter, r *http.Request) {
e.Encode(st)
}
func (h *Handler) serveLogout(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "logout access denied", http.StatusForbidden)
return
}
if r.Method != "POST" {
http.Error(w, "want POST", 400)
return
}
err := h.b.LogoutSync(r.Context())
if err == nil {
w.WriteHeader(http.StatusNoContent)
return
}
http.Error(w, err.Error(), 500)
}
func (h *Handler) servePrefs(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead {
http.Error(w, "prefs access denied", http.StatusForbidden)
return
}
var prefs *ipn.Prefs
switch r.Method {
case "PATCH":
if !h.PermitWrite {
http.Error(w, "prefs write access denied", http.StatusForbidden)
return
}
mp := new(ipn.MaskedPrefs)
if err := json.NewDecoder(r.Body).Decode(mp); err != nil {
http.Error(w, err.Error(), 400)
return
}
var err error
prefs, err = h.b.EditPrefs(mp)
if err != nil {
http.Error(w, err.Error(), 400)
return
}
case "GET", "HEAD":
prefs = h.b.Prefs()
default:
http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
e := json.NewEncoder(w)
e.SetIndent("", "\t")
e.Encode(prefs)
}
func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "file access denied", http.StatusForbidden)
@@ -229,6 +300,108 @@ func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
io.Copy(w, rc)
}
func writeErrorJSON(w http.ResponseWriter, err error) {
if err == nil {
err = errors.New("unexpected nil error")
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(500)
type E struct {
Error string `json:"error"`
}
json.NewEncoder(w).Encode(E{err.Error()})
}
func (h *Handler) serveFileTargets(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead {
http.Error(w, "access denied", http.StatusForbidden)
return
}
if r.Method != "GET" {
http.Error(w, "want GET to list targets", 400)
return
}
fts, err := h.b.FileTargets()
if err != nil {
writeErrorJSON(w, err)
return
}
makeNonNil(&fts)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(fts)
}
func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "file access denied", http.StatusForbidden)
return
}
if r.Method != "PUT" {
http.Error(w, "want PUT to put file", 400)
return
}
fts, err := h.b.FileTargets()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
upath := strings.TrimPrefix(r.URL.EscapedPath(), "/localapi/v0/file-put/")
slash := strings.Index(upath, "/")
if slash == -1 {
http.Error(w, "bogus URL", 400)
return
}
stableID, filenameEscaped := tailcfg.StableNodeID(upath[:slash]), upath[slash+1:]
var ft *apitype.FileTarget
for _, x := range fts {
if x.Node.StableID == stableID {
ft = x
break
}
}
if ft == nil {
http.Error(w, "node not found", 404)
return
}
dstURL, err := url.Parse(ft.PeerAPIURL)
if err != nil {
http.Error(w, "bogus peer URL", 500)
return
}
outReq, err := http.NewRequestWithContext(r.Context(), "PUT", "http://peer/v0/put/"+filenameEscaped, r.Body)
if err != nil {
http.Error(w, "bogus outreq", 500)
return
}
outReq.ContentLength = r.ContentLength
rp := httputil.NewSingleHostReverseProxy(dstURL)
rp.Transport = getDialPeerTransport(h.b)
rp.ServeHTTP(w, outReq)
}
var dialPeerTransportOnce struct {
sync.Once
v *http.Transport
}
func getDialPeerTransport(b *ipnlocal.LocalBackend) *http.Transport {
dialPeerTransportOnce.Do(func() {
t := http.DefaultTransport.(*http.Transport).Clone()
t.Dial = nil //lint:ignore SA1019 yes I know I'm setting it to nil defensively
dialer := net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
Control: b.PeerDialControlFunc(),
}
t.DialContext = dialer.DialContext
dialPeerTransportOnce.v = t
})
return dialPeerTransportOnce.v
}
func defBool(a string, def bool) bool {
if a == "" {
return def
@@ -239,3 +412,30 @@ func defBool(a string, def bool) bool {
}
return v
}
// makeNonNil takes a pointer to a Go data structure
// (currently only a slice or a map) and makes sure it's non-nil for
// JSON serialization. (In particular, JavaScript clients usually want
// the field to be defined after they decode the JSON.)
func makeNonNil(ptr interface{}) {
if ptr == nil {
panic("nil interface")
}
rv := reflect.ValueOf(ptr)
if rv.Kind() != reflect.Ptr {
panic(fmt.Sprintf("kind %v, not Ptr", rv.Kind()))
}
if rv.Pointer() == 0 {
panic("nil pointer")
}
rv = rv.Elem()
if rv.Pointer() != 0 {
return
}
switch rv.Type().Kind() {
case reflect.Slice:
rv.Set(reflect.MakeSlice(rv.Type(), 0, 0))
case reflect.Map:
rv.Set(reflect.MakeMap(rv.Type()))
}
}

View File

@@ -80,7 +80,6 @@ type Command struct {
Login *tailcfg.Oauth2Token
Logout *NoArgs
SetPrefs *SetPrefsArgs
EditPrefs *MaskedPrefs
RequestEngineStatus *NoArgs
RequestStatus *NoArgs
FakeExpireAfter *FakeExpireAfterArgs
@@ -95,11 +94,13 @@ type BackendServer struct {
}
func NewBackendServer(logf logger.Logf, b Backend, sendNotifyMsg func(b []byte)) *BackendServer {
return &BackendServer{
bs := &BackendServer{
logf: logf,
b: b,
sendNotifyMsg: sendNotifyMsg,
}
b.SetNotifyCallback(bs.send)
return bs
}
func (bs *BackendServer) send(n Notify) {
@@ -190,7 +191,6 @@ func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error {
return errors.New("Quit command received")
} else if c := cmd.Start; c != nil {
opts := c.Opts
opts.Notify = bs.send
return bs.b.Start(opts)
} else if c := cmd.StartLoginInteractive; c != nil {
bs.b.StartLoginInteractive()
@@ -204,9 +204,6 @@ func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error {
} else if c := cmd.SetPrefs; c != nil {
bs.b.SetPrefs(c.New)
return nil
} else if c := cmd.EditPrefs; c != nil {
bs.b.EditPrefs(c)
return nil
} else if c := cmd.FakeExpireAfter; c != nil {
bs.b.FakeExpireAfter(c.Duration)
return nil
@@ -247,7 +244,7 @@ func (bc *BackendClient) GotNotifyMsg(b []byte) {
}
n := Notify{}
if err := json.Unmarshal(b, &n); err != nil {
log.Fatalf("BackendClient.Notify: cannot decode message (length=%d)\n%#v", len(b), string(b))
log.Fatalf("BackendClient.Notify: cannot decode message (length=%d, %#q): %v", len(b), b, err)
}
if n.Version != version.Long && !bc.AllowVersionSkew {
vs := fmt.Sprintf("GotNotify: Version mismatch! frontend=%#v backend=%#v",
@@ -287,8 +284,6 @@ func (bc *BackendClient) Quit() error {
}
func (bc *BackendClient) Start(opts Options) error {
bc.notify = opts.Notify
opts.Notify = nil // server can't call our function pointer
bc.send(Command{Start: &StartArgs{Opts: opts}})
return nil // remote Start() errors must be handled remotely
}
@@ -309,10 +304,6 @@ func (bc *BackendClient) SetPrefs(new *Prefs) {
bc.send(Command{SetPrefs: &SetPrefsArgs{New: new}})
}
func (bc *BackendClient) EditPrefs(mp *MaskedPrefs) {
bc.send(Command{EditPrefs: mp})
}
func (bc *BackendClient) RequestEngineStatus() {
bc.send(Command{RequestEngineStatus: &NoArgs{}})
}

View File

@@ -90,13 +90,11 @@ func TestClientServer(t *testing.T) {
bc = NewBackendClient(clogf, clientToServer)
ch := make(chan Notify, 256)
h, err := NewHandle(bc, clogf, Options{
notify := func(n Notify) { ch <- n }
h, err := NewHandle(bc, clogf, notify, Options{
Prefs: &Prefs{
ControlURL: "http://example.com/fake",
},
Notify: func(n Notify) {
ch <- n
},
})
if err != nil {
t.Fatalf("NewHandle error: %v\n", err)

View File

@@ -66,6 +66,10 @@ type Prefs struct {
ExitNodeID tailcfg.StableNodeID
ExitNodeIP netaddr.IP
// ExitNodeAllowLANAccess indicates whether locally accessible subnets should be
// routed directly or via the exit node.
ExitNodeAllowLANAccess bool
// CorpDNS specifies whether to install the Tailscale network's
// DNS configuration, if it exists.
CorpDNS bool
@@ -152,23 +156,24 @@ type Prefs struct {
type MaskedPrefs struct {
Prefs
ControlURLSet bool `json:",omitempty"`
RouteAllSet bool `json:",omitempty"`
AllowSingleHostsSet bool `json:",omitempty"`
ExitNodeIDSet bool `json:",omitempty"`
ExitNodeIPSet bool `json:",omitempty"`
CorpDNSSet bool `json:",omitempty"`
WantRunningSet bool `json:",omitempty"`
ShieldsUpSet bool `json:",omitempty"`
AdvertiseTagsSet bool `json:",omitempty"`
HostnameSet bool `json:",omitempty"`
OSVersionSet bool `json:",omitempty"`
DeviceModelSet bool `json:",omitempty"`
NotepadURLsSet bool `json:",omitempty"`
ForceDaemonSet bool `json:",omitempty"`
AdvertiseRoutesSet bool `json:",omitempty"`
NoSNATSet bool `json:",omitempty"`
NetfilterModeSet bool `json:",omitempty"`
ControlURLSet bool `json:",omitempty"`
RouteAllSet bool `json:",omitempty"`
AllowSingleHostsSet bool `json:",omitempty"`
ExitNodeIDSet bool `json:",omitempty"`
ExitNodeIPSet bool `json:",omitempty"`
ExitNodeAllowLANAccessSet bool `json:",omitempty"`
CorpDNSSet bool `json:",omitempty"`
WantRunningSet bool `json:",omitempty"`
ShieldsUpSet bool `json:",omitempty"`
AdvertiseTagsSet bool `json:",omitempty"`
HostnameSet bool `json:",omitempty"`
OSVersionSet bool `json:",omitempty"`
DeviceModelSet bool `json:",omitempty"`
NotepadURLsSet bool `json:",omitempty"`
ForceDaemonSet bool `json:",omitempty"`
AdvertiseRoutesSet bool `json:",omitempty"`
NoSNATSet bool `json:",omitempty"`
NetfilterModeSet bool `json:",omitempty"`
}
// ApplyEdits mutates p, assigning fields from m.Prefs for each MaskedPrefs
@@ -237,9 +242,9 @@ func (p *Prefs) pretty(goos string) string {
sb.WriteString("shields=true ")
}
if !p.ExitNodeIP.IsZero() {
fmt.Fprintf(&sb, "exit=%v ", p.ExitNodeIP)
fmt.Fprintf(&sb, "exit=%v lan=%t ", p.ExitNodeIP, p.ExitNodeAllowLANAccess)
} else if !p.ExitNodeID.IsZero() {
fmt.Fprintf(&sb, "exit=%v ", p.ExitNodeID)
fmt.Fprintf(&sb, "exit=%v lan=%t ", p.ExitNodeID, p.ExitNodeAllowLANAccess)
}
if len(p.AdvertiseRoutes) > 0 || goos == "linux" {
fmt.Fprintf(&sb, "routes=%v ", p.AdvertiseRoutes)
@@ -256,6 +261,9 @@ func (p *Prefs) pretty(goos string) string {
if p.ControlURL != "" && p.ControlURL != "https://login.tailscale.com" {
fmt.Fprintf(&sb, "url=%q ", p.ControlURL)
}
if p.Hostname != "" {
fmt.Fprintf(&sb, "host=%q ", p.Hostname)
}
if p.Persist != nil {
sb.WriteString(p.Persist.Pretty())
} else {
@@ -287,6 +295,7 @@ func (p *Prefs) Equals(p2 *Prefs) bool {
p.AllowSingleHosts == p2.AllowSingleHosts &&
p.ExitNodeID == p2.ExitNodeID &&
p.ExitNodeIP == p2.ExitNodeIP &&
p.ExitNodeAllowLANAccess == p2.ExitNodeAllowLANAccess &&
p.CorpDNS == p2.CorpDNS &&
p.WantRunning == p2.WantRunning &&
p.NotepadURLs == p2.NotepadURLs &&

View File

@@ -33,22 +33,23 @@ func (src *Prefs) Clone() *Prefs {
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type Prefs
var _PrefsNeedsRegeneration = Prefs(struct {
ControlURL string
RouteAll bool
AllowSingleHosts bool
ExitNodeID tailcfg.StableNodeID
ExitNodeIP netaddr.IP
CorpDNS bool
WantRunning bool
ShieldsUp bool
AdvertiseTags []string
Hostname string
OSVersion string
DeviceModel string
NotepadURLs bool
ForceDaemon bool
AdvertiseRoutes []netaddr.IPPrefix
NoSNAT bool
NetfilterMode preftype.NetfilterMode
Persist *persist.Persist
ControlURL string
RouteAll bool
AllowSingleHosts bool
ExitNodeID tailcfg.StableNodeID
ExitNodeIP netaddr.IP
ExitNodeAllowLANAccess bool
CorpDNS bool
WantRunning bool
ShieldsUp bool
AdvertiseTags []string
Hostname string
OSVersion string
DeviceModel string
NotepadURLs bool
ForceDaemon bool
AdvertiseRoutes []netaddr.IPPrefix
NoSNAT bool
NetfilterMode preftype.NetfilterMode
Persist *persist.Persist
}{})

View File

@@ -33,7 +33,7 @@ func fieldsOf(t reflect.Type) (fields []string) {
func TestPrefsEqual(t *testing.T) {
tstest.PanicOnLog()
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "ExitNodeID", "ExitNodeIP", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "Persist"}
prefsHandles := []string{"ControlURL", "RouteAll", "AllowSingleHosts", "ExitNodeID", "ExitNodeIP", "ExitNodeAllowLANAccess", "CorpDNS", "WantRunning", "ShieldsUp", "AdvertiseTags", "Hostname", "OSVersion", "DeviceModel", "NotepadURLs", "ForceDaemon", "AdvertiseRoutes", "NoSNAT", "NetfilterMode", "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)
@@ -124,6 +124,17 @@ func TestPrefsEqual(t *testing.T) {
true,
},
{
&Prefs{},
&Prefs{ExitNodeAllowLANAccess: true},
false,
},
{
&Prefs{ExitNodeAllowLANAccess: true},
&Prefs{ExitNodeAllowLANAccess: true},
true,
},
{
&Prefs{CorpDNS: true},
&Prefs{CorpDNS: false},
@@ -384,14 +395,36 @@ func TestPrefsPretty(t *testing.T) {
ExitNodeIP: netaddr.MustParseIP("1.2.3.4"),
},
"linux",
`Prefs{ra=false mesh=false dns=false want=false exit=1.2.3.4 routes=[] nf=off Persist=nil}`,
`Prefs{ra=false mesh=false dns=false want=false exit=1.2.3.4 lan=false routes=[] nf=off Persist=nil}`,
},
{
Prefs{
ExitNodeID: tailcfg.StableNodeID("myNodeABC"),
},
"linux",
`Prefs{ra=false mesh=false dns=false want=false exit=myNodeABC routes=[] nf=off Persist=nil}`,
`Prefs{ra=false mesh=false dns=false want=false exit=myNodeABC lan=false routes=[] nf=off Persist=nil}`,
},
{
Prefs{
ExitNodeID: tailcfg.StableNodeID("myNodeABC"),
ExitNodeAllowLANAccess: true,
},
"linux",
`Prefs{ra=false mesh=false dns=false want=false exit=myNodeABC lan=true routes=[] nf=off Persist=nil}`,
},
{
Prefs{
ExitNodeAllowLANAccess: true,
},
"linux",
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off Persist=nil}`,
},
{
Prefs{
Hostname: "foo",
},
"linux",
`Prefs{ra=false mesh=false dns=false want=false routes=[] nf=off host="foo" Persist=nil}`,
},
}
for i, tt := range tests {

View File

@@ -121,14 +121,17 @@ func (id PublicID) MarshalText() ([]byte, error) {
}
func (id *PublicID) UnmarshalText(s []byte) error {
b, err := hex.DecodeString(string(s))
if err != nil {
return fmt.Errorf("logtail.PublicID.UnmarshalText: %v", err)
if len(s) != len(id)*2 {
return fmt.Errorf("logtail.PublicID.UnmarshalText: invalid hex length: %d", len(s))
}
if len(b) != len(id) {
return fmt.Errorf("logtail.PublicID.UnmarshalText: invalid hex length: %d", len(b))
for i := range id {
a, ok1 := fromHexChar(s[i*2+0])
b, ok2 := fromHexChar(s[i*2+1])
if !ok1 || !ok2 {
return errors.New("invalid hex character")
}
id[i] = (a << 4) | b
}
copy(id[:], b)
return nil
}

View File

@@ -303,3 +303,26 @@ func TestParseAndRemoveLogLevel(t *testing.T) {
}
}
}
func TestPublicIDUnmarshalText(t *testing.T) {
const hexStr = "6c60a9e0e7af57170bb1347b2d477e4cbc27d4571a4923b21651456f931e3d55"
x := []byte(hexStr)
var id PublicID
if err := id.UnmarshalText(x); err != nil {
t.Fatal(err)
}
if id.String() != hexStr {
t.Errorf("String = %q; want %q", id.String(), hexStr)
}
n := int(testing.AllocsPerRun(1000, func() {
var id PublicID
if err := id.UnmarshalText(x); err != nil {
t.Fatal(err)
}
}))
if n != 0 {
t.Errorf("allocs = %v; want 0", n)
}
}

View File

@@ -5,7 +5,10 @@
package dns
import (
"sort"
"inet.af/netaddr"
"tailscale.com/util/dnsname"
)
// Config is a DNS configuration.
@@ -19,27 +22,102 @@ type Config struct {
// for queries that fall within that suffix.
// If a query doesn't match any entry in Routes, the
// DefaultResolvers are used.
Routes map[string][]netaddr.IPPort
Routes map[dnsname.FQDN][]netaddr.IPPort
// SearchDomains are DNS suffixes to try when expanding
// single-label queries.
SearchDomains []string
SearchDomains []dnsname.FQDN
// Hosts maps DNS FQDNs to their IPs, which can be a mix of IPv4
// and IPv6.
// Queries matching entries in Hosts are resolved locally without
// recursing off-machine.
Hosts map[string][]netaddr.IP
Hosts map[dnsname.FQDN][]netaddr.IP
// AuthoritativeSuffixes is a list of fully-qualified DNS suffixes
// for which the in-process Tailscale resolver is authoritative.
// Queries for names within AuthoritativeSuffixes can only be
// fulfilled by entries in Hosts. Queries with no match in Hosts
// return NXDOMAIN.
AuthoritativeSuffixes []string
AuthoritativeSuffixes []dnsname.FQDN
}
// OSConfig is an OS DNS configuration.
type OSConfig struct {
// Nameservers are the IP addresses of the nameservers to use.
Nameservers []netaddr.IP
// Domains are the search domains to use.
Domains []string
// needsAnyResolvers reports whether c requires a resolver to be set
// at the OS level.
func (c Config) needsOSResolver() bool {
return c.hasDefaultResolvers() || c.hasRoutes() || c.hasHosts()
}
func (c Config) hasRoutes() bool {
return len(c.Routes) > 0
}
// hasDefaultResolversOnly reports whether the only resolvers in c are
// DefaultResolvers.
func (c Config) hasDefaultResolversOnly() bool {
return c.hasDefaultResolvers() && !c.hasRoutes() && !c.hasHosts()
}
func (c Config) hasDefaultResolvers() bool {
return len(c.DefaultResolvers) > 0
}
// singleResolverSet returns the resolvers used by c.Routes if all
// routes use the same resolvers, or nil if multiple sets of resolvers
// are specified.
func (c Config) singleResolverSet() []netaddr.IPPort {
var first []netaddr.IPPort
for _, resolvers := range c.Routes {
if first == nil {
first = resolvers
continue
}
if !sameIPPorts(first, resolvers) {
return nil
}
}
return first
}
// hasHosts reports whether c requires resolution of MagicDNS hosts or
// domains.
func (c Config) hasHosts() bool {
return len(c.Hosts) > 0 || len(c.AuthoritativeSuffixes) > 0
}
// matchDomains returns the list of match suffixes needed by Routes,
// AuthoritativeSuffixes. Hosts is not considered as we assume that
// they're covered by AuthoritativeSuffixes for now.
func (c Config) matchDomains() []dnsname.FQDN {
ret := make([]dnsname.FQDN, 0, len(c.Routes)+len(c.AuthoritativeSuffixes))
seen := map[dnsname.FQDN]bool{}
for _, suffix := range c.AuthoritativeSuffixes {
if seen[suffix] {
continue
}
ret = append(ret, suffix)
seen[suffix] = true
}
for suffix := range c.Routes {
if seen[suffix] {
continue
}
ret = append(ret, suffix)
seen[suffix] = true
}
sort.Slice(ret, func(i, j int) bool {
return ret[i].WithTrailingDot() < ret[j].WithTrailingDot()
})
return ret
}
func sameIPPorts(a, b []netaddr.IPPort) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}

View File

@@ -0,0 +1,171 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package dns
import (
"bufio"
"bytes"
_ "embed"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"tailscale.com/atomicfile"
"tailscale.com/types/logger"
)
//go:embed resolvconf-workaround.sh
var workaroundScript []byte
// resolvconfConfigName is the name of the config submitted to
// resolvconf.
// The name starts with 'tun' in order to match the hardcoded
// interface order in debian resolvconf, which will place this
// configuration ahead of regular network links. In theory, this
// doesn't matter because we then fix things up to ensure our config
// is the only one in use, but in case that fails, this will make our
// configuration slightly preferred.
// The 'inet' suffix has no specific meaning, but conventionally
// resolvconf implementations encourage adding a suffix roughly
// indicating where the config came from, and "inet" is the "none of
// the above" value (rather than, say, "ppp" or "dhcp").
const resolvconfConfigName = "tun-tailscale.inet"
// resolvconfLibcHookPath is the directory containing libc update
// scripts, which are run by Debian resolvconf when /etc/resolv.conf
// has been updated.
const resolvconfLibcHookPath = "/etc/resolvconf/update-libc.d"
// resolvconfHookPath is the name of the libc hook script we install
// to force Tailscale's DNS config to take effect.
var resolvconfHookPath = filepath.Join(resolvconfLibcHookPath, "tailscale")
// resolvconfManager manages DNS configuration using the Debian
// implementation of the `resolvconf` program, written by Thomas Hood.
type resolvconfManager struct {
logf logger.Logf
listRecordsPath string
interfacesDir string
scriptInstalled bool // libc update script has been installed
}
func newDebianResolvconfManager(logf logger.Logf) (*resolvconfManager, error) {
ret := &resolvconfManager{
logf: logf,
listRecordsPath: "/lib/resolvconf/list-records",
interfacesDir: "/etc/resolvconf/run/interface", // panic fallback if nothing seems to work
}
if _, err := os.Stat(ret.listRecordsPath); os.IsNotExist(err) {
// This might be a Debian system from before the big /usr
// merge, try /usr instead.
ret.listRecordsPath = "/usr" + ret.listRecordsPath
}
// The runtime directory is currently (2020-04) canonically
// /etc/resolvconf/run, but the manpage is making noise about
// switching to /run/resolvconf and dropping the /etc path. So,
// let's probe the possible directories and use the first one
// that works.
for _, path := range []string{
"/etc/resolvconf/run/interface",
"/run/resolvconf/interface",
"/var/run/resolvconf/interface",
} {
if _, err := os.Stat(path); err == nil {
ret.interfacesDir = path
break
}
}
if ret.interfacesDir == "" {
// None of the paths seem to work, use the canonical location
// that the current manpage says to use.
ret.interfacesDir = "/etc/resolvconf/run/interfaces"
}
return ret, nil
}
func (m *resolvconfManager) SetDNS(config OSConfig) error {
if !m.scriptInstalled {
m.logf("injecting resolvconf workaround script")
if err := os.MkdirAll(resolvconfLibcHookPath, 0755); err != nil {
return err
}
if err := atomicfile.WriteFile(resolvconfHookPath, workaroundScript, 0755); err != nil {
return err
}
m.scriptInstalled = true
}
stdin := new(bytes.Buffer)
writeResolvConf(stdin, config.Nameservers, config.SearchDomains) // dns_direct.go
// This resolvconf implementation doesn't support exclusive mode
// or interface priorities, so it will end up blending our
// configuration with other sources. However, this will get fixed
// up by the script we injected above.
cmd := exec.Command("resolvconf", "-a", resolvconfConfigName)
cmd.Stdin = stdin
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("running %s: %s", cmd, out)
}
return nil
}
func (m *resolvconfManager) SupportsSplitDNS() bool {
return false
}
func (m *resolvconfManager) GetBaseConfig() (OSConfig, error) {
var bs bytes.Buffer
cmd := exec.Command(m.listRecordsPath)
// list-records assumes it's being run with CWD set to the
// interfaces runtime dir, and returns nonsense otherwise.
cmd.Dir = m.interfacesDir
cmd.Stdout = &bs
if err := cmd.Run(); err != nil {
return OSConfig{}, err
}
var conf bytes.Buffer
sc := bufio.NewScanner(&bs)
for sc.Scan() {
if sc.Text() == resolvconfConfigName {
continue
}
bs, err := ioutil.ReadFile(filepath.Join(m.interfacesDir, sc.Text()))
if err != nil {
if os.IsNotExist(err) {
// Probably raced with a deletion, that's okay.
continue
}
return OSConfig{}, err
}
conf.Write(bs)
conf.WriteByte('\n')
}
return readResolv(&conf)
}
func (m *resolvconfManager) Close() error {
cmd := exec.Command("resolvconf", "-d", resolvconfConfigName)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("running %s: %s", cmd, out)
}
if m.scriptInstalled {
m.logf("removing resolvconf workaround script")
os.Remove(resolvconfHookPath) // Best-effort
}
return nil
}

View File

@@ -2,14 +2,11 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build linux freebsd openbsd
package dns
import (
"bufio"
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
@@ -20,16 +17,16 @@ import (
"inet.af/netaddr"
"tailscale.com/atomicfile"
"tailscale.com/util/dnsname"
)
const (
tsConf = "/etc/resolv.tailscale.conf"
backupConf = "/etc/resolv.pre-tailscale-backup.conf"
resolvConf = "/etc/resolv.conf"
)
// writeResolvConf writes DNS configuration in resolv.conf format to the given writer.
func writeResolvConf(w io.Writer, servers []netaddr.IP, domains []string) {
func writeResolvConf(w io.Writer, servers []netaddr.IP, domains []dnsname.FQDN) {
io.WriteString(w, "# resolv.conf(5) file generated by tailscale\n")
io.WriteString(w, "# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN\n\n")
for _, ns := range servers {
@@ -41,22 +38,14 @@ func writeResolvConf(w io.Writer, servers []netaddr.IP, domains []string) {
io.WriteString(w, "search")
for _, domain := range domains {
io.WriteString(w, " ")
io.WriteString(w, domain)
io.WriteString(w, domain.WithoutTrailingDot())
}
io.WriteString(w, "\n")
}
}
// readResolvConf reads DNS configuration from /etc/resolv.conf.
func readResolvConf() (OSConfig, error) {
var config OSConfig
f, err := os.Open("/etc/resolv.conf")
if err != nil {
return config, err
}
scanner := bufio.NewScanner(f)
func readResolv(r io.Reader) (config OSConfig, err error) {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
@@ -65,7 +54,7 @@ func readResolvConf() (OSConfig, error) {
nameserver = strings.TrimSpace(nameserver)
ip, err := netaddr.ParseIP(nameserver)
if err != nil {
return config, err
return OSConfig{}, err
}
config.Nameservers = append(config.Nameservers, ip)
continue
@@ -74,7 +63,11 @@ func readResolvConf() (OSConfig, error) {
if strings.HasPrefix(line, "search") {
domain := strings.TrimPrefix(line, "search")
domain = strings.TrimSpace(domain)
config.Domains = append(config.Domains, domain)
fqdn, err := dnsname.ToFQDN(domain)
if err != nil {
return OSConfig{}, fmt.Errorf("parsing search domains %q: %w", line, err)
}
config.SearchDomains = append(config.SearchDomains, fqdn)
continue
}
}
@@ -82,6 +75,53 @@ func readResolvConf() (OSConfig, error) {
return config, nil
}
func readResolvFile(path string) (OSConfig, error) {
var config OSConfig
f, err := os.Open(path)
if err != nil {
return config, err
}
defer f.Close()
return readResolv(f)
}
// readResolvConf reads DNS configuration from /etc/resolv.conf.
func readResolvConf() (OSConfig, error) {
return readResolvFile(resolvConf)
}
// resolvOwner returns the apparent owner of the resolv.conf
// configuration in bs - one of "resolvconf", "systemd-resolved" or
// "NetworkManager", or "" if no known owner was found.
func resolvOwner(bs []byte) string {
b := bytes.NewBuffer(bs)
for {
line, err := b.ReadString('\n')
if err != nil {
return ""
}
line = strings.TrimSpace(line)
if line == "" {
continue
}
if line[0] != '#' {
// First non-empty, non-comment line. Assume the owner
// isn't hiding further down.
return ""
}
if strings.Contains(line, "systemd-resolved") {
return "systemd-resolved"
} else if strings.Contains(line, "NetworkManager") {
return "NetworkManager"
} else if strings.Contains(line, "resolvconf") {
return "resolvconf"
}
}
}
// isResolvedRunning reports whether systemd-resolved is running on the system,
// even if it is not managing the system DNS settings.
func isResolvedRunning() bool {
@@ -110,77 +150,133 @@ func isResolvedRunning() bool {
// or as cleanup if the program terminates unexpectedly.
type directManager struct{}
func newDirectManager() directManager {
return directManager{}
func newDirectManager() (directManager, error) {
return directManager{}, nil
}
func (m directManager) SetDNS(config OSConfig) error {
// Write the tsConf file.
buf := new(bytes.Buffer)
writeResolvConf(buf, config.Nameservers, config.Domains)
if err := atomicfile.WriteFile(tsConf, buf.Bytes(), 0644); err != nil {
return err
}
if linkPath, err := os.Readlink(resolvConf); err != nil {
// Remove any old backup that may exist.
os.Remove(backupConf)
// Backup the existing /etc/resolv.conf file.
contents, err := ioutil.ReadFile(resolvConf)
// If the original did not exist, still back up an empty file.
// The presence of a backup file is the way we know that Up ran.
if err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
if err := atomicfile.WriteFile(backupConf, contents, 0644); err != nil {
return err
}
} else if linkPath != tsConf {
// Backup the existing symlink.
os.Remove(backupConf)
if err := os.Symlink(linkPath, backupConf); err != nil {
return err
}
} else {
// Nothing to do, resolvConf already points to tsConf.
return nil
}
os.Remove(resolvConf)
if err := os.Symlink(tsConf, resolvConf); err != nil {
return err
}
if isResolvedRunning() {
exec.Command("systemctl", "restart", "systemd-resolved.service").Run() // Best-effort.
}
return nil
}
func (m directManager) RoutingMode() RoutingMode {
return RoutingModeNone
}
func (m directManager) Close() error {
if _, err := os.Stat(backupConf); err != nil {
// If the backup file does not exist, then Up never ran successfully.
// ownedByTailscale reports whether /etc/resolv.conf seems to be a
// tailscale-managed file.
func (m directManager) ownedByTailscale() (bool, error) {
st, err := os.Stat(resolvConf)
if err != nil {
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
if !st.Mode().IsRegular() {
return false, nil
}
bs, err := ioutil.ReadFile(resolvConf)
if err != nil {
return false, err
}
if bytes.Contains(bs, []byte("generated by tailscale")) {
return true, nil
}
return false, nil
}
// backupConfig creates or updates a backup of /etc/resolv.conf, if
// resolv.conf does not currently contain a Tailscale-managed config.
func (m directManager) backupConfig() error {
if _, err := os.Stat(resolvConf); err != nil {
if os.IsNotExist(err) {
// No resolv.conf, nothing to back up. Also get rid of any
// existing backup file, to avoid restoring something old.
os.Remove(backupConf)
return nil
}
return err
}
if ln, err := os.Readlink(resolvConf); err != nil {
owned, err := m.ownedByTailscale()
if err != nil {
return err
} else if ln != tsConf {
return fmt.Errorf("resolv.conf is not a symlink to %s", tsConf)
}
if owned {
return nil
}
return os.Rename(resolvConf, backupConf)
}
func (m directManager) SetDNS(config OSConfig) error {
if err := m.backupConfig(); err != nil {
return err
}
buf := new(bytes.Buffer)
writeResolvConf(buf, config.Nameservers, config.SearchDomains)
if err := atomicfile.WriteFile(resolvConf, buf.Bytes(), 0644); err != nil {
return err
}
// We might have taken over a configuration managed by resolved,
// in which case it will notice this on restart and gracefully
// start using our configuration. This shouldn't happen because we
// try to manage DNS through resolved when it's around, but as a
// best-effort fallback if we messed up the detection, try to
// restart resolved to make the system configuration consistent.
if isResolvedRunning() {
exec.Command("systemctl", "restart", "systemd-resolved.service").Run()
}
return nil
}
func (m directManager) SupportsSplitDNS() bool {
return false
}
func (m directManager) GetBaseConfig() (OSConfig, error) {
owned, err := m.ownedByTailscale()
if err != nil {
return OSConfig{}, err
}
fileToRead := resolvConf
if owned {
fileToRead = backupConf
}
return readResolvFile(fileToRead)
}
func (m directManager) Close() error {
// We used to keep a file for the tailscale config and symlinked
// to it, but then we stopped because /etc/resolv.conf being a
// symlink to surprising places breaks snaps and other sandboxing
// things. Clean it up if it's still there.
os.Remove("/etc/resolv.tailscale.conf")
if _, err := os.Stat(backupConf); err != nil {
if os.IsNotExist(err) {
// No backup, nothing we can do.
return nil
}
return err
}
owned, err := m.ownedByTailscale()
if err != nil {
return err
}
_, err = os.Stat(resolvConf)
if err != nil && !os.IsNotExist(err) {
return err
}
resolvConfExists := !os.IsNotExist(err)
if resolvConfExists && !owned {
// There's already a non-tailscale config in place, get rid of
// our backup.
os.Remove(backupConf)
return nil
}
// We own resolv.conf, and a backup exists.
if err := os.Rename(backupConf, resolvConf); err != nil {
return err
}
os.Remove(tsConf)
if isResolvedRunning() {
exec.Command("systemctl", "restart", "systemd-resolved.service").Run() // Best-effort.

View File

@@ -5,12 +5,14 @@
package dns
import (
"strings"
"time"
"inet.af/netaddr"
"tailscale.com/net/dns/resolver"
"tailscale.com/net/tsaddr"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
"tailscale.com/wgengine/monitor"
)
@@ -45,54 +47,178 @@ func NewManager(logf logger.Logf, oscfg OSConfigurator, linkMon *monitor.Mon) *M
resolver: resolver.New(logf, linkMon),
os: oscfg,
}
m.logf("using %T", m.os)
return m
}
// forceSplitDNSForTesting alters cfg to be a split DNS configuration
// that only captures search paths. It's intended for testing split
// DNS until the functionality is linked up in the admin panel.
func forceSplitDNSForTesting(cfg *Config) {
if len(cfg.DefaultResolvers) == 0 {
return
}
if cfg.Routes == nil {
cfg.Routes = map[dnsname.FQDN][]netaddr.IPPort{}
}
for _, search := range cfg.SearchDomains {
cfg.Routes[search] = cfg.DefaultResolvers
}
cfg.DefaultResolvers = nil
}
func (m *Manager) Set(cfg Config) error {
m.logf("Set: %+v", cfg)
if len(cfg.DefaultResolvers) == 0 {
// TODO: make other settings work even if you didn't set a
// default resolver. For now, no default resolvers == no
// managed DNS config.
cfg = Config{}
}
resolverCfg := resolver.Config{
Hosts: cfg.Hosts,
LocalDomains: cfg.AuthoritativeSuffixes,
Routes: map[string][]netaddr.IPPort{},
}
osCfg := OSConfig{
Domains: cfg.SearchDomains,
}
// We must proxy through quad-100 if MagicDNS hosts are in
// use, or there are any per-domain routes.
mustProxy := len(cfg.Hosts) > 0 || len(cfg.Routes) > 0
if mustProxy {
osCfg.Nameservers = []netaddr.IP{tsaddr.TailscaleServiceIP()}
resolverCfg.Routes["."] = cfg.DefaultResolvers
for suffix, resolvers := range cfg.Routes {
resolverCfg.Routes[suffix] = resolvers
}
} else {
for _, resolver := range cfg.DefaultResolvers {
osCfg.Nameservers = append(osCfg.Nameservers, resolver.IP)
}
}
if err := m.resolver.SetConfig(resolverCfg); err != nil {
rcfg, ocfg, err := m.compileConfig(cfg)
if err != nil {
return err
}
if err := m.os.SetDNS(osCfg); err != nil {
m.logf("Resolvercfg: %+v", rcfg)
m.logf("OScfg: %+v", ocfg)
if err := m.resolver.SetConfig(rcfg); err != nil {
return err
}
if err := m.os.SetDNS(ocfg); err != nil {
return err
}
return nil
}
// compileConfig converts cfg into a quad-100 resolver configuration
// and an OS-level configuration.
func (m *Manager) compileConfig(cfg Config) (resolver.Config, OSConfig, error) {
// Deal with trivial configs first.
switch {
case !cfg.needsOSResolver():
// Set search domains, but nothing else. This also covers the
// case where cfg is entirely zero, in which case these
// configs clear all Tailscale DNS settings.
return resolver.Config{}, OSConfig{
SearchDomains: cfg.SearchDomains,
}, nil
case cfg.hasDefaultResolversOnly():
// Trivial CorpDNS configuration, just override the OS
// resolver.
return resolver.Config{}, OSConfig{
Nameservers: toIPsOnly(cfg.DefaultResolvers),
SearchDomains: cfg.SearchDomains,
}, nil
case cfg.hasDefaultResolvers():
// Default resolvers plus other stuff always ends up proxying
// through quad-100.
rcfg := resolver.Config{
Routes: map[dnsname.FQDN][]netaddr.IPPort{
".": cfg.DefaultResolvers,
},
Hosts: cfg.Hosts,
LocalDomains: cfg.AuthoritativeSuffixes,
}
for suffix, resolvers := range cfg.Routes {
rcfg.Routes[suffix] = resolvers
}
ocfg := OSConfig{
Nameservers: []netaddr.IP{tsaddr.TailscaleServiceIP()},
SearchDomains: cfg.SearchDomains,
}
return rcfg, ocfg, nil
}
// From this point on, we're figuring out split DNS
// configurations. The possible cases don't return directly any
// more, because as a final step we have to handle the case where
// the OS can't do split DNS.
var rcfg resolver.Config
var ocfg OSConfig
if !cfg.hasHosts() && cfg.singleResolverSet() != nil && m.os.SupportsSplitDNS() {
// Split DNS configuration requested, where all split domains
// go to the same resolvers. We can let the OS do it.
return resolver.Config{}, OSConfig{
Nameservers: toIPsOnly(cfg.singleResolverSet()),
SearchDomains: cfg.SearchDomains,
MatchDomains: cfg.matchDomains(),
}, nil
}
// Split DNS configuration with either multiple upstream routes,
// or routes + MagicDNS, or just MagicDNS, or on an OS that cannot
// split-DNS. Install a split config pointing at quad-100.
rcfg = resolver.Config{
Hosts: cfg.Hosts,
LocalDomains: cfg.AuthoritativeSuffixes,
Routes: map[dnsname.FQDN][]netaddr.IPPort{},
}
for suffix, resolvers := range cfg.Routes {
rcfg.Routes[suffix] = resolvers
}
ocfg = OSConfig{
Nameservers: []netaddr.IP{tsaddr.TailscaleServiceIP()},
SearchDomains: cfg.SearchDomains,
}
// If the OS can't do native split-dns, read out the underlying
// resolver config and blend it into our config.
if m.os.SupportsSplitDNS() {
ocfg.MatchDomains = cfg.matchDomains()
} else {
bcfg, err := m.os.GetBaseConfig()
if err != nil {
// Temporary hack to make OSes where split-DNS isn't fully
// implemented yet not completely crap out, but instead
// fall back to quad-9 as a hardcoded "backup resolver".
//
// This codepath currently only triggers when opted into
// the split-DNS feature server side, and when at least
// one search domain is something within tailscale.com, so
// we don't accidentally leak unstable user DNS queries to
// quad-9 if we accidentally go down this codepath.
canUseHack := false
for _, dom := range cfg.SearchDomains {
if strings.HasSuffix(dom.WithoutTrailingDot(), ".tailscale.com") {
canUseHack = true
break
}
}
if !canUseHack {
return resolver.Config{}, OSConfig{}, err
}
bcfg = OSConfig{
Nameservers: []netaddr.IP{netaddr.IPv4(9, 9, 9, 9)},
}
}
rcfg.Routes["."] = toIPPorts(bcfg.Nameservers)
ocfg.SearchDomains = append(ocfg.SearchDomains, bcfg.SearchDomains...)
}
return rcfg, ocfg, nil
}
// toIPsOnly returns only the IP portion of ipps.
// TODO: this discards port information on the assumption that we're
// always pointing at port 53.
// https://github.com/tailscale/tailscale/issues/1666 tracks making
// that not true, if we ever want to.
func toIPsOnly(ipps []netaddr.IPPort) (ret []netaddr.IP) {
ret = make([]netaddr.IP, 0, len(ipps))
for _, ipp := range ipps {
ret = append(ret, ipp.IP)
}
return ret
}
func toIPPorts(ips []netaddr.IP) (ret []netaddr.IPPort) {
ret = make([]netaddr.IPPort, 0, len(ips))
for _, ip := range ips {
ret = append(ret, netaddr.IPPort{IP: ip, Port: 53})
}
return ret
}
func (m *Manager) EnqueueRequest(bs []byte, from netaddr.IPPort) error {
return m.resolver.EnqueueRequest(bs, from)
}
@@ -113,7 +239,11 @@ func (m *Manager) Down() error {
// in case the Tailscale daemon terminated without closing the router.
// No other state needs to be instantiated before this runs.
func Cleanup(logf logger.Logf, interfaceName string) {
oscfg := NewOSConfigurator(logf, interfaceName)
oscfg, err := NewOSConfigurator(logf, interfaceName)
if err != nil {
logf("creating dns cleanup: %v", err)
return
}
dns := NewManager(logf, oscfg, nil)
if err := dns.Down(); err != nil {
logf("dns down: %v", err)

View File

@@ -8,7 +8,7 @@ package dns
import "tailscale.com/types/logger"
func NewOSConfigurator(logger.Logf, string) OSConfigurator {
func NewOSConfigurator(logger.Logf, string) (OSConfigurator, error) {
// TODO(dmytro): on darwin, we should use a macOS-specific method such as scutil.
// This is currently not implemented. Editing /etc/resolv.conf does not work,
// as most applications use the system resolver, which disregards it.

View File

@@ -4,11 +4,25 @@
package dns
import "tailscale.com/types/logger"
import (
"fmt"
"io/ioutil"
"os"
func NewOSConfigurator(logf logger.Logf, _ string) OSConfigurator {
switch {
case isResolvconfActive():
"tailscale.com/types/logger"
)
func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) {
bs, err := ioutil.ReadFile("/etc/resolv.conf")
if os.IsNotExist(err) {
return newDirectManager()
}
if err != nil {
return nil, fmt.Errorf("reading /etc/resolv.conf: %w", err)
}
switch resolvOwner(bs) {
case "resolvconf":
return newResolvconfManager(logf)
default:
return newDirectManager()

View File

@@ -4,18 +4,170 @@
package dns
import "tailscale.com/types/logger"
import (
"bytes"
"context"
"errors"
"fmt"
"io/ioutil"
"os"
"os/exec"
"time"
func NewOSConfigurator(logf logger.Logf, interfaceName string) OSConfigurator {
switch {
// TODO: rework NetworkManager and resolved support.
// case isResolvedActive():
// return newResolvedManager()
// case isNMActive():
// return newNMManager(interfaceName)
case isResolvconfActive():
"github.com/godbus/dbus/v5"
"tailscale.com/types/logger"
)
type kv struct {
k, v string
}
func (kv kv) String() string {
return fmt.Sprintf("%s=%s", kv.k, kv.v)
}
func NewOSConfigurator(logf logger.Logf, interfaceName string) (ret OSConfigurator, err error) {
var debug []kv
dbg := func(k, v string) {
debug = append(debug, kv{k, v})
}
defer func() {
if ret != nil {
dbg("ret", fmt.Sprintf("%T", ret))
}
logf("dns: %v", debug)
}()
bs, err := ioutil.ReadFile("/etc/resolv.conf")
if os.IsNotExist(err) {
dbg("rc", "missing")
return newDirectManager()
}
if err != nil {
return nil, fmt.Errorf("reading /etc/resolv.conf: %w", err)
}
switch resolvOwner(bs) {
case "systemd-resolved":
dbg("rc", "resolved")
if err := dbusPing("org.freedesktop.resolve1", "/org/freedesktop/resolve1"); err != nil {
dbg("resolved", "no")
return newDirectManager()
}
if err := dbusPing("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/DnsManager"); err != nil {
dbg("nm", "no")
return newResolvedManager(logf)
}
dbg("nm", "yes")
if err := nmIsUsingResolved(); err != nil {
dbg("nm-resolved", "no")
return newResolvedManager(logf)
}
dbg("nm-resolved", "yes")
return newNMManager(interfaceName)
case "resolvconf":
dbg("rc", "resolvconf")
if err := resolvconfSourceIsNM(bs); err == nil {
dbg("src-is-nm", "yes")
if err := dbusPing("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/DnsManager"); err == nil {
dbg("nm", "yes")
return newNMManager(interfaceName)
}
dbg("nm", "no")
}
dbg("src-is-nm", "no")
if _, err := exec.LookPath("resolvconf"); err != nil {
dbg("resolvconf", "no")
return newDirectManager()
}
dbg("resolvconf", "yes")
return newResolvconfManager(logf)
case "NetworkManager":
dbg("rc", "nm")
if err := dbusPing("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager/DnsManager"); err != nil {
dbg("nm", "no")
return newDirectManager()
}
dbg("nm", "yes")
return newNMManager(interfaceName)
default:
dbg("rc", "unknown")
return newDirectManager()
}
}
func resolvconfSourceIsNM(resolvDotConf []byte) error {
b := bytes.NewBuffer(resolvDotConf)
cfg, err := readResolv(b)
if err != nil {
return fmt.Errorf("parsing /etc/resolv.conf: %w", err)
}
var (
paths = []string{
"/etc/resolvconf/run/interface/NetworkManager",
"/run/resolvconf/interface/NetworkManager",
"/var/run/resolvconf/interface/NetworkManager",
"/run/resolvconf/interfaces/NetworkManager",
"/var/run/resolvconf/interfaces/NetworkManager",
}
nmCfg OSConfig
found bool
)
for _, path := range paths {
nmCfg, err = readResolvFile(path)
if os.IsNotExist(err) {
continue
} else if err != nil {
return err
}
found = true
break
}
if !found {
return errors.New("NetworkManager resolvconf snippet not found")
}
if !nmCfg.Equal(cfg) {
return errors.New("NetworkManager config not applied by resolvconf")
}
return nil
}
func nmIsUsingResolved() error {
conn, err := dbus.SystemBus()
if err != nil {
// DBus probably not running.
return err
}
nm := conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager/DnsManager"))
v, err := nm.GetProperty("org.freedesktop.NetworkManager.DnsManager.Mode")
if err != nil {
return fmt.Errorf("getting NM mode: %w", err)
}
mode, ok := v.Value().(string)
if !ok {
return fmt.Errorf("unexpected type %T for NM DNS mode", v.Value())
}
if mode != "systemd-resolved" {
return errors.New("NetworkManager is not using systemd-resolved for DNS")
}
return nil
}
func dbusPing(name, objectPath string) error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
conn, err := dbus.SystemBus()
if err != nil {
// DBus probably not running.
return err
}
obj := conn.Object(name, dbus.ObjectPath(objectPath))
call := obj.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0)
return call.Err
}

View File

@@ -6,6 +6,6 @@ package dns
import "tailscale.com/types/logger"
func NewOSConfigurator(logger.Logf, string) OSConfigurator {
func NewOSConfigurator(logger.Logf, string) (OSConfigurator, error) {
return newDirectManager()
}

440
net/dns/manager_test.go Normal file
View File

@@ -0,0 +1,440 @@
// 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 dns
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"inet.af/netaddr"
"tailscale.com/net/dns/resolver"
"tailscale.com/util/dnsname"
)
type fakeOSConfigurator struct {
SplitDNS bool
BaseConfig OSConfig
OSConfig OSConfig
ResolverConfig resolver.Config
}
func (c *fakeOSConfigurator) SetDNS(cfg OSConfig) error {
if !c.SplitDNS && len(cfg.MatchDomains) > 0 {
panic("split DNS config passed to non-split OSConfigurator")
}
c.OSConfig = cfg
return nil
}
func (c *fakeOSConfigurator) SetResolver(cfg resolver.Config) {
c.ResolverConfig = cfg
}
func (c *fakeOSConfigurator) SupportsSplitDNS() bool {
return c.SplitDNS
}
func (c *fakeOSConfigurator) GetBaseConfig() (OSConfig, error) {
return c.BaseConfig, nil
}
func (c *fakeOSConfigurator) Close() error { return nil }
func TestManager(t *testing.T) {
// Note: these tests assume that it's safe to switch the
// OSConfigurator's split-dns support on and off between Set
// calls. Empirically this is currently true, because we reprobe
// the support every time we generate configs. It would be
// reasonable to make this unsupported as well, in which case
// these tests will need tweaking.
tests := []struct {
name string
in Config
split bool
bs OSConfig
os OSConfig
rs resolver.Config
}{
{
name: "empty",
},
{
name: "search-only",
in: Config{
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
os: OSConfig{
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
},
{
name: "corp",
in: Config{
DefaultResolvers: mustIPPs("1.1.1.1:53", "9.9.9.9:53"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
os: OSConfig{
Nameservers: mustIPs("1.1.1.1", "9.9.9.9"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
},
{
name: "corp-split",
in: Config{
DefaultResolvers: mustIPPs("1.1.1.1:53", "9.9.9.9:53"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: true,
os: OSConfig{
Nameservers: mustIPs("1.1.1.1", "9.9.9.9"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
},
{
name: "corp-magic",
in: Config{
DefaultResolvers: mustIPPs("1.1.1.1:53", "9.9.9.9:53"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
AuthoritativeSuffixes: fqdns("ts.com"),
},
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
Routes: upstreams(".", "1.1.1.1:53", "9.9.9.9:53"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: fqdns("ts.com."),
},
},
{
name: "corp-magic-split",
in: Config{
DefaultResolvers: mustIPPs("1.1.1.1:53", "9.9.9.9:53"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
AuthoritativeSuffixes: fqdns("ts.com"),
},
split: true,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
Routes: upstreams(".", "1.1.1.1:53", "9.9.9.9:53"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: fqdns("ts.com."),
},
},
{
name: "corp-routes",
in: Config{
DefaultResolvers: mustIPPs("1.1.1.1:53", "9.9.9.9:53"),
Routes: upstreams("corp.com", "2.2.2.2:53"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
Routes: upstreams(
".", "1.1.1.1:53", "9.9.9.9:53",
"corp.com.", "2.2.2.2:53"),
},
},
{
name: "corp-routes-split",
in: Config{
DefaultResolvers: mustIPPs("1.1.1.1:53", "9.9.9.9:53"),
Routes: upstreams("corp.com", "2.2.2.2:53"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: true,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
Routes: upstreams(
".", "1.1.1.1:53", "9.9.9.9:53",
"corp.com.", "2.2.2.2:53"),
},
},
{
name: "routes",
in: Config{
Routes: upstreams("corp.com", "2.2.2.2:53"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
bs: OSConfig{
Nameservers: mustIPs("8.8.8.8"),
SearchDomains: fqdns("coffee.shop"),
},
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
SearchDomains: fqdns("tailscale.com", "universe.tf", "coffee.shop"),
},
rs: resolver.Config{
Routes: upstreams(
".", "8.8.8.8:53",
"corp.com.", "2.2.2.2:53"),
},
},
{
name: "routes-split",
in: Config{
Routes: upstreams("corp.com", "2.2.2.2:53"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: true,
os: OSConfig{
Nameservers: mustIPs("2.2.2.2"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
MatchDomains: fqdns("corp.com"),
},
},
{
name: "routes-multi",
in: Config{
Routes: upstreams(
"corp.com", "2.2.2.2:53",
"bigco.net", "3.3.3.3:53"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
bs: OSConfig{
Nameservers: mustIPs("8.8.8.8"),
SearchDomains: fqdns("coffee.shop"),
},
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
SearchDomains: fqdns("tailscale.com", "universe.tf", "coffee.shop"),
},
rs: resolver.Config{
Routes: upstreams(
".", "8.8.8.8:53",
"corp.com.", "2.2.2.2:53",
"bigco.net.", "3.3.3.3:53"),
},
},
{
name: "routes-multi-split",
in: Config{
Routes: upstreams(
"corp.com", "2.2.2.2:53",
"bigco.net", "3.3.3.3:53"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: true,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
MatchDomains: fqdns("bigco.net", "corp.com"),
},
rs: resolver.Config{
Routes: upstreams(
"corp.com.", "2.2.2.2:53",
"bigco.net.", "3.3.3.3:53"),
},
},
{
name: "magic",
in: Config{
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
AuthoritativeSuffixes: fqdns("ts.com"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
bs: OSConfig{
Nameservers: mustIPs("8.8.8.8"),
SearchDomains: fqdns("coffee.shop"),
},
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
SearchDomains: fqdns("tailscale.com", "universe.tf", "coffee.shop"),
},
rs: resolver.Config{
Routes: upstreams(".", "8.8.8.8:53"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: fqdns("ts.com."),
},
},
{
name: "magic-split",
in: Config{
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
AuthoritativeSuffixes: fqdns("ts.com"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: true,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
MatchDomains: fqdns("ts.com"),
},
rs: resolver.Config{
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: fqdns("ts.com."),
},
},
{
name: "routes-magic",
in: Config{
Routes: upstreams("corp.com", "2.2.2.2:53"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
AuthoritativeSuffixes: fqdns("ts.com"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
bs: OSConfig{
Nameservers: mustIPs("8.8.8.8"),
SearchDomains: fqdns("coffee.shop"),
},
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
SearchDomains: fqdns("tailscale.com", "universe.tf", "coffee.shop"),
},
rs: resolver.Config{
Routes: upstreams(
"corp.com.", "2.2.2.2:53",
".", "8.8.8.8:53"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: fqdns("ts.com."),
},
},
{
name: "routes-magic-split",
in: Config{
Routes: upstreams("corp.com", "2.2.2.2:53"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
AuthoritativeSuffixes: fqdns("ts.com"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: true,
os: OSConfig{
Nameservers: mustIPs("100.100.100.100"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
MatchDomains: fqdns("corp.com", "ts.com"),
},
rs: resolver.Config{
Routes: upstreams("corp.com.", "2.2.2.2:53"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: fqdns("ts.com."),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
f := fakeOSConfigurator{
SplitDNS: test.split,
BaseConfig: test.bs,
}
m := NewManager(t.Logf, &f, nil)
m.resolver.TestOnlySetHook(f.SetResolver)
if err := m.Set(test.in); err != nil {
t.Fatalf("m.Set: %v", err)
}
tr := cmp.Transformer("ipStr", func(ip netaddr.IP) string { return ip.String() })
if diff := cmp.Diff(f.OSConfig, test.os, tr, cmpopts.EquateEmpty()); diff != "" {
t.Errorf("wrong OSConfig (-got+want)\n%s", diff)
}
if diff := cmp.Diff(f.ResolverConfig, test.rs, tr, cmpopts.EquateEmpty()); diff != "" {
t.Errorf("wrong resolver.Config (-got+want)\n%s", diff)
}
})
}
}
func mustIPs(strs ...string) (ret []netaddr.IP) {
for _, s := range strs {
ret = append(ret, netaddr.MustParseIP(s))
}
return ret
}
func mustIPPs(strs ...string) (ret []netaddr.IPPort) {
for _, s := range strs {
ret = append(ret, netaddr.MustParseIPPort(s))
}
return ret
}
func fqdns(strs ...string) (ret []dnsname.FQDN) {
for _, s := range strs {
fqdn, err := dnsname.ToFQDN(s)
if err != nil {
panic(err)
}
ret = append(ret, fqdn)
}
return ret
}
func hosts(strs ...string) (ret map[dnsname.FQDN][]netaddr.IP) {
var key dnsname.FQDN
ret = map[dnsname.FQDN][]netaddr.IP{}
for _, s := range strs {
if ip, err := netaddr.ParseIP(s); err == nil {
if key == "" {
panic("IP provided before name")
}
ret[key] = append(ret[key], ip)
} else {
fqdn, err := dnsname.ToFQDN(s)
if err != nil {
panic(err)
}
key = fqdn
}
}
return ret
}
func upstreams(strs ...string) (ret map[dnsname.FQDN][]netaddr.IPPort) {
var key dnsname.FQDN
ret = map[dnsname.FQDN][]netaddr.IPPort{}
for _, s := range strs {
if ipp, err := netaddr.ParseIPPort(s); err == nil {
if key == "" {
panic("IPPort provided before suffix")
}
ret[key] = append(ret[key], ipp)
} else {
fqdn, err := dnsname.ToFQDN(s)
if err != nil {
panic(err)
}
key = fqdn
}
}
return ret
}

View File

@@ -5,31 +5,58 @@
package dns
import (
"errors"
"fmt"
"os/exec"
"strings"
"syscall"
"time"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"inet.af/netaddr"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
)
const (
ipv4RegBase = `SYSTEM\CurrentControlSet\Services\Tcpip\Parameters`
ipv6RegBase = `SYSTEM\CurrentControlSet\Services\Tcpip6\Parameters`
// the GUID is randomly generated. At present, Tailscale installs
// zero or one NRPT rules, so hardcoding a single GUID everywhere
// is fine.
nrptBase = `SYSTEM\CurrentControlSet\services\Dnscache\Parameters\DnsPolicyConfig\{5abe529b-675b-4486-8459-25a634dacc23}`
nrptOverrideDNS = 0x8 // bitmask value for "use the provided override DNS resolvers"
versionKey = `SOFTWARE\Microsoft\Windows NT\CurrentVersion`
)
type windowsManager struct {
logf logger.Logf
guid string
logf logger.Logf
guid string
nrptWorks bool
}
func NewOSConfigurator(logf logger.Logf, interfaceName string) OSConfigurator {
return windowsManager{
logf: logf,
guid: interfaceName,
func NewOSConfigurator(logf logger.Logf, interfaceName string) (OSConfigurator, error) {
ret := windowsManager{
logf: logf,
guid: interfaceName,
nrptWorks: !isWindows7(),
}
// Best-effort: if our NRPT rule exists, try to delete it. Unlike
// per-interface configuration, NRPT rules survive the unclean
// termination of the Tailscale process, and depending on the
// rule, it may prevent us from reaching login.tailscale.com to
// boot up. The bootstrap resolver logic will save us, but it
// slows down start-up a bunch.
if ret.nrptWorks {
ret.delKey(nrptBase)
}
return ret, nil
}
// keyOpenTimeout is how long we wait for a registry key to
@@ -38,37 +65,86 @@ func NewOSConfigurator(logf logger.Logf, interfaceName string) OSConfigurator {
// can end up racing with that.
const keyOpenTimeout = 20 * time.Second
func setRegistryString(path, name, value string) error {
func (m windowsManager) openKey(path string) (registry.Key, error) {
key, err := openKeyWait(registry.LOCAL_MACHINE, path, registry.SET_VALUE, keyOpenTimeout)
if err != nil {
return fmt.Errorf("opening %s: %w", path, err)
return 0, fmt.Errorf("opening %s: %w", path, err)
}
defer key.Close()
return key, nil
}
err = key.SetStringValue(name, value)
if err != nil {
return fmt.Errorf("setting %s[%s]: %w", path, name, err)
func (m windowsManager) ifPath(basePath string) string {
return fmt.Sprintf(`%s\Interfaces\%s`, basePath, m.guid)
}
func (m windowsManager) delKey(path string) error {
if err := registry.DeleteKey(registry.LOCAL_MACHINE, path); err != nil && err != registry.ErrNotExist {
return err
}
return nil
}
func (m windowsManager) setNameservers(basePath string, nameservers []string) error {
path := fmt.Sprintf(`%s\Interfaces\%s`, basePath, m.guid)
value := strings.Join(nameservers, ",")
return setRegistryString(path, "NameServer", value)
func delValue(key registry.Key, name string) error {
if err := key.DeleteValue(name); err != nil && err != registry.ErrNotExist {
return err
}
return nil
}
func (m windowsManager) setDomains(basePath string, domains []string) error {
path := fmt.Sprintf(`%s\Interfaces\%s`, basePath, m.guid)
value := strings.Join(domains, ",")
return setRegistryString(path, "SearchList", value)
// setSplitDNS configures an NRPT (Name Resolution Policy Table) rule
// to resolve queries for domains using resolvers, rather than the
// system's "primary" resolver.
//
// If no resolvers are provided, the Tailscale NRPT rule is deleted.
func (m windowsManager) setSplitDNS(resolvers []netaddr.IP, domains []dnsname.FQDN) error {
if len(resolvers) == 0 {
return m.delKey(nrptBase)
}
servers := make([]string, 0, len(resolvers))
for _, resolver := range resolvers {
servers = append(servers, resolver.String())
}
doms := make([]string, 0, len(domains))
for _, domain := range domains {
// NRPT rules must have a leading dot, which is not usual for
// DNS search paths.
doms = append(doms, "."+domain.WithoutTrailingDot())
}
// CreateKey is actually open-or-create, which suits us fine.
key, _, err := registry.CreateKey(registry.LOCAL_MACHINE, nrptBase, registry.SET_VALUE)
if err != nil {
return fmt.Errorf("opening %s: %w", nrptBase, err)
}
defer key.Close()
if err := key.SetDWordValue("Version", 1); err != nil {
return err
}
if err := key.SetStringsValue("Name", doms); err != nil {
return err
}
if err := key.SetStringValue("GenericDNSServers", strings.Join(servers, "; ")); err != nil {
return err
}
if err := key.SetDWordValue("ConfigOptions", nrptOverrideDNS); err != nil {
return err
}
return nil
}
func (m windowsManager) SetDNS(config OSConfig) error {
// setPrimaryDNS sets the given resolvers and domains as the Tailscale
// interface's DNS configuration.
// If resolvers is non-empty, those resolvers become the system's
// "primary" resolvers.
// domains can be set without resolvers, which just contributes new
// paths to the global DNS search list.
func (m windowsManager) setPrimaryDNS(resolvers []netaddr.IP, domains []dnsname.FQDN) error {
var ipsv4 []string
var ipsv6 []string
for _, ip := range config.Nameservers {
for _, ip := range resolvers {
if ip.Is4() {
ipsv4 = append(ipsv4, ip.String())
} else {
@@ -76,19 +152,111 @@ func (m windowsManager) SetDNS(config OSConfig) error {
}
}
if err := m.setNameservers(ipv4RegBase, ipsv4); err != nil {
domStrs := make([]string, 0, len(domains))
for _, dom := range domains {
domStrs = append(domStrs, dom.WithoutTrailingDot())
}
key4, err := m.openKey(m.ifPath(ipv4RegBase))
if err != nil {
return err
}
if err := m.setDomains(ipv4RegBase, config.Domains); err != nil {
defer key4.Close()
if len(ipsv4) == 0 {
if err := delValue(key4, "NameServer"); err != nil {
return err
}
} else if err := key4.SetStringValue("NameServer", strings.Join(ipsv4, ",")); err != nil {
return err
}
if err := m.setNameservers(ipv6RegBase, ipsv6); err != nil {
if len(domains) == 0 {
if err := delValue(key4, "SearchList"); err != nil {
return err
}
} else if err := key4.SetStringValue("SearchList", strings.Join(domStrs, ",")); err != nil {
return err
}
if err := m.setDomains(ipv6RegBase, config.Domains); err != nil {
key6, err := m.openKey(m.ifPath(ipv6RegBase))
if err != nil {
return err
}
defer key6.Close()
if len(ipsv6) == 0 {
if err := delValue(key6, "NameServer"); err != nil {
return err
}
} else if err := key6.SetStringValue("NameServer", strings.Join(ipsv6, ",")); err != nil {
return err
}
if len(domains) == 0 {
if err := delValue(key6, "SearchList"); err != nil {
return err
}
} else if err := key6.SetStringValue("SearchList", strings.Join(domStrs, ",")); err != nil {
return err
}
// Disable LLMNR on the Tailscale interface. We don't do
// multicast, and we certainly don't do LLMNR, so it's pointless
// to make Windows try it.
if err := key4.SetDWordValue("EnableMulticast", 0); err != nil {
return err
}
if err := key6.SetDWordValue("EnableMulticast", 0); err != nil {
return err
}
return nil
}
func (m windowsManager) SetDNS(cfg OSConfig) error {
// We can configure Windows DNS in one of two ways:
//
// - In primary DNS mode, we set the NameServer and SearchList
// registry keys on our interface. Because our interface metric
// is very low, this turns us into the one and only "primary"
// resolver for the OS, i.e. all queries flow to the
// resolver(s) we specify.
// - In split DNS mode, we set the Domain registry key on our
// interface (which adds that domain to the global search list,
// but does not contribute other DNS configuration from the
// interface), and configure an NRPT (Name Resolution Policy
// Table) rule to route queries for our suffixes to the
// provided resolver.
//
// When switching modes, we delete all the configuration related
// to the other mode, so these two are an XOR.
//
// Windows actually supports much more advanced configurations as
// well, with arbitrary routing of hosts and suffixes to arbitrary
// resolvers. However, we use it in a "simple" split domain
// configuration only, routing one set of things to the "split"
// resolver and the rest to the primary.
if len(cfg.MatchDomains) == 0 {
if err := m.setSplitDNS(nil, nil); err != nil {
return err
}
if err := m.setPrimaryDNS(cfg.Nameservers, cfg.SearchDomains); err != nil {
return err
}
} else if !m.nrptWorks {
return errors.New("cannot set per-domain resolvers on Windows 7")
} else {
if err := m.setSplitDNS(cfg.Nameservers, cfg.MatchDomains); err != nil {
return err
}
// Still set search domains on the interface, since NRPT only
// handles query routing and not search domain expansion.
if err := m.setPrimaryDNS(nil, cfg.SearchDomains); err != nil {
return err
}
}
// Force DNS re-registration in Active Directory. What we actually
// care about is that this command invokes the undocumented hidden
@@ -97,26 +265,134 @@ func (m windowsManager) SetDNS(config OSConfig) error {
// effect.
//
// This command can take a few seconds to run, so run it async, best effort.
//
// After re-registering DNS, also flush the DNS cache to clear out
// any cached split-horizon queries that are no longer the correct
// answer.
go func() {
t0 := time.Now()
m.logf("running ipconfig /registerdns ...")
cmd := exec.Command("ipconfig", "/registerdns")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
err := cmd.Run()
d := time.Since(t0).Round(time.Millisecond)
if err := cmd.Run(); err != nil {
if err != nil {
m.logf("error running ipconfig /registerdns after %v: %v", d, err)
} else {
m.logf("ran ipconfig /registerdns in %v", d)
}
t0 = time.Now()
m.logf("running ipconfig /registerdns ...")
cmd = exec.Command("ipconfig", "/flushdns")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
err = cmd.Run()
d = time.Since(t0).Round(time.Millisecond)
if err != nil {
m.logf("error running ipconfig /flushdns after %v: %v", d, err)
} else {
m.logf("ran ipconfig /flushdns in %v", d)
}
}()
return nil
}
func (m windowsManager) RoutingMode() RoutingMode {
return RoutingModeNone
func (m windowsManager) SupportsSplitDNS() bool {
return m.nrptWorks
}
func (m windowsManager) Close() error {
return m.SetDNS(OSConfig{})
}
func (m windowsManager) GetBaseConfig() (OSConfig, error) {
resolvers, err := m.getBasePrimaryResolver()
if err != nil {
return OSConfig{}, err
}
return OSConfig{
Nameservers: resolvers,
// Don't return any search domains here, because even Windows
// 7 correctly handles blending search domains from multiple
// sources, and any search domains we add here will get tacked
// onto the Tailscale config unnecessarily.
}, nil
}
// getBasePrimaryResolver returns a guess of the non-Tailscale primary
// resolver on the system.
// It's used on Windows 7 to emulate split DNS by trying to figure out
// what the "previous" primary resolver was. It might be wrong, or
// incomplete.
func (m windowsManager) getBasePrimaryResolver() (resolvers []netaddr.IP, err error) {
tsGUID, err := windows.GUIDFromString(m.guid)
if err != nil {
return nil, err
}
tsLUID, err := winipcfg.LUIDFromGUID(&tsGUID)
if err != nil {
return nil, err
}
ifrows, err := winipcfg.GetIPInterfaceTable(windows.AF_INET)
if err == windows.ERROR_NOT_FOUND {
// IPv4 seems disabled, try to get interface metrics from IPv6 instead.
ifrows, err = winipcfg.GetIPInterfaceTable(windows.AF_INET6)
}
if err != nil {
return nil, err
}
var (
primary winipcfg.LUID
best = ^uint32(0)
)
for _, row := range ifrows {
if !row.Connected {
continue
}
if row.InterfaceLUID == tsLUID {
continue
}
if row.Metric < best {
primary = row.InterfaceLUID
best = row.Metric
}
}
if primary == 0 {
// No resolvers set outside of Tailscale.
return nil, nil
}
ips, err := primary.DNS()
if err != nil {
return nil, err
}
for _, stdip := range ips {
if ip, ok := netaddr.FromStdIP(stdip); ok {
resolvers = append(resolvers, ip)
}
}
return resolvers, nil
}
func isWindows7() bool {
key, err := registry.OpenKey(registry.LOCAL_MACHINE, versionKey, registry.READ)
if err != nil {
// Fail safe, assume Windows 7.
return true
}
ver, _, err := key.GetStringValue("CurrentVersion")
if err != nil {
return true
}
// Careful to not assume anything about version numbers beyond
// 6.3, Microsoft deprecated this registry key and locked its
// value to what it was in Windows 8.1. We can only use this to
// probe for versions before that. Good thing we only need Windows
// 7 (so far).
//
// And yes, Windows 7 is version 6.1. Don't ask.
return ver == "6.1"
}

View File

@@ -9,66 +9,67 @@
package dns
import (
"bufio"
"bytes"
"context"
"fmt"
"os"
"os/exec"
"sort"
"time"
"github.com/godbus/dbus/v5"
"inet.af/netaddr"
"tailscale.com/util/dnsname"
"tailscale.com/util/endian"
)
// isNMActive determines if NetworkManager is currently managing system DNS settings.
func isNMActive() bool {
// This is somewhat tricky because NetworkManager supports a number
// of DNS configuration modes. In all cases, we expect it to be installed
// and /etc/resolv.conf to contain a mention of NetworkManager in the comments.
_, err := exec.LookPath("NetworkManager")
if err != nil {
return false
}
f, err := os.Open("/etc/resolv.conf")
if err != nil {
return false
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Bytes()
// Look for the word "NetworkManager" until comments end.
if len(line) > 0 && line[0] != '#' {
return false
}
if bytes.Contains(line, []byte("NetworkManager")) {
return true
}
}
return false
}
const (
highestPriority = int32(-1 << 31)
mediumPriority = int32(1) // Highest priority that doesn't hard-override
lowerPriority = int32(200) // lower than all builtin auto priorities
)
// nmManager uses the NetworkManager DBus API.
type nmManager struct {
interfaceName string
manager dbus.BusObject
dnsManager dbus.BusObject
}
func newNMManager(interfaceName string) nmManager {
return nmManager{
interfaceName: interfaceName,
func newNMManager(interfaceName string) (*nmManager, error) {
conn, err := dbus.SystemBus()
if err != nil {
return nil, err
}
return &nmManager{
interfaceName: interfaceName,
manager: conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager")),
dnsManager: conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager/DnsManager")),
}, nil
}
type nmConnectionSettings map[string]map[string]dbus.Variant
func (m nmManager) SetDNS(config OSConfig) error {
func (m *nmManager) SetDNS(config OSConfig) error {
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
defer cancel()
// conn is a shared connection whose lifecycle is managed by the dbus package.
// We should not interfere with that by closing it.
// NetworkManager only lets you set DNS settings on "active"
// connections, which requires an assigned IP address. This got
// configured before the DNS manager was invoked, but it might
// take a little time for the netlink notifications to propagate
// up. So, keep retrying for the duration of the reconfigTimeout.
var err error
for ctx.Err() == nil {
err = m.trySet(ctx, config)
if err == nil {
break
}
time.Sleep(10 * time.Millisecond)
}
return err
}
func (m *nmManager) trySet(ctx context.Context, config OSConfig) error {
conn, err := dbus.SystemBus()
if err != nil {
return fmt.Errorf("connecting to system bus: %w", err)
@@ -136,43 +137,58 @@ func (m nmManager) SetDNS(config OSConfig) error {
}
}
general := settings["connection"]
general["llmnr"] = dbus.MakeVariant(0)
general["mdns"] = dbus.MakeVariant(0)
ipv4Map := settings["ipv4"]
ipv4Map["dns"] = dbus.MakeVariant(dnsv4)
ipv4Map["dns-search"] = dbus.MakeVariant(config.Domains)
ipv4Map["dns-search"] = dbus.MakeVariant(config.SearchDomains)
// We should only request priority if we have nameservers to set.
if len(dnsv4) == 0 {
ipv4Map["dns-priority"] = dbus.MakeVariant(100)
ipv4Map["dns-priority"] = dbus.MakeVariant(lowerPriority)
} else if len(config.MatchDomains) > 0 {
// Set a fairly high priority, but don't override all other
// configs when in split-DNS mode.
ipv4Map["dns-priority"] = dbus.MakeVariant(mediumPriority)
} else {
// dns-priority = -1 ensures that we have priority
// over other interfaces, except those exploiting this same trick.
// Ref: https://bugs.launchpad.net/ubuntu/+source/network-manager/+bug/1211110/comments/92.
ipv4Map["dns-priority"] = dbus.MakeVariant(-1)
// Negative priority means only the settings from the most
// negative connection get used. The way this mixes with
// per-domain routing is unclear, but it _seems_ that the
// priority applies after routing has found possible
// candidates for a resolution.
ipv4Map["dns-priority"] = dbus.MakeVariant(highestPriority)
}
// In principle, we should not need set this to true,
// as our interface does not configure any automatic DNS settings (presumably via DHCP).
// All the same, better to be safe.
ipv4Map["ignore-auto-dns"] = dbus.MakeVariant(true)
ipv6Map := settings["ipv6"]
// This is a hack.
// Methods "disabled", "ignore", "link-local" (IPv6 default) prevent us from setting DNS.
// It seems that our only recourse is "manual" or "auto".
// "manual" requires addresses, so we use "auto", which will assign us a random IPv6 /64.
// In IPv6 settings, you're only allowed to provide additional
// static DNS settings in "auto" (SLAAC) or "manual" mode. In
// "manual" mode you also have to specify IP addresses, so we use
// "auto".
//
// NM actually documents that to set just DNS servers, you should
// use "auto" mode and then set ignore auto routes and DNS, which
// basically means "autoconfigure but ignore any autoconfiguration
// results you might get". As a safety, we also say that
// NetworkManager should never try to make us the default route
// (none of its business anyway, we handle our own default
// routing).
ipv6Map["method"] = dbus.MakeVariant("auto")
// Our IPv6 config is a fake, so it should never become the default route.
ipv6Map["never-default"] = dbus.MakeVariant(true)
// Moreover, we should ignore all autoconfigured routes (hopefully none), as they are bogus.
ipv6Map["ignore-auto-routes"] = dbus.MakeVariant(true)
// Finally, set the actual DNS config.
ipv6Map["dns"] = dbus.MakeVariant(dnsv6)
ipv6Map["dns-search"] = dbus.MakeVariant(config.Domains)
if len(dnsv6) == 0 {
ipv6Map["dns-priority"] = dbus.MakeVariant(100)
} else {
ipv6Map["dns-priority"] = dbus.MakeVariant(-1)
}
ipv6Map["ignore-auto-dns"] = dbus.MakeVariant(true)
ipv6Map["never-default"] = dbus.MakeVariant(true)
ipv6Map["dns"] = dbus.MakeVariant(dnsv6)
ipv6Map["dns-search"] = dbus.MakeVariant(config.SearchDomains)
if len(dnsv6) == 0 {
ipv6Map["dns-priority"] = dbus.MakeVariant(lowerPriority)
} else if len(config.MatchDomains) > 0 {
// Set a fairly high priority, but don't override all other
// configs when in split-DNS mode.
ipv6Map["dns-priority"] = dbus.MakeVariant(mediumPriority)
} else {
ipv6Map["dns-priority"] = dbus.MakeVariant(highestPriority)
}
// deprecatedProperties are the properties in interface settings
// that are deprecated by NetworkManager.
@@ -189,19 +205,134 @@ func (m nmManager) SetDNS(config OSConfig) error {
delete(ipv6Map, property)
}
err = device.CallWithContext(
ctx, "org.freedesktop.NetworkManager.Device.Reapply", 0,
settings, version, uint32(0),
).Store()
if err != nil {
if call := device.CallWithContext(ctx, "org.freedesktop.NetworkManager.Device.Reapply", 0, settings, version, uint32(0)); call.Err != nil {
return fmt.Errorf("reapply: %w", err)
}
return nil
}
func (m nmManager) RoutingMode() RoutingMode { return RoutingModeNone }
func (m *nmManager) SupportsSplitDNS() bool {
var mode string
v, err := m.dnsManager.GetProperty("org.freedesktop.NetworkManager.DnsManager.Mode")
if err != nil {
return false
}
mode, ok := v.Value().(string)
if !ok {
return false
}
func (m nmManager) Close() error {
return m.SetDNS(OSConfig{})
// Per NM's documentation, it only does split-DNS when it's
// programming dnsmasq or systemd-resolved. All other modes are
// primary-only.
return mode == "dnsmasq" || mode == "systemd-resolved"
}
func (m *nmManager) GetBaseConfig() (OSConfig, error) {
conn, err := dbus.SystemBus()
if err != nil {
return OSConfig{}, err
}
nm := conn.Object("org.freedesktop.NetworkManager", dbus.ObjectPath("/org/freedesktop/NetworkManager/DnsManager"))
v, err := nm.GetProperty("org.freedesktop.NetworkManager.DnsManager.Configuration")
if err != nil {
return OSConfig{}, err
}
cfgs, ok := v.Value().([]map[string]dbus.Variant)
if !ok {
return OSConfig{}, fmt.Errorf("unexpected NM config type %T", v.Value())
}
if len(cfgs) == 0 {
return OSConfig{}, nil
}
type dnsPrio struct {
resolvers []netaddr.IP
domains []string
priority int32
}
order := make([]dnsPrio, 0, len(cfgs)-1)
for _, cfg := range cfgs {
if name, ok := cfg["interface"]; ok {
if s, ok := name.Value().(string); ok && s == m.interfaceName {
// Config for the taislcale interface, skip.
continue
}
}
var p dnsPrio
if v, ok := cfg["nameservers"]; ok {
if ips, ok := v.Value().([]string); ok {
for _, s := range ips {
ip, err := netaddr.ParseIP(s)
if err != nil {
// hmm, what do? Shouldn't really happen.
continue
}
p.resolvers = append(p.resolvers, ip)
}
}
}
if v, ok := cfg["domains"]; ok {
if domains, ok := v.Value().([]string); ok {
p.domains = domains
}
}
if v, ok := cfg["priority"]; ok {
if prio, ok := v.Value().(int32); ok {
p.priority = prio
}
}
order = append(order, p)
}
sort.Slice(order, func(i, j int) bool {
return order[i].priority < order[j].priority
})
var (
ret OSConfig
seenResolvers = map[netaddr.IP]bool{}
seenSearch = map[string]bool{}
)
for _, cfg := range order {
for _, resolver := range cfg.resolvers {
if seenResolvers[resolver] {
continue
}
ret.Nameservers = append(ret.Nameservers, resolver)
seenResolvers[resolver] = true
}
for _, dom := range cfg.domains {
if seenSearch[dom] {
continue
}
fqdn, err := dnsname.ToFQDN(dom)
if err != nil {
continue
}
ret.SearchDomains = append(ret.SearchDomains, fqdn)
seenSearch[dom] = true
}
if cfg.priority < 0 {
// exclusive configurations preempt all other
// configurations, so we're done.
break
}
}
return ret, nil
}
func (m *nmManager) Close() error {
// No need to do anything on close, NetworkManager will delete our
// settings when the tailscale interface goes away.
return nil
}

View File

@@ -6,10 +6,13 @@ package dns
type noopManager struct{}
func (m noopManager) SetDNS(OSConfig) error { return nil }
func (m noopManager) RoutingMode() RoutingMode { return RoutingModeNone }
func (m noopManager) Close() error { return nil }
func NewNoopManager() noopManager {
return noopManager{}
func (m noopManager) SetDNS(OSConfig) error { return nil }
func (m noopManager) SupportsSplitDNS() bool { return false }
func (m noopManager) Close() error { return nil }
func (m noopManager) GetBaseConfig() (OSConfig, error) {
return OSConfig{}, ErrGetBaseConfigNotSupported
}
func NewNoopManager() (noopManager, error) {
return noopManager{}, nil
}

84
net/dns/openresolv.go Normal file
View File

@@ -0,0 +1,84 @@
// 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 dns
import (
"bytes"
"fmt"
"os/exec"
"strings"
)
// openresolvManager manages DNS configuration using the openresolv
// implementation of the `resolvconf` program.
type openresolvManager struct{}
func newOpenresolvManager() (openresolvManager, error) {
return openresolvManager{}, nil
}
func (m openresolvManager) SetDNS(config OSConfig) error {
var stdin bytes.Buffer
writeResolvConf(&stdin, config.Nameservers, config.SearchDomains)
cmd := exec.Command("resolvconf", "-m", "0", "-x", "-a", "tailscale")
cmd.Stdin = &stdin
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("running %s: %s", cmd, out)
}
return nil
}
func (m openresolvManager) SupportsSplitDNS() bool {
return false
}
func (m openresolvManager) GetBaseConfig() (OSConfig, error) {
// List the names of all config snippets openresolv is aware
// of. Snippets get listed in priority order (most to least),
// which we'll exploit later.
bs, err := exec.Command("resolvconf", "-i").CombinedOutput()
if err != nil {
return OSConfig{}, err
}
// Remove the "tailscale" snippet from the list.
args := []string{"-l"}
for _, f := range strings.Split(strings.TrimSpace(string(bs)), " ") {
if f == "tailscale" {
continue
}
args = append(args, f)
}
// List all resolvconf snippets except our own, and parse that as
// a resolv.conf. This effectively generates a blended config of
// "everyone except tailscale", which is what would be in use if
// tailscale hadn't set exclusive mode.
//
// Note that this is not _entirely_ true. To be perfectly correct,
// we should be looking for other interfaces marked exclusive that
// predated tailscale, and stick to only those. However, in
// practice, openresolv uses are generally quite limited, and boil
// down to 1-2 DHCP leases, for which the correct outcome is a
// blended config like the one we produce here.
var buf bytes.Buffer
cmd := exec.Command("resolvconf", args...)
cmd.Stdout = &buf
if err := cmd.Run(); err != nil {
return OSConfig{}, err
}
return readResolv(&buf)
}
func (m openresolvManager) Close() error {
cmd := exec.Command("resolvconf", "-f", "-d", "tailscale")
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("running %s: %s", cmd, out)
}
return nil
}

View File

@@ -4,21 +4,11 @@
package dns
// DNSRoutingMode describes the type of per-domain DNS routing that
// the OS is capable of.
type RoutingMode int
import (
"errors"
const (
// RoutingModeNone means the OS only supports setting a single
// primary set of DNS resolvers.
RoutingModeNone RoutingMode = iota
// RoutingModeSingle means the OS supports a set of
// primary resolvers, as well as one set of additional per-suffix
// resolvers per network interface.
RoutingModeSingle
// RoutingModeMulti means the OS supports a set of primary
// resolvers, as well as an arbitrary overlay of DNS routes.
RoutingModeMulti
"inet.af/netaddr"
"tailscale.com/util/dnsname"
)
// An OSConfigurator applies DNS settings to the operating system.
@@ -28,9 +18,71 @@ type OSConfigurator interface {
// configuration is removed.
// SetDNS must not be called after Close.
SetDNS(cfg OSConfig) error
// DNSRoutingMode reports the DNS routing capabilities of this OS
// configurator.
RoutingMode() RoutingMode
// SupportsSplitDNS reports whether the configurator is capable of
// installing a resolver only for specific DNS suffixes. If false,
// the configurator can only set a global resolver.
SupportsSplitDNS() bool
// GetBaseConfig returns the OS's "base" configuration, i.e. the
// resolver settings the OS would use without Tailscale
// contributing any configuration.
// GetBaseConfig must return the tailscale-free base config even
// after SetDNS has been called to set a Tailscale configuration.
// Only works when SupportsSplitDNS=false.
// Implementations that don't support getting the base config must
// return ErrGetBaseConfigNotSupported.
GetBaseConfig() (OSConfig, error)
// Close removes Tailscale-related DNS configuration from the OS.
Close() error
}
// OSConfig is an OS DNS configuration.
type OSConfig struct {
// Nameservers are the IP addresses of the nameservers to use.
Nameservers []netaddr.IP
// SearchDomains are the domain suffixes to use when expanding
// single-label name queries. SearchDomains is additive to
// whatever non-Tailscale search domains the OS has.
SearchDomains []dnsname.FQDN
// MatchDomains are the DNS suffixes for which Nameservers should
// be used. If empty, Nameservers is installed as the "primary" resolver.
// A non-empty MatchDomains requests a "split DNS" configuration
// from the OS, which will only work with OSConfigurators that
// report SupportsSplitDNS()=true.
MatchDomains []dnsname.FQDN
}
func (a OSConfig) Equal(b OSConfig) bool {
if len(a.Nameservers) != len(b.Nameservers) {
return false
}
if len(a.SearchDomains) != len(b.SearchDomains) {
return false
}
if len(a.MatchDomains) != len(b.MatchDomains) {
return false
}
for i := range a.Nameservers {
if a.Nameservers[i] != b.Nameservers[i] {
return false
}
}
for i := range a.SearchDomains {
if a.SearchDomains[i] != b.SearchDomains[i] {
return false
}
}
for i := range a.MatchDomains {
if a.MatchDomains[i] != b.MatchDomains[i] {
return false
}
}
return true
}
// ErrGetBaseConfigNotSupported is the error
// OSConfigurator.GetBaseConfig returns when the OSConfigurator
// doesn't support reading the underlying configuration out of the OS.
var ErrGetBaseConfigNotSupported = errors.New("getting OS base config is not supported")

View File

@@ -0,0 +1,63 @@
#!/bin/sh
# Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
#
# This script is a workaround for a vpn-unfriendly behavior of the
# original resolvconf by Thomas Hood. Unlike the `openresolv`
# implementation (whose binary is also called resolvconf,
# confusingly), the original resolvconf lacks a way to specify
# "exclusive mode" for a provider configuration. In practice, this
# means that if Tailscale wants to install a DNS configuration, that
# config will get "blended" with the configs from other sources,
# rather than override those other sources.
#
# This script gets installed at /etc/resolvconf/update-libc.d, which
# is a directory of hook scripts that get run after resolvconf's libc
# helper has finished rewriting /etc/resolv.conf. It's meant to notify
# consumers of resolv.conf of a new configuration.
#
# Instead, we use that hook mechanism to reach into resolvconf's
# stuff, and rewrite the libc-generated resolv.conf to exclusively
# contain Tailscale's configuration - effectively implementing
# exclusive mode ourselves in post-production.
set -e
if [ -n "$TAILSCALE_RESOLVCONF_HOOK_LOOP" ]; then
# Hook script being invoked by itself, skip.
exit 0
fi
if [ ! -f tun-tailscale.inet ]; then
# Tailscale isn't trying to manage DNS, do nothing.
exit 0
fi
if ! grep resolvconf /etc/resolv.conf >/dev/null; then
# resolvconf isn't managing /etc/resolv.conf, do nothing.
exit 0
fi
# Write out a modified /etc/resolv.conf containing just our config.
(
if [ -f /etc/resolvconf/resolv.conf.d/head ]; then
cat /etc/resolvconf/resolv.conf.d/head
fi
echo "# Tailscale workaround applied to set exclusive DNS configuration."
cat tun-tailscale.inet
if [ -f /etc/resolvconf/resolv.conf.d/base ]; then
# Keep options and sortlist, discard other base things since
# they're the things we're trying to override.
grep -e 'sortlist ' -e 'options ' /etc/resolvconf/resolv.conf.d/base || true
fi
if [ -f /etc/resolvconf/resolv.conf.d/tail ]; then
cat /etc/resolvconf/resolv.conf.d/tail
fi
) >/etc/resolv.conf
if [ -d /etc/resolvconf/update-libc.d ] ; then
# Re-notify libc watchers that we've changed resolv.conf again.
export TAILSCALE_RESOLVCONF_HOOK_LOOP=1
exec run-parts /etc/resolvconf/update-libc.d
fi

View File

@@ -1,161 +1,27 @@
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
// 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 freebsd
package dns
import (
"bufio"
"bytes"
"fmt"
"os"
"os/exec"
"tailscale.com/types/logger"
)
// isResolvconfActive indicates whether the system appears to be using resolvconf.
// If this is true, then directManager should be avoided:
// resolvconf has exclusive ownership of /etc/resolv.conf.
func isResolvconfActive() bool {
// Sanity-check first: if there is no resolvconf binary, then this is fruitless.
//
// However, this binary may be a shim like the one systemd-resolved provides.
// Such a shim may not behave as expected: in particular, systemd-resolved
// does not seem to respect the exclusive mode -x, saying:
// -x Send DNS traffic preferably over this interface
// whereas e.g. openresolv sends DNS traffix _exclusively_ over that interface,
// or not at all (in case of another exclusive-mode request later in time).
//
// Moreover, resolvconf may be installed but unused, in which case we should
// not use it either, lest we clobber existing configuration.
//
// To handle all the above correctly, we scan the comments in /etc/resolv.conf
// to ensure that it was generated by a resolvconf implementation.
_, err := exec.LookPath("resolvconf")
func newResolvconfManager(logf logger.Logf) (OSConfigurator, error) {
_, err := exec.Command("resolvconf", "--version").CombinedOutput()
if err != nil {
return false
}
f, err := os.Open("/etc/resolv.conf")
if err != nil {
return false
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Bytes()
// Look for the word "resolvconf" until comments end.
if len(line) > 0 && line[0] != '#' {
return false
}
if bytes.Contains(line, []byte("resolvconf")) {
return true
if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 99 {
// Debian resolvconf doesn't understand --version, and
// exits with a specific error code.
return newDebianResolvconfManager(logf)
}
}
return false
}
// resolvconfImpl enumerates supported implementations of the resolvconf CLI.
type resolvconfImpl uint8
const (
// resolvconfOpenresolv is the implementation packaged as "openresolv" on Ubuntu.
// It supports exclusive mode and interface metrics.
resolvconfOpenresolv resolvconfImpl = iota
// resolvconfLegacy is the implementation by Thomas Hood packaged as "resolvconf" on Ubuntu.
// It does not support exclusive mode or interface metrics.
resolvconfLegacy
)
func (impl resolvconfImpl) String() string {
switch impl {
case resolvconfOpenresolv:
return "openresolv"
case resolvconfLegacy:
return "legacy"
default:
return "unknown"
}
}
// getResolvconfImpl returns the implementation of resolvconf that appears to be in use.
func getResolvconfImpl() resolvconfImpl {
err := exec.Command("resolvconf", "-v").Run()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
// Thomas Hood's resolvconf has a minimal flag set
// and exits with code 99 when passed an unknown flag.
if exitErr.ExitCode() == 99 {
return resolvconfLegacy
}
}
}
return resolvconfOpenresolv
}
type resolvconfManager struct {
impl resolvconfImpl
}
func newResolvconfManager(logf logger.Logf) resolvconfManager {
impl := getResolvconfImpl()
logf("resolvconf implementation is %s", impl)
return resolvconfManager{
impl: impl,
}
}
// resolvconfConfigName is the name of the config submitted to resolvconf.
// It has this form to match the "tun*" rule in interface-order
// when running resolvconfLegacy, hopefully placing our config first.
const resolvconfConfigName = "tun-tailscale.inet"
func (m resolvconfManager) SetDNS(config OSConfig) error {
stdin := new(bytes.Buffer)
writeResolvConf(stdin, config.Nameservers, config.Domains) // dns_direct.go
var cmd *exec.Cmd
switch m.impl {
case resolvconfOpenresolv:
// Request maximal priority (metric 0) and exclusive mode.
cmd = exec.Command("resolvconf", "-m", "0", "-x", "-a", resolvconfConfigName)
case resolvconfLegacy:
// This does not quite give us the desired behavior (queries leak),
// but there is nothing else we can do without messing with other interfaces' settings.
cmd = exec.Command("resolvconf", "-a", resolvconfConfigName)
}
cmd.Stdin = stdin
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("running %s: %s", cmd, out)
}
return nil
}
func (m resolvconfManager) RoutingMode() RoutingMode {
return RoutingModeNone
}
func (m resolvconfManager) Close() error {
var cmd *exec.Cmd
switch m.impl {
case resolvconfOpenresolv:
cmd = exec.Command("resolvconf", "-f", "-d", resolvconfConfigName)
case resolvconfLegacy:
// resolvconfLegacy lacks the -f flag.
// Instead, it succeeds even when the config does not exist.
cmd = exec.Command("resolvconf", "-d", resolvconfConfigName)
}
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("running %s: %s", cmd, out)
}
return nil
// If --version works, or we got some surprising error while
// probing, use openresolv. It's the more common implementation,
// so in cases where we can't figure things out, it's the least
// likely to misbehave.
return newOpenresolvManager()
}

View File

@@ -12,12 +12,13 @@ import (
"context"
"errors"
"fmt"
"os/exec"
"github.com/godbus/dbus/v5"
"golang.org/x/sys/unix"
"inet.af/netaddr"
"tailscale.com/net/interfaces"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
)
// resolvedListenAddr is the listen address of the resolved stub resolver.
@@ -51,15 +52,20 @@ type resolvedLinkDomain struct {
// isResolvedActive determines if resolved is currently managing system DNS settings.
func isResolvedActive() bool {
// systemd-resolved is never installed without systemd.
_, err := exec.LookPath("systemctl")
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
defer cancel()
conn, err := dbus.SystemBus()
if err != nil {
// Probably no DBus on the system, or we're not allowed to use
// it. Cannot control resolved.
return false
}
// is-active exits with code 3 if the service is not active.
err = exec.Command("systemctl", "is-active", "systemd-resolved").Run()
if err != nil {
rd := conn.Object("org.freedesktop.resolve1", dbus.ObjectPath("/org/freedesktop/resolve1"))
call := rd.CallWithContext(ctx, "org.freedesktop.DBus.Peer.Ping", 0)
if call.Err != nil {
// Can't talk to resolved.
return false
}
@@ -77,29 +83,28 @@ func isResolvedActive() bool {
}
// resolvedManager uses the systemd-resolved DBus API.
type resolvedManager struct{}
type resolvedManager struct {
logf logger.Logf
resolved dbus.BusObject
}
func newResolvedManager() resolvedManager {
return resolvedManager{}
func newResolvedManager(logf logger.Logf) (*resolvedManager, error) {
conn, err := dbus.SystemBus()
if err != nil {
return nil, err
}
return &resolvedManager{
logf: logf,
resolved: conn.Object("org.freedesktop.resolve1", dbus.ObjectPath("/org/freedesktop/resolve1")),
}, nil
}
// Up implements managerImpl.
func (m resolvedManager) SetDNS(config OSConfig) error {
func (m *resolvedManager) SetDNS(config OSConfig) error {
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
defer cancel()
// conn is a shared connection whose lifecycle is managed by the dbus package.
// We should not interfere with that by closing it.
conn, err := dbus.SystemBus()
if err != nil {
return fmt.Errorf("connecting to system bus: %w", err)
}
resolved := conn.Object(
"org.freedesktop.resolve1",
dbus.ObjectPath("/org/freedesktop/resolve1"),
)
// In principle, we could persist this in the manager struct
// if we knew that interface indices are persistent. This does not seem to be the case.
_, iface, err := interfaces.Tailscale()
@@ -126,7 +131,7 @@ func (m resolvedManager) SetDNS(config OSConfig) error {
}
}
err = resolved.CallWithContext(
err = m.resolved.CallWithContext(
ctx, "org.freedesktop.resolve1.Manager.SetLinkDNS", 0,
iface.Index, linkNameservers,
).Store()
@@ -134,15 +139,40 @@ func (m resolvedManager) SetDNS(config OSConfig) error {
return fmt.Errorf("setLinkDNS: %w", err)
}
var linkDomains = make([]resolvedLinkDomain, len(config.Domains))
for i, domain := range config.Domains {
linkDomains[i] = resolvedLinkDomain{
Domain: domain,
RoutingOnly: false,
linkDomains := make([]resolvedLinkDomain, 0, len(config.SearchDomains)+len(config.MatchDomains))
seenDomains := map[dnsname.FQDN]bool{}
for _, domain := range config.SearchDomains {
if seenDomains[domain] {
continue
}
seenDomains[domain] = true
linkDomains = append(linkDomains, resolvedLinkDomain{
Domain: domain.WithTrailingDot(),
RoutingOnly: false,
})
}
for _, domain := range config.MatchDomains {
if seenDomains[domain] {
// Search domains act as both search and match in
// resolved, so it's correct to skip.
continue
}
seenDomains[domain] = true
linkDomains = append(linkDomains, resolvedLinkDomain{
Domain: domain.WithTrailingDot(),
RoutingOnly: true,
})
}
if len(config.MatchDomains) == 0 {
// Caller requested full DNS interception, install a
// routing-only root domain.
linkDomains = append(linkDomains, resolvedLinkDomain{
Domain: ".",
RoutingOnly: true,
})
}
err = resolved.CallWithContext(
err = m.resolved.CallWithContext(
ctx, "org.freedesktop.resolve1.Manager.SetLinkDomains", 0,
iface.Index, linkDomains,
).Store()
@@ -150,29 +180,53 @@ func (m resolvedManager) SetDNS(config OSConfig) error {
return fmt.Errorf("setLinkDomains: %w", err)
}
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDefaultRoute", 0, iface.Index, len(config.MatchDomains) == 0); call.Err != nil {
return fmt.Errorf("setLinkDefaultRoute: %w", err)
}
// Some best-effort setting of things, but resolved should do the
// right thing if these fail (e.g. a really old resolved version
// or something).
// Disable LLMNR, we don't do multicast.
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkLLMNR", 0, iface.Index, "no"); call.Err != nil {
m.logf("[v1] failed to disable LLMNR: %v", call.Err)
}
// Disable mdns.
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkMulticastDNS", 0, iface.Index, "no"); call.Err != nil {
m.logf("[v1] failed to disable mdns: %v", call.Err)
}
// We don't support dnssec consistently right now, force it off to
// avoid partial failures when we split DNS internally.
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDNSSEC", 0, iface.Index, "no"); call.Err != nil {
m.logf("[v1] failed to disable DNSSEC: %v", call.Err)
}
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDNSOverTLS", 0, iface.Index, "no"); call.Err != nil {
m.logf("[v1] failed to disable DoT: %v", call.Err)
}
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.FlushCaches", 0); call.Err != nil {
m.logf("failed to flush resolved DNS cache: %v", call.Err)
}
return nil
}
func (m resolvedManager) RoutingMode() RoutingMode {
return RoutingModeNone
func (m *resolvedManager) SupportsSplitDNS() bool {
return true
}
func (m resolvedManager) Close() error {
func (m *resolvedManager) GetBaseConfig() (OSConfig, error) {
return OSConfig{}, ErrGetBaseConfigNotSupported
}
func (m *resolvedManager) Close() error {
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
defer cancel()
// conn is a shared connection whose lifecycle is managed by the dbus package.
// We should not interfere with that by closing it.
conn, err := dbus.SystemBus()
if err != nil {
return fmt.Errorf("connecting to system bus: %w", err)
}
resolved := conn.Object(
"org.freedesktop.resolve1",
dbus.ObjectPath("/org/freedesktop/resolve1"),
)
_, iface, err := interfaces.Tailscale()
if err != nil {
return fmt.Errorf("getting interface index: %w", err)
@@ -181,12 +235,8 @@ func (m resolvedManager) Close() error {
return errNotReady
}
err = resolved.CallWithContext(
ctx, "org.freedesktop.resolve1.Manager.RevertLink", 0,
iface.Index,
).Store()
if err != nil {
return fmt.Errorf("RevertLink: %w", err)
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.RevertLink", 0, iface.Index); call.Err != nil {
return fmt.Errorf("RevertLink: %w", call.Err)
}
return nil

View File

@@ -20,7 +20,6 @@ import (
dns "golang.org/x/net/dns/dnsmessage"
"inet.af/netaddr"
"tailscale.com/logtail/backoff"
"tailscale.com/net/netns"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
)
@@ -103,7 +102,7 @@ func getTxID(packet []byte) txid {
}
type route struct {
suffix string
suffix dnsname.FQDN
resolvers []netaddr.IPPort
}
@@ -273,7 +272,7 @@ func (f *forwarder) forward(query packet) error {
var resolvers []netaddr.IPPort
for _, route := range routes {
if route.suffix != "." && !dnsname.HasSuffix(domain, route.suffix) {
if route.suffix != "." && !route.suffix.Contains(domain) {
continue
}
resolvers = route.resolvers
@@ -453,7 +452,7 @@ func (c *fwdConn) read(out []byte) int {
func (c *fwdConn) reconnectLocked() {
c.closeConnLocked()
// Make a new connection.
conn, err := netns.Listener().ListenPacket(context.Background(), "udp", "")
conn, err := net.ListenPacket("udp", "")
if err != nil {
c.logf("ListenPacket failed: %v", err)
} else {
@@ -490,7 +489,7 @@ func (c *fwdConn) close() {
}
// nameFromQuery extracts the normalized query name from bs.
func nameFromQuery(bs []byte) (string, error) {
func nameFromQuery(bs []byte) (dnsname.FQDN, error) {
var parser dns.Parser
hdr, err := parser.Start(bs)
@@ -507,5 +506,5 @@ func nameFromQuery(bs []byte) (string, error) {
}
n := q.Name.Data[:q.Name.Length]
return rawNameToLower(n), nil
return dnsname.ToFQDN(rawNameToLower(n))
}

View File

@@ -9,7 +9,6 @@ package resolver
import (
"encoding/hex"
"errors"
"fmt"
"sort"
"strings"
"sync"
@@ -59,12 +58,12 @@ type Config struct {
// queries within that suffix.
// Queries only match the most specific suffix.
// To register a "default route", add an entry for ".".
Routes map[string][]netaddr.IPPort
Routes map[dnsname.FQDN][]netaddr.IPPort
// LocalHosts is a map of FQDNs to corresponding IPs.
Hosts map[string][]netaddr.IP
Hosts map[dnsname.FQDN][]netaddr.IP
// LocalDomains is a list of DNS name suffixes that should not be
// routed to upstream resolvers.
LocalDomains []string
LocalDomains []dnsname.FQDN
}
// Resolver is a DNS resolver for nodes on the Tailscale network,
@@ -72,9 +71,10 @@ type Config struct {
// If it is asked to resolve a domain that is not of that form,
// it delegates to upstream nameservers if any are set.
type Resolver struct {
logf logger.Logf
linkMon *monitor.Mon // or nil
unregLinkMon func() // or nil
logf logger.Logf
linkMon *monitor.Mon // or nil
unregLinkMon func() // or nil
saveConfigForTests func(cfg Config) // used in tests to capture resolver config
// forwarder forwards requests to upstream nameservers.
forwarder *forwarder
@@ -91,9 +91,9 @@ type Resolver struct {
// mu guards the following fields from being updated while used.
mu sync.Mutex
localDomains []string
hostToIP map[string][]netaddr.IP
ipToHost map[netaddr.IP]string
localDomains []dnsname.FQDN
hostToIP map[dnsname.FQDN][]netaddr.IP
ipToHost map[netaddr.IP]dnsname.FQDN
}
// New returns a new resolver.
@@ -106,8 +106,8 @@ func New(logf logger.Logf, linkMon *monitor.Mon) *Resolver {
responses: make(chan packet),
errors: make(chan error),
closed: make(chan struct{}),
hostToIP: map[string][]netaddr.IP{},
ipToHost: map[netaddr.IP]string{},
hostToIP: map[dnsname.FQDN][]netaddr.IP{},
ipToHost: map[netaddr.IP]dnsname.FQDN{},
}
r.forwarder = newForwarder(r.logf, r.responses)
if r.linkMon != nil {
@@ -120,32 +120,23 @@ func New(logf logger.Logf, linkMon *monitor.Mon) *Resolver {
return r
}
func isFQDN(s string) bool {
return strings.HasSuffix(s, ".")
}
func (r *Resolver) TestOnlySetHook(hook func(Config)) { r.saveConfigForTests = hook }
func (r *Resolver) SetConfig(cfg Config) error {
if r.saveConfigForTests != nil {
r.saveConfigForTests(cfg)
}
routes := make([]route, 0, len(cfg.Routes))
reverse := make(map[netaddr.IP]string, len(cfg.Hosts))
reverse := make(map[netaddr.IP]dnsname.FQDN, len(cfg.Hosts))
for host, ips := range cfg.Hosts {
if !isFQDN(host) {
return fmt.Errorf("host entry %q is not a FQDN", host)
}
for _, ip := range ips {
reverse[ip] = host
}
}
for _, domain := range cfg.LocalDomains {
if !isFQDN(domain) {
return fmt.Errorf("local domain %q is not a FQDN", domain)
}
}
for suffix, ips := range cfg.Routes {
if !strings.HasSuffix(suffix, ".") {
return fmt.Errorf("route suffix %q is not a FQDN", suffix)
}
routes = append(routes, route{
suffix: suffix,
resolvers: ips,
@@ -153,7 +144,7 @@ func (r *Resolver) SetConfig(cfg Config) error {
}
// Sort from longest prefix to shortest.
sort.Slice(routes, func(i, j int) bool {
return dnsname.NumLabels(routes[i].suffix) > dnsname.NumLabels(routes[j].suffix)
return routes[i].suffix.NumLabels() > routes[j].suffix.NumLabels()
})
r.forwarder.setRoutes(routes)
@@ -222,12 +213,11 @@ func (r *Resolver) NextResponse() (packet []byte, to netaddr.IPPort, err error)
// resolveLocal returns an IP for the given domain, if domain is in
// the local hosts map and has an IP corresponding to the requested
// typ (A, AAAA, ALL).
// The domain name must be in canonical form (with a trailing period).
// Returns dns.RCodeRefused to indicate that the local map is not
// authoritative for domain.
func (r *Resolver) resolveLocal(domain string, typ dns.Type) (netaddr.IP, dns.RCode) {
func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netaddr.IP, dns.RCode) {
// Reject .onion domains per RFC 7686.
if dnsname.HasSuffix(domain, ".onion") {
if dnsname.HasSuffix(domain.WithoutTrailingDot(), ".onion") {
return netaddr.IP{}, dns.RCodeNameError
}
@@ -239,7 +229,7 @@ func (r *Resolver) resolveLocal(domain string, typ dns.Type) (netaddr.IP, dns.RC
addrs, found := hosts[domain]
if !found {
for _, suffix := range localDomains {
if dnsname.HasSuffix(domain, suffix) {
if suffix.Contains(domain) {
// We are authoritative for the queried domain.
return netaddr.IP{}, dns.RCodeNameError
}
@@ -297,8 +287,7 @@ func (r *Resolver) resolveLocal(domain string, typ dns.Type) (netaddr.IP, dns.RC
}
// resolveReverse returns the unique domain name that maps to the given address.
// The returned domain name is in canonical form (with a trailing period).
func (r *Resolver) resolveLocalReverse(ip netaddr.IP) (string, dns.RCode) {
func (r *Resolver) resolveLocalReverse(ip netaddr.IP) (dnsname.FQDN, dns.RCode) {
r.mu.Lock()
ips := r.ipToHost
r.mu.Unlock()
@@ -355,7 +344,7 @@ type response struct {
Header dns.Header
Question dns.Question
// Name is the response to a PTR query.
Name string
Name dnsname.FQDN
// IP is the response to an A, AAAA, or ALL query.
IP netaddr.IP
}
@@ -418,7 +407,7 @@ func marshalAAAARecord(name dns.Name, ip netaddr.IP, builder *dns.Builder) error
// marshalPTRRecord serializes a PTR record into an active builder.
// The caller may continue using the builder following the call.
func marshalPTRRecord(queryName dns.Name, name string, builder *dns.Builder) error {
func marshalPTRRecord(queryName dns.Name, name dnsname.FQDN, builder *dns.Builder) error {
var answer dns.PTRResource
var err error
@@ -428,7 +417,7 @@ func marshalPTRRecord(queryName dns.Name, name string, builder *dns.Builder) err
Class: dns.ClassINET,
TTL: uint32(defaultTTL / time.Second),
}
answer.PTR, err = dns.NewName(name)
answer.PTR, err = dns.NewName(name.WithTrailingDot())
if err != nil {
return err
}
@@ -501,12 +490,13 @@ const (
// r._dns-sd._udp.<domain>.
// dr._dns-sd._udp.<domain>.
// lb._dns-sd._udp.<domain>.
func hasRDNSBonjourPrefix(s string) bool {
func hasRDNSBonjourPrefix(name dnsname.FQDN) bool {
// Even the shortest name containing a Bonjour prefix is long,
// so check length (cheap) and bail early if possible.
if len(s) < len("*._dns-sd._udp.0.0.0.0.in-addr.arpa.") {
if len(name) < len("*._dns-sd._udp.0.0.0.0.in-addr.arpa.") {
return false
}
s := name.WithTrailingDot()
dot := strings.IndexByte(s, '.')
if dot == -1 {
return false // shouldn't happen
@@ -541,9 +531,9 @@ func rawNameToLower(name []byte) string {
// 4.3.2.1.in-addr.arpa
// is transformed to
// 1.2.3.4
func rdnsNameToIPv4(name string) (ip netaddr.IP, ok bool) {
name = strings.TrimSuffix(name, rdnsv4Suffix)
ip, err := netaddr.ParseIP(string(name))
func rdnsNameToIPv4(name dnsname.FQDN) (ip netaddr.IP, ok bool) {
s := strings.TrimSuffix(name.WithTrailingDot(), rdnsv4Suffix)
ip, err := netaddr.ParseIP(s)
if err != nil {
return netaddr.IP{}, false
}
@@ -560,21 +550,21 @@ func rdnsNameToIPv4(name string) (ip netaddr.IP, ok bool) {
// b.a.9.8.7.6.5.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa.
// is transformed to
// 2001:db8::567:89ab
func rdnsNameToIPv6(name string) (ip netaddr.IP, ok bool) {
func rdnsNameToIPv6(name dnsname.FQDN) (ip netaddr.IP, ok bool) {
var b [32]byte
var ipb [16]byte
name = strings.TrimSuffix(name, rdnsv6Suffix)
s := strings.TrimSuffix(name.WithTrailingDot(), rdnsv6Suffix)
// 32 nibbles and 31 dots between them.
if len(name) != 63 {
if len(s) != 63 {
return netaddr.IP{}, false
}
// Dots and hex digits alternate.
prevDot := true
// i ranges over name backward; j ranges over b forward.
for i, j := len(name)-1, 0; i >= 0; i-- {
thisDot := (name[i] == '.')
for i, j := len(s)-1, 0; i >= 0; i-- {
thisDot := (s[i] == '.')
if prevDot == thisDot {
return netaddr.IP{}, false
}
@@ -583,7 +573,7 @@ func rdnsNameToIPv6(name string) (ip netaddr.IP, ok bool) {
if !thisDot {
// This is safe assuming alternation.
// We do not check that non-dots are hex digits: hex.Decode below will do that.
b[j] = name[i]
b[j] = s[i]
j++
}
}
@@ -598,7 +588,7 @@ func rdnsNameToIPv6(name string) (ip netaddr.IP, ok bool) {
// respondReverse returns a DNS response to a PTR query.
// It is assumed that resp.Question is populated by respond before this is called.
func (r *Resolver) respondReverse(query []byte, name string, resp *response) ([]byte, error) {
func (r *Resolver) respondReverse(query []byte, name dnsname.FQDN, resp *response) ([]byte, error) {
if hasRDNSBonjourPrefix(name) {
return nil, errNotOurName
}
@@ -606,9 +596,9 @@ func (r *Resolver) respondReverse(query []byte, name string, resp *response) ([]
var ip netaddr.IP
var ok bool
switch {
case strings.HasSuffix(name, rdnsv4Suffix):
case strings.HasSuffix(name.WithTrailingDot(), rdnsv4Suffix):
ip, ok = rdnsNameToIPv4(name)
case strings.HasSuffix(name, rdnsv6Suffix):
case strings.HasSuffix(name.WithTrailingDot(), rdnsv6Suffix):
ip, ok = rdnsNameToIPv6(name)
default:
return nil, errNotOurName
@@ -649,7 +639,12 @@ func (r *Resolver) respond(query []byte) ([]byte, error) {
return marshalResponse(resp)
}
rawName := resp.Question.Name.Data[:resp.Question.Name.Length]
name := rawNameToLower(rawName)
name, err := dnsname.ToFQDN(rawNameToLower(rawName))
if err != nil {
// DNS packet unexpectedly contains an invalid FQDN.
resp.Header.RCode = dns.RCodeFormatError
return marshalResponse(resp)
}
// Always try to handle reverse lookups; delegate inside when not found.
// This way, queries for existent nodes do not leak,

View File

@@ -13,23 +13,24 @@ import (
dns "golang.org/x/net/dns/dnsmessage"
"inet.af/netaddr"
"tailscale.com/tstest"
"tailscale.com/util/dnsname"
)
var testipv4 = netaddr.MustParseIP("1.2.3.4")
var testipv6 = netaddr.MustParseIP("0001:0203:0405:0607:0809:0a0b:0c0d:0e0f")
var dnsCfg = Config{
Hosts: map[string][]netaddr.IP{
Hosts: map[dnsname.FQDN][]netaddr.IP{
"test1.ipn.dev.": []netaddr.IP{testipv4},
"test2.ipn.dev.": []netaddr.IP{testipv6},
},
LocalDomains: []string{"ipn.dev."},
LocalDomains: []dnsname.FQDN{"ipn.dev."},
}
func dnspacket(domain string, tp dns.Type) []byte {
func dnspacket(domain dnsname.FQDN, tp dns.Type) []byte {
var dnsHeader dns.Header
question := dns.Question{
Name: dns.MustNewName(domain),
Name: dns.MustNewName(domain.WithTrailingDot()),
Type: tp,
Class: dns.ClassINET,
}
@@ -44,7 +45,7 @@ func dnspacket(domain string, tp dns.Type) []byte {
type dnsResponse struct {
ip netaddr.IP
name string
name dnsname.FQDN
rcode dns.RCode
}
@@ -94,7 +95,10 @@ func unpackResponse(payload []byte) (dnsResponse, error) {
if err != nil {
return response, err
}
response.name = res.NS.String()
response.name, err = dnsname.ToFQDN(res.NS.String())
if err != nil {
return response, err
}
default:
return response, errors.New("type not in {A, AAAA, NS}")
}
@@ -119,7 +123,7 @@ func mustIP(str string) netaddr.IP {
func TestRDNSNameToIPv4(t *testing.T) {
tests := []struct {
name string
input string
input dnsname.FQDN
wantIP netaddr.IP
wantOK bool
}{
@@ -144,7 +148,7 @@ func TestRDNSNameToIPv4(t *testing.T) {
func TestRDNSNameToIPv6(t *testing.T) {
tests := []struct {
name string
input string
input dnsname.FQDN
wantIP netaddr.IP
wantOK bool
}{
@@ -194,7 +198,7 @@ func TestResolveLocal(t *testing.T) {
tests := []struct {
name string
qname string
qname dnsname.FQDN
qtype dns.Type
ip netaddr.IP
code dns.RCode
@@ -235,7 +239,7 @@ func TestResolveLocalReverse(t *testing.T) {
tests := []struct {
name string
ip netaddr.IP
want string
want dnsname.FQDN
code dns.RCode
}{
{"ipv4", testipv4, "test1.ipn.dev.", dns.RCodeSuccess},
@@ -285,7 +289,7 @@ func TestDelegate(t *testing.T) {
defer r.Close()
cfg := dnsCfg
cfg.Routes = map[string][]netaddr.IPPort{
cfg.Routes = map[dnsname.FQDN][]netaddr.IPPort{
".": {
netaddr.MustParseIPPort(v4server.PacketConn.LocalAddr().String()),
netaddr.MustParseIPPort(v6server.PacketConn.LocalAddr().String()),
@@ -360,7 +364,7 @@ func TestDelegateSplitRoute(t *testing.T) {
defer r.Close()
cfg := dnsCfg
cfg.Routes = map[string][]netaddr.IPPort{
cfg.Routes = map[dnsname.FQDN][]netaddr.IPPort{
".": {netaddr.MustParseIPPort(server1.PacketConn.LocalAddr().String())},
"other.": {netaddr.MustParseIPPort(server2.PacketConn.LocalAddr().String())},
}
@@ -417,7 +421,7 @@ func TestDelegateCollision(t *testing.T) {
defer r.Close()
cfg := dnsCfg
cfg.Routes = map[string][]netaddr.IPPort{
cfg.Routes = map[dnsname.FQDN][]netaddr.IPPort{
".": {
netaddr.MustParseIPPort(server.PacketConn.LocalAddr().String()),
},
@@ -425,7 +429,7 @@ func TestDelegateCollision(t *testing.T) {
r.SetConfig(cfg)
packets := []struct {
qname string
qname dnsname.FQDN
qtype dns.Type
addr netaddr.IPPort
}{
@@ -692,7 +696,7 @@ func TestAllocs(t *testing.T) {
func TestTrimRDNSBonjourPrefix(t *testing.T) {
tests := []struct {
in string
in dnsname.FQDN
want bool
}{
{"b._dns-sd._udp.0.10.20.172.in-addr.arpa.", true},
@@ -702,7 +706,6 @@ func TestTrimRDNSBonjourPrefix(t *testing.T) {
{"lb._dns-sd._udp.0.10.20.172.in-addr.arpa.", true},
{"qq._dns-sd._udp.0.10.20.172.in-addr.arpa.", false},
{"0.10.20.172.in-addr.arpa.", false},
{"i-have-no-dot", false},
}
for _, test := range tests {
@@ -722,7 +725,7 @@ func BenchmarkFull(b *testing.B) {
defer r.Close()
cfg := dnsCfg
cfg.Routes = map[string][]netaddr.IPPort{
cfg.Routes = map[dnsname.FQDN][]netaddr.IPPort{
".": {
netaddr.MustParseIPPort(server.PacketConn.LocalAddr().String()),
},

View File

@@ -5,21 +5,13 @@
package nettest
import (
"io"
"net"
"time"
)
// Conn is a bi-directional in-memory stream that looks like a TCP net.Conn.
// Conn is a net.Conn that can additionally have its reads and writes blocked and unblocked.
type Conn interface {
io.Reader
io.Writer
io.Closer
// The *Deadline methods follow the semantics of net.Conn.
SetDeadline(t time.Time) error
SetReadDeadline(t time.Time) error
SetWriteDeadline(t time.Time) error
net.Conn
// SetReadBlock blocks or unblocks the Read method of this Conn.
// It reports an error if the existing value matches the new value,
@@ -40,24 +32,37 @@ func NewConn(name string, maxBuf int) (Conn, Conn) {
return &connHalf{r: r, w: w}, &connHalf{r: w, w: r}
}
type connAddr string
func (a connAddr) Network() string { return "mem" }
func (a connAddr) String() string { return string(a) }
type connHalf struct {
r, w *Pipe
}
func (c *connHalf) LocalAddr() net.Addr {
return connAddr(c.r.name)
}
func (c *connHalf) RemoteAddr() net.Addr {
return connAddr(c.w.name)
}
func (c *connHalf) Read(b []byte) (n int, err error) {
return c.r.Read(b)
}
func (c *connHalf) Write(b []byte) (n int, err error) {
return c.w.Write(b)
}
func (c *connHalf) Close() error {
err1 := c.r.Close()
err2 := c.w.Close()
if err1 != nil {
return err1
if err := c.w.Close(); err != nil {
return err
}
return err2
return c.r.Close()
}
func (c *connHalf) SetDeadline(t time.Time) error {
err1 := c.SetReadDeadline(t)
err2 := c.SetWriteDeadline(t)
@@ -72,6 +77,7 @@ func (c *connHalf) SetReadDeadline(t time.Time) error {
func (c *connHalf) SetWriteDeadline(t time.Time) error {
return c.w.SetWriteDeadline(t)
}
func (c *connHalf) SetReadBlock(b bool) error {
if b {
return c.r.Block()

22
net/nettest/conn_test.go Normal file
View File

@@ -0,0 +1,22 @@
// 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 nettest
import (
"net"
"testing"
"golang.org/x/net/nettest"
)
func TestConn(t *testing.T) {
nettest.TestConn(t, func() (c1 net.Conn, c2 net.Conn, stop func(), err error) {
c1, c2 = NewConn("test", bufferSize)
return c1, c2, func() {
c1.Close()
c2.Close()
}, nil
})
}

83
net/nettest/listener.go Normal file
View File

@@ -0,0 +1,83 @@
// 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 nettest
import (
"context"
"net"
"strings"
"sync"
)
const (
bufferSize = 256 * 1024
)
// Listener is a net.Listener using using NewConn to create pairs of network
// connections connected in memory using a buffered pipe. It also provides a
// Dial method to establish new connections.
type Listener struct {
addr connAddr
ch chan Conn
closeOnce sync.Once
closed chan struct{}
}
// Listen returns a new Listener for the provided address.
func Listen(addr string) *Listener {
return &Listener{
addr: connAddr(addr),
ch: make(chan Conn),
closed: make(chan struct{}),
}
}
// Addr implements net.Listener.Addr.
func (l *Listener) Addr() net.Addr {
return l.addr
}
// Close closes the pipe listener.
func (l *Listener) Close() error {
l.closeOnce.Do(func() {
close(l.closed)
})
return nil
}
// Accept blocks until a new connection is available or the listener is closed.
func (l *Listener) Accept() (net.Conn, error) {
select {
case c := <-l.ch:
return c, nil
case <-l.closed:
return nil, net.ErrClosed
}
}
// Dial connects to the listener using the provided context.
// The provided Context must be non-nil. If the context expires before the
// connection is complete, an error is returned. Once successfully connected
// any expiration of the context will not affect the connection.
func (l *Listener) Dial(ctx context.Context, network, addr string) (net.Conn, error) {
if !strings.HasSuffix(network, "tcp") {
return nil, net.UnknownNetworkError(network)
}
if connAddr(addr) != l.addr {
return nil, &net.AddrError{
Err: "invalid address",
Addr: addr,
}
}
c, s := NewConn(addr, bufferSize)
select {
case <-ctx.Done():
return nil, ctx.Err()
case <-l.closed:
return nil, net.ErrClosed
case l.ch <- s:
return c, nil
}
}

View File

@@ -0,0 +1,34 @@
// 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 nettest
import (
"context"
"testing"
)
func TestListener(t *testing.T) {
l := Listen("srv.local")
defer l.Close()
go func() {
c, err := l.Accept()
if err != nil {
t.Error(err)
return
}
defer c.Close()
}()
if c, err := l.Dial(context.Background(), "tcp", "invalid"); err == nil {
c.Close()
t.Fatalf("dial to invalid address succeeded")
}
c, err := l.Dial(context.Background(), "tcp", "srv.local")
if err != nil {
t.Fatalf("dial failed: %v", err)
return
}
c.Close()
}

View File

@@ -5,11 +5,13 @@
package nettest
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"log"
"net"
"os"
"sync"
"time"
)
@@ -20,13 +22,12 @@ const debugPipe = false
type Pipe struct {
name string
maxBuf int
rCh chan struct{}
wCh chan struct{}
mu sync.Mutex
cnd *sync.Cond
mu sync.Mutex
closed bool
blocked bool
buf []byte
closed bool
buf bytes.Buffer
readTimeout time.Time
writeTimeout time.Time
cancelReadTimer func()
@@ -35,21 +36,42 @@ type Pipe struct {
// NewPipe creates a Pipe with a buffer size fixed at maxBuf.
func NewPipe(name string, maxBuf int) *Pipe {
return &Pipe{
p := &Pipe{
name: name,
maxBuf: maxBuf,
rCh: make(chan struct{}, 1),
wCh: make(chan struct{}, 1),
}
p.cnd = sync.NewCond(&p.mu)
return p
}
var (
ErrTimeout = errors.New("timeout")
ErrReadTimeout = fmt.Errorf("read %w", ErrTimeout)
ErrWriteTimeout = fmt.Errorf("write %w", ErrTimeout)
)
// readOrBlock attempts to read from the buffer, if the buffer is empty and
// the connection hasn't been closed it will block until there is a change.
func (p *Pipe) readOrBlock(b []byte) (int, error) {
p.mu.Lock()
defer p.mu.Unlock()
if !p.readTimeout.IsZero() && !time.Now().Before(p.readTimeout) {
return 0, os.ErrDeadlineExceeded
}
if p.blocked {
p.cnd.Wait()
return 0, nil
}
n, err := p.buf.Read(b)
// err will either be nil or io.EOF.
if err == io.EOF {
if p.closed {
return n, err
}
// Wait for something to change.
p.cnd.Wait()
}
return n, nil
}
// Read implements io.Reader.
// Once the buffer is drained (i.e. after Close), subsequent calls will
// return io.EOF.
func (p *Pipe) Read(b []byte) (n int, err error) {
if debugPipe {
orig := b
@@ -57,35 +79,48 @@ func (p *Pipe) Read(b []byte) (n int, err error) {
log.Printf("Pipe(%q).Read( %q) n=%d, err=%v", p.name, string(orig[:n]), n, err)
}()
}
for {
p.mu.Lock()
closed := p.closed
timedout := !p.readTimeout.IsZero() && !time.Now().Before(p.readTimeout)
blocked := p.blocked
if !closed && !timedout && len(p.buf) > 0 {
n2 := copy(b, p.buf)
p.buf = p.buf[n2:]
b = b[n2:]
n += n2
for n == 0 {
n2, err := p.readOrBlock(b)
if err != nil {
return n2, err
}
p.mu.Unlock()
if closed {
return 0, fmt.Errorf("nettest.Pipe(%q): closed: %w", p.name, io.EOF)
}
if timedout {
return 0, fmt.Errorf("nettest.Pipe(%q): %w", p.name, ErrReadTimeout)
}
if blocked {
<-p.rCh
continue
}
if n > 0 {
p.signalWrite()
return n, nil
}
<-p.rCh
n += n2
}
p.cnd.Signal()
return n, nil
}
// writeOrBlock attempts to write to the buffer, if the buffer is full it will
// block until there is a change.
func (p *Pipe) writeOrBlock(b []byte) (int, error) {
p.mu.Lock()
defer p.mu.Unlock()
if p.closed {
return 0, net.ErrClosed
}
if !p.writeTimeout.IsZero() && !time.Now().Before(p.writeTimeout) {
return 0, os.ErrDeadlineExceeded
}
if p.blocked {
p.cnd.Wait()
return 0, nil
}
// Optimistically we want to write the entire slice.
n := len(b)
if limit := p.maxBuf - p.buf.Len(); limit < n {
// However, we don't have enough capacity to write everything.
n = limit
}
if n == 0 {
// Wait for something to change.
p.cnd.Wait()
return 0, nil
}
p.buf.Write(b[:n])
p.cnd.Signal()
return n, nil
}
// Write implements io.Writer.
@@ -96,47 +131,23 @@ func (p *Pipe) Write(b []byte) (n int, err error) {
log.Printf("Pipe(%q).Write(%q) n=%d, err=%v", p.name, string(orig), n, err)
}()
}
for {
p.mu.Lock()
closed := p.closed
timedout := !p.writeTimeout.IsZero() && !time.Now().Before(p.writeTimeout)
blocked := p.blocked
if !closed && !timedout {
n2 := len(b)
if limit := p.maxBuf - len(p.buf); limit < n2 {
n2 = limit
}
p.buf = append(p.buf, b[:n2]...)
b = b[n2:]
n += n2
for len(b) > 0 {
n2, err := p.writeOrBlock(b)
if err != nil {
return n + n2, err
}
p.mu.Unlock()
if closed {
return n, fmt.Errorf("nettest.Pipe(%q): closed: %w", p.name, io.EOF)
}
if timedout {
return n, fmt.Errorf("nettest.Pipe(%q): %w", p.name, ErrWriteTimeout)
}
if blocked {
<-p.wCh
continue
}
if n > 0 {
p.signalRead()
}
if len(b) == 0 {
return n, nil
}
<-p.wCh
n += n2
b = b[n2:]
}
return n, nil
}
// Close implements io.Closer.
// Close closes the pipe.
func (p *Pipe) Close() error {
p.mu.Lock()
closed := p.closed
defer p.mu.Unlock()
p.closed = true
p.blocked = false
if p.cancelWriteTimer != nil {
p.cancelWriteTimer()
p.cancelWriteTimer = nil
@@ -145,77 +156,65 @@ func (p *Pipe) Close() error {
p.cancelReadTimer()
p.cancelReadTimer = nil
}
p.mu.Unlock()
p.cnd.Broadcast()
if closed {
return fmt.Errorf("nettest.Pipe(%q).Close: already closed", p.name)
}
p.signalRead()
p.signalWrite()
return nil
}
func (p *Pipe) deadlineTimer(t time.Time) func() {
if t.IsZero() {
return nil
}
if t.Before(time.Now()) {
p.cnd.Broadcast()
return nil
}
ctx, cancel := context.WithDeadline(context.Background(), t)
go func() {
<-ctx.Done()
if ctx.Err() == context.DeadlineExceeded {
p.cnd.Broadcast()
}
}()
return cancel
}
// SetReadDeadline sets the deadline for future Read calls.
func (p *Pipe) SetReadDeadline(t time.Time) error {
p.mu.Lock()
defer p.mu.Unlock()
p.readTimeout = t
// If we already have a deadline, cancel it and create a new one.
if p.cancelReadTimer != nil {
p.cancelReadTimer()
p.cancelReadTimer = nil
}
if d := time.Until(t); !t.IsZero() && d > 0 {
ctx, cancel := context.WithCancel(context.Background())
p.cancelReadTimer = cancel
go func() {
t := time.NewTimer(d)
defer t.Stop()
select {
case <-t.C:
p.signalRead()
case <-ctx.Done():
}
}()
}
p.mu.Unlock()
p.signalRead()
p.cancelReadTimer = p.deadlineTimer(t)
return nil
}
// SetWriteDeadline sets the deadline for future Write calls.
func (p *Pipe) SetWriteDeadline(t time.Time) error {
p.mu.Lock()
defer p.mu.Unlock()
p.writeTimeout = t
// If we already have a deadline, cancel it and create a new one.
if p.cancelWriteTimer != nil {
p.cancelWriteTimer()
p.cancelWriteTimer = nil
}
if d := time.Until(t); !t.IsZero() && d > 0 {
ctx, cancel := context.WithCancel(context.Background())
p.cancelWriteTimer = cancel
go func() {
t := time.NewTimer(d)
defer t.Stop()
select {
case <-t.C:
p.signalWrite()
case <-ctx.Done():
}
}()
}
p.mu.Unlock()
p.signalWrite()
p.cancelWriteTimer = p.deadlineTimer(t)
return nil
}
// Block will cause all calls to Read and Write to block until they either
// timeout, are unblocked or the pipe is closed.
func (p *Pipe) Block() error {
p.mu.Lock()
defer p.mu.Unlock()
closed := p.closed
blocked := p.blocked
p.blocked = true
p.mu.Unlock()
if closed {
return fmt.Errorf("nettest.Pipe(%q).Block: closed", p.name)
@@ -223,17 +222,17 @@ func (p *Pipe) Block() error {
if blocked {
return fmt.Errorf("nettest.Pipe(%q).Block: already blocked", p.name)
}
p.signalRead()
p.signalWrite()
p.cnd.Broadcast()
return nil
}
// Unblock will cause all blocked Read/Write calls to continue execution.
func (p *Pipe) Unblock() error {
p.mu.Lock()
defer p.mu.Unlock()
closed := p.closed
blocked := p.blocked
p.blocked = false
p.mu.Unlock()
if closed {
return fmt.Errorf("nettest.Pipe(%q).Block: closed", p.name)
@@ -241,21 +240,6 @@ func (p *Pipe) Unblock() error {
if !blocked {
return fmt.Errorf("nettest.Pipe(%q).Block: already unblocked", p.name)
}
p.signalRead()
p.signalWrite()
p.cnd.Broadcast()
return nil
}
func (p *Pipe) signalRead() {
select {
case p.rCh <- struct{}{}:
default:
}
}
func (p *Pipe) signalWrite() {
select {
case p.wCh <- struct{}{}:
default:
}
}

View File

@@ -7,6 +7,7 @@ package nettest
import (
"errors"
"fmt"
"os"
"testing"
"time"
)
@@ -35,7 +36,7 @@ func TestPipeTimeout(t *testing.T) {
p := NewPipe("p1", 1<<16)
p.SetWriteDeadline(time.Now().Add(-1 * time.Second))
n, err := p.Write([]byte{'h'})
if !errors.Is(err, ErrWriteTimeout) || !errors.Is(err, ErrTimeout) {
if !errors.Is(err, os.ErrDeadlineExceeded) {
t.Errorf("missing write timeout got err: %v", err)
}
if n != 0 {
@@ -49,7 +50,7 @@ func TestPipeTimeout(t *testing.T) {
p.SetReadDeadline(time.Now().Add(-1 * time.Second))
b := make([]byte, 1)
n, err := p.Read(b)
if !errors.Is(err, ErrReadTimeout) || !errors.Is(err, ErrTimeout) {
if !errors.Is(err, os.ErrDeadlineExceeded) {
t.Errorf("missing read timeout got err: %v", err)
}
if n != 0 {
@@ -65,7 +66,7 @@ func TestPipeTimeout(t *testing.T) {
if err := p.Block(); err != nil {
t.Fatal(err)
}
if _, err := p.Write([]byte{'h'}); !errors.Is(err, ErrWriteTimeout) {
if _, err := p.Write([]byte{'h'}); !errors.Is(err, os.ErrDeadlineExceeded) {
t.Fatalf("want write timeout got: %v", err)
}
})
@@ -80,7 +81,7 @@ func TestPipeTimeout(t *testing.T) {
if err := p.Block(); err != nil {
t.Fatal(err)
}
if _, err := p.Read(b); !errors.Is(err, ErrReadTimeout) {
if _, err := p.Read(b); !errors.Is(err, os.ErrDeadlineExceeded) {
t.Fatalf("want read timeout got: %v", err)
}
})

13
net/stun/stun_fuzzer.go Normal file
View File

@@ -0,0 +1,13 @@
// 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 gofuzz
package stun
func FuzzStunParser(data []byte) int {
_, _, _, _ = ParseResponse(data)
_, _ = ParseBindingRequest(data)
return 1
}

View File

@@ -35,6 +35,7 @@ var (
ulaRange oncePrefix
tsUlaRange oncePrefix
ula4To6Range oncePrefix
ulaEph6Range oncePrefix
)
// TailscaleServiceIP returns the listen address of services
@@ -72,6 +73,17 @@ func Tailscale4To6Range() netaddr.IPPrefix {
return ula4To6Range.v
}
// TailscaleEphemeral6Range returns the subset of TailscaleULARange
// used for ephemeral IPv6-only Tailscale nodes.
func TailscaleEphemeral6Range() netaddr.IPPrefix {
// This IP range has no significance, beyond being a subset of
// TailscaleULARange. The bits from /48 to /104 were picked at
// random, with the only criterion being to not be the conflict
// with the Tailscale4To6Range above.
ulaEph6Range.Do(func() { mustPrefix(&ulaEph6Range.v, "fd7a:115c:a1e0:7234:6e44:306d:2100::/104") })
return ulaEph6Range.v
}
// Tailscale4To6Placeholder returns an IP address that can be used as
// a source IP when one is required, but a netmap didn't provide
// any. This address never gets allocated by the 4-to-6 algorithm in

View File

@@ -28,16 +28,23 @@ import (
// discovery.
const minimalMTU = 1280
// New returns a tun.Device for the requested device name.
func New(logf logger.Logf, tunName string) (tun.Device, error) {
// New returns a tun.Device for the requested device name, along with
// the OS-dependent name that was allocated to the device.
func New(logf logger.Logf, tunName string) (tun.Device, string, error) {
dev, err := tun.CreateTUN(tunName, minimalMTU)
if err != nil {
return nil, err
return nil, "", err
}
if err := waitInterfaceUp(dev, 90*time.Second, logf); err != nil {
return nil, err
dev.Close()
return nil, "", err
}
return dev, nil
name, err := interfaceName(dev)
if err != nil {
dev.Close()
return nil, "", err
}
return dev, name, nil
}
// Diagnose tries to explain a tuntap device creation failure.

View File

@@ -0,0 +1,13 @@
// 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 tstun
import "github.com/tailscale/wireguard-go/tun"
func interfaceName(dev tun.Device) (string, error) {
return dev.Name()
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/tailscale/wireguard-go/tun"
"github.com/tailscale/wireguard-go/tun/wintun"
"golang.org/x/sys/windows"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
)
func init() {
@@ -22,3 +23,11 @@ func init() {
}
tun.WintunStaticRequestedGUID = &guid
}
func interfaceName(dev tun.Device) (string, error) {
guid, err := winipcfg.LUID(dev.(*tun.NativeTun).LUID()).GUID()
if err != nil {
return "", err
}
return guid.String(), nil
}

View File

@@ -115,6 +115,9 @@ type Wrapper struct {
// disableFilter disables all filtering when set. This should only be used in tests.
disableFilter bool
// disableTSMPRejected disables TSMP rejected responses. For tests.
disableTSMPRejected bool
}
func Wrap(logf logger.Logf, tdev tun.Device) *Wrapper {
@@ -351,13 +354,26 @@ func (t *Wrapper) filterIn(buf []byte) filter.Response {
return filter.Drop
}
if filt.RunIn(p, t.filterFlags) != filter.Accept {
outcome := filt.RunIn(p, t.filterFlags)
// Let peerapi through the filter; its ACLs are handled at L7,
// not at the packet level.
if outcome != filter.Accept &&
p.IPProto == ipproto.TCP &&
p.TCPFlags&packet.TCPSyn != 0 &&
t.PeerAPIPort != nil {
if port, ok := t.PeerAPIPort(p.Dst.IP); ok && port == p.Dst.Port {
outcome = filter.Accept
}
}
if outcome != filter.Accept {
// Tell them, via TSMP, we're dropping them due to the ACL.
// Their host networking stack can translate this into ICMP
// or whatnot as required. But notably, their GUI or tailscale CLI
// can show them a rejection history with reasons.
if p.IPVersion == 4 && p.IPProto == ipproto.TCP && p.TCPFlags&packet.TCPSyn != 0 {
if p.IPVersion == 4 && p.IPProto == ipproto.TCP && p.TCPFlags&packet.TCPSyn != 0 && !t.disableTSMPRejected {
rj := packet.TailscaleRejectedHeader{
IPSrc: p.Dst.IP,
IPDst: p.Src.IP,

View File

@@ -6,6 +6,7 @@ package tstun
import (
"bytes"
"encoding/binary"
"fmt"
"strconv"
"strings"
@@ -42,6 +43,34 @@ func udp4(src, dst string, sport, dport uint16) []byte {
return packet.Generate(header, []byte("udp_payload"))
}
func tcp4syn(src, dst string, sport, dport uint16) []byte {
sip, err := netaddr.ParseIP(src)
if err != nil {
panic(err)
}
dip, err := netaddr.ParseIP(dst)
if err != nil {
panic(err)
}
ipHeader := packet.IP4Header{
IPProto: ipproto.TCP,
Src: sip,
Dst: dip,
IPID: 0,
}
tcpHeader := make([]byte, 20)
binary.BigEndian.PutUint16(tcpHeader[0:], sport)
binary.BigEndian.PutUint16(tcpHeader[2:], dport)
tcpHeader[13] |= 2 // SYN
both := packet.Generate(ipHeader, tcpHeader)
// 20 byte IP4 + 20 byte TCP
binary.BigEndian.PutUint16(both[2:4], 40)
return both
}
func nets(nets ...string) (ret []netaddr.IPPrefix) {
for _, s := range nets {
if i := strings.IndexByte(s, '/'); i == -1 {
@@ -385,3 +414,70 @@ func TestAtomic64Alignment(t *testing.T) {
c := new(Wrapper)
atomic.StoreInt64(&c.lastActivityAtomic, 123)
}
func TestPeerAPIBypass(t *testing.T) {
wrapperWithPeerAPI := &Wrapper{
PeerAPIPort: func(ip netaddr.IP) (port uint16, ok bool) {
if ip == netaddr.MustParseIP("100.64.1.2") {
return 60000, true
}
return
},
}
tests := []struct {
name string
w *Wrapper
filter *filter.Filter
pkt []byte
want filter.Response
}{
{
name: "reject_nil_filter",
w: &Wrapper{
PeerAPIPort: func(netaddr.IP) (port uint16, ok bool) {
return 60000, true
},
},
pkt: tcp4syn("1.2.3.4", "100.64.1.2", 1234, 60000),
want: filter.Drop,
},
{
name: "reject_with_filter",
w: &Wrapper{},
filter: filter.NewAllowNone(logger.Discard, new(netaddr.IPSet)),
pkt: tcp4syn("1.2.3.4", "100.64.1.2", 1234, 60000),
want: filter.Drop,
},
{
name: "peerapi_bypass_filter",
w: wrapperWithPeerAPI,
filter: filter.NewAllowNone(logger.Discard, new(netaddr.IPSet)),
pkt: tcp4syn("1.2.3.4", "100.64.1.2", 1234, 60000),
want: filter.Accept,
},
{
name: "peerapi_dont_bypass_filter_wrong_port",
w: wrapperWithPeerAPI,
filter: filter.NewAllowNone(logger.Discard, new(netaddr.IPSet)),
pkt: tcp4syn("1.2.3.4", "100.64.1.2", 1234, 60001),
want: filter.Drop,
},
{
name: "peerapi_dont_bypass_filter_wrong_dst_ip",
w: wrapperWithPeerAPI,
filter: filter.NewAllowNone(logger.Discard, new(netaddr.IPSet)),
pkt: tcp4syn("1.2.3.4", "100.64.1.3", 1234, 60000),
want: filter.Drop,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.w.SetFilter(tt.filter)
tt.w.disableTSMPRejected = true
if got := tt.w.filterIn(tt.pkt); got != tt.want {
t.Errorf("got = %v; want %v", got, tt.want)
}
})
}
}

View File

@@ -17,16 +17,6 @@ import (
// containing a directory we can read/write in.
var IOSSharedDir atomic.Value
// LegacyConfigPath returns the path used by the pre-tailscaled
// "relaynode" daemon's config file. It returns the empty string for
// platforms where relaynode never ran.
func LegacyConfigPath() string {
if runtime.GOOS == "windows" {
return ""
}
return "/var/lib/tailscale/relay.conf"
}
// DefaultTailscaledSocket returns the path to the tailscaled Unix socket
// or the empty string if there's no reasonable default.
func DefaultTailscaledSocket() string {

View File

@@ -42,7 +42,9 @@ func parsePort(s string) int {
}
func isLoopbackAddr(s string) bool {
return strings.HasPrefix(s, "127.0.0.1:") || strings.HasPrefix(s, "127.0.0.1.")
return strings.HasPrefix(s, "127.") ||
strings.HasPrefix(s, "[::1]:") ||
strings.HasPrefix(s, "::1.")
}
type nothing struct{}

View File

@@ -5,6 +5,7 @@
package portlist
import (
"encoding/json"
"testing"
)
@@ -40,7 +41,12 @@ udp6 0 0 :::5354 :::*
// macOS
tcp4 0 0 *.23 *.* LISTEN
tcp6 0 0 *.24 *.* LISTEN
tcp6 0 0 *.24 *.* LISTEN
tcp4 0 0 *.8185 *.* LISTEN
tcp4 0 0 127.0.0.1.8186 *.* LISTEN
tcp6 0 0 ::1.8187 *.* LISTEN
tcp4 0 0 127.1.2.3.8188 *.* LISTEN
udp6 0 0 *.5453 *.*
udp4 0 0 *.5553 *.*
@@ -73,14 +79,21 @@ func TestParsePortsNetstat(t *testing.T) {
Port{"udp", 5354, "", ""},
Port{"udp", 5453, "", ""},
Port{"udp", 5553, "", ""},
Port{"tcp", 8185, "", ""}, // but not 8186, 8187, 8188 on localhost
Port{"udp", 9353, "iTunes", ""},
}
pl := parsePortsNetstat(netstatOutput)
jgot, _ := json.MarshalIndent(pl, "", "\t")
jwant, _ := json.MarshalIndent(want, "", "\t")
if len(pl) != len(want) {
t.Fatalf("Got:\n%s\n\nWant:\n%s\n", jgot, jwant)
}
for i := range pl {
if pl[i] != want[i] {
t.Errorf("row#%d\n got: %#v\n\nwant: %#v\n",
i, pl[i], want[i])
t.Fatalf("Got:\n%s\n\nWant:\n%s\n", jgot, jwant)
}
}
}

View File

@@ -81,7 +81,7 @@ func (p *Poller) Run(ctx context.Context) error {
p.Err = err
return err
}
if pl.SameInodes(p.prev) {
if pl.sameInodes(p.prev) {
continue
}
p.prev = pl

View File

@@ -6,6 +6,7 @@ package portlist
import (
"fmt"
"sort"
"strings"
)
@@ -48,7 +49,7 @@ func (a *Port) lessThan(b *Port) bool {
return false
}
func (a List) SameInodes(b List) bool {
func (a List) sameInodes(b List) bool {
if a == nil || b == nil || len(a) != len(b) {
return false
}
@@ -76,7 +77,8 @@ func GetList(prev List) (List, error) {
if err != nil {
return nil, fmt.Errorf("listPorts: %s", err)
}
if pl.SameInodes(prev) {
pl = sortAndDedup(pl)
if pl.sameInodes(prev) {
// Nothing changed, skip inode lookup
return prev, nil
}
@@ -86,3 +88,22 @@ func GetList(prev List) (List, error) {
}
return pl, nil
}
// sortAndDedup sorts ps in place (by Port.lessThan) and then returns
// a subset of it with duplicate (Proto, Port) removed.
func sortAndDedup(ps List) List {
sort.Slice(ps, func(i, j int) bool {
return (&ps[i]).lessThan(&ps[j])
})
out := ps[:0]
var last Port
for _, p := range ps {
protoPort := Port{Proto: p.Proto, Port: p.Port}
if last == protoPort {
continue
}
out = append(out, p)
last = protoPort
}
return out
}

View File

@@ -12,7 +12,6 @@ import (
"os"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"syscall"
@@ -25,8 +24,6 @@ import (
// Reading the sockfiles on Linux is very fast, so we can do it often.
const pollInterval = 1 * time.Second
// TODO(apenwarr): Include IPv6 ports eventually.
// Right now we don't route IPv6 anyway so it's better to exclude them.
var sockfiles = []string{"/proc/net/tcp", "/proc/net/tcp6", "/proc/net/udp", "/proc/net/udp6"}
var sawProcNetPermissionErr syncs.AtomicBool
@@ -73,11 +70,6 @@ func listPorts() (List, error) {
l = append(l, ports...)
}
sort.Slice(l, func(i, j int) bool {
return (&l[i]).lessThan(&l[j])
})
return l, nil
}

View File

@@ -33,8 +33,9 @@ var lsofFailed int64 // atomic bool
// However, "netstat -na" runs ~100x faster than lsof on my machine, so
// we should do it only if the list of open ports has actually changed.
//
// TODO(apenwarr): this fails in a macOS sandbox (ie. our usual case).
// We might as well just delete this code if we can't find a solution.
// This fails in a macOS sandbox (i.e. in the Mac App Store or System
// Extension GUI build), but does at least work in the
// tailscaled-on-macos mode.
func addProcesses(pl []Port) ([]Port, error) {
if atomic.LoadInt64(&lsofFailed) != 0 {
// This previously failed in the macOS sandbox, so don't try again.

View File

@@ -185,7 +185,7 @@ func TestSameInodes(t *testing.T) {
},
}
for _, tt := range tests {
got := tt.a.SameInodes(tt.b)
got := tt.a.sameInodes(tt.b)
if got != tt.want {
t.Errorf("%s: Equal = %v; want %v", tt.name, got, tt.want)
}

View File

@@ -16,11 +16,21 @@ import (
const pollInterval = 5 * time.Second
func listPorts() (List, error) {
// TODO(bradfitz): stop shelling out to netstat and use the
// net/netstat package instead. When doing so, be sure to filter
// out all of 127.0.0.0/8 and not just 127.0.0.1.
return listPortsNetstat("-na")
}
func addProcesses(pl []Port) ([]Port, error) {
if t := windows.GetCurrentProcessToken(); !t.IsElevated() {
// OpenCurrentProcessToken instead of GetCurrentProcessToken,
// as GetCurrentProcessToken only works on Windows 8+.
tok, err := windows.OpenCurrentProcessToken()
if err != nil {
return nil, err
}
defer tok.Close()
if !tok.IsElevated() {
return listPortsNetstat("-na")
}
return listPortsNetstat("-nab")

View File

@@ -4,7 +4,7 @@
package tailcfg
//go:generate go run tailscale.com/cmd/cloner --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse --clonefunc=true --output=tailcfg_clone.go
//go:generate go run tailscale.com/cmd/cloner --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse --clonefunc=true --output=tailcfg_clone.go
import (
"bytes"
@@ -36,7 +36,10 @@ import (
// 11: 2021-03-03: client understands IPv6, multiple default routes, and goroutine dumping
// 12: 2021-03-04: client understands PingRequest
// 13: 2021-03-19: client understands FilterRule.IPProto
const CurrentMapRequestVersion = 13
// 14: 2021-04-07: client understands DNSConfig.Routes and DNSConfig.Resolvers
// 15: 2021-04-12: client treats nil MapResponse.DNSConfig as meaning unchanged
// 16: 2021-04-15: client understands Node.Online, MapResponse.OnlineChange
const CurrentMapRequestVersion = 16
type StableID string
@@ -154,7 +157,17 @@ type Node struct {
DERP string `json:",omitempty"` // DERP-in-IP:port ("127.3.3.40:N") endpoint
Hostinfo Hostinfo
Created time.Time
LastSeen *time.Time `json:",omitempty"`
// LastSeen is when the node was last online. It is not
// updated when Online is true. It is nil if the current
// node doesn't have permission to know, or the node
// has never been online.
LastSeen *time.Time `json:",omitempty"`
// Online is whether the node is currently connected to the
// coordination server. A value of nil means unknown, or the
// current node doesn't have permission to know.
Online *bool `json:",omitempty"`
KeepAlive bool `json:",omitempty"` // open and keep open a connection to this peer
@@ -164,7 +177,7 @@ type Node struct {
// They're free-form strings, but should be in the form of URLs/URIs
// such as:
// "https://tailscale.com/cap/is-admin"
// "https://tailscale.com/cap/recv-file"
// "https://tailscale.com/cap/file-sharing"
Capabilities []string `json:",omitempty"`
// The following three computed fields hold the various names that can
@@ -590,8 +603,12 @@ type RegisterRequest struct {
Oauth2Token *Oauth2Token
AuthKey string
}
Expiry time.Time // requested key expiry, server policy may override
Followup string // response waits until AuthURL is visited
// Expiry optionally specifies the requested key expiry.
// The server policy may override.
// As a special case, if Expiry is in the past and NodeKey is
// the node's current key, the key is expired.
Expiry time.Time
Followup string // response waits until AuthURL is visited
Hostinfo *Hostinfo
// The following fields are not used for SignatureNone and are required for
@@ -630,6 +647,42 @@ type RegisterResponse struct {
AuthURL string // if set, authorization pending
}
// EndpointType distinguishes different sources of MapRequest.Endpoint values.
type EndpointType int
const (
EndpointUnknownType = EndpointType(0)
EndpointLocal = EndpointType(1)
EndpointSTUN = EndpointType(2)
EndpointPortmapped = EndpointType(3)
EndpointSTUN4LocalPort = EndpointType(4) // hard NAT: STUN'ed IPv4 address + local fixed port
)
func (et EndpointType) String() string {
switch et {
case EndpointUnknownType:
return "?"
case EndpointLocal:
return "local"
case EndpointSTUN:
return "stun"
case EndpointPortmapped:
return "portmap"
case EndpointSTUN4LocalPort:
return "stun4localport"
}
return "other"
}
// Endpoint is an endpoint IPPort and an associated type.
// It doesn't currently go over the wire as is but is instead
// broken up into two parallel slices in MapReqeust, for compatibility
// reasons. But this type is used in the codebase.
type Endpoint struct {
Addr netaddr.IPPort
Type EndpointType
}
// MapRequest is sent by a client to start a long-poll network map updates.
// The request includes a copy of the client's current set of WireGuard
// endpoints and general host information.
@@ -649,11 +702,15 @@ type MapRequest struct {
KeepAlive bool // whether server should send keep-alives back to us
NodeKey NodeKey
DiscoKey DiscoKey
Endpoints []string // caller's endpoints (IPv4 or IPv6)
IncludeIPv6 bool `json:",omitempty"` // include IPv6 endpoints in returned Node Endpoints (for Version 4 clients)
Stream bool // if true, multiple MapResponse objects are returned
IncludeIPv6 bool `json:",omitempty"` // include IPv6 endpoints in returned Node Endpoints (for Version 4 clients)
Stream bool // if true, multiple MapResponse objects are returned
Hostinfo *Hostinfo
// Endpoints are the client's magicsock UDP ip:port endpoints (IPv4 or IPv6).
Endpoints []string
// EndpointTypes are the types of the corresponding endpoints in Endpoints.
EndpointTypes []EndpointType `json:",omitempty"`
// ReadOnly is whether the client just wants to fetch the
// MapResponse, without updating their Endpoints. The
// Endpoints field will be ignored and LastSeen will not be
@@ -763,19 +820,52 @@ var FilterAllowAll = []FilterRule{
},
}
// DNSResolver is the configuration for one DNS resolver.
type DNSResolver struct {
// Addr is the address of the DNS resolver, one of:
// - A plain IP address for a "classic" UDP+TCP DNS resolver
// - [TODO] "tls://resolver.com" for DNS over TCP+TLS
// - [TODO] "https://resolver.com/query-tmpl" for DNS over HTTPS
Addr string `json:",omitempty"`
// BootstrapResolution is an optional suggested resolution for the
// DoT/DoH resolver, if the resolver URL does not reference an IP
// address directly.
// BootstrapResolution may be empty, in which case clients should
// look up the DoT/DoH server using their local "classic" DNS
// resolver.
BootstrapResolution []netaddr.IP `json:",omitempty"`
}
// DNSConfig is the DNS configuration.
type DNSConfig struct {
// Resolvers are the DNS resolvers to use, in order of preference.
Resolvers []DNSResolver `json:",omitempty"`
// Routes maps DNS name suffixes to a set of DNS resolvers to
// use. It is used to implement "split DNS" and other advanced DNS
// routing overlays.
// Map keys must be fully-qualified DNS name suffixes, with a
// trailing dot but no leading dot.
Routes map[string][]DNSResolver `json:",omitempty"`
// Domains are the search domains to use.
// Search domains must be FQDNs, but *without* the trailing dot.
Domains []string `json:",omitempty"`
// Proxied turns on automatic resolution of hostnames for devices
// in the network map, aka MagicDNS.
// Despite the (legacy) name, does not necessarily cause request
// proxying to be enabled.
Proxied bool `json:",omitempty"`
// The following fields are only set and used by
// MapRequest.Version >=9 and <14.
// Nameservers are the IP addresses of the nameservers to use.
Nameservers []netaddr.IP `json:",omitempty"`
// Domains are the search domains to use.
Domains []string `json:",omitempty"`
// PerDomain is not set by the control server, and does nothing.
// TODO(danderson): revise DNS configuration to make this useful
// again.
PerDomain bool
// Proxied indicates whether DNS requests are proxied through a dns.Resolver.
// This enables MagicDNS.
Proxied bool
PerDomain bool `json:",omitempty"`
}
// PingRequest is a request to send an HTTP request to prove the
@@ -828,24 +918,20 @@ type MapResponse struct {
// the LastSeen time is now. Absent means unchanged.
PeerSeenChange map[NodeID]bool `json:",omitempty"`
// OnlineChange changes the value of a Peer Node.Online value.
OnlineChange map[NodeID]bool `json:",omitempty"`
// DNS is the same as DNSConfig.Nameservers.
//
// TODO(dmytro): should be sent in DNSConfig.Nameservers once clients have updated.
// Only populated if MapRequest.Version < 9.
DNS []netaddr.IP `json:",omitempty"`
// SearchPaths is the old way to specify DNS search
// domains. Clients should use these values if set, but the
// server will omit this field for clients with
// MapRequest.Version >= 9. Clients should prefer to use
// DNSConfig.Domains instead.
// SearchPaths is the old way to specify DNS search domains.
// Only populated if MapRequest.Version < 9.
SearchPaths []string `json:",omitempty"`
// DNSConfig contains the DNS settings for the client to use.
//
// TODO(bradfitz): make this a pointer and conditionally sent
// only if changed, like DERPMap, PacketFilter, etc. It's
// small, though.
DNSConfig DNSConfig `json:",omitempty"`
// A nil value means no change from an earlier non-nil value.
DNSConfig *DNSConfig `json:",omitempty"`
// Domain is the name of the network that this node is
// in. It's either of the form "example.com" (for user
@@ -976,6 +1062,7 @@ func (n *Node) Equal(n2 *Node) bool {
n.KeyExpiry.Equal(n2.KeyExpiry) &&
n.Machine == n2.Machine &&
n.DiscoKey == n2.DiscoKey &&
eqBoolPtr(n.Online, n2.Online) &&
eqCIDRs(n.Addresses, n2.Addresses) &&
eqCIDRs(n.AllowedIPs, n2.AllowedIPs) &&
eqStrings(n.Endpoints, n2.Endpoints) &&
@@ -990,6 +1077,17 @@ func (n *Node) Equal(n2 *Node) bool {
n.ComputedNameWithHost == n2.ComputedNameWithHost
}
func eqBoolPtr(a, b *bool) bool {
if a == b { // covers nil
return true
}
if a == nil || b == nil {
return false
}
return *a == *b
}
func eqStrings(a, b []string) bool {
if len(a) != len(b) || ((a == nil) != (b == nil)) {
return false
@@ -1018,12 +1116,6 @@ func eqTimePtr(a, b *time.Time) bool {
return ((a == nil) == (b == nil)) && (a == nil || a.Equal(*b))
}
// WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler.
type WhoIsResponse struct {
Node *Node
UserProfile *UserProfile
}
// Oauth2Token is a copy of golang.org/x/oauth2.Token, to avoid the
// go.mod dependency on App Engine and grpc, which was causing problems.
// All we actually needed was this struct on the client side.
@@ -1048,3 +1140,8 @@ type Oauth2Token struct {
// mechanisms for that TokenSource will not be used.
Expiry time.Time `json:"expiry,omitempty"`
}
const (
CapabilityFileSharing = "https://tailscale.com/cap/file-sharing"
CapabilityAdmin = "https://tailscale.com/cap/is-admin"
)

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.
// Code generated by tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse; DO NOT EDIT.
// Code generated by tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse; DO NOT EDIT.
package tailcfg
@@ -26,7 +26,7 @@ func (src *User) Clone() *User {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse
var _UserNeedsRegeneration = User(struct {
ID UserID
LoginName string
@@ -53,12 +53,16 @@ func (src *Node) Clone() *Node {
dst.LastSeen = new(time.Time)
*dst.LastSeen = *src.LastSeen
}
if dst.Online != nil {
dst.Online = new(bool)
*dst.Online = *src.Online
}
dst.Capabilities = append(src.Capabilities[:0:0], src.Capabilities...)
return dst
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse
var _NodeNeedsRegeneration = Node(struct {
ID NodeID
StableID StableNodeID
@@ -76,6 +80,7 @@ var _NodeNeedsRegeneration = Node(struct {
Hostinfo Hostinfo
Created time.Time
LastSeen *time.Time
Online *bool
KeepAlive bool
MachineAuthorized bool
Capabilities []string
@@ -100,7 +105,7 @@ func (src *Hostinfo) Clone() *Hostinfo {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse
var _HostinfoNeedsRegeneration = Hostinfo(struct {
IPNVersion string
FrontendLogID string
@@ -137,7 +142,7 @@ func (src *NetInfo) Clone() *NetInfo {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse
var _NetInfoNeedsRegeneration = NetInfo(struct {
MappingVariesByDestIP opt.Bool
HairPinning opt.Bool
@@ -164,7 +169,7 @@ func (src *Login) Clone() *Login {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse
var _LoginNeedsRegeneration = Login(struct {
_ structs.Incomparable
ID LoginID
@@ -183,18 +188,49 @@ func (src *DNSConfig) Clone() *DNSConfig {
}
dst := new(DNSConfig)
*dst = *src
dst.Nameservers = append(src.Nameservers[:0:0], src.Nameservers...)
dst.Resolvers = make([]DNSResolver, len(src.Resolvers))
for i := range dst.Resolvers {
dst.Resolvers[i] = *src.Resolvers[i].Clone()
}
if dst.Routes != nil {
dst.Routes = map[string][]DNSResolver{}
for k := range src.Routes {
dst.Routes[k] = append([]DNSResolver{}, src.Routes[k]...)
}
}
dst.Domains = append(src.Domains[:0:0], src.Domains...)
dst.Nameservers = append(src.Nameservers[:0:0], src.Nameservers...)
return dst
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse
var _DNSConfigNeedsRegeneration = DNSConfig(struct {
Nameservers []netaddr.IP
Resolvers []DNSResolver
Routes map[string][]DNSResolver
Domains []string
PerDomain bool
Proxied bool
Nameservers []netaddr.IP
PerDomain bool
}{})
// Clone makes a deep copy of DNSResolver.
// The result aliases no memory with the original.
func (src *DNSResolver) Clone() *DNSResolver {
if src == nil {
return nil
}
dst := new(DNSResolver)
*dst = *src
dst.BootstrapResolution = append(src.BootstrapResolution[:0:0], src.BootstrapResolution...)
return dst
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse
var _DNSResolverNeedsRegeneration = DNSResolver(struct {
Addr string
BootstrapResolution []netaddr.IP
}{})
// Clone makes a deep copy of RegisterResponse.
@@ -210,7 +246,7 @@ func (src *RegisterResponse) Clone() *RegisterResponse {
}
// A compilation failure here means this code must be regenerated, with command:
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse
// tailscale.com/cmd/cloner -type User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse
var _RegisterResponseNeedsRegeneration = RegisterResponse(struct {
User User
Login Login
@@ -221,7 +257,7 @@ var _RegisterResponseNeedsRegeneration = RegisterResponse(struct {
// Clone duplicates src into dst and reports whether it succeeded.
// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,
// where T is one of User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse.
// where T is one of User,Node,Hostinfo,NetInfo,Login,DNSConfig,DNSResolver,RegisterResponse.
func Clone(dst, src interface{}) bool {
switch src := src.(type) {
case *User:
@@ -278,6 +314,15 @@ func Clone(dst, src interface{}) bool {
*dst = src.Clone()
return true
}
case *DNSResolver:
switch dst := dst.(type) {
case *DNSResolver:
*dst = *src.Clone()
return true
case **DNSResolver:
*dst = src.Clone()
return true
}
case *RegisterResponse:
switch dst := dst.(type) {
case *RegisterResponse:

View File

@@ -193,7 +193,7 @@ func TestNodeEqual(t *testing.T) {
"ID", "StableID", "Name", "User", "Sharer",
"Key", "KeyExpiry", "Machine", "DiscoKey",
"Addresses", "AllowedIPs", "Endpoints", "DERP", "Hostinfo",
"Created", "LastSeen", "KeepAlive", "MachineAuthorized",
"Created", "LastSeen", "Online", "KeepAlive", "MachineAuthorized",
"Capabilities",
"ComputedName", "computedHostIfDifferent", "ComputedNameWithHost",
}
@@ -500,3 +500,21 @@ func TestUserProfileJSONMarshalForMac(t *testing.T) {
t.Fatalf("Unmarshal: %v", err)
}
}
func TestEndpointTypeMarshal(t *testing.T) {
eps := []EndpointType{
EndpointUnknownType,
EndpointLocal,
EndpointSTUN,
EndpointPortmapped,
EndpointSTUN4LocalPort,
}
got, err := json.Marshal(eps)
if err != nil {
t.Fatal(err)
}
const want = `[0,1,2,3,4]`
if string(got) != want {
t.Errorf("got %s; want %s", got, want)
}
}

View File

@@ -18,6 +18,10 @@ import (
"tailscale.com/wgengine/filter"
)
// NetworkMap is the current state of the world.
//
// The fields should all be considered read-only. They might
// alias parts of previous NetworkMap values.
type NetworkMap struct {
// Core networking

View File

@@ -5,45 +5,134 @@
// Package dnsname contains string functions for working with DNS names.
package dnsname
import "strings"
import (
"fmt"
"strings"
)
var separators = map[byte]bool{
' ': true,
'.': true,
'@': true,
'_': true,
}
const (
// maxLabelLength is the maximum length of a label permitted by RFC 1035.
maxLabelLength = 63
// maxNameLength is the maximum length of a DNS name.
maxNameLength = 253
)
func islower(c byte) bool {
return 'a' <= c && c <= 'z'
}
// A FQDN is a fully-qualified DNS name or name suffix.
type FQDN string
func isupper(c byte) bool {
return 'A' <= c && c <= 'Z'
}
func isalpha(c byte) bool {
return islower(c) || isupper(c)
}
func isalphanum(c byte) bool {
return isalpha(c) || ('0' <= c && c <= '9')
}
func isdnschar(c byte) bool {
return isalphanum(c) || c == '-'
}
func tolower(c byte) byte {
if isupper(c) {
return c + 'a' - 'A'
} else {
return c
func ToFQDN(s string) (FQDN, error) {
if isValidFQDN(s) {
return FQDN(s), nil
}
if len(s) == 0 {
return FQDN("."), nil
}
if s[len(s)-1] == '.' {
s = s[:len(s)-1]
}
if len(s) > maxNameLength {
return "", fmt.Errorf("%q is too long to be a DNS name", s)
}
fs := strings.Split(s, ".")
for _, f := range fs {
if !validLabel(f) {
return "", fmt.Errorf("%q is not a valid DNS label", f)
}
}
return FQDN(s + "."), nil
}
// maxLabelLength is the maximal length of a label permitted by RFC 1035.
const maxLabelLength = 63
func validLabel(s string) bool {
if len(s) == 0 || len(s) > maxLabelLength {
return false
}
if !isalphanum(s[0]) || !isalphanum(s[len(s)-1]) {
return false
}
for i := 1; i < len(s)-1; i++ {
if !isalphanum(s[i]) && s[i] != '-' {
return false
}
}
return true
}
// WithTrailingDot returns f as a string, with a trailing dot.
func (f FQDN) WithTrailingDot() string {
return string(f)
}
// WithoutTrailingDot returns f as a string, with the trailing dot
// removed.
func (f FQDN) WithoutTrailingDot() string {
return string(f[:len(f)-1])
}
func (f FQDN) NumLabels() int {
if f == "." {
return 0
}
return strings.Count(f.WithTrailingDot(), ".")
}
func (f FQDN) Contains(other FQDN) bool {
if f == other {
return true
}
cmp := f.WithTrailingDot()
if cmp != "." {
cmp = "." + cmp
}
return strings.HasSuffix(other.WithTrailingDot(), cmp)
}
// isValidFQDN reports whether s is already a valid FQDN, without
// allocating.
func isValidFQDN(s string) bool {
if len(s) == 0 {
return false
}
if len(s) > maxNameLength {
return false
}
// DNS root name.
if s == "." {
return true
}
// Missing trailing dot.
if s[len(s)-1] != '.' {
return false
}
// Leading dots not allowed.
if s[0] == '.' {
return false
}
st := 0
for i := 0; i < len(s); i++ {
if s[i] != '.' {
continue
}
label := s[st:i]
if len(label) == 0 || len(label) > maxLabelLength {
return false
}
if !isalphanum(label[0]) || !isalphanum(label[len(label)-1]) {
return false
}
for j := 1; j < len(label)-1; j++ {
if !isalphanum(label[j]) && label[j] != '-' {
return false
}
}
st = i + 1
}
return true
}
// SanitizeLabel takes a string intended to be a DNS name label
// and turns it into a valid name label according to RFC 1035.
@@ -133,3 +222,46 @@ func NumLabels(hostname string) int {
}
return strings.Count(hostname, ".")
}
// FirstLabel returns the first DNS label of hostname.
func FirstLabel(hostname string) string {
if i := strings.IndexByte(hostname, '.'); i != -1 {
return hostname[:i]
}
return hostname
}
var separators = map[byte]bool{
' ': true,
'.': true,
'@': true,
'_': true,
}
func islower(c byte) bool {
return 'a' <= c && c <= 'z'
}
func isupper(c byte) bool {
return 'A' <= c && c <= 'Z'
}
func isalpha(c byte) bool {
return islower(c) || isupper(c)
}
func isalphanum(c byte) bool {
return isalpha(c) || ('0' <= c && c <= '9')
}
func isdnschar(c byte) bool {
return isalphanum(c) || c == '-'
}
func tolower(c byte) byte {
if isupper(c) {
return c + 'a' - 'A'
} else {
return c
}
}

View File

@@ -9,6 +9,87 @@ import (
"testing"
)
func TestFQDN(t *testing.T) {
tests := []struct {
in string
want FQDN
wantErr bool
wantLabels int
}{
{"", ".", false, 0},
{".", ".", false, 0},
{"foo.com", "foo.com.", false, 2},
{"foo.com.", "foo.com.", false, 2},
{"com", "com.", false, 1},
{"www.tailscale.com", "www.tailscale.com.", false, 3},
{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.com", "", true, 0},
{strings.Repeat("aaaaa.", 60) + "com", "", true, 0},
{".com", "", true, 0},
{"foo..com", "", true, 0},
}
for _, test := range tests {
t.Run(test.in, func(t *testing.T) {
got, err := ToFQDN(test.in)
if got != test.want {
t.Errorf("ToFQDN(%q) got %q, want %q", test.in, got, test.want)
}
if (err != nil) != test.wantErr {
t.Errorf("ToFQDN(%q) err %v, wantErr=%v", test.in, err, test.wantErr)
}
if err != nil {
return
}
gotDot := got.WithTrailingDot()
if gotDot != string(test.want) {
t.Errorf("ToFQDN(%q).WithTrailingDot() got %q, want %q", test.in, gotDot, test.want)
}
gotNoDot := got.WithoutTrailingDot()
wantNoDot := string(test.want)[:len(test.want)-1]
if gotNoDot != wantNoDot {
t.Errorf("ToFQDN(%q).WithoutTrailingDot() got %q, want %q", test.in, gotNoDot, wantNoDot)
}
if gotLabels := got.NumLabels(); gotLabels != test.wantLabels {
t.Errorf("ToFQDN(%q).NumLabels() got %v, want %v", test.in, gotLabels, test.wantLabels)
}
})
}
}
func TestFQDNContains(t *testing.T) {
tests := []struct {
a, b string
want bool
}{
{"", "", true},
{"", "foo.com", true},
{"foo.com", "", false},
{"tailscale.com", "www.tailscale.com", true},
{"www.tailscale.com", "tailscale.com", false},
{"scale.com", "tailscale.com", false},
{"foo.com", "foo.com", true},
}
for _, test := range tests {
t.Run(test.a+"_"+test.b, func(t *testing.T) {
a, err := ToFQDN(test.a)
if err != nil {
t.Fatalf("ToFQDN(%q): %v", test.a, err)
}
b, err := ToFQDN(test.b)
if err != nil {
t.Fatalf("ToFQDN(%q): %v", test.b, err)
}
if got := a.Contains(b); got != test.want {
t.Errorf("ToFQDN(%q).Contains(%q) got %v, want %v", a, b, got, test.want)
}
})
}
}
func TestSanitizeLabel(t *testing.T) {
tests := []struct {
name string

View File

@@ -10,7 +10,7 @@ package version
// Long is a full version number for this build, of the form
// "x.y.z-commithash", or "date.yyyymmdd" if no actual version was
// provided.
const Long = "date.20210316"
const Long = "date.20210415"
// Short is a short version number for this build, of the form
// "x.y.z", or "date.yyyymmdd" if no actual version was provided.

View File

@@ -116,7 +116,7 @@ type Conn struct {
logf logger.Logf
port uint16 // the preferred port from opts.Port; 0 means auto
epFunc func(endpoints []string)
epFunc func([]tailcfg.Endpoint)
derpActiveFunc func()
idleFunc func() time.Duration // nil means unknown
packetListener nettype.PacketListener
@@ -201,7 +201,7 @@ type Conn struct {
// lastEndpoints records the endpoints found during the previous
// endpoint discovery. It's used to avoid duplicate endpoint
// change notifications.
lastEndpoints []string
lastEndpoints []tailcfg.Endpoint
// lastEndpointsTime is the last time the endpoints were updated,
// even if there was no change.
@@ -381,7 +381,7 @@ type Options struct {
// EndpointsFunc optionally provides a func to be called when
// endpoints change. The called func does not own the slice.
EndpointsFunc func(endpoint []string)
EndpointsFunc func([]tailcfg.Endpoint)
// DERPActiveFunc optionally provides a func to be called when
// a connection is made to a DERP server.
@@ -432,9 +432,9 @@ func (o *Options) logf() logger.Logf {
return o.Logf
}
func (o *Options) endpointsFunc() func([]string) {
func (o *Options) endpointsFunc() func([]tailcfg.Endpoint) {
if o == nil || o.EndpointsFunc == nil {
return func([]string) {}
return func([]tailcfg.Endpoint) {}
}
return o.EndpointsFunc
}
@@ -578,7 +578,7 @@ func (c *Conn) updateEndpoints(why string) {
}()
c.logf("[v1] magicsock: starting endpoint update (%s)", why)
endpoints, reasons, err := c.determineEndpoints(c.connCtx)
endpoints, err := c.determineEndpoints(c.connCtx)
if err != nil {
c.logf("magicsock: endpoint update (%s) failed: %v", why, err)
// TODO(crawshaw): are there any conditions under which
@@ -586,18 +586,18 @@ func (c *Conn) updateEndpoints(why string) {
return
}
if c.setEndpoints(endpoints, reasons) {
c.logEndpointChange(endpoints, reasons)
if c.setEndpoints(endpoints) {
c.logEndpointChange(endpoints)
c.epFunc(endpoints)
}
}
// setEndpoints records the new endpoints, reporting whether they're changed.
// It takes ownership of the slice.
func (c *Conn) setEndpoints(endpoints []string, reasons map[string]string) (changed bool) {
func (c *Conn) setEndpoints(endpoints []tailcfg.Endpoint) (changed bool) {
anySTUN := false
for _, reason := range reasons {
if reason == "stun" {
for _, ep := range endpoints {
if ep.Type == tailcfg.EndpointSTUN {
anySTUN = true
}
}
@@ -625,7 +625,7 @@ func (c *Conn) setEndpoints(endpoints []string, reasons map[string]string) (chan
delete(c.onEndpointRefreshed, de)
}
if stringSetsEqual(endpoints, c.lastEndpoints) {
if endpointSetsEqual(endpoints, c.lastEndpoints) {
return false
}
c.lastEndpoints = endpoints
@@ -975,35 +975,39 @@ func (c *Conn) goDerpConnect(node int) {
// does a STUN lookup (via netcheck) to determine its public address.
//
// c.mu must NOT be held.
func (c *Conn) determineEndpoints(ctx context.Context) (ipPorts []string, reasons map[string]string, err error) {
func (c *Conn) determineEndpoints(ctx context.Context) ([]tailcfg.Endpoint, error) {
nr, err := c.updateNetInfo(ctx)
if err != nil {
c.logf("magicsock.Conn.determineEndpoints: updateNetInfo: %v", err)
return nil, nil, err
return nil, err
}
already := make(map[string]string) // endpoint -> how it was found
var eps []string // unique endpoints
already := make(map[netaddr.IPPort]tailcfg.EndpointType) // endpoint -> how it was found
var eps []tailcfg.Endpoint // unique endpoints
addAddr := func(s, reason string) {
if debugOmitLocalAddresses && (reason == "localAddresses" || reason == "socket") {
ipp := func(s string) (ipp netaddr.IPPort) {
ipp, _ = netaddr.ParseIPPort(s)
return
}
addAddr := func(ipp netaddr.IPPort, et tailcfg.EndpointType) {
if ipp.IsZero() || (debugOmitLocalAddresses && et == tailcfg.EndpointLocal) {
return
}
if _, ok := already[s]; !ok {
already[s] = reason
eps = append(eps, s)
if _, ok := already[ipp]; !ok {
already[ipp] = et
eps = append(eps, tailcfg.Endpoint{Addr: ipp, Type: et})
}
}
if ext, err := c.portMapper.CreateOrGetMapping(ctx); err == nil {
addAddr(ext.String(), "portmap")
addAddr(ext, tailcfg.EndpointPortmapped)
c.setNetInfoHavePortMap()
} else if !portmapper.IsNoMappingError(err) {
c.logf("portmapper: %v", err)
}
if nr.GlobalV4 != "" {
addAddr(nr.GlobalV4, "stun")
addAddr(ipp(nr.GlobalV4), tailcfg.EndpointSTUN)
// If they're behind a hard NAT and are using a fixed
// port locally, assume they might've added a static
@@ -1012,12 +1016,12 @@ func (c *Conn) determineEndpoints(ctx context.Context) (ipPorts []string, reason
// it's an invalid candidate mapping.
if nr.MappingVariesByDestIP.EqualBool(true) && c.port != 0 {
if ip, _, err := net.SplitHostPort(nr.GlobalV4); err == nil {
addAddr(net.JoinHostPort(ip, strconv.Itoa(int(c.port))), "port_in")
addAddr(ipp(net.JoinHostPort(ip, strconv.Itoa(int(c.port)))), tailcfg.EndpointSTUN4LocalPort)
}
}
}
if nr.GlobalV6 != "" {
addAddr(nr.GlobalV6, "stun")
addAddr(ipp(nr.GlobalV6), tailcfg.EndpointSTUN)
}
c.ignoreSTUNPackets()
@@ -1025,9 +1029,8 @@ func (c *Conn) determineEndpoints(ctx context.Context) (ipPorts []string, reason
if localAddr := c.pconn4.LocalAddr(); localAddr.IP.IsUnspecified() {
ips, loopback, err := interfaces.LocalAddresses()
if err != nil {
return nil, nil, err
return nil, err
}
reason := "localAddresses"
if len(ips) == 0 && len(eps) == 0 {
// Only include loopback addresses if we have no
// interfaces at all to use as endpoints and don't
@@ -1035,15 +1038,14 @@ func (c *Conn) determineEndpoints(ctx context.Context) (ipPorts []string, reason
// for localhost testing when you're on a plane and
// offline, for example.
ips = loopback
reason = "loopback"
}
for _, ip := range ips {
addAddr(netaddr.IPPort{IP: ip, Port: uint16(localAddr.Port)}.String(), reason)
addAddr(netaddr.IPPort{IP: ip, Port: uint16(localAddr.Port)}, tailcfg.EndpointLocal)
}
} else {
// Our local endpoint is bound to a particular address.
// Do not offer addresses on other local interfaces.
addAddr(localAddr.String(), "socket")
addAddr(ipp(localAddr.String()), tailcfg.EndpointLocal)
}
// Note: the endpoints are intentionally returned in priority order,
@@ -1056,14 +1058,17 @@ func (c *Conn) determineEndpoints(ctx context.Context) (ipPorts []string, reason
// The STUN address(es) are always first so that legacy wireguard
// can use eps[0] as its only known endpoint address (although that's
// obviously non-ideal).
return eps, already, nil
//
// Despite this sorting, though, clients since 0.100 haven't relied
// on the sorting order for any decisions.
return eps, nil
}
// stringSetsEqual reports whether x and y represent the same set of
// strings. The order doesn't matter.
// endpointSetsEqual reports whether x and y represent the same set of
// endpoints. The order doesn't matter.
//
// It does not mutate the slices.
func stringSetsEqual(x, y []string) bool {
func endpointSetsEqual(x, y []tailcfg.Endpoint) bool {
if len(x) == len(y) {
orderMatches := true
for i := range x {
@@ -1076,7 +1081,7 @@ func stringSetsEqual(x, y []string) bool {
return true
}
}
m := map[string]int{}
m := map[tailcfg.Endpoint]int{}
for _, v := range x {
m[v] |= 1
}
@@ -1988,9 +1993,7 @@ func (c *Conn) enqueueCallMeMaybe(derpAddr netaddr.IPPort, de *discoEndpoint) {
eps := make([]netaddr.IPPort, 0, len(c.lastEndpoints))
for _, ep := range c.lastEndpoints {
if ipp, err := netaddr.ParseIPPort(ep); err == nil {
eps = append(eps, ipp)
}
eps = append(eps, ep.Addr)
}
go de.sendDiscoMessage(derpAddr, &disco.CallMeMaybe{MyNumber: eps}, discoLog)
}
@@ -2299,13 +2302,13 @@ func (c *Conn) logActiveDerpLocked() {
}))
}
func (c *Conn) logEndpointChange(endpoints []string, reasons map[string]string) {
func (c *Conn) logEndpointChange(endpoints []tailcfg.Endpoint) {
c.logf("magicsock: endpoints changed: %s", logger.ArgWriter(func(buf *bufio.Writer) {
for i, ep := range endpoints {
if i > 0 {
buf.WriteString(", ")
}
fmt.Fprintf(buf, "%s (%s)", ep, reasons[ep])
fmt.Fprintf(buf, "%s (%s)", ep.Addr, ep.Type)
}
}))
}
@@ -2950,30 +2953,42 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) {
c.mu.Lock()
defer c.mu.Unlock()
var tailAddr string
var tailAddr4 string
var tailscaleIPs []netaddr.IP
if c.netMap != nil {
tailscaleIPs = make([]netaddr.IP, 0, len(c.netMap.Addresses))
for _, addr := range c.netMap.Addresses {
if !addr.IsSingleIP() {
continue
}
sb.AddTailscaleIP(addr.IP)
// TailAddr only allows for a single Tailscale IP. For
// readability of `tailscale status`, make it the IPv4
// address.
// TailAddr previously only allowed for a
// single Tailscale IP. For compatibility for
// a couple releases starting with 1.8, keep
// that field pulled out separately.
if addr.IP.Is4() {
tailAddr = addr.IP.String()
tailAddr4 = addr.IP.String()
}
tailscaleIPs = append(tailscaleIPs, addr.IP)
}
}
sb.MutateSelfStatus(func(ss *ipnstate.PeerStatus) {
ss.PublicKey = c.privateKey.Public()
ss.Addrs = c.lastEndpoints
ss.Addrs = make([]string, 0, len(c.lastEndpoints))
for _, ep := range c.lastEndpoints {
ss.Addrs = append(ss.Addrs, ep.Addr.String())
}
ss.OS = version.OS()
if c.netMap != nil {
ss.HostName = c.netMap.Hostinfo.Hostname
ss.DNSName = c.netMap.Name
ss.UserID = c.netMap.User
if c.netMap.SelfNode != nil {
if c := c.netMap.SelfNode.Capabilities; len(c) > 0 {
ss.Capabilities = append([]string(nil), c...)
}
}
} else {
ss.HostName, _ = os.Hostname()
}
@@ -2983,7 +2998,8 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) {
ss.Relay = derpRegion.RegionCode
}
}
ss.TailAddr = tailAddr
ss.TailscaleIPs = tailscaleIPs
ss.TailAddrDeprecated = tailAddr4
})
for dk, n := range c.nodeOfDisco {

View File

@@ -126,12 +126,12 @@ func runDERPAndStun(t *testing.T, logf logger.Logf, l nettype.PacketListener, st
// happiness.
type magicStack struct {
privateKey wgkey.Private
epCh chan []string // endpoint updates produced by this peer
conn *Conn // the magicsock itself
tun *tuntest.ChannelTUN // TUN device to send/receive packets
tsTun *tstun.Wrapper // wrapped tun that implements filtering and wgengine hooks
dev *device.Device // the wireguard-go Device that connects the previous things
wgLogger *wglog.Logger // wireguard-go log wrapper
epCh chan []tailcfg.Endpoint // endpoint updates produced by this peer
conn *Conn // the magicsock itself
tun *tuntest.ChannelTUN // TUN device to send/receive packets
tsTun *tstun.Wrapper // wrapped tun that implements filtering and wgengine hooks
dev *device.Device // the wireguard-go Device that connects the previous things
wgLogger *wglog.Logger // wireguard-go log wrapper
}
// newMagicStack builds and initializes an idle magicsock and
@@ -145,11 +145,11 @@ func newMagicStack(t testing.TB, logf logger.Logf, l nettype.PacketListener, der
t.Fatalf("generating private key: %v", err)
}
epCh := make(chan []string, 100) // arbitrary
epCh := make(chan []tailcfg.Endpoint, 100) // arbitrary
conn, err := NewConn(Options{
Logf: logf,
PacketListener: l,
EndpointsFunc: func(eps []string) {
EndpointsFunc: func(eps []tailcfg.Endpoint) {
epCh <- eps
},
SimulatedNetwork: l != nettype.Std{},
@@ -245,7 +245,7 @@ func meshStacks(logf logger.Logf, ms []*magicStack) (cleanup func()) {
// simpler.
var (
mu sync.Mutex
eps = make([][]string, len(ms))
eps = make([][]tailcfg.Endpoint, len(ms))
)
buildNetmapLocked := func(myIdx int) *netmap.NetworkMap {
@@ -267,7 +267,7 @@ func meshStacks(logf logger.Logf, ms []*magicStack) (cleanup func()) {
DiscoKey: peer.conn.DiscoPublicKey(),
Addresses: addrs,
AllowedIPs: addrs,
Endpoints: eps[i],
Endpoints: epStrings(eps[i]),
DERP: "127.3.3.40:1",
}
nm.Peers = append(nm.Peers, peer)
@@ -276,7 +276,7 @@ func meshStacks(logf logger.Logf, ms []*magicStack) (cleanup func()) {
return nm
}
updateEps := func(idx int, newEps []string) {
updateEps := func(idx int, newEps []tailcfg.Endpoint) {
mu.Lock()
defer mu.Unlock()
@@ -331,9 +331,9 @@ func TestNewConn(t *testing.T) {
tstest.ResourceCheck(t)
epCh := make(chan string, 16)
epFunc := func(endpoints []string) {
epFunc := func(endpoints []tailcfg.Endpoint) {
for _, ep := range endpoints {
epCh <- ep
epCh <- ep.Addr.String()
}
}
@@ -503,7 +503,7 @@ func TestDeviceStartStop(t *testing.T) {
tstest.ResourceCheck(t)
conn, err := NewConn(Options{
EndpointsFunc: func(eps []string) {},
EndpointsFunc: func(eps []tailcfg.Endpoint) {},
Logf: t.Logf,
DisableLegacyNetworking: true,
})
@@ -1392,7 +1392,7 @@ func newNonLegacyTestConn(t testing.TB) *Conn {
conn, err := NewConn(Options{
Logf: t.Logf,
Port: port,
EndpointsFunc: func(eps []string) {
EndpointsFunc: func(eps []tailcfg.Endpoint) {
t.Logf("endpoints: %q", eps)
},
DisableLegacyNetworking: true,
@@ -1694,15 +1694,17 @@ func TestRebindStress(t *testing.T) {
}
}
func TestStringSetsEqual(t *testing.T) {
s := func(nn ...int) (ret []string) {
for _, n := range nn {
ret = append(ret, strconv.Itoa(n))
func TestEndpointSetsEqual(t *testing.T) {
s := func(ports ...uint16) (ret []tailcfg.Endpoint) {
for _, port := range ports {
ret = append(ret, tailcfg.Endpoint{
Addr: netaddr.IPPort{Port: port},
})
}
return
}
tests := []struct {
a, b []string
a, b []tailcfg.Endpoint
want bool
}{
{
@@ -1745,7 +1747,7 @@ func TestStringSetsEqual(t *testing.T) {
},
}
for _, tt := range tests {
if got := stringSetsEqual(tt.a, tt.b); got != tt.want {
if got := endpointSetsEqual(tt.a, tt.b); got != tt.want {
t.Errorf("%q vs %q = %v; want %v", tt.a, tt.b, got, tt.want)
}
}
@@ -1804,3 +1806,10 @@ func TestBetterAddr(t *testing.T) {
}
}
func epStrings(eps []tailcfg.Endpoint) (ret []string) {
for _, ep := range eps {
ret = append(ret, ep.Addr.String())
}
return
}

View File

@@ -98,7 +98,7 @@ func (c *nlConn) Receive() (message, error) {
dst := netaddrIPPrefix(rmsg.Attributes.Dst, rmsg.DstLength)
gw := netaddrIP(rmsg.Attributes.Gateway)
if msg.Header.Type == unix.RTM_NEWROUTE && rmsg.Table == tsTable && dst.IsSingleIP() {
if rmsg.Table == tsTable && dst.IsSingleIP() {
// Don't log. Spammy and normal to see a bunch of these on start-up,
// which we make ourselves.
} else {

View File

@@ -7,7 +7,9 @@
package monitor
import (
"bytes"
"errors"
"os"
"runtime"
"sync"
"time"
@@ -42,6 +44,20 @@ func (pm *pollingMon) Close() error {
return nil
}
func (pm *pollingMon) isCloudRun() bool {
// https: //cloud.google.com/run/docs/reference/container-contract#env-vars
if os.Getenv("K_REVISION") == "" || os.Getenv("K_CONFIGURATION") == "" ||
os.Getenv("K_SERVICE") == "" || os.Getenv("PORT") == "" {
return false
}
vers, err := os.ReadFile("/proc/version")
if err != nil {
pm.logf("Failed to read /proc/version: %v", err)
return false
}
return string(bytes.TrimSpace(vers)) == "Linux version 4.4.0 #1 SMP Sun Jan 10 15:06:54 PST 2016"
}
func (pm *pollingMon) Receive() (message, error) {
d := 10 * time.Second
if runtime.GOOS == "android" {
@@ -50,8 +66,12 @@ func (pm *pollingMon) Receive() (message, error) {
// https://github.com/tailscale/tailscale/issues/1427
d = 10 * time.Minute
}
// TODO: detect if we're running in Cloud Run, and reduce frequency of
// polling as its routes never change.
if pm.isCloudRun() {
// Cloud Run routes never change at runtime. the containers are killed within
// 15 minutes by default, set the interval long enough to be effectively infinite.
pm.logf("monitor polling: Cloud Run detected, reduce polling interval to 24h")
d = 24 * time.Hour
}
ticker := time.NewTicker(d)
defer ticker.Stop()
base := pm.m.InterfaceState()

View File

@@ -47,12 +47,13 @@ const debugNetstack = false
// and implements wgengine.FakeImpl to act as a userspace network
// stack when Tailscale is running in fake mode.
type Impl struct {
ipstack *stack.Stack
linkEP *channel.Endpoint
tundev *tstun.Wrapper
e wgengine.Engine
mc *magicsock.Conn
logf logger.Logf
ipstack *stack.Stack
linkEP *channel.Endpoint
tundev *tstun.Wrapper
e wgengine.Engine
mc *magicsock.Conn
logf logger.Logf
onlySubnets bool // whether we only want to handle subnet relaying
mu sync.Mutex
dns DNSMap
@@ -67,7 +68,7 @@ const nicID = 1
const mtu = 1500
// Create creates and populates a new Impl.
func Create(logf logger.Logf, tundev *tstun.Wrapper, e wgengine.Engine, mc *magicsock.Conn) (*Impl, error) {
func Create(logf logger.Logf, tundev *tstun.Wrapper, e wgengine.Engine, mc *magicsock.Conn, onlySubnets bool) (*Impl, error) {
if mc == nil {
return nil, errors.New("nil magicsock.Conn")
}
@@ -116,11 +117,13 @@ func Create(logf logger.Logf, tundev *tstun.Wrapper, e wgengine.Engine, mc *magi
e: e,
mc: mc,
connsOpenBySubnetIP: make(map[netaddr.IP]int),
onlySubnets: onlySubnets,
}
return ns, nil
}
// Start sets up all the handlers so netstack can start working. Implements
// wgengine.FakeImpl.
func (ns *Impl) Start() error {
ns.e.AddNetworkMapCallback(ns.updateIPs)
@@ -223,7 +226,15 @@ func (ns *Impl) updateIPs(nm *netmap.NetworkMap) {
oldIPs[protocolAddr.AddressWithPrefix] = true
}
newIPs := make(map[tcpip.AddressWithPrefix]bool)
isAddr := map[netaddr.IPPrefix]bool{}
for _, ipp := range nm.SelfNode.Addresses {
isAddr[ipp] = true
}
for _, ipp := range nm.SelfNode.AllowedIPs {
if ns.onlySubnets && isAddr[ipp] {
continue
}
newIPs[ipPrefixToAddressWithPrefix(ipp)] = true
}

View File

@@ -0,0 +1,36 @@
// 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 netstack
import (
"reflect"
"tailscale.com/wgengine"
"tailscale.com/wgengine/router"
)
func init() {
wgengine.NetstackRouterType = reflect.TypeOf(&subnetRouter{})
}
type subnetRouter struct {
router.Router
}
// NewSubnetRouterWrapper returns a Router wrapper that prevents the
// underlying Router r from seeing any advertised subnet routes, as
// netstack will handle them instead.
func NewSubnetRouterWrapper(r router.Router) router.Router {
return &subnetRouter{
Router: r,
}
}
func (r *subnetRouter) Set(c *router.Config) error {
if c != nil {
c.SubnetRoutes = nil // netstack will handle
}
return r.Router.Set(c)
}

View File

@@ -15,8 +15,8 @@ import (
// Mainly used as a shim for OSes that want to set both network and
// DNS configuration simultaneously (iOS, android).
type CallbackRouter struct {
SetBoth func(rcfg *Config, dcfg *dns.OSConfig) error
DNSMode dns.RoutingMode
SetBoth func(rcfg *Config, dcfg *dns.OSConfig) error
SplitDNS bool
mu sync.Mutex // protects all the following
rcfg *Config // last applied router config
@@ -44,9 +44,13 @@ func (r *CallbackRouter) SetDNS(dcfg dns.OSConfig) error {
return r.SetBoth(r.rcfg, r.dcfg)
}
// RoutingMode implements dns.OSConfigurator.
func (r *CallbackRouter) RoutingMode() dns.RoutingMode {
return r.DNSMode
// SupportsSplitDNS implements dns.OSConfigurator.
func (r *CallbackRouter) SupportsSplitDNS() bool {
return r.SplitDNS
}
func (r *CallbackRouter) GetBaseConfig() (dns.OSConfig, error) {
return dns.OSConfig{}, dns.ErrGetBaseConfigNotSupported
}
func (r *CallbackRouter) Close() error {

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