Compare commits

...

31 Commits

Author SHA1 Message Date
James Tucker
20cd690970 .github/workflows: add cross-android
Fixes 4482

Signed-off-by: James Tucker <james@tailscale.com>
2022-04-21 10:26:40 -07:00
James Tucker
26fe00e7bd .github/workflows: add cross-android
Fixes 4482

Signed-off-by: James Tucker <james@tailscale.com>
2022-04-21 10:13:27 -07:00
James Tucker
972df80be2 .github/workflows: add cross-android
Fixes 4482

Signed-off-by: James Tucker <james@tailscale.com>
2022-04-21 10:00:02 -07:00
Brad Fitzpatrick
53588f632d Revert "wgengine/router,util/kmod: load & log xt_mark"
This reverts commit 8d6793fd70.

Reason: breaks Android build (cgo/pthreads addition)

We can try again next cycle.

Change-Id: I5e7e1730a8bf399a8acfce546a6d22e11fb835d5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-21 09:53:23 -07:00
Tom DNetto
df26c63793 net/dns/resolver, net/tsaddr: fix reverse lookups in 4to6 IP range
Fixes #4439

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-04-21 09:37:21 -07:00
James Tucker
8d6793fd70 wgengine/router,util/kmod: load & log xt_mark
Attempt to load the xt_mark kernel module when it is not present. If the
load fails, log error information.

It may be tempting to promote this failure to an error once it has been
in use for some time, so as to avoid reaching an error with the iptables
invocation, however, there are conditions under which the two stages may
disagree - this change adds more useful breadcrumbs.

Example new output from tailscaled running under my WSL2:

```
router: ensure module xt_mark: "/usr/sbin/modprobe xt_mark" failed: exit status 1; modprobe: FATAL: Module xt_mark not found in directory /lib/modules/5.10.43.3-microsoft-standard-WSL2
```

Background:

There are two places to lookup modules, one is `/proc/modules` "old",
the other is `/sys/module/` "new".

There was query_modules(2) in linux <2.6, alas, it is gone.

In a docker container in the default configuration, you would get
/proc/modules and /sys/module/ both populated. lsmod may work file,
modprobe will fail with EPERM at `finit_module()` for an unpriviliged
container.

In a priviliged container the load may *succeed*, if some conditions are
met. This condition should be avoided, but the code landing in this
change does not attempt to avoid this scenario as it is both difficult
to detect, and has a very uncertain impact.

In an nspawn container `/proc/modules` is populated, but `/sys/module`
does not exist. Modern `lsmod` versions will fail to gather most module
information, without sysfs being populated with module information.

In WSL2 modules are likely missing, as the in-use kernel typically is
not provided by the distribution filesystem, and WSL does not mount in a
module filesystem of its own. Notably the WSL2 kernel supports iptables
marks without listing the xt_mark module in /sys/module, and
/proc/modules is empty.

On a recent kernel, we can ask the capabilities system about SYS_MODULE,
that will help to disambiguate between the non-privileged container case
and just being root. On older kernels these calls may fail.

Update #4329

Signed-off-by: James Tucker <james@tailscale.com>
2022-04-20 22:21:35 -07:00
Brad Fitzpatrick
f7cb6630e7 tailcfg: document SSHPrincipal.PubKeys URL expansions
From f74ee80abe which lacked docs.

Updates #3802

Change-Id: Ia7df05a486ae383cc6d9aca9dfe487b04e243ad5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-20 20:12:37 -07:00
Brad Fitzpatrick
5b4154342e ssh/tailssh: fix double SSH-2.0- prefix in greeting banner
gliderlabs/ssh was already adding the "SSH-2.0-" prefix.

Updates #3802

Change-Id: I19a1cd9308371a2898e7883cf26e94c9b54bab29
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-20 20:08:39 -07:00
Brad Fitzpatrick
7a097ccc83 ipn/ipnlocal: close peerapi listeners on LocalBackend.Shutdown
For tests.

Now that we can always listen (whereas we used to fail prior to
a2c330c496), some goroutine leak
checks were failing in tests in another repo after that change.

Change-Id: Id95a4b71167eca61962a48616d79741b9991e0bc
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-20 19:25:42 -07:00
Maisem Ali
2b8b887d55 ssh/tailssh: send banner messages during auth, move more to conn
(VSCode Live Share between Brad & Maisem!)

Updates #3802

Change-Id: Id8edca4481b0811debfdf56d4ccb1a46f71dd6d3
Co-Authored-By: Brad Fitzpatrick <bradfitz@tailscale.com>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-20 18:34:11 -07:00
Denton Gentry
13f75b9667 scripts/install: add Alma Linux.
Tested using an Alma Linux 8.5 VM.
Updates https://github.com/tailscale/tailscale/issues/2915

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-04-20 17:43:49 -07:00
Denton Gentry
c2b907c965 scripts/installer: support LinuxMint Debian.
The primary distribution for LinuxMint is based on Ubuntu,
but there is an alternate Debian-based distribution called
LMDE. Both variations identify themselves as "linuxmint"

We added UBUNTU_VERSION to the Ubuntu handling for linuxmint,
the only distribution so far found to do this. Instead, split
linuxmint out into its own case and use either UBUNTU_VERSION
or DEBIAN_VERSION, whichever is present.

Tested on an LMDE 5 (elsie) VM.

Updates https://github.com/tailscale/tailscale/issues/2915

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-04-20 17:43:49 -07:00
Denton Gentry
61868f281e scripts/installer: call emerge with --ask=n
Fixes https://github.com/tailscale/tailscale/issues/4354

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-04-20 17:43:49 -07:00
Denton Gentry
db7da6622a scripts/installer: add ParrotOS support
Support ParrotSec https://parrotsec.org/
Tested using a Parrot 5.0 VM.

Updates https://github.com/tailscale/tailscale/issues/2915

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-04-20 17:43:49 -07:00
Brad Fitzpatrick
d413850bd7 cmd/tailscale: add "debug via" subcommand to do CIDR math for via ranges
$ tailscale debug via 0xb 10.2.0.0/16
fd7a:115c:a1e0:b1a:0:b:a02:0/112
$ tailscale debug via fd7a:115c:a1e0:b1a:0:b:a02:0/112
site 11 (0xb), 10.2.0.0/16

Previously: 3ae701f0eb

This adds a little debug tool to do CIDR math to make converting between
those ranges easier for now.

Updates #3616

Change-Id: I98302e95d17765bfaced3ecbb71cbd43e84bff46
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-20 14:47:48 -07:00
Brad Fitzpatrick
f74ee80abe ssh/tailssh: support expansions in public key fetch URL too
Updates #3802

Change-Id: I5aa98bdab14fd1c1c00ba63b93f8d7e670f72437
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-20 14:04:50 -07:00
Maisem Ali
14d077fc3a ssh/tailssh: terminate ssh auth early if no policy can match
Also bump github.com/tailscale/golang-x-crypto/ssh

Updates #3802

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-20 13:44:04 -07:00
Brad Fitzpatrick
a2c330c496 ipn/ipnlocal: use the fake peerapi listener as fallback if netstack available
The previous commit (1b89662eff) this for Android, but we can also use
this on any platform if we we would otherwise fail.

Change-Id: I4cd78b40e9e77fca5cc8e717dd48ac173101bed4
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-20 13:41:44 -07:00
Maisem Ali
136f30fc92 wgengine/monitor: split the unexpected stringification log line
It unfortuantely gets truncated because it's too long, split it into 3
different log lines to circumvent truncation.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-20 12:32:15 -07:00
Maisem Ali
8e40bfc6ea wgengine/monitor: ignore OS-specific uninteresting interfaces
Currently we ignore these interfaces in the darwin osMon but then would consider it
interesting when checking if anything had changed.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2022-04-20 12:32:15 -07:00
Brad Fitzpatrick
1b89662eff ipn/ipnlocal: make peerapi listener on Android avoid the kernel
We intercept the peerapi port in netstack anyway, so there's no reason
the linux kernel on Android needs to know about it. It's only getting
in the way and causing problems for reasons we don't fully understand.
But we don't even need to understand it because it's not relevant
anymore.

Instead, provide a dummy net.Listener that just sits and blocks to
pacify the rest of the code that assumes it can be stuck in a
Listener.Accept call and call Listener.Close and Listener.Addr.

We'll likely do this for all platforms in the future, if/when we also
link in netstack on iOS.

Updates #4449
Updates #4293
Updates #3986

Change-Id: Ic2d3fe2f3cee60fc527356a3368830f17aeb75ae
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-20 12:31:12 -07:00
Brad Fitzpatrick
cf9b9a7fec tstest/iosdeps: add test for forbidden iOS dependencies
Fixes #4463

Change-Id: I8305710e8a075263ae9a88a29624b19032d5beeb
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-20 12:30:23 -07:00
Brad Fitzpatrick
8b81254992 ipn/ipnlocal: reject tailscale up --ssh if disabled on tailnet
Updates #3802

Change-Id: I3f1e839391fe9b28270f506f4bb8d8e3d36716f5
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-20 11:38:27 -07:00
Brad Fitzpatrick
0ce67ccda6 wgengine/router: make supportsV6NAT check catch more cases
Updates #4459

Change-Id: Ic27621569d2739298e652769d10e38608c6012be
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-20 10:28:33 -07:00
Xe Iaso
fc2f628d4c cmd/nginx-auth: maintainer scripts and tailnet checking (#4460)
* cmd/nginx-auth: add maintainer scripts

Signed-off-by: Xe <xe@tailscale.com>

* cmd/nginx-auth: add Expected-Tailnet header and documentation

Signed-off-by: Xe <xe@tailscale.com>
2022-04-20 13:06:05 -04:00
Blake Mizerany
33fa43252e cmd/proxy-to-grafana: prevent premature termination
This commit changes proxy-to-grafana to report errors while polling for
tailscaled status instead of terminating at the first sign of an error.
This allows tailscale some time to come up before the proxy decides to
give up.

Signed-off-by: Blake Mizerany <blake.mizerany@gmail.com>
2022-04-19 13:31:36 -07:00
Tom DNetto
c8f4dfc8c0 derp/derphttp,net/netcheck: improve netcheck behavior under MITM proxies
In cases where tailscale is operating behind a MITM proxy, we need to consider
that a lot more of the internals of our HTTP requests are visible and may be
used as part of authorization checks. As such, we need to 'behave' as closely
as possible to ideal.

 - Some proxies do authorization or consistency checks based the on Host header
   or HTTP URI, instead of just the IP/hostname/SNI. As such, we need to
   construct a `*http.Request` with a valid URI everytime HTTP is going to be
   used on the wire, even if its over TLS.
   Aside from the singular instance in net/netcheck, I couldn't find anywhere
   else a http.Request was constructed incorrectly.

 - Some proxies may deny requests, typically by returning a 403 status code. We
   should not consider these requests as a valid latency check, so netcheck
   semantics have been updated to consider >299 status codes as a failed probe.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2022-04-19 12:47:57 -07:00
Brad Fitzpatrick
cc575fe4d6 net/dns: schedule DoH upgrade explicitly, fix Resolver.Addr confusion
Two changes in one:

* make DoH upgrades an explicitly scheduled send earlier, when we come
  up with the resolvers-and-delay send plan. Previously we were
  getting e.g.  four Google DNS IPs and then spreading them out in
  time (for back when we only did UDP) but then later we added DoH
  upgrading at the UDP packet layer, which resulted in sometimes
  multiple DoH queries to the same provider running (each doing happy
  eyeballs dialing to 4x IPs themselves) for each of the 4 source IPs.
  Instead, take those 4 Google/Cloudflare IPs and schedule 5 things:
  first the DoH query (which can use all 4 IPs), and then each of the
  4 IPs as UDP later.

* clean up the dnstype.Resolver.Addr confusion; half the code was
  using it as an IP string (as documented) as half was using it as
  an IP:port (from some prior type we used), primarily for tests.
  Instead, document it was being primarily an IP string but also
  accepting an IP:port for tests, then add an accessor method on it
  to get the IPPort and use that consistently everywhere.

Change-Id: Ifdd72b9e45433a5b9c029194d50db2b9f9217b53
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-19 12:00:22 -07:00
Brad Fitzpatrick
e3a4952527 net/dns/resolver: count errors when racing DNS queries, fail earlier
If all N queries failed, we waited until context timeout (in 5
seconds) to return.

This makes (*forwarder).forward fail fast when the network's
unavailable.

Change-Id: Ibbb3efea7ed34acd3f3b29b5fee00ba8c7492569
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-19 11:07:31 -07:00
Brad Fitzpatrick
d9efbd97cb net/dns: remove an unused function
Change-Id: I7c920c76223ffac37954ef2a18754afc52177598
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-19 10:53:54 -07:00
Brad Fitzpatrick
c13be0c509 tailcfg: clarify how SSHPolicy.Rules are evaluated between auth phases
Updates #3802

Change-Id: I321183a8a2b065a40dca8dd95ca90cd822a17ff8
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2022-04-18 22:03:52 -07:00
49 changed files with 1133 additions and 496 deletions

53
.github/workflows/cross-android.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
name: Android-Cross
on:
push:
branches:
- main
pull_request:
branches:
- '*'
jobs:
build:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[ci skip]')"
steps:
- name: Set up Go
uses: actions/setup-go@v3
with:
go-version: 1.18
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Android build cmd
env:
GOOS: android
GOARCH: amd64
run: go build ./cmd/...
- name: Android build tests (does not run tests)
env:
GOOS: android
GOARCH: amd64
run: for d in $(go list -f '{{if .TestGoFiles}}{{.Dir}}{{end}}' ./... ); do (echo $d; cd $d && go test -run '^$' -c ); done
- uses: k0kubun/action-slack@v2.0.0
with:
payload: |
{
"attachments": [{
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
"color": "danger"
}]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
if: failure() && github.event_name == 'push'

View File

@@ -117,10 +117,32 @@ header.
The `Tailscale-Tailnet` header can help you identify which tailnet the session
is coming from. If you are using node sharing, this can help you make sure that
you aren't giving administrative access to people outside your tailnet. You will
need to be sure to check this in your application code. If you use OpenResty,
you may be able to do more complicated access controls than you can with NGINX
alone.
you aren't giving administrative access to people outside your tailnet.
### Allow Requests From Only One Tailnet
If you want to prevent node sharing from allowing users to access a service, add
the `Expected-Tailnet` header to your auth request:
```nginx
location /auth {
# ...
proxy_set_header Expected-Tailnet "tailscale.com";
}
```
If a user from a different tailnet tries to use that service, this will return a
generic "forbidden" error page:
```html
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>
```
## Building

14
cmd/nginx-auth/deb/postinst.sh Executable file
View File

@@ -0,0 +1,14 @@
if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then
deb-systemd-helper unmask 'tailscale.nginx-auth.socket' >/dev/null || true
if deb-systemd-helper --quiet was-enabled 'tailscale.nginx-auth.socket'; then
deb-systemd-helper enable 'tailscale.nginx-auth.socket' >/dev/null || true
else
deb-systemd-helper update-state 'tailscale.nginx-auth.socket' >/dev/null || true
fi
if systemctl is-active tailscale.nginx-auth.socket >/dev/null; then
systemctl --system daemon-reload >/dev/null || true
deb-systemd-invoke stop 'tailscale.nginx-auth.service' >/dev/null || true
deb-systemd-invoke restart 'tailscale.nginx-auth.socket' >/dev/null || true
fi
fi

19
cmd/nginx-auth/deb/postrm.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/bin/sh
set -e
if [ -d /run/systemd/system ] ; then
systemctl --system daemon-reload >/dev/null || true
fi
if [ -x "/usr/bin/deb-systemd-helper" ]; then
if [ "$1" = "remove" ]; then
deb-systemd-helper mask 'tailscale.nginx-auth.socket' >/dev/null || true
deb-systemd-helper mask 'tailscale.nginx-auth.service' >/dev/null || true
fi
if [ "$1" = "purge" ]; then
deb-systemd-helper purge 'tailscale.nginx-auth.socket' >/dev/null || true
deb-systemd-helper unmask 'tailscale.nginx-auth.socket' >/dev/null || true
deb-systemd-helper purge 'tailscale.nginx-auth.service' >/dev/null || true
deb-systemd-helper unmask 'tailscale.nginx-auth.service' >/dev/null || true
fi
fi

8
cmd/nginx-auth/deb/prerm.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
set -e
if [ "$1" = "remove" ]; then
if [ -d /run/systemd/system ]; then
deb-systemd-invoke stop 'tailscale.nginx-auth.service' >/dev/null || true
deb-systemd-invoke stop 'tailscale.nginx-auth.socket' >/dev/null || true
fi
fi

View File

@@ -4,20 +4,28 @@ set -e
CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -o tailscale.nginx-auth .
mkpkg \
--out tailscale-nginx-auth-0.1.0-amd64.deb \
--name=tailscale-nginx-auth \
--version=0.1.0 \
--type=deb\
--arch=amd64 \
--description="Tailscale NGINX authentication protocol handler" \
--files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service
VERSION=0.1.1
mkpkg \
--out tailscale-nginx-auth-0.1.0-amd64.rpm \
--out=tailscale-nginx-auth-${VERSION}-amd64.deb \
--name=tailscale-nginx-auth \
--version=0.1.0 \
--version=${VERSION} \
--type=deb \
--arch=amd64 \
--postinst=deb/postinst.sh \
--postrm=deb/postrm.sh \
--prerm=deb/prerm.sh \
--description="Tailscale NGINX authentication protocol handler" \
--files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service,./README.md:/usr/share/tailscale/nginx-auth/README.md
mkpkg \
--out=tailscale-nginx-auth-${VERSION}-amd64.rpm \
--name=tailscale-nginx-auth \
--version=${VERSION} \
--type=rpm \
--arch=amd64 \
--postinst=rpm/postinst.sh \
--postrm=rpm/postrm.sh \
--prerm=rpm/prerm.sh \
--description="Tailscale NGINX authentication protocol handler" \
--files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service
--files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service,./README.md:/usr/share/tailscale/nginx-auth/README.md

View File

@@ -17,6 +17,7 @@ import (
"net"
"net/http"
"net/netip"
"net/url"
"os"
"strings"
@@ -75,6 +76,12 @@ func main() {
return
}
if expectedTailnet := r.Header.Get("Expected-Tailnet"); expectedTailnet != "" && expectedTailnet != tailnet {
w.WriteHeader(http.StatusForbidden)
log.Printf("user is part of tailnet %s, wanted: %s", tailnet, url.QueryEscape(expectedTailnet))
return
}
h := w.Header()
h.Set("Tailscale-Login", strings.Split(info.UserProfile.LoginName, "@")[0])
h.Set("Tailscale-User", info.UserProfile.LoginName)

0
cmd/nginx-auth/rpm/postinst.sh Executable file
View File

9
cmd/nginx-auth/rpm/postrm.sh Executable file
View File

@@ -0,0 +1,9 @@
# $1 == 0 for uninstallation.
# $1 == 1 for removing old package during upgrade.
systemctl daemon-reload >/dev/null 2>&1 || :
if [ $1 -ge 1 ] ; then
# Package upgrade, not uninstall
systemctl stop tailscale.nginx-auth.service >/dev/null 2>&1 || :
systemctl try-restart tailscale.nginx-auth.socket >/dev/null 2>&1 || :
fi

9
cmd/nginx-auth/rpm/prerm.sh Executable file
View File

@@ -0,0 +1,9 @@
# $1 == 0 for uninstallation.
# $1 == 1 for removing old package during upgrade.
if [ $1 -eq 0 ] ; then
# Package removal, not upgrade
systemctl --no-reload disable tailscale.nginx-auth.socket > /dev/null 2>&1 || :
systemctl stop tailscale.nginx-auth.socket > /dev/null 2>&1 || :
systemctl stop tailscale.nginx-auth.service > /dev/null 2>&1 || :
fi

View File

@@ -86,11 +86,12 @@ func main() {
for i := 0; i < 60; i++ {
st, err := tailscale.Status(context.Background())
if err != nil {
log.Fatal(err)
}
log.Printf("tailscale status: %v", st.BackendState)
if st.BackendState == "Running" {
break
log.Printf("error retrieving tailscale status; retrying: %v", err)
} else {
log.Printf("tailscale status: %v", st.BackendState)
if st.BackendState == "Running" {
break
}
}
time.Sleep(time.Second)
}

View File

@@ -8,6 +8,7 @@ import (
"bufio"
"bytes"
"context"
"encoding/binary"
"encoding/json"
"errors"
"flag"
@@ -21,9 +22,11 @@ import (
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"inet.af/netaddr"
"tailscale.com/client/tailscale"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/net/tsaddr"
"tailscale.com/paths"
"tailscale.com/safesocket"
)
@@ -106,6 +109,11 @@ var debugCmd = &ffcli.Command{
return fs
})(),
},
{
Name: "via",
Exec: runVia,
ShortHelp: "convert between site-specific IPv4 CIDRs and IPv6 'via' routes",
},
},
}
@@ -348,3 +356,46 @@ func runDaemonMetrics(ctx context.Context, args []string) error {
time.Sleep(time.Second)
}
}
func runVia(ctx context.Context, args []string) error {
switch len(args) {
default:
return errors.New("expect either <site-id> <v4-cidr> or <v6-route>")
case 1:
ipp, err := netaddr.ParseIPPrefix(args[0])
if err != nil {
return err
}
if !ipp.IP().Is6() {
return errors.New("with one argument, expect an IPv6 CIDR")
}
if !tsaddr.TailscaleViaRange().Contains(ipp.IP()) {
return errors.New("not a via route")
}
if ipp.Bits() < 96 {
return errors.New("short length, want /96 or more")
}
v4 := tsaddr.UnmapVia(ipp.IP())
a := ipp.IP().As16()
siteID := binary.BigEndian.Uint32(a[8:12])
fmt.Printf("site %v (0x%x), %v\n", siteID, siteID, netaddr.IPPrefixFrom(v4, ipp.Bits()-96))
case 2:
siteID, err := strconv.ParseUint(args[0], 0, 32)
if err != nil {
return fmt.Errorf("invalid site-id %q; must be decimal or hex with 0x prefix", args[0])
}
if siteID > 0xff {
return fmt.Errorf("site-id values over 255 are currently reserved")
}
ipp, err := netaddr.ParseIPPrefix(args[1])
if err != nil {
return err
}
via, err := tsaddr.MapVia(uint32(siteID), ipp)
if err != nil {
return err
}
fmt.Println(via)
}
return nil
}

View File

@@ -538,12 +538,17 @@ func (c *Client) tlsClient(nc net.Conn, node *tailcfg.DERPNode) *tls.Conn {
return tls.Client(nc, tlsConf)
}
func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tlsConn *tls.Conn, connClose io.Closer, err error) {
// DialRegionTLS returns a TLS connection to a DERP node in the given region.
//
// DERP nodes for a region are tried in sequence according to their order
// in the DERP map. TLS is initiated on the first node where a socket is
// established.
func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tlsConn *tls.Conn, connClose io.Closer, node *tailcfg.DERPNode, err error) {
tcpConn, node, err := c.dialRegion(ctx, reg)
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
done := make(chan bool) // unbufferd
done := make(chan bool) // unbuffered
defer close(done)
tlsConn = c.tlsClient(tcpConn, node)
@@ -556,13 +561,13 @@ func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tl
}()
err = tlsConn.Handshake()
if err != nil {
return nil, nil, err
return nil, nil, nil, err
}
select {
case done <- true:
return tlsConn, tcpConn, nil
return tlsConn, tcpConn, node, nil
case <-ctx.Done():
return nil, nil, ctx.Err()
return nil, nil, nil, ctx.Err()
}
}

View File

@@ -149,3 +149,9 @@ func UseWIPCode() bool { return Bool("TAILSCALE_USE_WIP_CODE") }
// if already enabled and any attempt to re-enable it will result in
// an error.
func CanSSHD() bool { return !Bool("TS_DISABLE_SSH_SERVER") }
// SSHPolicyFile returns the path, if any, to the SSHPolicy JSON file for development.
func SSHPolicyFile() string { return String("TS_DEBUG_SSH_POLICY_FILE") }
// SSHIgnoreTailnetPolicy is whether to ignore the Tailnet SSH policy for development.
func SSHIgnoreTailnetPolicy() bool { return Bool("TS_DEBUG_SSH_IGNORE_TAILNET_POLICY") }

4
go.mod
View File

@@ -40,7 +40,7 @@ require (
github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
github.com/tailscale/golang-x-crypto v0.0.0-20220330002111-62119522bbcf
github.com/tailscale/golang-x-crypto v0.0.0-20220420224200-c602b5dfaa7f
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
@@ -49,7 +49,7 @@ require (
github.com/u-root/u-root v0.8.0
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54
go4.org/mem v0.0.0-20210711025021-927187094b94
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad

8
go.sum
View File

@@ -1067,8 +1067,8 @@ github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502 h1:34icjjmqJ2HP
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41/go.mod h1:/roCdA6gg6lQyw/Oz6gIIGu3ggJKYhF+WC/AQReE5XQ=
github.com/tailscale/golang-x-crypto v0.0.0-20220330002111-62119522bbcf h1:+DSoknr7gaiW2LlViX6+ko8TBdxTLkvOBbIWQtYyMaE=
github.com/tailscale/golang-x-crypto v0.0.0-20220330002111-62119522bbcf/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=
github.com/tailscale/golang-x-crypto v0.0.0-20220420224200-c602b5dfaa7f h1:3CuODoSnBXS+ZkQlGakDqtX1o2RteR1870yF+dS61PY=
github.com/tailscale/golang-x-crypto v0.0.0-20220420224200-c602b5dfaa7f/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83 h1:f7nwzdAHTUUOJjHZuDvLz9CEAlUM228amCRvwzlPvsA=
@@ -1227,8 +1227,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 h1:S25/rfnfsMVgORT4/J61MJ7rdyseOZOyvLIrZEZ7s6s=
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=

View File

@@ -259,10 +259,10 @@ func TestDNSConfigForNetmap(t *testing.T) {
want: &dns.Config{
Hosts: map[dnsname.FQDN][]netaddr.IP{},
DefaultResolvers: []dnstype.Resolver{
{Addr: "8.8.8.8:53"},
{Addr: "8.8.8.8"},
},
Routes: map[dnsname.FQDN][]dnstype.Resolver{
"foo.com.": {{Addr: "1.2.3.4:53"}},
"foo.com.": {{Addr: "1.2.3.4"}},
},
},
},
@@ -283,7 +283,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
Hosts: map[dnsname.FQDN][]netaddr.IP{},
Routes: map[dnsname.FQDN][]dnstype.Resolver{},
DefaultResolvers: []dnstype.Resolver{
{Addr: "8.8.4.4:53"},
{Addr: "8.8.4.4"},
},
},
},

View File

@@ -344,6 +344,7 @@ func (b *LocalBackend) onHealthChange(sys health.Subsystem, err error) {
func (b *LocalBackend) Shutdown() {
b.mu.Lock()
cc := b.cc
b.closePeerAPIListenersLocked()
b.mu.Unlock()
b.unregisterLinkMon()
@@ -1808,6 +1809,10 @@ func (b *LocalBackend) checkPrefsLocked(p *ipn.Prefs) error {
if !canSSH {
return errors.New("The Tailscale SSH server has been administratively disabled.")
}
if b.netMap != nil && b.netMap.SSHPolicy == nil &&
envknob.SSHPolicyFile() == "" && !envknob.SSHIgnoreTailnetPolicy() {
return errors.New("Unable to enable local Tailscale SSH server; not enabled/configured on Tailnet.")
}
}
return nil
}
@@ -2207,7 +2212,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
addDefault := func(resolvers []dnstype.Resolver) {
for _, r := range resolvers {
dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, normalizeResolver(r))
dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, r)
}
}
@@ -2236,7 +2241,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
dcfg.Routes[fqdn] = make([]dnstype.Resolver, 0, len(resolvers))
for _, r := range resolvers {
dcfg.Routes[fqdn] = append(dcfg.Routes[fqdn], normalizeResolver(r))
dcfg.Routes[fqdn] = append(dcfg.Routes[fqdn], r)
}
}
@@ -2267,16 +2272,6 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
return dcfg
}
func normalizeResolver(cfg dnstype.Resolver) dnstype.Resolver {
if ip, err := netaddr.ParseIP(cfg.Addr); err == nil {
// Add 53 here for bare IPs for consistency with previous data type.
return dnstype.Resolver{
Addr: netaddr.IPPortFrom(ip, 53).String(),
}
}
return cfg
}
// SetVarRoot sets the root directory of Tailscale's writable
// storage area . (e.g. "/var/lib/tailscale")
//

View File

@@ -386,6 +386,14 @@ func (s *peerAPIServer) OpenFile(baseName string) (rc io.ReadCloser, size int64,
}
func (s *peerAPIServer) listen(ip netaddr.IP, ifState *interfaces.State) (ln net.Listener, err error) {
// Android for whatever reason often has problems creating the peerapi listener.
// But since we started intercepting it with netstack, it's not even important that
// we have a real kernel-level listener. So just create a dummy listener on Android
// and let netstack intercept it.
if runtime.GOOS == "android" {
return newFakePeerAPIListener(ip), nil
}
ipStr := ip.String()
var lc net.ListenConfig
@@ -428,8 +436,15 @@ func (s *peerAPIServer) listen(ip netaddr.IP, ifState *interfaces.State) (ln net
return ln, nil
}
}
// Fall back to random ephemeral port.
return lc.Listen(context.Background(), tcp4or6, net.JoinHostPort(ipStr, "0"))
// Fall back to some random ephemeral port.
ln, err = lc.Listen(context.Background(), tcp4or6, net.JoinHostPort(ipStr, "0"))
// And if we're on a platform with netstack (anything but iOS), then just fallback to netstack.
if err != nil && runtime.GOOS != "ios" {
s.b.logf("peerapi: failed to do peerAPI listen, harmless (netstack available) but error was: %v", err)
return newFakePeerAPIListener(ip), nil
}
return ln, err
}
type peerAPIListener struct {
@@ -1047,3 +1062,49 @@ func writePrettyDNSReply(w io.Writer, res []byte) (err error) {
w.Write(j)
return nil
}
// newFakePeerAPIListener creates a new net.Listener that acts like
// it's listening on the provided IP address and on TCP port 1.
//
// See docs on fakePeerAPIListener.
func newFakePeerAPIListener(ip netaddr.IP) net.Listener {
return &fakePeerAPIListener{
addr: netaddr.IPPortFrom(ip, 1).TCPAddr(),
closed: make(chan struct{}),
}
}
// fakePeerAPIListener is a net.Listener that has an Addr method returning a TCPAddr
// for a given IP on port 1 (arbitrary) and can be Closed, but otherwise Accept
// just blocks forever until closed. The purpose of this is to let the rest
// of the LocalBackend/PeerAPI code run and think it's talking to the kernel,
// even if the kernel isn't cooperating (like on Android: Issue 4449, 4293, etc)
// or we lack permission to listen on a port. It's okay to not actually listen via
// the kernel because on almost all platforms (except iOS as of 2022-04-20) we
// also intercept netstack TCP requests in to our peerapi port and hand it over
// directly to peerapi, without involving the kernel. So this doesn't need to be
// real. But the port number we return (1, in this case) is the port number we advertise
// to peers and they connect to. 1 seems pretty safe to use. Even if the kernel's
// using it, it doesn't matter, as we intercept it first in netstack and the kernel
// never notices.
//
// Eventually we'll remove this code and do this on all platforms, when iOS also uses
// netstack.
type fakePeerAPIListener struct {
addr net.Addr
closeOnce sync.Once
closed chan struct{}
}
func (fl *fakePeerAPIListener) Close() error {
fl.closeOnce.Do(func() { close(fl.closed) })
return nil
}
func (fl *fakePeerAPIListener) Accept() (net.Conn, error) {
<-fl.closed
return nil, io.EOF
}
func (fl *fakePeerAPIListener) Addr() net.Addr { return fl.addr }

View File

@@ -84,10 +84,7 @@ func (c Config) hasDefaultIPResolversOnly() bool {
return false
}
for _, r := range c.DefaultResolvers {
if ipp, err := netaddr.ParseIPPort(r.Addr); err == nil && ipp.Port() == 53 {
continue
}
if _, err := netaddr.ParseIP(r.Addr); err != nil {
if ipp, ok := r.IPPort(); !ok || ipp.Port() != 53 {
return false
}
}

View File

@@ -174,7 +174,7 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
}
var defaultRoutes []dnstype.Resolver
for _, ip := range bcfg.Nameservers {
defaultRoutes = append(defaultRoutes, dnstype.ResolverFromIP(ip))
defaultRoutes = append(defaultRoutes, dnstype.Resolver{Addr: ip.String()})
}
rcfg.Routes["."] = defaultRoutes
ocfg.SearchDomains = append(ocfg.SearchDomains, bcfg.SearchDomains...)
@@ -188,23 +188,13 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
// DoH or custom-port entries with something like hasDefaultIPResolversOnly.
func toIPsOnly(resolvers []dnstype.Resolver) (ret []netaddr.IP) {
for _, r := range resolvers {
if ipp, err := netaddr.ParseIPPort(r.Addr); err == nil && ipp.Port() == 53 {
if ipp, ok := r.IPPort(); ok && ipp.Port() == 53 {
ret = append(ret, ipp.IP())
} else if ip, err := netaddr.ParseIP(r.Addr); err == nil {
ret = append(ret, 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.IPPortFrom(ip, 53))
}
return ret
}
func (m *Manager) EnqueuePacket(bs []byte, proto ipproto.Proto, from, to netaddr.IPPort) error {
return m.resolver.EnqueuePacket(bs, proto, from, to)
}

View File

@@ -96,7 +96,7 @@ func TestManager(t *testing.T) {
{
name: "corp",
in: Config{
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
DefaultResolvers: mustRes("1.1.1.1", "9.9.9.9"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
os: OSConfig{
@@ -107,7 +107,7 @@ func TestManager(t *testing.T) {
{
name: "corp-split",
in: Config{
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
DefaultResolvers: mustRes("1.1.1.1", "9.9.9.9"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: true,
@@ -119,7 +119,7 @@ func TestManager(t *testing.T) {
{
name: "corp-magic",
in: Config{
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
DefaultResolvers: mustRes("1.1.1.1", "9.9.9.9"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
Routes: upstreams("ts.com", ""),
Hosts: hosts(
@@ -131,7 +131,7 @@ func TestManager(t *testing.T) {
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
Routes: upstreams(".", "1.1.1.1:53", "9.9.9.9:53"),
Routes: upstreams(".", "1.1.1.1", "9.9.9.9"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
@@ -141,7 +141,7 @@ func TestManager(t *testing.T) {
{
name: "corp-magic-split",
in: Config{
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
DefaultResolvers: mustRes("1.1.1.1", "9.9.9.9"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
Routes: upstreams("ts.com", ""),
Hosts: hosts(
@@ -154,7 +154,7 @@ func TestManager(t *testing.T) {
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
rs: resolver.Config{
Routes: upstreams(".", "1.1.1.1:53", "9.9.9.9:53"),
Routes: upstreams(".", "1.1.1.1", "9.9.9.9"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
@@ -164,8 +164,8 @@ func TestManager(t *testing.T) {
{
name: "corp-routes",
in: Config{
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
Routes: upstreams("corp.com", "2.2.2.2:53"),
DefaultResolvers: mustRes("1.1.1.1", "9.9.9.9"),
Routes: upstreams("corp.com", "2.2.2.2"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
os: OSConfig{
@@ -174,15 +174,15 @@ func TestManager(t *testing.T) {
},
rs: resolver.Config{
Routes: upstreams(
".", "1.1.1.1:53", "9.9.9.9:53",
"corp.com.", "2.2.2.2:53"),
".", "1.1.1.1", "9.9.9.9",
"corp.com.", "2.2.2.2"),
},
},
{
name: "corp-routes-split",
in: Config{
DefaultResolvers: mustRes("1.1.1.1:53", "9.9.9.9:53"),
Routes: upstreams("corp.com", "2.2.2.2:53"),
DefaultResolvers: mustRes("1.1.1.1", "9.9.9.9"),
Routes: upstreams("corp.com", "2.2.2.2"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: true,
@@ -192,14 +192,14 @@ func TestManager(t *testing.T) {
},
rs: resolver.Config{
Routes: upstreams(
".", "1.1.1.1:53", "9.9.9.9:53",
"corp.com.", "2.2.2.2:53"),
".", "1.1.1.1", "9.9.9.9",
"corp.com.", "2.2.2.2"),
},
},
{
name: "routes",
in: Config{
Routes: upstreams("corp.com", "2.2.2.2:53"),
Routes: upstreams("corp.com", "2.2.2.2"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
bs: OSConfig{
@@ -212,14 +212,14 @@ func TestManager(t *testing.T) {
},
rs: resolver.Config{
Routes: upstreams(
".", "8.8.8.8:53",
"corp.com.", "2.2.2.2:53"),
".", "8.8.8.8",
"corp.com.", "2.2.2.2"),
},
},
{
name: "routes-split",
in: Config{
Routes: upstreams("corp.com", "2.2.2.2:53"),
Routes: upstreams("corp.com", "2.2.2.2"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: true,
@@ -233,8 +233,8 @@ func TestManager(t *testing.T) {
name: "routes-multi",
in: Config{
Routes: upstreams(
"corp.com", "2.2.2.2:53",
"bigco.net", "3.3.3.3:53"),
"corp.com", "2.2.2.2",
"bigco.net", "3.3.3.3"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
bs: OSConfig{
@@ -247,17 +247,17 @@ func TestManager(t *testing.T) {
},
rs: resolver.Config{
Routes: upstreams(
".", "8.8.8.8:53",
"corp.com.", "2.2.2.2:53",
"bigco.net.", "3.3.3.3:53"),
".", "8.8.8.8",
"corp.com.", "2.2.2.2",
"bigco.net.", "3.3.3.3"),
},
},
{
name: "routes-multi-split",
in: Config{
Routes: upstreams(
"corp.com", "2.2.2.2:53",
"bigco.net", "3.3.3.3:53"),
"corp.com", "2.2.2.2",
"bigco.net", "3.3.3.3"),
SearchDomains: fqdns("tailscale.com", "universe.tf"),
},
split: true,
@@ -268,8 +268,8 @@ func TestManager(t *testing.T) {
},
rs: resolver.Config{
Routes: upstreams(
"corp.com.", "2.2.2.2:53",
"bigco.net.", "3.3.3.3:53"),
"corp.com.", "2.2.2.2",
"bigco.net.", "3.3.3.3"),
},
},
{
@@ -290,7 +290,7 @@ func TestManager(t *testing.T) {
SearchDomains: fqdns("tailscale.com", "universe.tf", "coffee.shop"),
},
rs: resolver.Config{
Routes: upstreams(".", "8.8.8.8:53"),
Routes: upstreams(".", "8.8.8.8"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
@@ -322,7 +322,7 @@ func TestManager(t *testing.T) {
{
name: "routes-magic",
in: Config{
Routes: upstreams("corp.com", "2.2.2.2:53", "ts.com", ""),
Routes: upstreams("corp.com", "2.2.2.2", "ts.com", ""),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
@@ -338,8 +338,8 @@ func TestManager(t *testing.T) {
},
rs: resolver.Config{
Routes: upstreams(
"corp.com.", "2.2.2.2:53",
".", "8.8.8.8:53"),
"corp.com.", "2.2.2.2",
".", "8.8.8.8"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
@@ -350,7 +350,7 @@ func TestManager(t *testing.T) {
name: "routes-magic-split",
in: Config{
Routes: upstreams(
"corp.com", "2.2.2.2:53",
"corp.com", "2.2.2.2",
"ts.com", ""),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
@@ -364,7 +364,7 @@ func TestManager(t *testing.T) {
MatchDomains: fqdns("corp.com", "ts.com"),
},
rs: resolver.Config{
Routes: upstreams("corp.com.", "2.2.2.2:53"),
Routes: upstreams("corp.com.", "2.2.2.2"),
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
@@ -393,6 +393,14 @@ func TestManager(t *testing.T) {
},
}
trIP := cmp.Transformer("ipStr", func(ip netaddr.IP) string { return ip.String() })
trIPPort := cmp.Transformer("ippStr", func(ipp netaddr.IPPort) string {
if ipp.Port() == 53 {
return ipp.IP().String()
}
return ipp.String()
})
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
f := fakeOSConfigurator{
@@ -405,8 +413,6 @@ func TestManager(t *testing.T) {
if err := m.Set(test.in); err != nil {
t.Fatalf("m.Set: %v", err)
}
trIP := cmp.Transformer("ipStr", func(ip netaddr.IP) string { return ip.String() })
trIPPort := cmp.Transformer("ippStr", func(ipp netaddr.IPPort) string { return ipp.String() })
if diff := cmp.Diff(f.OSConfig, test.os, trIP, trIPPort, cmpopts.EquateEmpty()); diff != "" {
t.Errorf("wrong OSConfig (-got+want)\n%s", diff)
}
@@ -503,6 +509,11 @@ func upstreams(strs ...string) (ret map[dnsname.FQDN][]dnstype.Resolver) {
panic("IPPort provided before suffix")
}
ret[key] = append(ret[key], dnstype.Resolver{Addr: ipp.String()})
} else if _, err := netaddr.ParseIP(s); err == nil {
if key == "" {
panic("IPPort provided before suffix")
}
ret[key] = append(ret[key], dnstype.Resolver{Addr: s})
} else if strings.HasPrefix(s, "http") {
ret[key] = append(ret[key], dnstype.Resolver{Addr: s})
} else {

View File

@@ -49,9 +49,9 @@ func TestDoH(t *testing.T) {
dohSem: make(chan struct{}, 10),
}
for ip := range publicdns.KnownDoH() {
t.Run(ip.String(), func(t *testing.T) {
urlBase, c, ok := f.getKnownDoHClient(ip)
for urlBase := range publicdns.DoHIPsOfBase() {
t.Run(urlBase, func(t *testing.T) {
c, ok := f.getKnownDoHClientForProvider(urlBase)
if !ok {
t.Fatal("expected DoH")
}

View File

@@ -25,6 +25,7 @@ import (
dns "golang.org/x/net/dns/dnsmessage"
"inet.af/netaddr"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
"tailscale.com/net/dns/publicdns"
"tailscale.com/net/dnscache"
@@ -49,6 +50,11 @@ const (
// arbitrary.
dohTransportTimeout = 30 * time.Second
// dohTransportTimeout is how much of a head start to give a DoH query
// that was upgraded from a well-known public DNS provider's IP before
// normal UDP mode is attempted as a fallback.
dohHeadStart = 500 * time.Millisecond
// wellKnownHostBackupDelay is how long to artificially delay upstream
// DNS queries to the "fallback" DNS server IP for a known provider
// (e.g. how long to wait to query Google's 8.8.4.4 after 8.8.8.8).
@@ -228,65 +234,55 @@ func (f *forwarder) Close() error {
return nil
}
// resolversWithDelays maps from a set of DNS server names to a slice of
// a type that included a startDelay. So if resolvers contains e.g. four
// Google DNS IPs (two IPv4 + twoIPv6), this function partition adds
// delays to some.
// resolversWithDelays maps from a set of DNS server names to a slice of a type
// that included a startDelay, upgrading any well-known DoH (DNS-over-HTTP)
// servers in the process, insert a DoH lookup first before UDP fallbacks.
func resolversWithDelays(resolvers []dnstype.Resolver) []resolverAndDelay {
rr := make([]resolverAndDelay, 0, len(resolvers)+2)
// Add the known DoH ones first, starting immediately.
didDoH := map[string]bool{}
for _, r := range resolvers {
ipp, ok := r.IPPort()
if !ok {
continue
}
dohBase, ok := publicdns.KnownDoH()[ipp.IP()]
if !ok || didDoH[dohBase] {
continue
}
didDoH[dohBase] = true
rr = append(rr, resolverAndDelay{name: dnstype.Resolver{Addr: dohBase}})
}
type hostAndFam struct {
host string // some arbitrary string representing DNS host (currently the DoH base)
bits uint8 // either 32 or 128 for IPv4 vs IPv6s address family
}
// Track how many of each known resolver host are in the list,
// per address family.
total := map[hostAndFam]int{}
rr := make([]resolverAndDelay, len(resolvers))
for _, r := range resolvers {
if ip, err := netaddr.ParseIP(r.Addr); err == nil {
if host, ok := publicdns.KnownDoH()[ip]; ok {
total[hostAndFam{host, ip.BitLen()}]++
}
}
}
done := map[hostAndFam]int{}
for i, r := range resolvers {
var startDelay time.Duration
if ip, err := netaddr.ParseIP(r.Addr); err == nil {
if host, ok := publicdns.KnownDoH()[ip]; ok {
key4 := hostAndFam{host, 32}
key6 := hostAndFam{host, 128}
switch {
case ip.Is4():
if done[key4] > 0 {
startDelay += wellKnownHostBackupDelay
}
case ip.Is6():
total4 := total[key4]
if total4 >= 2 {
// If we have two IPv4 IPs of the same provider
// already in the set, delay the IPv6 queries
// until halfway through the timeout (so wait
// 2.5 seconds). Even the network is IPv6-only,
// the DoH dialer will fallback to IPv6
// immediately anyway.
startDelay = responseTimeout / 2
} else if total4 == 1 {
startDelay += wellKnownHostBackupDelay
}
if done[key6] > 0 {
startDelay += wellKnownHostBackupDelay
}
}
done[hostAndFam{host, ip.BitLen()}]++
}
for _, r := range resolvers {
ipp, ok := r.IPPort()
if !ok {
// Pass non-IP ones through unchanged, without delay.
// (e.g. DNS-over-ExitDNS when using an exit node)
rr = append(rr, resolverAndDelay{name: r})
continue
}
rr[i] = resolverAndDelay{
ip := ipp.IP()
var startDelay time.Duration
if host, ok := publicdns.KnownDoH()[ip]; ok {
// We already did the DoH query early. These
startDelay = dohHeadStart
key := hostAndFam{host, ip.BitLen()}
if done[key] > 0 {
startDelay += wellKnownHostBackupDelay
}
done[key]++
}
rr = append(rr, resolverAndDelay{
name: r,
startDelay: startDelay,
}
})
}
return rr
}
@@ -334,23 +330,6 @@ func (f *forwarder) packetListener(ip netaddr.IP) (packetListener, error) {
return lc, nil
}
// getKnownDoHClient returns an HTTP client for a DoH provider (such as Google
// or Cloudflare DNS), as a function of one of its (usually four) IPs.
//
// The provided IP is only used to determine the DoH provider; it is not
// prioritized among the set of IPs that are used by the provider.
func (f *forwarder) getKnownDoHClient(ip netaddr.IP) (urlBase string, c *http.Client, ok bool) {
urlBase, ok = publicdns.KnownDoH()[ip]
if !ok {
return "", nil, false
}
c, ok = f.getKnownDoHClientForProvider(urlBase)
if !ok {
return "", nil, false
}
return urlBase, c, true
}
// getKnownDoHClientForProvider returns an HTTP client for a specific DoH
// provider named by its DoH base URL (like "https://dns.google/dns-query").
//
@@ -444,34 +423,43 @@ func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client,
return res, err
}
var verboseDNSForward = envknob.Bool("TS_DEBUG_DNS_FORWARD_SEND")
// send sends packet to dst. It is best effort.
//
// send expects the reply to have the same txid as txidOut.
func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDelay) ([]byte, error) {
func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDelay) (ret []byte, err error) {
if verboseDNSForward {
f.logf("forwarder.send(%q) ...", rr.name.Addr)
defer func() {
f.logf("forwarder.send(%q) = %v, %v", rr.name.Addr, len(ret), err)
}()
}
if strings.HasPrefix(rr.name.Addr, "http://") {
return f.sendDoH(ctx, rr.name.Addr, f.dialer.PeerAPIHTTPClient(), fq.packet)
}
if strings.HasPrefix(rr.name.Addr, "https://") {
// Only known DoH providers are supported currently. Specifically, we
// only support DoH providers where we can TCP connect to them on port
// 443 at the same IP address they serve normal UDP DNS from (1.1.1.1,
// 8.8.8.8, 9.9.9.9, etc.) That's why OpenDNS and custon DoH providers
// aren't currently supported. There's no backup DNS resolution path for
// them.
urlBase := rr.name.Addr
if hc, ok := f.getKnownDoHClientForProvider(urlBase); ok {
return f.sendDoH(ctx, urlBase, hc, fq.packet)
}
metricDNSFwdErrorType.Add(1)
return nil, fmt.Errorf("https:// resolvers not supported yet")
return nil, fmt.Errorf("arbitrary https:// resolvers not supported yet")
}
if strings.HasPrefix(rr.name.Addr, "tls://") {
metricDNSFwdErrorType.Add(1)
return nil, fmt.Errorf("tls:// resolvers not supported yet")
}
ipp, err := netaddr.ParseIPPort(rr.name.Addr)
if err != nil {
return nil, err
}
// Upgrade known DNS IPs to DoH (DNS-over-HTTPs).
// All known DoH is over port 53.
if urlBase, dc, ok := f.getKnownDoHClient(ipp.IP()); ok {
res, err := f.sendDoH(ctx, urlBase, dc, fq.packet)
if err == nil || ctx.Err() != nil {
return res, err
}
f.logf("DoH error from %v: %v", ipp.IP(), err)
ipp, ok := rr.name.IPPort()
if !ok {
metricDNSFwdErrorType.Add(1)
return nil, fmt.Errorf("unrecognized resolver type %q", rr.name.Addr)
}
metricDNSFwdUDP.Add(1)
@@ -615,6 +603,10 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
return err
}
// Guarantee that the ctx we use below is done when this function returns.
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Drop DNS service discovery spam, primarily for battery life
// on mobile. Things like Spotify on iOS generate this traffic,
// when browsing for LAN devices. But even when filtering this
@@ -655,12 +647,8 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
}
defer fq.closeOnCtxDone.Close()
resc := make(chan []byte, 1)
var (
mu sync.Mutex
firstErr error
)
resc := make(chan []byte, 1) // it's fine buffered or not
errc := make(chan error, 1) // it's fine buffered or not too
for i := range resolvers {
go func(rr *resolverAndDelay) {
if rr.startDelay > 0 {
@@ -674,39 +662,48 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
}
resb, err := f.send(ctx, fq, *rr)
if err != nil {
mu.Lock()
defer mu.Unlock()
if firstErr == nil {
firstErr = err
select {
case errc <- err:
case <-ctx.Done():
}
return
}
select {
case resc <- resb:
default:
case <-ctx.Done():
}
}(&resolvers[i])
}
select {
case v := <-resc:
var firstErr error
var numErr int
for {
select {
case v := <-resc:
select {
case <-ctx.Done():
metricDNSFwdErrorContext.Add(1)
return ctx.Err()
case responseChan <- packet{v, query.addr}:
metricDNSFwdSuccess.Add(1)
return nil
}
case err := <-errc:
if firstErr == nil {
firstErr = err
}
numErr++
if numErr == len(resolvers) {
return firstErr
}
case <-ctx.Done():
metricDNSFwdErrorContext.Add(1)
if firstErr != nil {
metricDNSFwdErrorContextGotError.Add(1)
return firstErr
}
return ctx.Err()
case responseChan <- packet{v, query.addr}:
metricDNSFwdSuccess.Add(1)
return nil
}
case <-ctx.Done():
mu.Lock()
defer mu.Unlock()
metricDNSFwdErrorContext.Add(1)
if firstErr != nil {
metricDNSFwdErrorContextGotError.Add(1)
return firstErr
}
return ctx.Err()
}
}

View File

@@ -7,7 +7,6 @@ package resolver
import (
"flag"
"fmt"
"net"
"reflect"
"strings"
"testing"
@@ -25,11 +24,7 @@ func (rr resolverAndDelay) String() string {
func TestResolversWithDelays(t *testing.T) {
// query
q := func(ss ...string) (ipps []dnstype.Resolver) {
for _, s := range ss {
host, _, err := net.SplitHostPort(s)
if err != nil {
t.Fatal(err)
}
for _, host := range ss {
ipps = append(ipps, dnstype.Resolver{Addr: host})
}
return
@@ -46,12 +41,8 @@ func TestResolversWithDelays(t *testing.T) {
panic(fmt.Sprintf("parsing duration in %q: %v", s, err))
}
}
host, _, err := net.SplitHostPort(s)
if err != nil {
t.Fatal(err)
}
rr = append(rr, resolverAndDelay{
name: dnstype.Resolver{Addr: host},
name: dnstype.Resolver{Addr: s},
startDelay: d,
})
}
@@ -65,28 +56,28 @@ func TestResolversWithDelays(t *testing.T) {
}{
{
name: "unknown-no-delays",
in: q("1.2.3.4:53", "2.3.4.5:53"),
want: o("1.2.3.4:53", "2.3.4.5:53"),
in: q("1.2.3.4", "2.3.4.5"),
want: o("1.2.3.4", "2.3.4.5"),
},
{
name: "google-all-ipv4",
in: q("8.8.8.8:53", "8.8.4.4:53"),
want: o("8.8.8.8:53", "8.8.4.4:53+200ms"),
in: q("8.8.8.8", "8.8.4.4"),
want: o("https://dns.google/dns-query", "8.8.8.8+0.5s", "8.8.4.4+0.7s"),
},
{
name: "google-only-ipv6",
in: q("[2001:4860:4860::8888]:53", "[2001:4860:4860::8844]:53"),
want: o("[2001:4860:4860::8888]:53", "[2001:4860:4860::8844]:53+200ms"),
in: q("2001:4860:4860::8888", "2001:4860:4860::8844"),
want: o("https://dns.google/dns-query", "2001:4860:4860::8888+0.5s", "2001:4860:4860::8844+0.7s"),
},
{
name: "google-all-four",
in: q("8.8.8.8:53", "8.8.4.4:53", "[2001:4860:4860::8888]:53", "[2001:4860:4860::8844]:53"),
want: o("8.8.8.8:53", "8.8.4.4:53+200ms", "[2001:4860:4860::8888]:53+2.5s", "[2001:4860:4860::8844]:53+2.7s"),
in: q("8.8.8.8", "8.8.4.4", "2001:4860:4860::8888", "2001:4860:4860::8844"),
want: o("https://dns.google/dns-query", "8.8.8.8+0.5s", "8.8.4.4+0.7s", "2001:4860:4860::8888+0.5s", "2001:4860:4860::8844+0.7s"),
},
{
name: "quad9-one-v4-one-v6",
in: q("9.9.9.9:53", "[2620:fe::fe]:53"),
want: o("9.9.9.9:53", "[2620:fe::fe]:53+200ms"),
in: q("9.9.9.9", "2620:fe::fe"),
want: o("https://dns.quad9.net/dns-query", "9.9.9.9+0.5s", "2620:fe::fe+0.5s"),
},
}

View File

@@ -725,6 +725,22 @@ func (r *Resolver) resolveLocalReverse(name dnsname.FQDN) (dnsname.FQDN, dns.RCo
return "", dns.RCodeRefused
}
r.mu.Lock()
defer r.mu.Unlock()
// If the requested IP is part of the IPv6 4-to-6 range, it might
// correspond to an IPv4 address (assuming IPv4 is enabled).
if ip4, ok := tsaddr.Tailscale6to4(ip); ok {
fqdn, code := r.fqdnForIPLocked(ip4, name)
if code == dns.RCodeSuccess {
return fqdn, code
}
}
return r.fqdnForIPLocked(ip, name)
}
// r.mu must be held.
func (r *Resolver) fqdnForIPLocked(ip netaddr.IP, name dnsname.FQDN) (dnsname.FQDN, dns.RCode) {
// If someone curiously does a reverse lookup on the DNS IP, we
// return a domain that helps indicate that Tailscale is using
// this IP for a special purpose and it is not a node on their
@@ -733,8 +749,6 @@ func (r *Resolver) resolveLocalReverse(name dnsname.FQDN) (dnsname.FQDN, dns.RCo
return dnsSymbolicFQDN, dns.RCodeSuccess
}
r.mu.Lock()
defer r.mu.Unlock()
ret, ok := r.ipToHost[ip]
if !ok {
for _, suffix := range r.localDomains {

View File

@@ -382,6 +382,7 @@ func TestResolveLocalReverse(t *testing.T) {
{"ipv6_nxdomain", dnsname.FQDN("0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0.ip6.arpa."), "", dns.RCodeNameError},
{"nxdomain", dnsname.FQDN("2.3.4.5.in-addr.arpa."), "", dns.RCodeRefused},
{"magicdns", dnsname.FQDN("100.100.100.100.in-addr.arpa."), dnsSymbolicFQDN, dns.RCodeSuccess},
{"ipv6_4to6", dnsname.FQDN("4.6.4.6.4.6.2.6.6.9.d.c.3.4.8.4.2.1.b.a.0.e.1.a.c.5.1.1.a.7.d.f.ip6.arpa."), dnsSymbolicFQDN, dns.RCodeSuccess},
}
for _, tt := range tests {
@@ -755,6 +756,9 @@ func TestDelegateCollision(t *testing.T) {
if err != nil {
t.Error(err)
}
if len(ans) == 0 {
t.Fatal("no answers")
}
var wantType dns.Type
switch ans[0].Body.(type) {

View File

@@ -979,13 +979,21 @@ func (c *Client) runHTTPOnlyChecks(ctx context.Context, last *Report, rs *report
// One warm-up one to get HTTP connection set
// up and get a connection from the browser's
// pool.
if _, err := http.DefaultClient.Do(req); err != nil {
c.logf("probing %s: %v", node.HostName, err)
if r, err := http.DefaultClient.Do(req); err != nil || r.StatusCode > 299 {
if err != nil {
c.logf("probing %s: %v", node.HostName, err)
} else {
c.logf("probing %s: unexpected status %s", node.HostName, r.Status)
}
return
}
t0 := c.timeNow()
if _, err := http.DefaultClient.Do(req); err != nil {
c.logf("probing %s: %v", node.HostName, err)
if r, err := http.DefaultClient.Do(req); err != nil || r.StatusCode > 299 {
if err != nil {
c.logf("probing %s: %v", node.HostName, err)
} else {
c.logf("probing %s: unexpected status %s", node.HostName, r.Status)
}
return
}
d := c.timeNow().Sub(t0)
@@ -1005,7 +1013,7 @@ func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegio
var ip netaddr.IP
dc := derphttp.NewNetcheckClient(c.logf)
tlsConn, tcpConn, err := dc.DialRegionTLS(ctx, reg)
tlsConn, tcpConn, node, err := dc.DialRegionTLS(ctx, reg)
if err != nil {
return 0, ip, err
}
@@ -1036,7 +1044,7 @@ func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegio
}
hc := &http.Client{Transport: tr}
req, err := http.NewRequestWithContext(ctx, "GET", "https://derp-unused-hostname.tld/derp/latency-check", nil)
req, err := http.NewRequestWithContext(ctx, "GET", "https://" + node.HostName + "/derp/latency-check", nil)
if err != nil {
return 0, ip, err
}
@@ -1047,6 +1055,13 @@ func (c *Client) measureHTTPSLatency(ctx context.Context, reg *tailcfg.DERPRegio
}
defer resp.Body.Close()
// DERPs should give us a nominal status code, so anything else is probably
// an access denied by a MITM proxy (or at the very least a signal not to
// trust this latency check).
if resp.StatusCode > 299 {
return 0, ip, fmt.Errorf("unexpected status code: %d (%s)", resp.StatusCode, resp.Status)
}
_, err = io.Copy(ioutil.Discard, io.LimitReader(resp.Body, 8<<10))
if err != nil {
return 0, ip, err

View File

@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build linux && !android
// +build linux,!android
package netns
import (

View File

@@ -6,6 +6,8 @@
package tsaddr
import (
"encoding/binary"
"errors"
"sync"
"inet.af/netaddr"
@@ -126,6 +128,18 @@ func Tailscale4To6(ipv4 netaddr.IP) netaddr.IP {
return netaddr.IPFrom16(ret)
}
// Tailscale6to4 returns the IPv4 address corresponding to the given
// tailscale IPv6 address within the 4To6 range. The IPv4 address
// and true are returned if the given address was in the correct range,
// false if not.
func Tailscale6to4(ipv6 netaddr.IP) (netaddr.IP, bool) {
if !ipv6.Is6() || !Tailscale4To6Range().Contains(ipv6) {
return netaddr.IP{}, false
}
v6 := ipv6.As16()
return netaddr.IPv4(100, v6[13], v6[14], v6[15]), true
}
func mustPrefix(v *netaddr.IPPrefix, prefix string) {
var err error
*v, err = netaddr.ParseIPPrefix(prefix)
@@ -280,3 +294,17 @@ func UnmapVia(ip netaddr.IP) netaddr.IP {
}
return ip
}
// MapVia returns an IPv6 "via" route for an IPv4 CIDR in a given siteID.
func MapVia(siteID uint32, v4 netaddr.IPPrefix) (via netaddr.IPPrefix, err error) {
if !v4.IP().Is4() {
return via, errors.New("want IPv4 CIDR with a site ID")
}
viaRange16 := TailscaleViaRange().IP().As16()
var a [16]byte
copy(a[:], viaRange16[:8])
binary.BigEndian.PutUint32(a[8:], siteID)
ip4a := v4.IP().As4()
copy(a[12:], ip4a[:])
return netaddr.IPPrefixFrom(netaddr.IPFrom16(a), v4.Bits()+64+32), nil
}

View File

@@ -41,16 +41,11 @@ main() {
# - ID: the short name of the OS (e.g. "debian", "freebsd")
# - VERSION_ID: the numeric release version for the OS, if any (e.g. "18.04")
# - VERSION_CODENAME: the codename of the OS release, if any (e.g. "buster")
# - UBUNTU_CODENAME: if it exists, as in linuxmint, use instead of VERSION_CODENAME
. /etc/os-release
case "$ID" in
ubuntu|pop|neon|zorin|elementary|linuxmint)
ubuntu|pop|neon|zorin|elementary)
OS="ubuntu"
if [ "${UBUNTU_CODENAME:-}" != "" ]; then
VERSION="$UBUNTU_CODENAME"
else
VERSION="$VERSION_CODENAME"
fi
VERSION="$VERSION_CODENAME"
PACKAGETYPE="apt"
# Third-party keyrings became the preferred method of
# installation in Ubuntu 20.04.
@@ -72,6 +67,35 @@ main() {
APT_KEY_TYPE="keyring"
fi
;;
linuxmint)
if [ "${UBUNTU_CODENAME:-}" != "" ]; then
OS="ubuntu"
VERSION="$UBUNTU_CODENAME"
elif [ "${DEBIAN_CODENAME:-}" != "" ]; then
OS="debian"
VERSION="$DEBIAN_CODENAME"
else
OS="ubuntu"
VERSION="$VERSION_CODENAME"
fi
PACKAGETYPE="apt"
if [ "$VERSION_ID" -lt 5 ]; then
APT_KEY_TYPE="legacy"
else
APT_KEY_TYPE="keyring"
fi
;;
parrot)
OS="debian"
PACKAGETYPE="apt"
if [ "$VERSION_ID" -lt 5 ]; then
VERSION="buster"
APT_KEY_TYPE="legacy"
else
VERSION="bullseye"
APT_KEY_TYPE="keyring"
fi
;;
raspbian)
OS="$ID"
VERSION="$VERSION_CODENAME"
@@ -127,7 +151,7 @@ main() {
VERSION=""
PACKAGETYPE="dnf"
;;
rocky)
rocky|almalinux)
OS="fedora"
VERSION=""
PACKAGETYPE="dnf"
@@ -474,7 +498,7 @@ main() {
;;
emerge)
set -x
$SUDO emerge net-vpn/tailscale
$SUDO emerge --ask=n net-vpn/tailscale
set +x
;;
appstore)

View File

@@ -59,11 +59,11 @@ var maybeStartLoginSession = func(logf logger.Logf, uid uint32, localUser, remot
// If ss.srv.tailscaledPath is empty, this method is equivalent to
// exec.CommandContext.
func (ss *sshSession) newIncubatorCommand(ctx context.Context, name string, args []string) *exec.Cmd {
if ss.srv.tailscaledPath == "" {
if ss.conn.srv.tailscaledPath == "" {
return exec.CommandContext(ctx, name, args...)
}
lu := ss.localUser
ci := ss.connInfo
lu := ss.conn.localUser
ci := ss.conn.info
remoteUser := ci.uprof.LoginName
if len(ci.node.Tags) > 0 {
remoteUser = strings.Join(ci.node.Tags, ",")
@@ -85,7 +85,7 @@ func (ss *sshSession) newIncubatorCommand(ctx context.Context, name string, args
incubatorArgs = append(incubatorArgs, args...)
}
return exec.CommandContext(ctx, ss.srv.tailscaledPath, incubatorArgs...)
return exec.CommandContext(ctx, ss.conn.srv.tailscaledPath, incubatorArgs...)
}
const debugIncubator = false
@@ -166,7 +166,7 @@ func beIncubator(args []string) error {
//
// It sets ss.cmd, stdin, stdout, and stderr.
func (ss *sshSession) launchProcess(ctx context.Context) error {
shell := loginShell(ss.localUser.Uid)
shell := loginShell(ss.conn.localUser.Uid)
var args []string
if rawCmd := ss.RawCommand(); rawCmd != "" {
args = append(args, "-c", rawCmd)
@@ -174,10 +174,10 @@ func (ss *sshSession) launchProcess(ctx context.Context) error {
args = append(args, "-l") // login shell
}
ci := ss.connInfo
ci := ss.conn.info
cmd := ss.newIncubatorCommand(ctx, shell, args)
cmd.Dir = ss.localUser.HomeDir
cmd.Env = append(cmd.Env, envForUser(ss.localUser)...)
cmd.Dir = ss.conn.localUser.HomeDir
cmd.Env = append(cmd.Env, envForUser(ss.conn.localUser)...)
cmd.Env = append(cmd.Env, ss.Environ()...)
cmd.Env = append(cmd.Env,
fmt.Sprintf("SSH_CLIENT=%s %d %d", ci.src.IP(), ci.src.Port(), ci.dst.Port()),

View File

@@ -43,8 +43,8 @@ import (
)
var (
debugPolicyFile = envknob.String("TS_DEBUG_SSH_POLICY_FILE")
debugIgnoreTailnetSSHPolicy = envknob.Bool("TS_DEBUG_SSH_IGNORE_TAILNET_POLICY")
debugPolicyFile = envknob.SSHPolicyFile()
debugIgnoreTailnetSSHPolicy = envknob.SSHIgnoreTailnetPolicy()
sshVerboseLogging = envknob.Bool("TS_DEBUG_SSH_VLOG")
)
@@ -87,7 +87,7 @@ func init() {
// HandleSSHConn handles a Tailscale SSH connection from c.
func (srv *server) HandleSSHConn(c net.Conn) error {
ss, err := srv.newSSHServer()
ss, err := srv.newConn()
if err != nil {
return err
}
@@ -109,34 +109,135 @@ func (srv *server) OnPolicyChange() {
}
}
func (srv *server) newSSHServer() (*ssh.Server, error) {
ss := &ssh.Server{
Handler: srv.handleSSH,
// conn represents a single SSH connection and its associated
// ssh.Server.
type conn struct {
*ssh.Server
// now is the time to consider the present moment for the
// purposes of rule evaluation.
now time.Time
action0 *tailcfg.SSHAction // first matching action
srv *server
info *sshConnInfo // set by setInfo
localUser *user.User // set by checkAuth
insecureSkipTailscaleAuth bool // used by tests.
}
func (c *conn) logf(format string, args ...any) {
if c.info == nil {
c.srv.logf(format, args...)
return
}
format = fmt.Sprintf("%v: %v", c.info.String(), format)
c.srv.logf(format, args...)
}
// PublicKeyHandler implements ssh.PublicKeyHandler is called by the the
// ssh.Server when the client presents a public key.
func (c *conn) PublicKeyHandler(ctx ssh.Context, pubKey ssh.PublicKey) error {
if c.info == nil {
return gossh.ErrDenied
}
if err := c.checkAuth(pubKey); err != nil {
// TODO(maisem/bradfitz): surface the error here.
c.logf("rejecting SSH public key %s: %v", bytes.TrimSpace(gossh.MarshalAuthorizedKey(pubKey)), err)
return err
}
c.logf("accepting SSH public key %s", bytes.TrimSpace(gossh.MarshalAuthorizedKey(pubKey)))
return nil
}
// errPubKeyRequired is returned by NoClientAuthCallback to make the client
// resort to public-key auth; not user visible.
var errPubKeyRequired = errors.New("ssh publickey required")
// NoClientAuthCallback implements gossh.NoClientAuthCallback and is called by
// the the ssh.Server when the client first connects with the "none"
// authentication method.
func (c *conn) NoClientAuthCallback(cm gossh.ConnMetadata) (*gossh.Permissions, error) {
if c.insecureSkipTailscaleAuth {
return nil, nil
}
if err := c.setInfo(cm); err != nil {
c.logf("failed to get conninfo: %v", err)
return nil, gossh.ErrDenied
}
return nil, c.checkAuth(nil /* no pub key */)
}
// checkAuth verifies that conn can proceed with the specified (optional)
// pubKey. It returns nil if the matching policy action is Accept or
// HoldAndDelegate. If pubKey is nil, there was no policy match but there is a
// policy that might match a public key it returns errPubKeyRequired. Otherwise,
// it returns gossh.ErrDenied possibly wrapped in gossh.WithBannerError.
func (c *conn) checkAuth(pubKey ssh.PublicKey) error {
a, localUser, err := c.evaluatePolicy(pubKey)
if err != nil {
if pubKey == nil && c.havePubKeyPolicy(c.info) {
return errPubKeyRequired
}
return fmt.Errorf("%w: %v", gossh.ErrDenied, err)
}
c.action0 = a
if a.Accept || a.HoldAndDelegate != "" {
lu, err := user.Lookup(localUser)
if err != nil {
c.logf("failed to lookup %v: %v", localUser, err)
return gossh.WithBannerError{
Err: gossh.ErrDenied,
Message: fmt.Sprintf("failed to lookup %v\r\n", localUser),
}
}
c.localUser = lu
return nil
}
if a.Reject {
err := gossh.ErrDenied
if a.Message != "" {
err = gossh.WithBannerError{
Err: err,
Message: a.Message,
}
}
return err
}
// Shouldn't get here, but:
return gossh.ErrDenied
}
// ServerConfig implements ssh.ServerConfigCallback.
func (c *conn) ServerConfig(ctx ssh.Context) *gossh.ServerConfig {
return &gossh.ServerConfig{
// OpenSSH presents this on failure as `Permission denied (tailscale).`
ImplictAuthMethod: "tailscale",
NoClientAuth: true, // required for the NoClientAuthCallback to run
NoClientAuthCallback: c.NoClientAuthCallback,
}
}
func (srv *server) newConn() (*conn, error) {
c := &conn{srv: srv, now: srv.now()}
c.Server = &ssh.Server{
Version: "Tailscale",
Handler: c.handleConnPostSSHAuth,
RequestHandlers: map[string]ssh.RequestHandler{},
SubsystemHandlers: map[string]ssh.SubsystemHandler{},
// Note: the direct-tcpip channel handler and LocalPortForwardingCallback
// only adds support for forwarding ports from the local machine.
// TODO(maisem/bradfitz): add remote port forwarding support.
ChannelHandlers: map[string]ssh.ChannelHandler{
"direct-tcpip": ssh.DirectTCPIPHandler,
},
Version: "SSH-2.0-Tailscale",
LocalPortForwardingCallback: srv.mayForwardLocalPortTo,
NoClientAuthCallback: func(m gossh.ConnMetadata) (*gossh.Permissions, error) {
if srv.requiresPubKey(m.User(), toIPPort(m.LocalAddr()), toIPPort(m.RemoteAddr())) {
return nil, errors.New("public key required") // any non-nil error will do
}
return nil, nil
},
PublicKeyHandler: func(ctx ssh.Context, key ssh.PublicKey) bool {
if srv.acceptPubKey(ctx.User(), toIPPort(ctx.LocalAddr()), toIPPort(ctx.RemoteAddr()), key) {
srv.logf("accepting SSH public key %s", bytes.TrimSpace(gossh.MarshalAuthorizedKey(key)))
return true
}
srv.logf("rejecting SSH public key %s", bytes.TrimSpace(gossh.MarshalAuthorizedKey(key)))
return false
},
PublicKeyHandler: c.PublicKeyHandler,
ServerConfigCallback: c.ServerConfig,
}
ss := c.Server
for k, v := range ssh.DefaultRequestHandlers {
ss.RequestHandlers[k] = v
}
@@ -153,7 +254,7 @@ func (srv *server) newSSHServer() (*ssh.Server, error) {
for _, signer := range keys {
ss.AddHostKey(signer)
}
return ss, nil
return c, nil
}
// mayForwardLocalPortTo reports whether the ctx should be allowed to port forward
@@ -167,37 +268,24 @@ func (srv *server) mayForwardLocalPortTo(ctx ssh.Context, destinationHost string
return ss.action.AllowLocalPortForwarding
}
// requiresPubKey reports whether the SSH server, during the auth negotiation
// phase, should requires that the client send an SSH public key. (or, more
// specifically, that "none" auth isn't acceptable)
func (srv *server) requiresPubKey(sshUser string, localAddr, remoteAddr netaddr.IPPort) bool {
pol, ok := srv.sshPolicy()
// havePubKeyPolicy reports whether any policy rule may provide access by means
// of a ssh.PublicKey.
func (c *conn) havePubKeyPolicy(ci *sshConnInfo) bool {
// Is there any rule that looks like it'd require a public key for this
// sshUser?
pol, ok := c.sshPolicy()
if !ok {
return false
}
a, ci, _, err := srv.evaluatePolicy(sshUser, localAddr, remoteAddr, nil)
if err == nil && (a.Accept || a.HoldAndDelegate != "") {
// Policy doesn't require a public key.
return false
}
if ci == nil {
// If we didn't get far enough along through evaluatePolicy to know the Tailscale
// identify of the remote side then it's going to fail quickly later anyway.
// Return false to accept "none" auth and reject the conn.
return false
}
// Is there any rule that looks like it'd require a public key for this
// sshUser?
for _, r := range pol.Rules {
if ci.ruleExpired(r) {
if c.ruleExpired(r) {
continue
}
if mapLocalUser(r.SSHUsers, sshUser) == "" {
if mapLocalUser(r.SSHUsers, ci.sshUser) == "" {
continue
}
for _, p := range r.Principals {
if principalMatchesTailscaleIdentity(p, ci) && len(p.PubKeys) > 0 {
if len(p.PubKeys) > 0 && c.principalMatchesTailscaleIdentity(p) {
return true
}
}
@@ -205,19 +293,11 @@ func (srv *server) requiresPubKey(sshUser string, localAddr, remoteAddr netaddr.
return false
}
func (srv *server) acceptPubKey(sshUser string, localAddr, remoteAddr netaddr.IPPort, pubKey ssh.PublicKey) bool {
a, _, _, err := srv.evaluatePolicy(sshUser, localAddr, remoteAddr, pubKey)
if err != nil {
return false
}
return a.Accept || a.HoldAndDelegate != ""
}
// sshPolicy returns the SSHPolicy for current node.
// If there is no SSHPolicy in the netmap, it returns a debugPolicy
// if one is defined.
func (srv *server) sshPolicy() (_ *tailcfg.SSHPolicy, ok bool) {
lb := srv.lb
func (c *conn) sshPolicy() (_ *tailcfg.SSHPolicy, ok bool) {
lb := c.srv.lb
nm := lb.NetMap()
if nm == nil {
return nil, false
@@ -226,14 +306,15 @@ func (srv *server) sshPolicy() (_ *tailcfg.SSHPolicy, ok bool) {
return pol, true
}
if debugPolicyFile != "" {
c.logf("reading debug SSH policy file: %v", debugPolicyFile)
f, err := os.ReadFile(debugPolicyFile)
if err != nil {
srv.logf("error reading debug SSH policy file: %v", err)
c.logf("error reading debug SSH policy file: %v", err)
return nil, false
}
p := new(tailcfg.SSHPolicy)
if err := json.Unmarshal(f, p); err != nil {
srv.logf("invalid JSON in %v: %v", debugPolicyFile, err)
c.logf("invalid JSON in %v: %v", debugPolicyFile, err)
return nil, false
}
return p, true
@@ -253,42 +334,45 @@ func toIPPort(a net.Addr) (ipp netaddr.IPPort) {
return netaddr.IPPortFrom(tanetaddr, uint16(ta.Port))
}
// connInfo returns a populated sshConnInfo from the provided arguments,
// validating only that they represent a known Tailscale identity.
func (c *conn) setInfo(cm gossh.ConnMetadata) error {
ci := &sshConnInfo{
sshUser: cm.User(),
src: toIPPort(cm.RemoteAddr()),
dst: toIPPort(cm.LocalAddr()),
}
if !tsaddr.IsTailscaleIP(ci.dst.IP()) {
return fmt.Errorf("tailssh: rejecting non-Tailscale local address %v", ci.dst)
}
if !tsaddr.IsTailscaleIP(ci.src.IP()) {
return fmt.Errorf("tailssh: rejecting non-Tailscale remote address %v", ci.src)
}
node, uprof, ok := c.srv.lb.WhoIs(ci.src)
if !ok {
return fmt.Errorf("unknown Tailscale identity from src %v", ci.src)
}
ci.node = node
ci.uprof = &uprof
c.info = ci
return nil
}
// evaluatePolicy returns the SSHAction, sshConnInfo and localUser after
// evaluating the sshUser and remoteAddr against the SSHPolicy. The remoteAddr
// and localAddr params must be Tailscale IPs.
//
// The return sshConnInfo will be non-nil, even on some errors, if the
// evaluation made it far enough to resolve the remoteAddr to a Tailscale IP.
func (srv *server) evaluatePolicy(sshUser string, localAddr, remoteAddr netaddr.IPPort, pubKey ssh.PublicKey) (_ *tailcfg.SSHAction, _ *sshConnInfo, localUser string, _ error) {
pol, ok := srv.sshPolicy()
// and localAddr params must be Tailscale IPs. The pubKey may be nil for "none"
// auth.
func (c *conn) evaluatePolicy(pubKey gossh.PublicKey) (_ *tailcfg.SSHAction, localUser string, _ error) {
pol, ok := c.sshPolicy()
if !ok {
return nil, nil, "", fmt.Errorf("tailssh: rejecting connection; no SSH policy")
return nil, "", fmt.Errorf("tailssh: rejecting connection; no SSH policy")
}
if !tsaddr.IsTailscaleIP(remoteAddr.IP()) {
return nil, nil, "", fmt.Errorf("tailssh: rejecting non-Tailscale remote address %v", remoteAddr)
}
if !tsaddr.IsTailscaleIP(localAddr.IP()) {
return nil, nil, "", fmt.Errorf("tailssh: rejecting non-Tailscale remote address %v", localAddr)
}
node, uprof, ok := srv.lb.WhoIs(remoteAddr)
a, localUser, ok := c.evalSSHPolicy(pol, pubKey)
if !ok {
return nil, nil, "", fmt.Errorf("unknown Tailscale identity from src %v", remoteAddr)
return nil, "", fmt.Errorf("tailssh: rejecting connection; no matching policy")
}
ci := &sshConnInfo{
now: srv.now(),
fetchPublicKeysURL: srv.fetchPublicKeysURL,
sshUser: sshUser,
src: remoteAddr,
dst: localAddr,
node: node,
uprof: &uprof,
pubKey: pubKey,
}
a, localUser, ok := evalSSHPolicy(pol, ci)
if !ok {
return nil, ci, "", fmt.Errorf("ssh: access denied for %q from %v", uprof.LoginName, ci.src.IP())
}
return a, ci, localUser, nil
return a, localUser, nil
}
// pubKeyCacheEntry is the cache value for an HTTPS URL of public keys (like
@@ -334,6 +418,9 @@ func (srv *server) pubKeyClient() *http.Client {
return http.DefaultClient
}
// fetchPublicKeysURL fetches the public keys from a URL. The strings are in the
// the typical public key "type base64-string [comment]" format seen at e.g.
// https://github.com/USER.keys
func (srv *server) fetchPublicKeysURL(url string) ([]string, error) {
if !strings.HasPrefix(url, "https://") {
return nil, errors.New("invalid URL scheme")
@@ -386,52 +473,38 @@ func (srv *server) fetchPublicKeysURL(url string) ([]string, error) {
return lines, err
}
// handleSSH is invoked when a new SSH connection attempt is made.
func (srv *server) handleSSH(s ssh.Session) {
logf := srv.logf
// handleConnPostSSHAuth runs an SSH session after the SSH-level authentication,
// but not necessarily before all the Tailscale-level extra verification has
// completed.
func (c *conn) handleConnPostSSHAuth(s ssh.Session) {
sshUser := s.User()
action, ci, localUser, err := srv.evaluatePolicy(sshUser, toIPPort(s.LocalAddr()), toIPPort(s.RemoteAddr()), s.PublicKey())
action, err := c.resolveTerminalAction(s)
if err != nil {
logf(err.Error())
s.Exit(1)
return
}
var lu *user.User
if localUser != "" {
lu, err = user.Lookup(localUser)
if err != nil {
logf("ssh: user Lookup %q: %v", localUser, err)
s.Exit(1)
return
}
}
ss := srv.newSSHSession(s, ci, lu)
ss.logf("handling new SSH connection from %v (%v) to ssh-user %q", ci.uprof.LoginName, ci.src.IP(), sshUser)
action, err = ss.resolveTerminalAction(action)
if err != nil {
ss.logf("resolveTerminalAction: %v", err)
io.WriteString(s.Stderr(), "Access denied: failed to resolve SSHAction.\n")
c.logf("resolveTerminalAction: %v", err)
io.WriteString(s.Stderr(), "Access Denied: failed during authorization check.\r\n")
s.Exit(1)
return
}
if action.Reject || !action.Accept {
ss.logf("access denied for %v (%v)", ci.uprof.LoginName, ci.src.IP())
c.logf("access denied for %v", c.info.uprof.LoginName)
s.Exit(1)
return
}
ss.logf("access granted for %v (%v) to ssh-user %q", ci.uprof.LoginName, ci.src.IP(), sshUser)
ss.action = action
ss := c.newSSHSession(s, action)
ss.logf("handling new SSH connection from %v (%v) to ssh-user %q", c.info.uprof.LoginName, c.info.src.IP(), sshUser)
ss.logf("access granted to %v as ssh-user %q", c.info.uprof.LoginName, sshUser)
ss.run()
}
// resolveTerminalAction either returns action (if it's Accept or Reject) or else
// loops, fetching new SSHActions from the control plane.
// resolveTerminalAction either returns action0 (if it's Accept or Reject) or
// else loops, fetching new SSHActions from the control plane.
//
// Any action with a Message in the chain will be printed to ss.
// Any action with a Message in the chain will be printed to s.
//
// The returned SSHAction will be either Reject or Accept.
func (ss *sshSession) resolveTerminalAction(action *tailcfg.SSHAction) (*tailcfg.SSHAction, error) {
func (c *conn) resolveTerminalAction(s ssh.Session) (*tailcfg.SSHAction, error) {
action := c.action0
// Loop processing/fetching Actions until one reaches a
// terminal state (Accept, Reject, or invalid Action), or
// until fetchSSHAction times out due to the context being
@@ -440,7 +513,7 @@ func (ss *sshSession) resolveTerminalAction(action *tailcfg.SSHAction) (*tailcfg
// instructions and go to a URL to do something.)
for {
if action.Message != "" {
io.WriteString(ss.Stderr(), strings.Replace(action.Message, "\n", "\r\n", -1))
io.WriteString(s.Stderr(), strings.Replace(action.Message, "\n", "\r\n", -1))
}
if action.Accept || action.Reject {
return action, nil
@@ -449,31 +522,48 @@ func (ss *sshSession) resolveTerminalAction(action *tailcfg.SSHAction) (*tailcfg
if url == "" {
return nil, errors.New("reached Action that lacked Accept, Reject, and HoldAndDelegate")
}
url = ss.expandDelegateURL(url)
url = c.expandDelegateURL(url)
var err error
action, err = ss.srv.fetchSSHAction(ss.Context(), url)
action, err = c.fetchSSHAction(s.Context(), url)
if err != nil {
return nil, fmt.Errorf("fetching SSHAction from %s: %w", url, err)
}
}
}
func (ss *sshSession) expandDelegateURL(actionURL string) string {
nm := ss.srv.lb.NetMap()
func (c *conn) expandDelegateURL(actionURL string) string {
nm := c.srv.lb.NetMap()
ci := c.info
var dstNodeID string
if nm != nil {
dstNodeID = fmt.Sprint(int64(nm.SelfNode.ID))
}
return strings.NewReplacer(
"$SRC_NODE_IP", url.QueryEscape(ss.connInfo.src.IP().String()),
"$SRC_NODE_ID", fmt.Sprint(int64(ss.connInfo.node.ID)),
"$DST_NODE_IP", url.QueryEscape(ss.connInfo.dst.IP().String()),
"$SRC_NODE_IP", url.QueryEscape(ci.src.IP().String()),
"$SRC_NODE_ID", fmt.Sprint(int64(ci.node.ID)),
"$DST_NODE_IP", url.QueryEscape(ci.dst.IP().String()),
"$DST_NODE_ID", dstNodeID,
"$SSH_USER", url.QueryEscape(ss.connInfo.sshUser),
"$LOCAL_USER", url.QueryEscape(ss.localUser.Username),
"$SSH_USER", url.QueryEscape(ci.sshUser),
"$LOCAL_USER", url.QueryEscape(c.localUser.Username),
).Replace(actionURL)
}
func (c *conn) expandPublicKeyURL(pubKeyURL string) string {
if !strings.Contains(pubKeyURL, "$") {
return pubKeyURL
}
var localPart string
var loginName string
if c.info.uprof != nil {
loginName = c.info.uprof.LoginName
localPart, _, _ = strings.Cut(loginName, "@")
}
return strings.NewReplacer(
"$LOGINNAME_EMAIL", loginName,
"$LOGINNAME_LOCALPART", localPart,
).Replace(pubKeyURL)
}
// sshSession is an accepted Tailscale SSH session.
type sshSession struct {
ssh.Session
@@ -482,10 +572,8 @@ type sshSession struct {
logf logger.Logf
ctx *sshContext // implements context.Context
srv *server
connInfo *sshConnInfo
conn *conn
action *tailcfg.SSHAction
localUser *user.User
agentListener net.Listener // non-nil if agent-forwarding requested+allowed
// initialized by launchProcess:
@@ -506,39 +594,48 @@ func (ss *sshSession) vlogf(format string, args ...interface{}) {
}
}
func (srv *server) newSSHSession(s ssh.Session, ci *sshConnInfo, lu *user.User) *sshSession {
sharedID := fmt.Sprintf("%s-%02x", ci.now.UTC().Format("20060102T150405"), randBytes(5))
func (c *conn) newSSHSession(s ssh.Session, action *tailcfg.SSHAction) *sshSession {
sharedID := fmt.Sprintf("%s-%02x", c.now.UTC().Format("20060102T150405"), randBytes(5))
c.logf("starting session: %v", sharedID)
return &sshSession{
Session: s,
idH: s.Context().(ssh.Context).SessionID(),
sharedID: sharedID,
ctx: newSSHContext(),
srv: srv,
localUser: lu,
connInfo: ci,
logf: logger.WithPrefix(srv.logf, "ssh-session("+sharedID+"): "),
Session: s,
idH: s.Context().(ssh.Context).SessionID(),
sharedID: sharedID,
ctx: newSSHContext(),
conn: c,
logf: logger.WithPrefix(c.srv.logf, "ssh-session("+sharedID+"): "),
action: action,
}
}
func (c *conn) isStillValid(pubKey ssh.PublicKey) bool {
a, localUser, err := c.evaluatePolicy(pubKey)
if err != nil {
return false
}
if !a.Accept && a.HoldAndDelegate == "" {
return false
}
return c.localUser.Username == localUser
}
// checkStillValid checks that the session is still valid per the latest SSHPolicy.
// If not, it terminates the session.
func (ss *sshSession) checkStillValid() {
ci := ss.connInfo
a, _, lu, err := ss.srv.evaluatePolicy(ci.sshUser, ci.src, ci.dst, ci.pubKey)
if err == nil && (a.Accept || a.HoldAndDelegate != "") && lu == ss.localUser.Username {
if ss.conn.isStillValid(ss.PublicKey()) {
return
}
ss.logf("session no longer valid per new SSH policy; closing")
ss.ctx.CloseWithError(userVisibleError{
fmt.Sprintf("Access revoked.\n"),
fmt.Sprintf("Access revoked.\r\n"),
context.Canceled,
})
}
func (srv *server) fetchSSHAction(ctx context.Context, url string) (*tailcfg.SSHAction, error) {
func (c *conn) fetchSSHAction(ctx context.Context, url string) (*tailcfg.SSHAction, error) {
ctx, cancel := context.WithTimeout(ctx, 30*time.Minute)
defer cancel()
bo := backoff.NewBackoff("fetch-ssh-action", srv.logf, 10*time.Second)
bo := backoff.NewBackoff("fetch-ssh-action", c.logf, 10*time.Second)
for {
if err := ctx.Err(); err != nil {
return nil, err
@@ -547,7 +644,7 @@ func (srv *server) fetchSSHAction(ctx context.Context, url string) (*tailcfg.SSH
if err != nil {
return nil, err
}
res, err := srv.lb.DoNoiseRequest(req)
res, err := c.srv.lb.DoNoiseRequest(req)
if err != nil {
bo.BackOff(ctx, err)
continue
@@ -558,7 +655,7 @@ func (srv *server) fetchSSHAction(ctx context.Context, url string) (*tailcfg.SSH
if len(body) > 1<<10 {
body = body[:1<<10]
}
srv.logf("fetch of %v: %s, %s", url, res.Status, body)
c.logf("fetch of %v: %s, %s", url, res.Status, body)
bo.BackOff(ctx, fmt.Errorf("unexpected status: %v", res.Status))
continue
}
@@ -566,7 +663,7 @@ func (srv *server) fetchSSHAction(ctx context.Context, url string) (*tailcfg.SSH
err = json.NewDecoder(res.Body).Decode(a)
res.Body.Close()
if err != nil {
srv.logf("invalid next SSHAction JSON from %v: %v", url, err)
c.logf("invalid next SSHAction JSON from %v: %v", url, err)
bo.BackOff(ctx, err)
continue
}
@@ -588,7 +685,7 @@ func (ss *sshSession) killProcessOnContextDone() {
io.WriteString(ss.Stderr(), "\r\n\r\n"+msg+"\r\n\r\n")
}
}
ss.logf("terminating SSH session from %v: %v", ss.connInfo.src.IP(), err)
ss.logf("terminating SSH session from %v: %v", ss.conn.info.src.IP(), err)
ss.cmd.Process.Kill()
})
}
@@ -683,7 +780,7 @@ var recordSSH = envknob.Bool("TS_DEBUG_LOG_SSH")
// It handles ss once it's been accepted and determined
// that it should run.
func (ss *sshSession) run() {
srv := ss.srv
srv := ss.conn.srv
srv.startSession(ss)
defer srv.endSession(ss)
@@ -700,13 +797,13 @@ func (ss *sshSession) run() {
}
logf := srv.logf
lu := ss.localUser
lu := ss.conn.localUser
localUser := lu.Username
if euid := os.Geteuid(); euid != 0 {
if lu.Uid != fmt.Sprint(euid) {
ss.logf("can't switch to user %q from process euid %v", localUser, euid)
fmt.Fprintf(ss, "can't switch user\n")
fmt.Fprintf(ss, "can't switch user\r\n")
ss.Exit(1)
return
}
@@ -728,7 +825,7 @@ func (ss *sshSession) run() {
var err error
rec, err = ss.startNewRecording()
if err != nil {
fmt.Fprintf(ss, "can't start new recording\n")
fmt.Fprintf(ss, "can't start new recording\r\n")
ss.logf("startNewRecording: %v", err)
ss.Exit(1)
return
@@ -801,14 +898,6 @@ func (ss *sshSession) shouldRecord() bool {
}
type sshConnInfo struct {
// now is the time to consider the present moment for the
// purposes of rule evaluation.
now time.Time
// fetchPublicKeysURL, if non-nil, is a func to fetch the public
// keys of a URL. The strings are in the the typical public
// key "type base64-string [comment]" format seen at e.g. https://github.com/USER.keys
fetchPublicKeysURL func(url string) ([]string, error)
// sshUser is the requested local SSH username ("root", "alice", etc).
sshUser string
@@ -823,23 +912,22 @@ type sshConnInfo struct {
// uprof is node's UserProfile.
uprof *tailcfg.UserProfile
// pubKey is the public key presented by the client, or nil
// if they haven't yet sent one (as in the early "none" phase
// of authentication negotiation).
pubKey ssh.PublicKey
}
func (ci *sshConnInfo) ruleExpired(r *tailcfg.SSHRule) bool {
func (ci *sshConnInfo) String() string {
return fmt.Sprintf("%v->%v@%v", ci.src, ci.sshUser, ci.dst)
}
func (c *conn) ruleExpired(r *tailcfg.SSHRule) bool {
if r.RuleExpires == nil {
return false
}
return r.RuleExpires.Before(ci.now)
return r.RuleExpires.Before(c.now)
}
func evalSSHPolicy(pol *tailcfg.SSHPolicy, ci *sshConnInfo) (a *tailcfg.SSHAction, localUser string, ok bool) {
func (c *conn) evalSSHPolicy(pol *tailcfg.SSHPolicy, pubKey gossh.PublicKey) (a *tailcfg.SSHAction, localUser string, ok bool) {
for _, r := range pol.Rules {
if a, localUser, err := matchRule(r, ci); err == nil {
if a, localUser, err := c.matchRule(r, pubKey); err == nil {
return a, localUser, true
}
}
@@ -855,23 +943,25 @@ var (
errUserMatch = errors.New("user didn't match")
)
func matchRule(r *tailcfg.SSHRule, ci *sshConnInfo) (a *tailcfg.SSHAction, localUser string, err error) {
func (c *conn) matchRule(r *tailcfg.SSHRule, pubKey gossh.PublicKey) (a *tailcfg.SSHAction, localUser string, err error) {
if r == nil {
return nil, "", errNilRule
}
if r.Action == nil {
return nil, "", errNilAction
}
if ci.ruleExpired(r) {
if c.ruleExpired(r) {
return nil, "", errRuleExpired
}
if !r.Action.Reject || r.SSHUsers != nil {
localUser = mapLocalUser(r.SSHUsers, ci.sshUser)
localUser = mapLocalUser(r.SSHUsers, c.info.sshUser)
if localUser == "" {
return nil, "", errUserMatch
}
}
if !anyPrincipalMatches(r.Principals, ci) {
if ok, err := c.anyPrincipalMatches(r.Principals, pubKey); err != nil {
return nil, "", err
} else if !ok {
return nil, "", errPrincipalMatch
}
return r.Action, localUser, nil
@@ -888,27 +978,32 @@ func mapLocalUser(ruleSSHUsers map[string]string, reqSSHUser string) (localUser
return v
}
func anyPrincipalMatches(ps []*tailcfg.SSHPrincipal, ci *sshConnInfo) bool {
func (c *conn) anyPrincipalMatches(ps []*tailcfg.SSHPrincipal, pubKey gossh.PublicKey) (bool, error) {
for _, p := range ps {
if p == nil {
continue
}
if principalMatches(p, ci) {
return true
if ok, err := c.principalMatches(p, pubKey); err != nil {
return false, err
} else if ok {
return true, nil
}
}
return false
return false, nil
}
func principalMatches(p *tailcfg.SSHPrincipal, ci *sshConnInfo) bool {
return principalMatchesTailscaleIdentity(p, ci) &&
principalMatchesPubKey(p, ci)
func (c *conn) principalMatches(p *tailcfg.SSHPrincipal, pubKey gossh.PublicKey) (bool, error) {
if !c.principalMatchesTailscaleIdentity(p) {
return false, nil
}
return c.principalMatchesPubKey(p, pubKey)
}
// principalMatchesTailscaleIdentity reports whether one of p's four fields
// that match the Tailscale identity match (Node, NodeIP, UserLogin, Any).
// This function does not consider PubKeys.
func principalMatchesTailscaleIdentity(p *tailcfg.SSHPrincipal, ci *sshConnInfo) bool {
func (c *conn) principalMatchesTailscaleIdentity(p *tailcfg.SSHPrincipal) bool {
ci := c.info
if p.Any {
return true
}
@@ -926,32 +1021,27 @@ func principalMatchesTailscaleIdentity(p *tailcfg.SSHPrincipal, ci *sshConnInfo)
return false
}
func principalMatchesPubKey(p *tailcfg.SSHPrincipal, ci *sshConnInfo) bool {
func (c *conn) principalMatchesPubKey(p *tailcfg.SSHPrincipal, clientPubKey gossh.PublicKey) (bool, error) {
if len(p.PubKeys) == 0 {
return true
return true, nil
}
if ci.pubKey == nil {
return false
if clientPubKey == nil {
return false, nil
}
pubKeys := p.PubKeys
if len(pubKeys) == 1 && strings.HasPrefix(pubKeys[0], "https://") {
if ci.fetchPublicKeysURL == nil {
// TODO: log?
return false
}
knownKeys := p.PubKeys
if len(knownKeys) == 1 && strings.HasPrefix(knownKeys[0], "https://") {
var err error
pubKeys, err = ci.fetchPublicKeysURL(pubKeys[0])
knownKeys, err = c.srv.fetchPublicKeysURL(c.expandPublicKeyURL(knownKeys[0]))
if err != nil {
// TODO: log?
return false
return false, err
}
}
for _, pubKey := range pubKeys {
if pubKeyMatchesAuthorizedKey(ci.pubKey, pubKey) {
return true
for _, knownKey := range knownKeys {
if pubKeyMatchesAuthorizedKey(clientPubKey, knownKey) {
return true, nil
}
}
return false
return false, nil
}
func pubKeyMatchesAuthorizedKey(pubKey ssh.PublicKey, wantKey string) bool {
@@ -995,7 +1085,7 @@ func (ss *sshSession) startNewRecording() (*recording, error) {
ss: ss,
start: now,
}
varRoot := ss.srv.lb.TailscaleVarRoot()
varRoot := ss.conn.srv.lb.TailscaleVarRoot()
if varRoot == "" {
return nil, errors.New("no var root for recording storage")
}

View File

@@ -63,7 +63,7 @@ func TestMatchRule(t *testing.T) {
Action: someAction,
RuleExpires: timePtr(time.Unix(100, 0)),
},
ci: &sshConnInfo{now: time.Unix(200, 0)},
ci: &sshConnInfo{},
wantErr: errRuleExpired,
},
{
@@ -178,7 +178,11 @@ func TestMatchRule(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, gotUser, err := matchRule(tt.rule, tt.ci)
c := &conn{
now: time.Unix(200, 0),
info: tt.ci,
}
got, gotUser, err := c.matchRule(tt.rule, nil)
if err != tt.wantErr {
t.Errorf("err = %v; want %v", err, tt.wantErr)
}
@@ -215,17 +219,19 @@ func TestSSH(t *testing.T) {
lb: lb,
logf: logf,
}
ss, err := srv.newSSHServer()
sc, err := srv.newConn()
if err != nil {
t.Fatal(err)
}
// Remove the auth checks for the test
sc.insecureSkipTailscaleAuth = true
u, err := user.Current()
if err != nil {
t.Fatal(err)
}
ci := &sshConnInfo{
sc.localUser = u
sc.info = &sshConnInfo{
sshUser: "test",
src: netaddr.MustParseIPPort("1.2.3.4:32342"),
dst: netaddr.MustParseIPPort("1.2.3.5:22"),
@@ -233,10 +239,8 @@ func TestSSH(t *testing.T) {
uprof: &tailcfg.UserProfile{},
}
ss.Handler = func(s ssh.Session) {
ss := srv.newSSHSession(s, ci, u)
ss.action = &tailcfg.SSHAction{Accept: true}
ss.run()
sc.Handler = func(s ssh.Session) {
sc.newSSHSession(s, &tailcfg.SSHAction{Accept: true}).run()
}
ln, err := net.Listen("tcp4", "127.0.0.1:0")
@@ -255,12 +259,13 @@ func TestSSH(t *testing.T) {
}
return
}
go ss.HandleConn(c)
go sc.HandleConn(c)
}
}()
execSSH := func(args ...string) *exec.Cmd {
cmd := exec.Command("ssh",
"-v",
"-p", fmt.Sprint(port),
"-o", "StrictHostKeyChecking=no",
"user@127.0.0.1")
@@ -276,7 +281,7 @@ func TestSSH(t *testing.T) {
cmd.Env = append(os.Environ(), "LOCAL_ENV=bar")
got, err := cmd.CombinedOutput()
if err != nil {
t.Fatal(err)
t.Fatal(err, string(got))
}
m := parseEnv(got)
if got := m["USER"]; got == "" || got != u.Username {
@@ -403,3 +408,26 @@ func TestPublicKeyFetching(t *testing.T) {
}
}
func TestExpandPublicKeyURL(t *testing.T) {
c := &conn{
info: &sshConnInfo{
uprof: &tailcfg.UserProfile{
LoginName: "bar@baz.tld",
},
},
}
if got, want := c.expandPublicKeyURL("foo"), "foo"; got != want {
t.Errorf("basic: got %q; want %q", got, want)
}
if got, want := c.expandPublicKeyURL("https://example.com/$LOGINNAME_LOCALPART.keys"), "https://example.com/bar.keys"; got != want {
t.Errorf("localpart: got %q; want %q", got, want)
}
if got, want := c.expandPublicKeyURL("https://example.com/keys?email=$LOGINNAME_EMAIL"), "https://example.com/keys?email=bar@baz.tld"; got != want {
t.Errorf("email: got %q; want %q", got, want)
}
c.info = new(sshConnInfo)
if got, want := c.expandPublicKeyURL("https://example.com/keys?email=$LOGINNAME_EMAIL"), "https://example.com/keys?email="; got != want {
t.Errorf("on empty: got %q; want %q", got, want)
}
}

View File

@@ -1636,9 +1636,21 @@ type SetDNSResponse struct{}
// SSHPolicy is the policy for how to handle incoming SSH connections
// over Tailscale.
type SSHPolicy struct {
// Rules are the rules to process for an incoming SSH
// connection. The first matching rule takes its action and
// stops processing further rules.
// Rules are the rules to process for an incoming SSH connection. The first
// matching rule takes its action and stops processing further rules.
//
// When an incoming connection first starts, all rules are evaluated in
// "none" auth mode, where the client hasn't even been asked to send a
// public key. All SSHRule.Principals requiring a public key won't match. If
// a rule matches on the first pass and its Action is reject, the
// authentication fails with that action's rejection message, if any.
//
// If the first pass rule evaluation matches nothing without matching an
// Action with Reject set, the rules are considered to see whether public
// keys might still result in a match. If not, "none" auth is terminated
// before proceeding to public key mode. If so, the client is asked to try
// public key authentication and the rules are evaluated again for each of
// the client's present keys.
Rules []*SSHRule `json:"rules"`
}
@@ -1694,6 +1706,10 @@ type SSHPrincipal struct {
//
// As a special case, if len(PubKeys) == 1 and PubKeys[0] starts
// with "https://", then it's fetched (like https://github.com/username.keys).
// In that case, the following variable expansions are also supported
// in the URL:
// * $LOGINNAME_EMAIL ("foo@bar.com" or "foo@github")
// * $LOGINNAME_LOCALPART (the "foo" from either of the above)
PubKeys []string `json:"pubKeys,omitempty"`
}

View File

@@ -1,6 +1,7 @@
package ssh_test
import (
"errors"
"io"
"io/ioutil"
@@ -27,10 +28,19 @@ func ExampleNoPty() {
func ExamplePublicKeyAuth() {
ssh.ListenAndServe(":2222", nil,
ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
data, _ := ioutil.ReadFile("/path/to/allowed/key.pub")
allowed, _, _, _, _ := ssh.ParseAuthorizedKey(data)
return ssh.KeysEqual(key, allowed)
ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) error {
data, err := ioutil.ReadFile("/path/to/allowed/key.pub")
if err != nil {
return err
}
allowed, _, _, _, err := ssh.ParseAuthorizedKey(data)
if err != nil {
return err
}
if !ssh.KeysEqual(key, allowed) {
return errors.New("some error")
}
return nil
}),
)
}

View File

@@ -38,8 +38,6 @@ type Server struct {
HostSigners []Signer // private keys for the host key, must have at least one
Version string // server version to be sent before the initial handshake
NoClientAuthCallback func(gossh.ConnMetadata) (*gossh.Permissions, error)
KeyboardInteractiveHandler KeyboardInteractiveHandler // keyboard-interactive authentication handler
PasswordHandler PasswordHandler // password authentication handler
PublicKeyHandler PublicKeyHandler // public key authentication handler
@@ -131,10 +129,6 @@ func (srv *Server) config(ctx Context) *gossh.ServerConfig {
if srv.PasswordHandler == nil && srv.PublicKeyHandler == nil && srv.KeyboardInteractiveHandler == nil {
config.NoClientAuth = true
}
if srv.NoClientAuthCallback != nil {
config.NoClientAuth = true
config.NoClientAuthCallback = srv.NoClientAuthCallback
}
if srv.Version != "" {
config.ServerVersion = "SSH-2.0-" + srv.Version
}
@@ -150,8 +144,8 @@ func (srv *Server) config(ctx Context) *gossh.ServerConfig {
if srv.PublicKeyHandler != nil {
config.PublicKeyCallback = func(conn gossh.ConnMetadata, key gossh.PublicKey) (*gossh.Permissions, error) {
applyConnMetadata(ctx, conn)
if ok := srv.PublicKeyHandler(ctx, key); !ok {
return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
if err := srv.PublicKeyHandler(ctx, key); err != nil {
return ctx.Permissions().Permissions, err
}
ctx.SetValue(ContextKeyPublicKey, key)
return ctx.Permissions().Permissions, nil

View File

@@ -36,7 +36,7 @@ type Option func(*Server) error
type Handler func(Session)
// PublicKeyHandler is a callback for performing public key authentication.
type PublicKeyHandler func(ctx Context, key PublicKey) bool
type PublicKeyHandler func(ctx Context, key PublicKey) error
// PasswordHandler is a callback for performing password authentication.
type PasswordHandler func(ctx Context, password string) bool

62
tstest/iosdeps/iosdeps.go Normal file
View File

@@ -0,0 +1,62 @@
// Copyright (c) 2022 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 iosdeps is a just a list of the packages we import on iOS, to let us
// test that our transitive closure of dependencies on iOS doesn't accidentally
// grow too large, as we've historically been memory constrained there.
package iosdeps
import (
_ "bufio"
_ "bytes"
_ "context"
_ "crypto/rand"
_ "crypto/sha256"
_ "encoding/json"
_ "errors"
_ "fmt"
_ "io"
_ "io/fs"
_ "io/ioutil"
_ "log"
_ "math"
_ "net"
_ "net/http"
_ "os"
_ "os/signal"
_ "path/filepath"
_ "runtime"
_ "runtime/debug"
_ "strings"
_ "sync"
_ "sync/atomic"
_ "syscall"
_ "time"
_ "unsafe"
_ "go4.org/mem"
_ "golang.org/x/sys/unix"
_ "golang.zx2c4.com/wireguard/device"
_ "golang.zx2c4.com/wireguard/tun"
_ "inet.af/netaddr"
_ "tailscale.com/hostinfo"
_ "tailscale.com/ipn"
_ "tailscale.com/ipn/ipnlocal"
_ "tailscale.com/ipn/localapi"
_ "tailscale.com/log/logheap"
_ "tailscale.com/logtail"
_ "tailscale.com/logtail/filch"
_ "tailscale.com/net/dns"
_ "tailscale.com/net/tsdial"
_ "tailscale.com/net/tstun"
_ "tailscale.com/paths"
_ "tailscale.com/tempfork/pprof"
_ "tailscale.com/types/empty"
_ "tailscale.com/types/logger"
_ "tailscale.com/util/clientmetric"
_ "tailscale.com/util/dnsname"
_ "tailscale.com/version"
_ "tailscale.com/wgengine"
_ "tailscale.com/wgengine/router"
)

View File

@@ -0,0 +1,42 @@
// Copyright (c) 2022 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.
// No need to run this on Windows where CI's slow enough. Then we don't need to
// worry about "go.exe" etc.
//go:build !windows
// +build !windows
package iosdeps
import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"
)
func TestDeps(t *testing.T) {
cmd := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"), "list", "-json", ".")
cmd.Env = append(os.Environ(), "GOOS=ios", "GOARCH=arm64")
out, err := cmd.Output()
if err != nil {
t.Fatal(err)
}
var res struct {
Deps []string
}
if err := json.Unmarshal(out, &res); err != nil {
t.Fatal(err)
}
for _, dep := range res.Deps {
switch dep {
case "regexp", "regexp/syntax", "text/template", "html/template":
t.Errorf("package %q is not allowed as a dependency on iOS", dep)
}
}
t.Logf("got %d dependencies", len(res.Deps))
}

View File

@@ -12,7 +12,9 @@ import "inet.af/netaddr"
// Resolver is the configuration for one DNS resolver.
type Resolver struct {
// Addr is the address of the DNS resolver, one of:
// - A plain IP address for a "classic" UDP+TCP DNS resolver
// - A plain IP address for a "classic" UDP+TCP DNS resolver.
// This is the common format as sent by the control plane.
// - An IP:port, for tests.
// - [TODO] "tls://resolver.com" for DNS over TCP+TLS
// - [TODO] "https://resolver.com/query-tmpl" for DNS over HTTPS
Addr string `json:",omitempty"`
@@ -26,7 +28,20 @@ type Resolver struct {
BootstrapResolution []netaddr.IP `json:",omitempty"`
}
// ResolverFromIP defines a Resolver for ip on port 53.
func ResolverFromIP(ip netaddr.IP) Resolver {
return Resolver{Addr: netaddr.IPPortFrom(ip, 53).String()}
// IPPort returns r.Addr as an IP address and port if either
// r.Addr is an IP address (the common case) or if r.Addr
// is an IP:port (as done in tests).
func (r *Resolver) IPPort() (ipp netaddr.IPPort, ok bool) {
if r.Addr == "" || r.Addr[0] == 'h' || r.Addr[0] == 't' {
// Fast path to avoid ParseIP error allocation for obviously not IP
// cases.
return
}
if ip, err := netaddr.ParseIP(r.Addr); err == nil {
return netaddr.IPPortFrom(ip, 53), true
}
if ipp, err := netaddr.ParseIPPort(r.Addr); err == nil {
return ipp, true
}
return
}

View File

@@ -42,6 +42,10 @@ type osMon interface {
// until the osMon is closed. After a Close, the returned
// error is ignored.
Receive() (message, error)
// IsInterestingInterface reports whether the provided interface should
// be considered for network change events.
IsInterestingInterface(iface string) bool
}
// ChangeFunc is a callback function that's called when the network
@@ -282,6 +286,13 @@ func (m *Mon) notifyRuleDeleted(rdm ipRuleDeletedMessage) {
}
}
// isInterestingInterface reports whether the provided interface should be
// considered when checking for network state changes.
// The ips parameter should be the IPs of the provided interface.
func (m *Mon) isInterestingInterface(i interfaces.Interface, ips []netaddr.IPPrefix) bool {
return m.om.IsInterestingInterface(i.Name) && interfaces.UseInterestingInterfaces(i, ips)
}
// debounce calls the callback function with a delay between events
// and exits when a stop is issued.
func (m *Mon) debounce() {
@@ -299,14 +310,15 @@ func (m *Mon) debounce() {
m.mu.Lock()
oldState := m.ifState
changed := !curState.EqualFiltered(oldState, interfaces.UseInterestingInterfaces, interfaces.UseInterestingIPs)
changed := !curState.EqualFiltered(oldState, m.isInterestingInterface, interfaces.UseInterestingIPs)
if changed {
m.gwValid = false
m.ifState = curState
if s1, s2 := oldState.String(), curState.String(); s1 == s2 {
m.logf("[unexpected] network state changed, but stringification didn't: %v\nold: %s\nnew: %s\n", s1,
jsonSummary(oldState), jsonSummary(curState))
m.logf("[unexpected] network state changed, but stringification didn't: %v", s1)
m.logf("[unexpected] old: %s", jsonSummary(oldState))
m.logf("[unexpected] new: %s", jsonSummary(curState))
}
}
// See if we have a queued or new time jump signal.

View File

@@ -112,11 +112,18 @@ func addrType(addrs []route.Addr, rtaxType int) route.Addr {
return nil
}
func (m *darwinRouteMon) IsInterestingInterface(iface string) bool {
baseName := strings.TrimRight(iface, "0123456789")
switch baseName {
case "llw", "awdl", "pdp_ip", "ipsec":
return false
}
return true
}
func (m *darwinRouteMon) skipInterfaceAddrMessage(msg *route.InterfaceAddrMessage) bool {
if la, ok := addrType(msg.Addrs, unix.RTAX_IFP).(*route.LinkAddr); ok {
baseName := strings.TrimRight(la.Name, "0123456789")
switch baseName {
case "llw", "awdl", "pdp_ip", "ipsec":
if !m.IsInterestingInterface(la.Name) {
return true
}
}

View File

@@ -34,6 +34,8 @@ func newOSMon(logf logger.Logf, m *Mon) (osMon, error) {
return &devdConn{conn}, nil
}
func (c *devdConn) IsInterestingInterface(iface string) bool { return true }
func (c *devdConn) Close() error {
return c.conn.Close()
}

View File

@@ -64,6 +64,8 @@ func newOSMon(logf logger.Logf, m *Mon) (osMon, error) {
return &nlConn{logf: logf, conn: conn, addrCache: make(map[uint32]map[netaddr.IP]bool)}, nil
}
func (c *nlConn) IsInterestingInterface(iface string) bool { return true }
func (c *nlConn) Close() error { return c.conn.Close() }
func (c *nlConn) Receive() (message, error) {

View File

@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !android
// +build !android
package monitor
import (

View File

@@ -72,6 +72,8 @@ func newOSMon(logf logger.Logf, _ *Mon) (osMon, error) {
return m, nil
}
func (m *winMon) IsInterestingInterface(iface string) bool { return true }
func (m *winMon) Close() (ret error) {
m.cancel()
m.noDeadlockTicker.Stop()

View File

@@ -38,6 +38,10 @@ type pollingMon struct {
stop chan struct{}
}
func (pm *pollingMon) IsInterestingInterface(iface string) bool {
return true
}
func (pm *pollingMon) Close() error {
pm.closeOnce.Do(func() {
close(pm.stop)

View File

@@ -1514,8 +1514,14 @@ func supportsV6NAT() bool {
// Can't read the file. Assume SNAT works.
return true
}
return bytes.Contains(bs, []byte("nat\n"))
if bytes.Contains(bs, []byte("nat\n")) {
return true
}
// In nftables mode, that proc file will be empty. Try another thing:
if exec.Command("modprobe", "ip6table_nat").Run() == nil {
return true
}
return false
}
func checkIPRuleSupportsV6(logf logger.Logf) error {