Compare commits

...

46 Commits

Author SHA1 Message Date
Brad Fitzpatrick
4c7ee0c9f9 net/dns: make exit node DNS ask OSConfigurator for backup resolvers
Updates #1713

Change-Id: I7be9dab2b2c03749b4c2d99f9f45c11422ac915a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-29 10:05:23 -08:00
Brad Fitzpatrick
c2efe46f72 ipn/ipnlocal: restrict exit node DoH server based on ACL'ed packet filter
Don't be a DoH DNS server to peers unless the Tailnet admin has permitted
that peer autogroup:internet access.

Updates #1713

Change-Id: Iec69360d8e4d24d5187c26904b6a75c1dabc8979
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-29 09:56:59 -08:00
Brad Fitzpatrick
ff9727c9ff wgengine/filter: fix, test NewAllowAllForTest
I probably broke it when SCTP support was added but nothing apparently
ever used NewAllowAllForTest so it wasn't noticed when it broke.

Change-Id: Ib5a405be233d53cb7fcc61d493ae7aa2d1d590a2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-29 09:56:59 -08:00
Thomas Weiß
f8cef1ba08 ipn/store/aws: support using sub-paths in parameters
Fixes #3431

Signed-off-by: Thomas Weiß <panos@unbunt.org>
2021-11-29 07:48:01 -08:00
Thomas Weiß
6dc6ea9b37 cmd/tailscaled: log error on state store init failure
Signed-off-by: Thomas Weiß <panos@unbunt.org>
2021-11-29 07:48:01 -08:00
Brad Fitzpatrick
78b0bd2957 net/dns/resolver: add clientmetrics for DNS
Fixes tailscale/corp#1811

Change-Id: I864d11e0332a177e8c5ff403591bff6fec548f5a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-26 17:57:48 -08:00
David Anderson
097602b3ca ipn/ipnlocal: warn more precisely about IP forwarding issues on linux.
If IP forwarding is disabled globally, but enabled per-interface on all interfaces,
don't complain. If only some interfaces have forwarding enabled, warn that some
subnet routing/exit node traffic may not work.

Fixes #1586

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-26 11:03:10 -08:00
David Anderson
db800ddeac cmd/derper: set Content-Security-Policy on DERPs.
It's a basic "deny everything" policy, since DERP's HTTP
server is very uninteresting from a browser POV. But it
stops every security scanner under the sun from reporting
"dangerously configured" HTTP servers.

Updates tailscale/corp#3119

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-26 11:00:44 -08:00
David Anderson
33c541ae30 ipn/ipnlocal: populate self status from netmap in ipnlocal, not magicsock.
Fixes #1933

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-26 10:56:42 -08:00
Denton Gentry
e121c2f724 logpolicy: export NewLogtailTransport for Android
Android doesn't use logpolicy and currently has enough
unique stuff about its logging that makes it difficult to
do so. For example, its logsDir comes from Gio.

Export NewLogtailTransport to let Android use it.

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

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-11-26 07:45:13 -08:00
Brad Fitzpatrick
25525b7754 net/dns/resolver, ipn/ipnlocal: wire up peerapi DoH server to DNS forwarder
Updates #1713

Change-Id: Ia4ed9d8c9cef0e70aa6d30f2852eaab80f5f695a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-23 18:59:36 -08:00
Maya Kaczorowski
9bb91cb977 Merge pull request #3214 from tailscale/mayakacz-patch-1
.github: feature request template change
2021-11-23 19:09:51 -05:00
Maya Kaczorowski
259163dfe1 Update feature_request.yml
Signed-off-by: Maya Kaczorowski <15946341+mayakacz@users.noreply.github.com>
2021-11-23 18:52:52 -05:00
Denton Gentry
f56a7559ce scripts/installer.sh: add more Linux variants.
Updates https://github.com/tailscale/tailscale/issues/2915

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-11-23 15:12:29 -08:00
Josh Bleecher Snyder
d10cefdb9b net/dns: require space after nameserver/search parsing resolv.conf
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-23 15:11:46 -08:00
Josh Bleecher Snyder
9f00510833 net/dns: handle comments in resolv.conf
Currently, comments in resolv.conf cause our parser to fail,
with error messages like:

ParseIP("192.168.0.100 # comment"): unexpected character (at " # comment")

Fix that.

Noticed while looking through logs.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-23 15:11:46 -08:00
Josh Bleecher Snyder
955aa188b3 ipn/ipnlocal: fix logging
We were missing an argument here.
Also, switch to %q, in case anything weird
is happening with these strings.

Updates tailscale/corp#461

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-23 13:36:59 -08:00
Josh Bleecher Snyder
73beaaf360 net/tstun: rate limit "self disco out packet" logging
When this happens, it is incredibly noisy in the logs.
It accounts for about a third of all remaining
"unexpected" log lines from a recent investigation.

It's not clear that we know how to fix this,
we have a functioning workaround,
and we now have a (cheap and efficient) metric for this
that we can use for measurements.

So reduce the logging to approximately once per minute.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-23 12:52:52 -08:00
Josh Bleecher Snyder
b0d543f7a1 cmd/tailscale: add ip -1 flag
This limits the output to a single IP address.

RELNOTE=tailscale ip now has a -1 flag (TODO: update docs to use it)

Fixes #1921

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-23 12:05:32 -08:00
Josh Bleecher Snyder
73beaf59fb cmd/tailscale: improve ip subcommand docs
Streamline the prose.
Clarify what peer may be.
Improve an error message.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-23 12:05:32 -08:00
Joonas Loppi
a3b709f0c4 tsshd: fix double exit with different exit codes
Signed-off-by: Joonas Loppi <joonas@joonas.fi>
2021-11-23 09:19:59 -08:00
Brad Fitzpatrick
283ae702c1 ipn/ipnlocal: start adding DoH DNS server to peerapi when exit node
Updates #1713

Change-Id: I8d9c488f779e7acc811a9bc18166a2726198a429
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-23 08:21:41 -08:00
dependabot[bot]
6fd6fe11f2 go.mod: bump honnef.co/go/tools from 0.2.1 to 0.2.2
Bumps [honnef.co/go/tools](https://github.com/dominikh/go-tools) from 0.2.1 to 0.2.2.
- [Release notes](https://github.com/dominikh/go-tools/releases)
- [Commits](https://github.com/dominikh/go-tools/compare/v0.2.1...v0.2.2)

---
updated-dependencies:
- dependency-name: honnef.co/go/tools
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-22 22:01:32 -08:00
Josh Bleecher Snyder
027b46d0c1 ipn/ipnstate: clarify PeerStatusLite.LastHandshake
And document the other fields, as long as we're here.

Updates #1182

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-22 22:01:20 -08:00
Brad Fitzpatrick
0de1b74fbb util/clientmetric: add tests omitted from earlier commit
These were supposed to be part of
3b541c833e but I guess I forgot to "git
add" them. Whoops.

Updates #3307

Change-Id: I8c768a61ec7102a01799e81dc502a22399b9e9f0
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-22 21:49:28 -08:00
Josh Bleecher Snyder
ad5e04249b wgengine/monitor: ignore adding/removing uninteresting IPs
One of the most common "unexpected" log lines is:

"network state changed, but stringification didn't"

One way that this can occur is if an interesting interface
(non-Tailscale, has interesting IP address)
gains or loses an uninteresting IP address (link local or loopback).

The fact that the interface is interesting is enough for EqualFiltered
to inspect it. The fact that an IP address changed is enough for
EqualFiltered to declare that the interfaces are not equal.

But the State.String method reasonably declines to print any
uninteresting IP addresses. As a result, the network state appears
to have changed, but the stringification did not.

The String method is correct; nothing interesting happened.

This change fixes this by adding an IP address filter to EqualFiltered
in addition to the interface filter. This lets the network monitor
ignore the addition/removal of uninteresting IP addresses.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-22 16:33:15 -08:00
Josh Bleecher Snyder
60510a6ae7 .github/workflows: check that repo is clean after build and test
Linux-only for now, to avoid having to figure out why
powershell doesn't like my shell scripting. (Not that I blame it.)
That'll be enough to catch most regressions.

Fixes #1083

Co-authored-by: Aaron Klotz <aaron@tailscale.com>
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-22 15:28:59 -08:00
Denton Gentry
1ea270375a hostinfo: report when running in Docker Desktop.
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2021-11-22 13:45:54 -08:00
Josh Bleecher Snyder
ca1b3fe235 net/tshttpproxy: use correct size for Windows BOOL argument
The Windows BOOL type is an int32. We were using a bool,
which is a one byte wide. This could be responsible for the
ERROR_INVALID_PARAMETER errors we were seeing for calls to
WinHttpGetProxyForUrl.

We manually checked all other existing Windows syscalls
for similar mistakes and did not find any.

Updates #879

Co-authored-by: Aaron Klotz <aaron@tailscale.com>
Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-22 12:24:24 -08:00
David Anderson
9a217ec841 cmd/derper: increase HSTS cache lifetime to 2 years.
Fixes #3373.

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-22 11:59:01 -08:00
Maisem Ali
9feb483ad3 build_docker.sh: use github.com/tailscale/mkctr instead of docker
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2021-11-22 11:39:30 -08:00
Aaron Klotz
7d8feb2784 hostinfo: change Windows implementation to directly query version information using API and registry
We replace the cmd.exe invocation with RtlGetNtVersionNumbers for the first
three fields. On Windows 10+, we query for the fourth field which is available
via the registry.

The fourth field is not really documented anywhere; Firefox has been querying
it successfully since Windows 10 was released, so we can be pretty confident in
its longevity at this point.

Fixes https://github.com/tailscale/tailscale/issues/1478

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2021-11-22 12:26:42 -07:00
Josh Bleecher Snyder
1a629a4715 net/portmapper: mark fewer PMP probe failures as unexpected
There are lots of lines in the logs of the form:

portmapper: unexpected PMP probe response: {OpCode:128 ResultCode:3
SecondsSinceEpoch:NNN MappingValidSeconds:0 InternalPort:0
ExternalPort:0 PublicAddr:0.0.0.0}

ResultCode 3 here means a network failure, e.g. the NAT box itself has
not obtained a DHCP lease. This is not an indication that something
is wrong in the Tailscale client, so use different wording here
to reflect that. Keep logging, so that we can analyze and debug
the reasons that PMP probes fail.

Signed-off-by: Josh Bleecher Snyder <josh@tailscale.com>
2021-11-22 11:13:15 -08:00
Brad Fitzpatrick
e8db43e8fa wgengine/router: demote TestDebugListRules fail to skip
Updates #3360

Change-Id: Ic5c98ea03f3171c13ab9293a0ae74d17fd04d149
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-22 11:04:45 -08:00
David Anderson
937e96f43d cmd/derper: enable HSTS when serving over HTTPS.
Starting with a short lifetime, to verify nothing breaks.

Updates #3373

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-22 09:57:34 -08:00
dependabot[bot]
f76a8d93da go.mod: bump github.com/godbus/dbus/v5 from 5.0.5 to 5.0.6
Bumps [github.com/godbus/dbus/v5](https://github.com/godbus/dbus) from 5.0.5 to 5.0.6.
- [Release notes](https://github.com/godbus/dbus/releases)
- [Commits](https://github.com/godbus/dbus/compare/v5.0.5...v5.0.6)

---
updated-dependencies:
- dependency-name: github.com/godbus/dbus/v5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-22 08:40:09 -08:00
Brad Fitzpatrick
2ea765e5d8 go.mod: bump inet.af/netstack
Updates #2642 (I'd hoped, but doesn't seem to fix it)

Change-Id: Id54af7c90a1206bc7018215957e20e954782b911
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-21 09:18:31 -08:00
AdamKorcz
def659d1ec Fuzzing: Add CIFuzz
Signed-off-by: AdamKorcz <adam@adalogics.com>
2021-11-19 13:06:20 -08:00
Brad Fitzpatrick
946dfec98a wgengine/router: fix checkIPRuleSupportsV6 to actually use IPv6
Updates #3358 (should fix it)
Updates #391

Change-Id: Ia62437dfa81247b0b5994d554cf279c3d540e4e7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-19 11:37:05 -08:00
Brad Fitzpatrick
9259377a7f wgengine/router: don't assume Linux was built with IP_MULTIPLE_TABLES
Updates #3351
Updates #391

Change-Id: I7e66b686e05f3c970846513679cc62556ebe322a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-19 11:19:03 -08:00
David Anderson
88b8a09d37 net/dns: make constants for the various DBus strings.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-19 11:09:32 -08:00
David Anderson
6c82cebe57 health: add a health state for net/dns.OSConfigurator.
Lets the systemd-resolved OSConfigurator report health changes
for out of band config resyncs.

Updates #3327

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-19 11:09:32 -08:00
David Anderson
4ef3fed100 net/dns: resync config to systemd-resolved when it restarts.
Fixes #3327

Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-19 11:09:32 -08:00
David Anderson
cf9169e4be net/dns: remove unused Config struct element.
Signed-off-by: David Anderson <danderson@tailscale.com>
2021-11-19 11:09:32 -08:00
Brad Fitzpatrick
0350cf0438 wgengine{,/router}: annotate some more errors
Updates #3351

Change-Id: I8b4f957d2051b3e29401bb449dbadbdada3a7c46
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-19 10:46:01 -08:00
Brad Fitzpatrick
5294125e7a cmd/tailscaled: disambiguate some startup failure error messages
Updates #3351

Change-Id: I0afead4a084623567f56b19187574fa97b295b2a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2021-11-19 08:58:29 -08:00
59 changed files with 1401 additions and 208 deletions

View File

@@ -1,5 +1,8 @@
blank_issues_enabled: true
contact_links:
- name: Support requests and Troubleshooting
- name: Support
url: https://tailscale.com/contact/support/
about: Contact us for support
- name: Troubleshooting
url: https://tailscale.com/kb/1023/troubleshooting
about: Troubleshoot common issues. Contact us by email at support@tailscale.com.
about: Troubleshoot common issues

View File

@@ -7,14 +7,7 @@ body:
attributes:
value: |
Please check if your feature request is [already filed](https://github.com/tailscale/tailscale/issues).
- type: input
id: request
attributes:
label: Tell us about your idea!
description: What is your feature request?
placeholder: e.g., A pet pangolin
validations:
required: true
Tell us about your idea!
- type: textarea
id: problem
attributes:

26
.github/workflows/cifuzz.yml vendored Normal file
View File

@@ -0,0 +1,26 @@
name: CIFuzz
on: [pull_request]
jobs:
Fuzzing:
runs-on: ubuntu-latest
steps:
- name: Build Fuzzers
id: build
uses: google/oss-fuzz/infra/cifuzz/actions/build_fuzzers@master
with:
oss-fuzz-project-name: 'tailscale'
dry-run: false
language: go
- name: Run Fuzzers
uses: google/oss-fuzz/infra/cifuzz/actions/run_fuzzers@master
with:
oss-fuzz-project-name: 'tailscale'
fuzz-seconds: 300
dry-run: false
language: go
- name: Upload Crash
uses: actions/upload-artifact@v1
if: failure() && steps.build.outcome == 'success'
with:
name: artifacts
path: ./out/artifacts

View File

@@ -31,6 +31,21 @@ jobs:
- name: Run tests and benchmarks with -race flag on linux
run: go test -race -bench=. -benchtime=1x ./...
- name: Check that no tracked files in the repo have been modified
run: git diff --no-ext-diff --name-only --exit-code || (echo "Build/test modified the files above."; exit 1)
- name: Check that no files have been added to the repo
run: |
# Note: The "error: pathspec..." you see below is normal!
# In the success case in which there are no new untracked files,
# git ls-files complains about the pathspec not matching anything.
# That's OK. It's not worth the effort to suppress. Please ignore it.
if git ls-files --others --exclude-standard --directory --no-empty-directory --error-unmatch -- ':/*'
then
echo "Build/test created untracked files in the repo (file names above)."
exit 1
fi
- uses: k0kubun/action-slack@v2.0.0
with:
payload: |

View File

@@ -40,6 +40,21 @@ jobs:
- name: Run tests on linux
run: go test -bench=. -benchtime=1x ./...
- name: Check that no tracked files in the repo have been modified
run: git diff --no-ext-diff --name-only --exit-code || (echo "Build/test modified the files above."; exit 1)
- name: Check that no files have been added to the repo
run: |
# Note: The "error: pathspec..." you see below is normal!
# In the success case in which there are no new untracked files,
# git ls-files complains about the pathspec not matching anything.
# That's OK. It's not worth the effort to suppress. Please ignore it.
if git ls-files --others --exclude-standard --directory --no-empty-directory --error-unmatch -- ':/*'
then
echo "Build/test created untracked files in the repo (file names above)."
exit 1
fi
- uses: k0kubun/action-slack@v2.0.0
with:
payload: |

View File

@@ -31,6 +31,21 @@ jobs:
- name: Run tests on linux
run: GOARCH=386 go test -bench=. -benchtime=1x ./...
- name: Check that no tracked files in the repo have been modified
run: git diff --no-ext-diff --name-only --exit-code || (echo "Build/test modified the files above."; exit 1)
- name: Check that no files have been added to the repo
run: |
# Note: The "error: pathspec..." you see below is normal!
# In the success case in which there are no new untracked files,
# git ls-files complains about the pathspec not matching anything.
# That's OK. It's not worth the effort to suppress. Please ignore it.
if git ls-files --others --exclude-standard --directory --no-empty-directory --error-unmatch -- ':/*'
then
echo "Build/test created untracked files in the repo (file names above)."
exit 1
fi
- uses: k0kubun/action-slack@v2.0.0
with:
payload: |

View File

@@ -56,6 +56,5 @@ RUN GOARCH=$TARGETARCH go install -tags=xversion -ldflags="\
-X tailscale.com/version.GitCommit=$VERSION_GIT_HASH" \
-v ./cmd/tailscale ./cmd/tailscaled
FROM alpine:3.14
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
FROM ghcr.io/tailscale/alpine-base:3.14
COPY --from=build-env /go/bin/* /usr/local/bin/

6
Dockerfile.base Normal file
View File

@@ -0,0 +1,6 @@
# Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file.
FROM alpine:3.14
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables

View File

@@ -23,9 +23,8 @@ build386:
buildlinuxarm:
GOOS=linux GOARCH=arm go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
buildmultiarchimage:
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t ${IMAGE_REPO}:latest --push -f Dockerfile .
./build_docker.sh
check: staticcheck vet depaware buildwindows build386 buildlinuxarm

View File

@@ -30,12 +30,14 @@ else
fi
long_suffix="$change_suffix-t$short_hash"
SHORT="$major.$minor.$patch"
MINOR="$major.$minor"
SHORT="$MINOR.$patch"
LONG="${SHORT}$long_suffix"
GIT_HASH="$git_hash"
if [ "$1" = "shellvars" ]; then
cat <<EOF
VERSION_MINOR="$MINOR"
VERSION_SHORT="$SHORT"
VERSION_LONG="$LONG"
VERSION_GIT_HASH="$GIT_HASH"

View File

@@ -21,8 +21,15 @@ set -eu
eval $(./build_dist.sh shellvars)
docker build \
--build-arg VERSION_LONG=$VERSION_LONG \
--build-arg VERSION_SHORT=$VERSION_SHORT \
--build-arg VERSION_GIT_HASH=$VERSION_GIT_HASH \
-t tailscale:$VERSION_SHORT -t tailscale:latest .
go run github.com/tailscale/mkctr@latest \
--base="ghcr.io/tailscale/alpine-base:3.14" \
--gopaths="\
tailscale.com/cmd/tailscale:/usr/local/bin/tailscale, \
tailscale.com/cmd/tailscaled:/usr/local/bin/tailscaled" \
--ldflags="\
-X tailscale.com/version.Long=${VERSION_LONG} \
-X tailscale.com/version.Short=${VERSION_SHORT} \
-X tailscale.com/version.GitCommit=${VERSION_GIT_HASH}" \
--tags="${VERSION_SHORT},${VERSION_MINOR}" \
--repos="tailscale/tailscale,ghcr.io/tailscale/tailscale" \
--push

View File

@@ -235,6 +235,21 @@ func main() {
cert.Certificate = append(cert.Certificate, s.MetaCert())
return cert, nil
}
httpsrv.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Set HTTP headers to appease automated security scanners.
//
// Security automation gets cranky when HTTPS sites don't
// set HSTS, and when they don't specify a content
// security policy for XSS mitigation.
//
// DERP's HTTP interface is only ever used for debug
// access (for which trivial safe policies work just
// fine), and by DERP clients which don't obey any of
// these browser-centric headers anyway.
w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; form-action 'none'; base-uri 'self'; block-all-mixed-content; plugin-types 'none'")
mux.ServeHTTP(w, r)
})
go func() {
port80srv := &http.Server{
Addr: net.JoinHostPort(listenHost, "80"),

View File

@@ -18,12 +18,13 @@ import (
var ipCmd = &ffcli.Command{
Name: "ip",
ShortUsage: "ip [-4] [-6] [peername]",
ShortHelp: "Show current Tailscale IP address(es)",
LongHelp: "Shows the Tailscale IP address of the current machine without an argument. With an argument, it shows the IP of a named peer.",
ShortUsage: "ip [-1] [-4] [-6] [peer hostname or ip address]",
ShortHelp: "Show Tailscale IP addresses",
LongHelp: "Show Tailscale IP addresses for peer. Peer defaults to the current machine.",
Exec: runIP,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("ip")
fs.BoolVar(&ipArgs.want1, "1", false, "only print one IP address")
fs.BoolVar(&ipArgs.want4, "4", false, "only print IPv4 address")
fs.BoolVar(&ipArgs.want6, "6", false, "only print IPv6 address")
return fs
@@ -31,13 +32,14 @@ var ipCmd = &ffcli.Command{
}
var ipArgs struct {
want1 bool
want4 bool
want6 bool
}
func runIP(ctx context.Context, args []string) error {
if len(args) > 1 {
return errors.New("unknown arguments")
return errors.New("too many arguments, expected at most one peer")
}
var of string
if len(args) == 1 {
@@ -45,8 +47,14 @@ func runIP(ctx context.Context, args []string) error {
}
v4, v6 := ipArgs.want4, ipArgs.want6
if v4 && v6 {
return errors.New("tailscale ip -4 and -6 are mutually exclusive")
nflags := 0
for _, b := range []bool{ipArgs.want1, v4, v6} {
if b {
nflags++
}
}
if nflags > 1 {
return errors.New("tailscale ip -1, -4, and -6 are mutually exclusive")
}
if !v4 && !v6 {
v4, v6 = true, true
@@ -71,6 +79,9 @@ func runIP(ctx context.Context, args []string) error {
return fmt.Errorf("no current Tailscale IPs; state: %v", st.BackendState)
}
if ipArgs.want1 {
ips = ips[:1]
}
match := false
for _, ip := range ips {
if ip.Is4() && v4 || ip.Is6() && v6 {

View File

@@ -103,7 +103,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
LD golang.org/x/sys/unix from tailscale.com/net/netns+
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+

View File

@@ -116,10 +116,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
inet.af/netaddr from inet.af/wf+
inet.af/netstack/atomicbitops from inet.af/netstack/tcpip+
💣 inet.af/netstack/buffer from inet.af/netstack/tcpip/stack
inet.af/netstack/context from inet.af/netstack/refs+
💣 inet.af/netstack/gohacks from inet.af/netstack/state/wire+
inet.af/netstack/linewriter from inet.af/netstack/log
inet.af/netstack/log from inet.af/netstack/state+
inet.af/netstack/rand from inet.af/netstack/tcpip/network/hash+
inet.af/netstack/refs from inet.af/netstack/refsvfs2
inet.af/netstack/refsvfs2 from inet.af/netstack/tcpip/stack
💣 inet.af/netstack/sleep from inet.af/netstack/tcpip/transport/tcp
💣 inet.af/netstack/state from inet.af/netstack/atomicbitops+
inet.af/netstack/state/wire from inet.af/netstack/state
@@ -130,6 +133,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
inet.af/netstack/tcpip/hash/jenkins from inet.af/netstack/tcpip/stack+
inet.af/netstack/tcpip/header from inet.af/netstack/tcpip/header/parse+
inet.af/netstack/tcpip/header/parse from inet.af/netstack/tcpip/network/ipv4+
inet.af/netstack/tcpip/internal/tcp from inet.af/netstack/tcpip/stack+
inet.af/netstack/tcpip/link/channel from tailscale.com/wgengine/netstack
inet.af/netstack/tcpip/network/hash from inet.af/netstack/tcpip/network/ipv4+
inet.af/netstack/tcpip/network/internal/fragmentation from inet.af/netstack/tcpip/network/ipv4+
@@ -139,7 +143,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
inet.af/netstack/tcpip/ports from inet.af/netstack/tcpip/stack+
inet.af/netstack/tcpip/seqnum from inet.af/netstack/tcpip/header+
💣 inet.af/netstack/tcpip/stack from inet.af/netstack/tcpip/adapters/gonet+
inet.af/netstack/tcpip/transport from inet.af/netstack/tcpip/transport/icmp+
inet.af/netstack/tcpip/transport/icmp from tailscale.com/wgengine/netstack
inet.af/netstack/tcpip/transport/internal/network from inet.af/netstack/tcpip/transport/icmp+
inet.af/netstack/tcpip/transport/internal/noop from inet.af/netstack/tcpip/transport/raw
inet.af/netstack/tcpip/transport/packet from inet.af/netstack/tcpip/transport/raw
inet.af/netstack/tcpip/transport/raw from inet.af/netstack/tcpip/transport/icmp+
💣 inet.af/netstack/tcpip/transport/tcp from inet.af/netstack/tcpip/adapters/gonet+
@@ -217,7 +224,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/types/netmap from tailscale.com/control/controlclient+
tailscale.com/types/nettype from tailscale.com/wgengine/magicsock
tailscale.com/types/opt from tailscale.com/control/controlclient+
tailscale.com/types/pad32 from tailscale.com/derp+
tailscale.com/types/pad32 from tailscale.com/derp
tailscale.com/types/persist from tailscale.com/control/controlclient+
tailscale.com/types/preftype from tailscale.com/ipn+
tailscale.com/types/structs from tailscale.com/control/controlclient+

View File

@@ -310,6 +310,9 @@ func run() error {
logf("wgengine.New: %v", err)
return err
}
if _, ok := e.(wgengine.ResolvingEngine).GetResolver(); !ok {
panic("internal error: exit node resolver not wired up")
}
ns, err := newNetstack(logf, e)
if err != nil {
@@ -361,6 +364,7 @@ func run() error {
store, err := ipnserver.StateStore(statePathOrDefault(), logf)
if err != nil {
logf("ipnserver.StateStore: %v", err)
return err
}
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, nil, opts)
@@ -440,14 +444,14 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
log.Printf("Connecting to BIRD at %s ...", args.birdSocketPath)
conf.BIRDClient, err = createBIRDClient(args.birdSocketPath)
if err != nil {
return nil, false, err
return nil, false, fmt.Errorf("createBIRDClient: %w", err)
}
}
if !useNetstack {
dev, devName, err := tstun.New(logf, name)
if err != nil {
tstun.Diagnose(logf, name)
return nil, false, err
return nil, false, fmt.Errorf("tstun.New(%q): %w", name, err)
}
conf.Tun = dev
if strings.HasPrefix(name, "tap:") {
@@ -459,11 +463,11 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.
r, err := router.New(logf, dev, linkMon)
if err != nil {
dev.Close()
return nil, false, err
return nil, false, fmt.Errorf("creating router: %w", err)
}
d, err := dns.NewOSConfigurator(logf, devName)
if err != nil {
return nil, false, err
return nil, false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
}
conf.DNS = d
conf.Router = r

View File

@@ -157,8 +157,9 @@ func handleSSH(s ssh.Session) {
cmd.Process.Kill()
if err := cmd.Wait(); err != nil {
s.Exit(1)
} else {
s.Exit(0)
}
s.Exit(0)
return
}

6
go.mod
View File

@@ -17,7 +17,7 @@ require (
github.com/frankban/quicktest v1.14.0
github.com/gliderlabs/ssh v0.3.3
github.com/go-ole/go-ole v1.2.6-0.20210915003542-8b1f7f90f6b1
github.com/godbus/dbus/v5 v5.0.5
github.com/godbus/dbus/v5 v5.0.6
github.com/google/go-cmp v0.5.6
github.com/google/uuid v1.3.0
github.com/goreleaser/nfpm v1.10.3
@@ -54,9 +54,9 @@ require (
golang.org/x/tools v0.1.7
golang.zx2c4.com/wireguard v0.0.0-20211116201604-de7c702ace45
golang.zx2c4.com/wireguard/windows v0.4.10
honnef.co/go/tools v0.2.1
honnef.co/go/tools v0.2.2
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
inet.af/netstack v0.0.0-20211101182044-1c1bcf452982
inet.af/netstack v0.0.0-20211120045802-8aa80cf23d3c
inet.af/peercred v0.0.0-20210318190834-4259e17bb763
inet.af/wf v0.0.0-20210516214145-a5343001b756
nhooyr.io/websocket v1.8.7

16
go.sum
View File

@@ -203,8 +203,8 @@ github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6Wezm
github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo=
github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM=
github.com/godbus/dbus/v5 v5.0.5 h1:9Eg0XUhQxtkV8ykTMKtMMYY72g4NgxtRq4jgh4Ih5YM=
github.com/godbus/dbus/v5 v5.0.5/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/flock v0.8.0 h1:MSdYClljsF3PbENUUEx85nkWfJSGfzYI9yEBZOJz6CY=
github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@@ -975,10 +975,6 @@ golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8T
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY=
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20210905140043-2ef39d47540c/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
golang.zx2c4.com/wireguard v0.0.0-20211115224047-111e0566dce3 h1:7BFThRTwBwTLoMomQ/Y0GqY1VLH9D7kbbTNsfxl2fU0=
golang.zx2c4.com/wireguard v0.0.0-20211115224047-111e0566dce3/go.mod h1:evxZIqfCetExY5piKXGAxJYwvXWkps9zTCkWpkoGFxw=
golang.zx2c4.com/wireguard v0.0.0-20211116194326-3cae233d69f7 h1:ZeHUKruJlkbSvafSH7GrDzMDXf7+/0T5sEKE8A9rEiE=
golang.zx2c4.com/wireguard v0.0.0-20211116194326-3cae233d69f7/go.mod h1:evxZIqfCetExY5piKXGAxJYwvXWkps9zTCkWpkoGFxw=
golang.zx2c4.com/wireguard v0.0.0-20211116201604-de7c702ace45 h1:mEVhdMPTuebD9IUXOUB5Q2sjZpcmzkahHWd6DrGpLHA=
golang.zx2c4.com/wireguard v0.0.0-20211116201604-de7c702ace45/go.mod h1:evxZIqfCetExY5piKXGAxJYwvXWkps9zTCkWpkoGFxw=
golang.zx2c4.com/wireguard/windows v0.4.10 h1:HmjzJnb+G4NCdX+sfjsQlsxGPuYaThxRbZUZFLyR0/s=
@@ -1049,15 +1045,15 @@ honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.6/go.mod h1:pyyisuGw24ruLjrr1ddx39WE0y9OooInRzEYLhQB2YY=
honnef.co/go/tools v0.2.1 h1:/EPr//+UMMXwMTkXvCCoaJDq8cpjMO80Ou+L4PDo2mY=
honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
honnef.co/go/tools v0.2.2 h1:MNh1AVMyVX23VUHE2O27jm6lNj3vjO5DexS4A1xvnzk=
honnef.co/go/tools v0.2.2/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
howett.net/plist v0.0.0-20181124034731-591f970eefbb h1:jhnBjNi9UFpfpl8YZhA9CrOqpnJdvzuiHsl/dnxl11M=
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
inet.af/netaddr v0.0.0-20210515010201-ad03edc7c841/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 h1:acCzuUSQ79tGsM/O50VRFySfMm19IoMKL+sZztZkCxw=
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6/go.mod h1:y3MGhcFMlh0KZPMuXXow8mpjxxAk3yoDNsp4cQz54i8=
inet.af/netstack v0.0.0-20211101182044-1c1bcf452982 h1:hYciifHEv98/p8ln52ybKhgQpGouZWALFxxFE65RVdU=
inet.af/netstack v0.0.0-20211101182044-1c1bcf452982/go.mod h1:fG3G1dekmK8oDX3iVzt8c0zICLMLSN8SjdxbXVt0WjU=
inet.af/netstack v0.0.0-20211120045802-8aa80cf23d3c h1:nr31qYr+91rWD8klUkPx3eGTZzumCC414UJG1QRKZTc=
inet.af/netstack v0.0.0-20211120045802-8aa80cf23d3c/go.mod h1:KOJdAzQzMLKzwFEdOOnrnSrLIhaFVB+NQoME/e5wllA=
inet.af/peercred v0.0.0-20210318190834-4259e17bb763 h1:gPSJmmVzmdy4kHhlCMx912GdiUz3k/RzJGg0ADqy1dg=
inet.af/peercred v0.0.0-20210318190834-4259e17bb763/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU=
inet.af/wf v0.0.0-20210516214145-a5343001b756 h1:muIT3C1rH3/xpvIH8blKkMvhctV7F+OtZqs7kcwHDBQ=

View File

@@ -58,6 +58,9 @@ const (
// SysDNS is the name of the net/dns subsystem.
SysDNS = Subsystem("dns")
// SysDNSOS is the name of the net/dns OSConfigurator subsystem.
SysDNSOS = Subsystem("dns-os")
// SysNetworkCategory is the name of the subsystem that sets
// the Windows network adapter's "category" (public, private, domain).
// If it's unhealthy, the Windows firewall rules won't match.
@@ -101,6 +104,12 @@ func SetDNSHealth(err error) { set(SysDNS, err) }
// DNSHealth returns the net/dns.Manager error state.
func DNSHealth() error { return get(SysDNS) }
// SetDNSOSHealth sets the state of the net/dns.OSConfigurator
func SetDNSOSHealth(err error) { set(SysDNSOS, err) }
// DNSOSHealth returns the net/dns.OSConfigurator error state.
func DNSOSHealth() error { return get(SysDNSOS) }
// SetNetworkCategoryHealth sets the state of setting the network adaptor's category.
// This only applies on Windows.
func SetNetworkCategoryHealth(err error) { set(SysNetworkCategory, err) }

View File

@@ -87,6 +87,7 @@ const (
AWSFargate = EnvType("fg")
FlyDotIo = EnvType("fly")
Kubernetes = EnvType("k8s")
DockerDesktop = EnvType("dde")
)
var envType atomic.Value // of EnvType
@@ -144,6 +145,9 @@ func getEnvType() EnvType {
if inKubernetes() {
return Kubernetes
}
if inDockerDesktop() {
return DockerDesktop
}
return ""
}
@@ -228,6 +232,13 @@ func inKubernetes() bool {
return false
}
func inDockerDesktop() bool {
if os.Getenv("TS_HOST_ENV") == "dde" {
return true
}
return false
}
type etcAptSrcResult struct {
mod time.Time
disabled bool

View File

@@ -5,10 +5,11 @@
package hostinfo
import (
"os/exec"
"strings"
"fmt"
"sync/atomic"
"syscall"
"golang.org/x/sys/windows"
"golang.org/x/sys/windows/registry"
)
func init() {
@@ -21,19 +22,37 @@ func osVersionWindows() string {
if s, ok := winVerCache.Load().(string); ok {
return s
}
cmd := exec.Command("cmd", "/c", "ver")
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
out, _ := cmd.Output() // "\nMicrosoft Windows [Version 10.0.19041.388]\n\n"
s := strings.TrimSpace(string(out))
s = strings.TrimPrefix(s, "Microsoft Windows [")
s = strings.TrimSuffix(s, "]")
// "Version 10.x.y.z", with "Version" localized. Keep only stuff after the space.
if sp := strings.Index(s, " "); sp != -1 {
s = s[sp+1:]
major, minor, build := windows.RtlGetNtVersionNumbers()
s := fmt.Sprintf("%d.%d.%d", major, minor, build)
// Windows 11 still uses 10 as its major number internally
if major == 10 {
if ubr, err := getUBR(); err == nil {
s += fmt.Sprintf(".%d", ubr)
}
}
if s != "" {
winVerCache.Store(s)
}
return s // "10.0.19041.388", ideally
}
// getUBR obtains a fourth version field, the "Update Build Revision",
// from the registry. This field is only available beginning with Windows 10.
func getUBR() (uint32, error) {
key, err := registry.OpenKey(registry.LOCAL_MACHINE,
`SOFTWARE\Microsoft\Windows NT\CurrentVersion`, registry.QUERY_VALUE|registry.WOW64_64KEY)
if err != nil {
return 0, err
}
defer key.Close()
val, valType, err := key.GetIntegerValue("UBR")
if err != nil {
return 0, err
}
if valType != registry.DWORD {
return 0, registry.ErrUnexpectedType
}
return uint32(val), nil
}

View File

@@ -100,6 +100,8 @@ type LocalBackend struct {
filterHash deephash.Sum
filterAtomic atomic.Value // of *filter.Filter
// The mutex protects the following elements.
mu sync.Mutex
httpTestClient *http.Client // for controlclient. nil by default, used by tests.
@@ -160,9 +162,6 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge
osshare.SetFileSharingEnabled(false, logf)
// Default filter blocks everything and logs nothing, until Start() is called.
e.SetFilter(filter.NewAllowNone(logf, &netaddr.IPSet{}))
ctx, cancel := context.WithCancel(context.Background())
portpoll, err := portlist.NewPoller()
if err != nil {
@@ -182,6 +181,9 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge
portpoll: portpoll,
gotPortPollRes: make(chan struct{}),
}
// Default filter blocks everything and logs nothing, until Start() is called.
b.setFilter(filter.NewAllowNone(logf, &netaddr.IPSet{}))
b.statusChanged = sync.NewCond(&b.statusLock)
b.e.SetStatusCallback(b.setWgengineStatus)
@@ -350,8 +352,18 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
}
})
sb.MutateSelfStatus(func(ss *ipnstate.PeerStatus) {
if b.netMap != nil && b.netMap.SelfNode != nil {
ss.ID = b.netMap.SelfNode.StableID
if b.netMap != nil {
ss.HostName = b.netMap.Hostinfo.Hostname
ss.DNSName = b.netMap.Name
ss.UserID = b.netMap.User
if sn := b.netMap.SelfNode; sn != nil {
ss.ID = sn.StableID
if c := sn.Capabilities; len(c) > 0 {
ss.Capabilities = append([]string(nil), c...)
}
}
} else {
ss.HostName, _ = os.Hostname()
}
for _, pln := range b.peerAPIListeners {
ss.PeerAPIURL = append(ss.PeerAPIURL, pln.urlStr)
@@ -1001,20 +1013,25 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
if !haveNetmap {
b.logf("netmap packet filter: (not ready yet)")
b.e.SetFilter(filter.NewAllowNone(b.logf, logNets))
b.setFilter(filter.NewAllowNone(b.logf, logNets))
return
}
oldFilter := b.e.GetFilter()
if shieldsUp {
b.logf("netmap packet filter: (shields up)")
b.e.SetFilter(filter.NewShieldsUpFilter(localNets, logNets, oldFilter, b.logf))
b.setFilter(filter.NewShieldsUpFilter(localNets, logNets, oldFilter, b.logf))
} else {
b.logf("netmap packet filter: %v filters", len(packetFilter))
b.e.SetFilter(filter.New(packetFilter, localNets, logNets, oldFilter, b.logf))
b.setFilter(filter.New(packetFilter, localNets, logNets, oldFilter, b.logf))
}
}
func (b *LocalBackend) setFilter(f *filter.Filter) {
b.filterAtomic.Store(f)
b.e.SetFilter(f)
}
var removeFromDefaultRoute = []netaddr.IPPrefix{
// RFC1918 LAN ranges
netaddr.MustParseIPPrefix("192.168.0.0/16"),
@@ -1694,7 +1711,7 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
// notified (to update its prefs/persist) on
// account switch. Log this while we figure it
// out.
b.logf("active login: %s ([unexpected] corp#461, not %s)", newp.Persist.LoginName)
b.logf("active login: %q ([unexpected] corp#461, not %q)", newp.Persist.LoginName, login)
}
}
}
@@ -2142,6 +2159,11 @@ func (b *LocalBackend) initPeerAPIListener() {
selfNode: selfNode,
directFileMode: b.directFileRoot != "",
}
if re, ok := b.e.(wgengine.ResolvingEngine); ok {
if r, ok := re.GetResolver(); ok {
ps.resolver = r
}
}
b.peerAPIServer = ps
isNetstack := wgengine.IsNetstack(b.e)
@@ -2894,35 +2916,97 @@ func (b *LocalBackend) CheckIPForwarding() error {
if wgengine.IsNetstackRouter(b.e) {
return nil
}
if isBSD(runtime.GOOS) {
switch {
case isBSD(runtime.GOOS):
return fmt.Errorf("Subnet routing and exit nodes only work with additional manual configuration on %v, and is not currently officially supported.", runtime.GOOS)
case runtime.GOOS == "linux":
return checkIPForwardingLinux()
default:
// TODO: subnet routing and exit nodes probably don't work
// correctly on non-linux, non-netstack OSes either. Warn
// instead of being silent?
return nil
}
}
// checkIPForwardingLinux checks if IP forwarding is enabled correctly
// for subnet routing and exit node functionality. Returns an error
// describing configuration issues if the configuration is not
// definitely good.
func checkIPForwardingLinux() error {
const kbLink = "\nSee https://tailscale.com/kb/1104/enable-ip-forwarding/"
disabled, err := disabledSysctls("net.ipv4.ip_forward", "net.ipv6.conf.all.forwarding")
if err != nil {
return fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, kbLink)
}
var keys []string
if runtime.GOOS == "linux" {
keys = append(keys, "net.ipv4.ip_forward", "net.ipv6.conf.all.forwarding")
} else if isBSD(runtime.GOOS) {
keys = append(keys, "net.inet.ip.forwarding")
} else {
if len(disabled) == 0 {
// IP forwarding is enabled systemwide, all is well.
return nil
}
const suffix = "\nSubnet routes won't work without IP forwarding.\nSee https://tailscale.com/kb/1104/enable-ip-forwarding/"
for _, key := range keys {
bs, err := exec.Command("sysctl", "-n", key).Output()
// IP forwarding isn't enabled globally, but it might be enabled
// on a per-interface basis. Check if it's on for all interfaces,
// and warn appropriately if it's not.
ifaces, err := interfaces.GetList()
if err != nil {
return fmt.Errorf("Couldn't enumerate network interfaces, subnet routing/exit nodes may not work: %w%s", err, kbLink)
}
var (
warnings []string
anyEnabled bool
)
for _, iface := range ifaces {
if iface.Name == "lo" {
continue
}
disabled, err = disabledSysctls(fmt.Sprintf("net.ipv4.conf.%s.forwarding", iface.Name), fmt.Sprintf("net.ipv6.conf.%s.forwarding", iface.Name))
if err != nil {
return fmt.Errorf("couldn't check %s (%v)%s", key, err, suffix)
return fmt.Errorf("Couldn't check system's IP forwarding configuration, subnet routing/exit nodes may not work: %w%s", err, kbLink)
}
if len(disabled) > 0 {
warnings = append(warnings, fmt.Sprintf("Traffic received on %s won't be forwarded (%s disabled)", iface.Name, strings.Join(disabled, ", ")))
} else {
anyEnabled = true
}
}
if !anyEnabled {
// IP forwarding is compeltely disabled, just say that rather
// than enumerate all the interfaces on the system.
return fmt.Errorf("IP forwarding is disabled, subnet routing/exit nodes will not work.%s", kbLink)
}
if len(warnings) > 0 {
// If partially enabled, enumerate the bits that won't work.
return fmt.Errorf("%s\nSubnet routes and exit nodes may not work correctly.%s", strings.Join(warnings, "\n"), kbLink)
}
return nil
}
// disabledSysctls checks if the given sysctl keys are off, according
// to strconv.ParseBool. Returns a list of keys that are disabled, or
// err if something went wrong which prevented the lookups from
// completing.
func disabledSysctls(sysctls ...string) (disabled []string, err error) {
for _, k := range sysctls {
// TODO: on linux, we can get at these values via /proc/sys,
// rather than fork subcommands that may not be installed.
bs, err := exec.Command("sysctl", "-n", k).Output()
if err != nil {
return nil, fmt.Errorf("couldn't check %s (%v)", k, err)
}
on, err := strconv.ParseBool(string(bytes.TrimSpace(bs)))
if err != nil {
return fmt.Errorf("couldn't parse %s (%v)%s.", key, err, suffix)
return nil, fmt.Errorf("couldn't parse %s (%v)", k, err)
}
if !on {
return fmt.Errorf("%s is disabled.%s", key, suffix)
disabled = append(disabled, k)
}
}
return nil
return disabled, nil
}
// peerDialControlFunc is non-nil on platforms that require a way to
@@ -2947,3 +3031,25 @@ func (b *LocalBackend) DERPMap() *tailcfg.DERPMap {
}
return b.netMap.DERPMap
}
// OfferingExitNode reports whether b is currently offering exit node
// access.
func (b *LocalBackend) OfferingExitNode() bool {
b.mu.Lock()
defer b.mu.Unlock()
if b.prefs == nil {
return false
}
var def4, def6 bool
for _, r := range b.prefs.AdvertiseRoutes {
if r.Bits() != 0 {
continue
}
if r.IP().Is4() {
def4 = true
} else if r.IP().Is6() {
def6 = true
}
}
return def4 && def6
}

View File

@@ -6,6 +6,7 @@ package ipnlocal
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@@ -28,16 +29,19 @@ import (
"unicode"
"unicode/utf8"
"golang.org/x/net/dns/dnsmessage"
"inet.af/netaddr"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/logtail/backoff"
"tailscale.com/net/dns/resolver"
"tailscale.com/net/interfaces"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/util/clientmetric"
"tailscale.com/wgengine"
"tailscale.com/wgengine/filter"
)
var initListenConfig func(*net.ListenConfig, netaddr.IP, *interfaces.State, string) error
@@ -48,6 +52,7 @@ type peerAPIServer struct {
tunName string
selfNode *tailcfg.Node
knownEmpty syncs.AtomicBool
resolver *resolver.Resolver
// directFileMode is whether we're writing files directly to a
// download directory (as *.partial files), rather than making
@@ -503,6 +508,10 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.handlePeerPut(w, r)
return
}
if strings.HasPrefix(r.URL.Path, "/dns-query") {
h.handleDNSQuery(w, r)
return
}
switch r.URL.Path {
case "/v0/goroutines":
h.handleServeGoroutines(w, r)
@@ -749,3 +758,213 @@ func (h *peerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Reque
w.Header().Set("Content-Type", "text/plain")
clientmetric.WritePrometheusExpositionFormat(w)
}
func (h *peerAPIHandler) replyToDNSQueries() bool {
if h.isSelf {
// If the peer is owned by the same user, just allow it
// without further checks.
return true
}
b := h.ps.b
if !b.OfferingExitNode() {
// If we're not an exit node, there's no point to
// being a DNS server for somebody.
return false
}
if !h.remoteAddr.IsValid() {
// This should never be the case if the peerAPIHandler
// was wired up correctly, but just in case.
return false
}
// Otherwise, we're an exit node but the peer is not us, so
// we need to check if they're allowed access to the internet.
// As peerapi bypasses wgengine/filter checks, we need to check
// ourselves. As a proxy for autogroup:internet access, we see
// if we would've accepted a packet to 0.0.0.0:53. We treat
// the IP 0.0.0.0 as being "the internet".
f, ok := b.filterAtomic.Load().(*filter.Filter)
if !ok {
return false
}
// Note: we check TCP here because the Filter type already had
// a CheckTCP method (for unit tests), but it's pretty
// arbitrary. DNS runs over TCP and UDP, so sure... we check
// TCP.
dstIP := netaddr.IPv4(0, 0, 0, 0)
remoteIP := h.remoteAddr.IP()
if remoteIP.Is6() {
// autogroup:internet for IPv6 is defined to start with 2000::/3,
// so use 2000::0 as the probe "the internet" address.
dstIP = netaddr.MustParseIP("2000::")
}
verdict := f.CheckTCP(remoteIP, dstIP, 53)
return verdict == filter.Accept
}
// handleDNSQuery implements a DoH server (RFC 8484) over the peerapi.
// It's not over HTTPS as the spec dictates, but rather HTTP-over-WireGuard.
func (h *peerAPIHandler) handleDNSQuery(w http.ResponseWriter, r *http.Request) {
if h.ps.resolver == nil {
http.Error(w, "DNS not wired up", http.StatusNotImplemented)
return
}
if !h.replyToDNSQueries() {
http.Error(w, "DNS access denied", http.StatusForbidden)
return
}
pretty := false // non-DoH debug mode for humans
q, publicError := dohQuery(r)
if publicError != "" && r.Method == "GET" {
if name := r.FormValue("q"); name != "" {
pretty = true
publicError = ""
q = dnsQueryForName(name, r.FormValue("t"))
}
}
if publicError != "" {
http.Error(w, publicError, http.StatusBadRequest)
return
}
// Some timeout that's short enough to be noticed by humans
// but long enough that it's longer than real DNS timeouts.
const arbitraryTimeout = 5 * time.Second
ctx, cancel := context.WithTimeout(r.Context(), arbitraryTimeout)
defer cancel()
res, err := h.ps.resolver.HandleExitNodeDNSQuery(ctx, q, h.remoteAddr)
if err != nil {
h.logf("handleDNS fwd error: %v", err)
if err := ctx.Err(); err != nil {
http.Error(w, err.Error(), 500)
} else {
http.Error(w, "DNS forwarding error", 500)
}
return
}
if pretty {
// Non-standard response for interactive debugging.
w.Header().Set("Content-Type", "application/json")
writePrettyDNSReply(w, res)
return
}
w.Header().Set("Content-Type", "application/dns-message")
w.Header().Set("Content-Length", strconv.Itoa(len(q)))
w.Write(res)
}
func dohQuery(r *http.Request) (dnsQuery []byte, publicErr string) {
const maxQueryLen = 256 << 10
switch r.Method {
default:
return nil, "bad HTTP method"
case "GET":
q64 := r.FormValue("dns")
if q64 == "" {
return nil, "missing 'dns' parameter"
}
if base64.RawURLEncoding.DecodedLen(len(q64)) > maxQueryLen {
return nil, "query too large"
}
q, err := base64.RawURLEncoding.DecodeString(q64)
if err != nil {
return nil, "invalid 'dns' base64 encoding"
}
return q, ""
case "POST":
if r.Header.Get("Content-Type") != "application/dns-message" {
return nil, "unexpected Content-Type"
}
q, err := io.ReadAll(io.LimitReader(r.Body, maxQueryLen+1))
if err != nil {
return nil, "error reading post body with DNS query"
}
if len(q) > maxQueryLen {
return nil, "query too large"
}
return q, ""
}
}
func dnsQueryForName(name, typStr string) []byte {
typ := dnsmessage.TypeA
switch strings.ToLower(typStr) {
case "aaaa":
typ = dnsmessage.TypeAAAA
case "txt":
typ = dnsmessage.TypeTXT
}
b := dnsmessage.NewBuilder(nil, dnsmessage.Header{
OpCode: 0, // query
RecursionDesired: true,
ID: 0,
})
if !strings.HasSuffix(name, ".") {
name += "."
}
b.StartQuestions()
b.Question(dnsmessage.Question{
Name: dnsmessage.MustNewName(name),
Type: typ,
Class: dnsmessage.ClassINET,
})
msg, _ := b.Finish()
return msg
}
func writePrettyDNSReply(w io.Writer, res []byte) (err error) {
defer func() {
if err != nil {
j, _ := json.Marshal(struct {
Error string
}{err.Error()})
w.Write(j)
return
}
}()
var p dnsmessage.Parser
if _, err := p.Start(res); err != nil {
return err
}
if err := p.SkipAllQuestions(); err != nil {
return err
}
var gotIPs []string
for {
h, err := p.AnswerHeader()
if err == dnsmessage.ErrSectionDone {
break
}
if err != nil {
return err
}
if h.Class != dnsmessage.ClassINET {
continue
}
switch h.Type {
case dnsmessage.TypeA:
r, err := p.AResource()
if err != nil {
return err
}
gotIPs = append(gotIPs, net.IP(r.A[:]).String())
case dnsmessage.TypeAAAA:
r, err := p.AAAAResource()
if err != nil {
return err
}
gotIPs = append(gotIPs, net.IP(r.AAAA[:]).String())
case dnsmessage.TypeTXT:
r, err := p.TXTResource()
if err != nil {
return err
}
gotIPs = append(gotIPs, r.TXT...)
}
}
j, _ := json.Marshal(gotIPs)
j = append(j, '\n')
w.Write(j)
return nil
}

View File

@@ -19,8 +19,13 @@ import (
"strings"
"testing"
"inet.af/netaddr"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/types/logger"
"tailscale.com/wgengine"
"tailscale.com/wgengine/filter"
)
type peerAPITestEnv struct {
@@ -568,3 +573,55 @@ func TestDeletedMarkers(t *testing.T) {
}
}
func TestPeerAPIReplyToDNSQueries(t *testing.T) {
var h peerAPIHandler
h.isSelf = true
if !h.replyToDNSQueries() {
t.Errorf("for isSelf = false; want true")
}
h.isSelf = false
h.remoteAddr = netaddr.MustParseIPPort("100.150.151.152:12345")
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0)
h.ps = &peerAPIServer{
b: &LocalBackend{
e: eng,
},
}
if h.ps.b.OfferingExitNode() {
t.Fatal("unexpectedly offering exit node")
}
h.ps.b.prefs = &ipn.Prefs{
AdvertiseRoutes: []netaddr.IPPrefix{
netaddr.MustParseIPPrefix("0.0.0.0/0"),
netaddr.MustParseIPPrefix("::/0"),
},
}
if !h.ps.b.OfferingExitNode() {
t.Fatal("unexpectedly not offering exit node")
}
if h.replyToDNSQueries() {
t.Errorf("unexpectedly doing DNS without filter")
}
h.ps.b.setFilter(filter.NewAllowNone(logger.Discard, new(netaddr.IPSet)))
if h.replyToDNSQueries() {
t.Errorf("unexpectedly doing DNS without filter")
}
f := filter.NewAllowAllForTest(logger.Discard)
h.ps.b.setFilter(f)
if !h.replyToDNSQueries() {
t.Errorf("unexpectedly deny; wanted to be a DNS server")
}
// Also test IPv6.
h.remoteAddr = netaddr.MustParseIPPort("[fe70::1]:12345")
if !h.replyToDNSQueries() {
t.Errorf("unexpectedly IPv6 deny; wanted to be a DNS server")
}
}

View File

@@ -70,9 +70,14 @@ func (s *Status) Peers() []key.NodePublic {
}
type PeerStatusLite struct {
// TxBytes/RxBytes is the total number of bytes transmitted to/received from this peer.
TxBytes, RxBytes int64
LastHandshake time.Time
NodeKey key.NodePublic
// LastHandshake is the last time a handshake succeeded with this peer.
// (Or we got key confirmation via the first data message,
// which is approximately the same thing.)
LastHandshake time.Time
// NodeKey is this peer's public node key.
NodeKey key.NodePublic
}
type PeerStatus struct {

View File

@@ -23,7 +23,7 @@ import (
)
const (
parameterNameRxStr = `^parameter/(.*)`
parameterNameRxStr = `^parameter(/.*)`
)
var parameterNameRx = regexp.MustCompile(parameterNameRxStr)

View File

@@ -501,7 +501,7 @@ func New(collection string) *Policy {
}
return w
},
HTTPC: &http.Client{Transport: newLogtailTransport(logtail.DefaultHost)},
HTTPC: &http.Client{Transport: NewLogtailTransport(logtail.DefaultHost)},
}
if collection == logtail.CollectionNode {
c.MetricsDelta = clientmetric.EncodeLogTailMetricsDelta
@@ -511,7 +511,7 @@ func New(collection string) *Policy {
log.Println("You have enabled a non-default log target. Doing without being told to by Tailscale staff or your network administrator will make getting support difficult.")
c.BaseURL = val
u, _ := url.Parse(val)
c.HTTPC = &http.Client{Transport: newLogtailTransport(u.Host)}
c.HTTPC = &http.Client{Transport: NewLogtailTransport(u.Host)}
}
filchBuf, filchErr := filch.New(filepath.Join(dir, cmdName), filch.Options{
@@ -571,9 +571,12 @@ func (p *Policy) Shutdown(ctx context.Context) error {
return nil
}
// newLogtailTransport returns the HTTP Transport we use for uploading
// logs to the given host name.
func newLogtailTransport(host string) *http.Transport {
// NewLogtailTransport returns an HTTP Transport particularly suited to uploading
// logs to the given host name. This includes:
// - If DNS lookup fails, consult the bootstrap DNS list of Tailscale hostnames.
// - If TLS connection fails, try again using LetsEncrypt's built-in root certificate,
// for the benefit of older OS platforms which might not include it.
func NewLogtailTransport(host string) *http.Transport {
// Start with a copy of http.DefaultTransport and tweak it a bit.
tr := http.DefaultTransport.(*http.Transport).Clone()

View File

@@ -18,6 +18,7 @@ import (
"path/filepath"
"tailscale.com/atomicfile"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
)
@@ -173,6 +174,10 @@ func (m *resolvconfManager) GetBaseConfig() (OSConfig, error) {
return readResolv(&conf)
}
func (m *resolvconfManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return getExitNodeForwardResolverFromBaseConfig(m)
}
func (m *resolvconfManager) Close() error {
if err := m.deleteTailscaleConfig(); err != nil {
return err

View File

@@ -18,6 +18,7 @@ import (
"strings"
"inet.af/netaddr"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
)
@@ -50,10 +51,17 @@ func readResolv(r io.Reader) (config OSConfig, err error) {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
i := strings.IndexByte(line, '#')
if i >= 0 {
line = line[:i]
}
if strings.HasPrefix(line, "nameserver") {
nameserver := strings.TrimPrefix(line, "nameserver")
nameserver = strings.TrimSpace(nameserver)
s := strings.TrimPrefix(line, "nameserver")
nameserver := strings.TrimSpace(s)
if len(nameserver) == len(s) {
return OSConfig{}, fmt.Errorf("missing space after \"nameserver\" in %q", line)
}
ip, err := netaddr.ParseIP(nameserver)
if err != nil {
return OSConfig{}, err
@@ -63,8 +71,12 @@ func readResolv(r io.Reader) (config OSConfig, err error) {
}
if strings.HasPrefix(line, "search") {
domain := strings.TrimPrefix(line, "search")
domain = strings.TrimSpace(domain)
s := strings.TrimPrefix(line, "search")
domain := strings.TrimSpace(s)
if len(domain) == len(s) {
// No leading space?!
return OSConfig{}, fmt.Errorf("missing space after \"domain\" in %q", line)
}
fqdn, err := dnsname.ToFQDN(domain)
if err != nil {
return OSConfig{}, fmt.Errorf("parsing search domains %q: %w", line, err)
@@ -172,6 +184,24 @@ func (m *directManager) readResolvFile(path string) (OSConfig, error) {
return readResolv(bytes.NewReader(b))
}
func (m *directManager) GetExitNodeForwardResolver() (ret []dnstype.Resolver, retErr error) {
for _, filename := range []string{backupConf, resolvConf} {
if oc, err := m.readResolvFile(filename); err == nil {
for _, ip := range oc.Nameservers {
if ip != netaddr.IPv4(100, 100, 100, 100) {
ret = append(ret, dnstype.Resolver{Addr: netaddr.IPPortFrom(ip, 53).String()})
}
}
if len(ret) > 0 {
return ret, nil
}
} else if !os.IsNotExist(err) && retErr == nil {
retErr = err
}
}
return nil, retErr
}
// ownedByTailscale reports whether /etc/resolv.conf seems to be a
// tailscale-managed file.
func (m *directManager) ownedByTailscale() (bool, error) {

View File

@@ -14,6 +14,7 @@ import (
"syscall"
"testing"
qt "github.com/frankban/quicktest"
"inet.af/netaddr"
"tailscale.com/util/dnsname"
)
@@ -138,3 +139,61 @@ func TestDirectBrokenRemove(t *testing.T) {
}
testDirect(t, brokenRemoveFS{directFS{prefix: tmp}})
}
func TestReadResolve(t *testing.T) {
c := qt.New(t)
tests := []struct {
in string
want OSConfig
wantErr bool
}{
{in: `nameserver 192.168.0.100`,
want: OSConfig{
Nameservers: []netaddr.IP{
netaddr.MustParseIP("192.168.0.100"),
},
},
},
{in: `nameserver 192.168.0.100 # comment`,
want: OSConfig{
Nameservers: []netaddr.IP{
netaddr.MustParseIP("192.168.0.100"),
},
},
},
{in: `nameserver 192.168.0.100#`,
want: OSConfig{
Nameservers: []netaddr.IP{
netaddr.MustParseIP("192.168.0.100"),
},
},
},
{in: `nameserver #192.168.0.100`, wantErr: true},
{in: `nameserver`, wantErr: true},
{in: `# nameserver 192.168.0.100`, want: OSConfig{}},
{in: `nameserver192.168.0.100`, wantErr: true},
{in: `search tailsacle.com`,
want: OSConfig{
SearchDomains: []dnsname.FQDN{"tailsacle.com."},
},
},
{in: `search tailsacle.com # typo`,
want: OSConfig{
SearchDomains: []dnsname.FQDN{"tailsacle.com."},
},
},
{in: `searchtailsacle.com`, wantErr: true},
{in: `search`, wantErr: true},
}
for _, test := range tests {
cfg, err := readResolv(strings.NewReader(test.in))
if test.wantErr {
c.Assert(err, qt.IsNotNil)
} else {
c.Assert(err, qt.IsNil)
}
c.Assert(cfg, qt.DeepEquals, test.want)
}
}

View File

@@ -10,6 +10,7 @@ import (
"time"
"inet.af/netaddr"
"tailscale.com/health"
"tailscale.com/net/dns/resolver"
"tailscale.com/net/tsaddr"
"tailscale.com/types/dnstype"
@@ -35,8 +36,6 @@ type Manager struct {
resolver *resolver.Resolver
os OSConfigurator
config Config
}
// NewManagers created a new manager from the given config.
@@ -51,6 +50,9 @@ func NewManager(logf logger.Logf, oscfg OSConfigurator, linkMon *monitor.Mon, li
return m
}
// Resolver returns the Manager's DNS Resolver.
func (m *Manager) Resolver() *resolver.Resolver { return m.resolver }
func (m *Manager) Set(cfg Config) error {
m.logf("Set: %v", logger.ArgWriter(func(w *bufio.Writer) {
cfg.WriteToBufioWriter(w)
@@ -60,6 +62,11 @@ func (m *Manager) Set(cfg Config) error {
if err != nil {
return err
}
exitNodeBackupResolvers, err := m.os.GetExitNodeForwardResolver()
if err != nil {
return err
}
rcfg.ExitNodeBackupResolvers = exitNodeBackupResolvers
m.logf("Resolvercfg: %v", logger.ArgWriter(func(w *bufio.Writer) {
rcfg.WriteToBufioWriter(w)
@@ -70,8 +77,10 @@ func (m *Manager) Set(cfg Config) error {
return err
}
if err := m.os.SetDNS(ocfg); err != nil {
health.SetDNSOSHealth(err)
return err
}
health.SetDNSOSHealth(nil)
return nil
}
@@ -161,6 +170,7 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
if !m.os.SupportsSplitDNS() || isWindows {
bcfg, err := m.os.GetBaseConfig()
if err != nil {
health.SetDNSOSHealth(err)
return resolver.Config{}, OSConfig{}, err
}
var defaultRoutes []dnstype.Resolver

View File

@@ -45,6 +45,10 @@ func (c *fakeOSConfigurator) GetBaseConfig() (OSConfig, error) {
return c.BaseConfig, nil
}
func (c *fakeOSConfigurator) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return getExitNodeForwardResolverFromBaseConfig(c)
}
func (c *fakeOSConfigurator) Close() error { return nil }
func TestManager(t *testing.T) {
@@ -213,6 +217,7 @@ func TestManager(t *testing.T) {
Routes: upstreams(
".", "8.8.8.8:53",
"corp.com.", "2.2.2.2:53"),
ExitNodeBackupResolvers: []dnstype.Resolver{{Addr: "8.8.8.8:53"}},
},
},
{
@@ -249,6 +254,7 @@ func TestManager(t *testing.T) {
".", "8.8.8.8:53",
"corp.com.", "2.2.2.2:53",
"bigco.net.", "3.3.3.3:53"),
ExitNodeBackupResolvers: []dnstype.Resolver{{Addr: "8.8.8.8:53"}},
},
},
{
@@ -293,7 +299,8 @@ func TestManager(t *testing.T) {
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: fqdns("ts.com."),
LocalDomains: fqdns("ts.com."),
ExitNodeBackupResolvers: []dnstype.Resolver{{Addr: "8.8.8.8:53"}},
},
},
{
@@ -342,7 +349,8 @@ func TestManager(t *testing.T) {
Hosts: hosts(
"dave.ts.com.", "1.2.3.4",
"bradfitz.ts.com.", "2.3.4.5"),
LocalDomains: fqdns("ts.com."),
LocalDomains: fqdns("ts.com."),
ExitNodeBackupResolvers: []dnstype.Resolver{{Addr: "8.8.8.8:53"}},
},
},
{

View File

@@ -17,6 +17,7 @@ import (
"golang.org/x/sys/windows/registry"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"inet.af/netaddr"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
)
@@ -340,6 +341,10 @@ func (m windowsManager) GetBaseConfig() (OSConfig, error) {
}, nil
}
func (m windowsManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return getExitNodeForwardResolverFromBaseConfig(m)
}
// getBasePrimaryResolver returns a guess of the non-Tailscale primary
// resolver on the system.
// It's used on Windows 7 to emulate split DNS by trying to figure out

View File

@@ -16,6 +16,7 @@ import (
"github.com/godbus/dbus/v5"
"inet.af/netaddr"
"tailscale.com/net/interfaces"
"tailscale.com/types/dnstype"
"tailscale.com/util/dnsname"
"tailscale.com/util/endian"
)
@@ -374,6 +375,10 @@ func (m *nmManager) GetBaseConfig() (OSConfig, error) {
return ret, nil
}
func (m *nmManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return getExitNodeForwardResolverFromBaseConfig(m)
}
func (m *nmManager) Close() error {
// No need to do anything on close, NetworkManager will delete our
// settings when the tailscale interface goes away.

View File

@@ -4,14 +4,21 @@
package dns
import "tailscale.com/types/dnstype"
type noopManager struct{}
var _ OSConfigurator = noopManager{}
func (m noopManager) SetDNS(OSConfig) error { return nil }
func (m noopManager) SupportsSplitDNS() bool { return false }
func (m noopManager) Close() error { return nil }
func (m noopManager) GetBaseConfig() (OSConfig, error) {
return OSConfig{}, ErrGetBaseConfigNotSupported
}
func (m noopManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return nil, nil
}
func NewNoopManager() (noopManager, error) {
return noopManager{}, nil

View File

@@ -12,6 +12,8 @@ import (
"fmt"
"os/exec"
"strings"
"tailscale.com/types/dnstype"
)
// openresolvManager manages DNS configuration using the openresolv
@@ -90,6 +92,10 @@ func (m openresolvManager) GetBaseConfig() (OSConfig, error) {
return readResolv(&buf)
}
func (m openresolvManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return getExitNodeForwardResolverFromBaseConfig(m)
}
func (m openresolvManager) Close() error {
return m.deleteTailscaleConfig()
}

View File

@@ -8,6 +8,7 @@ import (
"errors"
"inet.af/netaddr"
"tailscale.com/types/dnstype"
"tailscale.com/util/dnsname"
)
@@ -18,20 +19,35 @@ type OSConfigurator interface {
// configuration is removed.
// SetDNS must not be called after Close.
SetDNS(cfg OSConfig) error
// SupportsSplitDNS reports whether the configurator is capable of
// installing a resolver only for specific DNS suffixes. If false,
// the configurator can only set a global resolver.
SupportsSplitDNS() bool
// GetBaseConfig returns the OS's "base" configuration, i.e. the
// resolver settings the OS would use without Tailscale
// contributing any configuration.
// GetBaseConfig must return the tailscale-free base config even
// after SetDNS has been called to set a Tailscale configuration.
// Only works when SupportsSplitDNS=false.
//
// Implementations that don't support getting the base config must
// return ErrGetBaseConfigNotSupported.
GetBaseConfig() (OSConfig, error)
// GetExitNodeForwardResolver returns the resolver(s) that should
// be used as a fallback for the exit node's DNS-over-HTTP peerapi
// to send DNS queries from peers on to, in the case where the tailnet
// doesn't have global DNS servers configured.
//
// For example, on Linux with systemd-resolved, this will
// return 127.0.0.53:53.
//
// On other systems, it'll usually be the value of
// GetBaseConfig.Nameservers.
GetExitNodeForwardResolver() ([]dnstype.Resolver, error)
// Close removes Tailscale-related DNS configuration from the OS.
Close() error
}
@@ -90,3 +106,16 @@ func (a OSConfig) Equal(b OSConfig) bool {
// OSConfigurator.GetBaseConfig returns when the OSConfigurator
// doesn't support reading the underlying configuration out of the OS.
var ErrGetBaseConfigNotSupported = errors.New("getting OS base config is not supported")
func getExitNodeForwardResolverFromBaseConfig(o OSConfigurator) (ret []dnstype.Resolver, retErr error) {
oc, err := o.GetBaseConfig()
if err != nil {
return nil, err
}
for _, ip := range oc.Nameservers {
if ip != netaddr.IPv4(100, 100, 100, 100) {
ret = append(ret, dnstype.Resolver{Addr: netaddr.IPPortFrom(ip, 53).String()})
}
}
return ret, nil
}

View File

@@ -13,10 +13,13 @@ import (
"fmt"
"net"
"strings"
"sync"
"github.com/godbus/dbus/v5"
"golang.org/x/sys/unix"
"inet.af/netaddr"
"tailscale.com/health"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
)
@@ -40,6 +43,28 @@ var resolvedListenAddr = netaddr.IPv4(127, 0, 0, 53)
var errNotReady = errors.New("interface not ready")
// DBus entities we talk to.
//
// DBus is an RPC bus. In particular, the bus we're talking to is the
// system-wide bus (there is also a per-user session bus for
// user-specific applications).
//
// Daemons connect to the bus, and advertise themselves under a
// well-known object name. That object exposes paths, and each path
// implements one or more interfaces that contain methods, properties,
// and signals.
//
// Clients connect to the bus and walk that same hierarchy to invoke
// RPCs, get/set properties, or listen for signals.
const (
dbusResolvedObject = "org.freedesktop.resolve1"
dbusResolvedPath dbus.ObjectPath = "/org/freedesktop/resolve1"
dbusResolvedInterface = "org.freedesktop.resolve1.Manager"
dbusPath dbus.ObjectPath = "/org/freedesktop/DBus"
dbusInterface = "org.freedesktop.DBus"
dbusOwnerSignal = "NameOwnerChanged" // broadcast when a well-known name's owning process changes.
)
type resolvedLinkNameserver struct {
Family int32
Address []byte
@@ -84,9 +109,15 @@ func isResolvedActive() bool {
// resolvedManager is an OSConfigurator which uses the systemd-resolved DBus API.
type resolvedManager struct {
logf logger.Logf
ifidx int
resolved dbus.BusObject
logf logger.Logf
ifidx int
cancelSyncer context.CancelFunc // run to shut down syncer goroutine
syncerDone chan struct{} // closed when syncer is stopped
resolved dbus.BusObject
mu sync.Mutex // guards RPCs made by syncLocked, and the following
config OSConfig // last SetDNS config
}
func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManager, error) {
@@ -100,19 +131,88 @@ func newResolvedManager(logf logger.Logf, interfaceName string) (*resolvedManage
return nil, err
}
return &resolvedManager{
logf: logf,
ifidx: iface.Index,
resolved: conn.Object("org.freedesktop.resolve1", dbus.ObjectPath("/org/freedesktop/resolve1")),
}, nil
ctx, cancel := context.WithCancel(context.Background())
ret := &resolvedManager{
logf: logf,
ifidx: iface.Index,
cancelSyncer: cancel,
syncerDone: make(chan struct{}),
resolved: conn.Object(dbusResolvedObject, dbus.ObjectPath(dbusResolvedPath)),
}
signals := make(chan *dbus.Signal, 16)
go ret.resync(ctx, signals)
// Only receive the DBus signals we need to resync our config on
// resolved restart. Failure to set filters isn't a fatal error,
// we'll just receive all broadcast signals and have to ignore
// them on our end.
if err := conn.AddMatchSignal(dbus.WithMatchObjectPath(dbusPath), dbus.WithMatchInterface(dbusInterface), dbus.WithMatchMember(dbusOwnerSignal), dbus.WithMatchArg(0, dbusResolvedObject)); err != nil {
logf("[v1] Setting DBus signal filter failed: %v", err)
}
conn.Signal(signals)
return ret, nil
}
func (m *resolvedManager) SetDNS(config OSConfig) error {
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
m.mu.Lock()
defer m.mu.Unlock()
m.config = config
return m.syncLocked(context.TODO()) // would be nice to plumb context through from SetDNS
}
func (m *resolvedManager) resync(ctx context.Context, signals chan *dbus.Signal) {
defer close(m.syncerDone)
for {
select {
case <-ctx.Done():
return
case signal := <-signals:
// In theory the signal was filtered by DBus, but if
// AddMatchSignal in the constructor failed, we may be
// getting other spam.
if signal.Path != dbusPath || signal.Name != dbusInterface+"."+dbusOwnerSignal {
continue
}
// signal.Body is a []interface{} of 3 strings: bus name, previous owner, new owner.
if len(signal.Body) != 3 {
m.logf("[unexpectected] DBus NameOwnerChanged len(Body) = %d, want 3")
}
if name, ok := signal.Body[0].(string); !ok || name != dbusResolvedObject {
continue
}
newOwner, ok := signal.Body[2].(string)
if !ok {
m.logf("[unexpected] DBus NameOwnerChanged.new_owner is a %T, not a string", signal.Body[2])
}
if newOwner == "" {
// systemd-resolved left the bus, no current owner,
// nothing to do.
continue
}
// The resolved bus name has a new owner, meaning resolved
// restarted. Reprogram current config.
m.logf("systemd-resolved restarted, syncing DNS config")
m.mu.Lock()
err := m.syncLocked(ctx)
// Set health while holding the lock, because this will
// graciously serialize the resync's health outcome with a
// concurrent SetDNS call.
health.SetDNSOSHealth(err)
m.mu.Unlock()
if err != nil {
m.logf("failed to configure systemd-resolved: %v", err)
}
}
}
}
func (m *resolvedManager) syncLocked(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, reconfigTimeout)
defer cancel()
var linkNameservers = make([]resolvedLinkNameserver, len(config.Nameservers))
for i, server := range config.Nameservers {
var linkNameservers = make([]resolvedLinkNameserver, len(m.config.Nameservers))
for i, server := range m.config.Nameservers {
ip := server.As16()
if server.Is4() {
linkNameservers[i] = resolvedLinkNameserver{
@@ -128,16 +228,16 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
}
err := m.resolved.CallWithContext(
ctx, "org.freedesktop.resolve1.Manager.SetLinkDNS", 0,
ctx, dbusResolvedInterface+".SetLinkDNS", 0,
m.ifidx, linkNameservers,
).Store()
if err != nil {
return fmt.Errorf("setLinkDNS: %w", err)
}
linkDomains := make([]resolvedLinkDomain, 0, len(config.SearchDomains)+len(config.MatchDomains))
linkDomains := make([]resolvedLinkDomain, 0, len(m.config.SearchDomains)+len(m.config.MatchDomains))
seenDomains := map[dnsname.FQDN]bool{}
for _, domain := range config.SearchDomains {
for _, domain := range m.config.SearchDomains {
if seenDomains[domain] {
continue
}
@@ -147,7 +247,7 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
RoutingOnly: false,
})
}
for _, domain := range config.MatchDomains {
for _, domain := range m.config.MatchDomains {
if seenDomains[domain] {
// Search domains act as both search and match in
// resolved, so it's correct to skip.
@@ -159,7 +259,7 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
RoutingOnly: true,
})
}
if len(config.MatchDomains) == 0 && len(config.Nameservers) > 0 {
if len(m.config.MatchDomains) == 0 && len(m.config.Nameservers) > 0 {
// Caller requested full DNS interception, install a
// routing-only root domain.
linkDomains = append(linkDomains, resolvedLinkDomain{
@@ -169,14 +269,14 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
}
err = m.resolved.CallWithContext(
ctx, "org.freedesktop.resolve1.Manager.SetLinkDomains", 0,
ctx, dbusResolvedInterface+".SetLinkDomains", 0,
m.ifidx, linkDomains,
).Store()
if err != nil && err.Error() == "Argument list too long" { // TODO: better error match
// Issue 3188: older systemd-resolved had argument length limits.
// Trim out the *.arpa. entries and try again.
err = m.resolved.CallWithContext(
ctx, "org.freedesktop.resolve1.Manager.SetLinkDomains", 0,
ctx, dbusResolvedInterface+".SetLinkDomains", 0,
m.ifidx, linkDomainsWithoutReverseDNS(linkDomains),
).Store()
}
@@ -184,7 +284,7 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
return fmt.Errorf("setLinkDomains: %w", err)
}
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDefaultRoute", 0, m.ifidx, len(config.MatchDomains) == 0); call.Err != nil {
if call := m.resolved.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDefaultRoute", 0, m.ifidx, len(m.config.MatchDomains) == 0); call.Err != nil {
if dbusErr, ok := call.Err.(dbus.Error); ok && dbusErr.Name == dbus.ErrMsgUnknownMethod.Name {
// on some older systems like Kubuntu 18.04.6 with systemd 237 method SetLinkDefaultRoute is absent,
// but otherwise it's working good
@@ -199,26 +299,26 @@ func (m *resolvedManager) SetDNS(config OSConfig) error {
// or something).
// Disable LLMNR, we don't do multicast.
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkLLMNR", 0, m.ifidx, "no"); call.Err != nil {
if call := m.resolved.CallWithContext(ctx, dbusResolvedInterface+".SetLinkLLMNR", 0, m.ifidx, "no"); call.Err != nil {
m.logf("[v1] failed to disable LLMNR: %v", call.Err)
}
// Disable mdns.
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkMulticastDNS", 0, m.ifidx, "no"); call.Err != nil {
if call := m.resolved.CallWithContext(ctx, dbusResolvedInterface+".SetLinkMulticastDNS", 0, m.ifidx, "no"); call.Err != nil {
m.logf("[v1] failed to disable mdns: %v", call.Err)
}
// We don't support dnssec consistently right now, force it off to
// avoid partial failures when we split DNS internally.
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDNSSEC", 0, m.ifidx, "no"); call.Err != nil {
if call := m.resolved.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDNSSEC", 0, m.ifidx, "no"); call.Err != nil {
m.logf("[v1] failed to disable DNSSEC: %v", call.Err)
}
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.SetLinkDNSOverTLS", 0, m.ifidx, "no"); call.Err != nil {
if call := m.resolved.CallWithContext(ctx, dbusResolvedInterface+".SetLinkDNSOverTLS", 0, m.ifidx, "no"); call.Err != nil {
m.logf("[v1] failed to disable DoT: %v", call.Err)
}
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.FlushCaches", 0); call.Err != nil {
if call := m.resolved.CallWithContext(ctx, dbusResolvedInterface+".FlushCaches", 0); call.Err != nil {
m.logf("failed to flush resolved DNS cache: %v", call.Err)
}
@@ -233,14 +333,25 @@ func (m *resolvedManager) GetBaseConfig() (OSConfig, error) {
return OSConfig{}, ErrGetBaseConfigNotSupported
}
func (m *resolvedManager) GetExitNodeForwardResolver() ([]dnstype.Resolver, error) {
return []dnstype.Resolver{{Addr: "127.0.0.53:53"}}, nil
}
func (m *resolvedManager) Close() error {
m.cancelSyncer()
ctx, cancel := context.WithTimeout(context.Background(), reconfigTimeout)
defer cancel()
if call := m.resolved.CallWithContext(ctx, "org.freedesktop.resolve1.Manager.RevertLink", 0, m.ifidx); call.Err != nil {
if call := m.resolved.CallWithContext(ctx, dbusResolvedInterface+".RevertLink", 0, m.ifidx); call.Err != nil {
return fmt.Errorf("RevertLink: %w", call.Err)
}
select {
case <-m.syncerDone:
case <-ctx.Done():
m.logf("timeout in systemd-resolved syncer shutdown")
}
return nil
}

View File

@@ -385,6 +385,7 @@ func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client,
}
defer f.releaseDoHSem()
metricDNSFwdDoH.Add(1)
req, err := http.NewRequestWithContext(ctx, "POST", urlBase, bytes.NewReader(packet))
if err != nil {
return nil, err
@@ -398,16 +399,23 @@ func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client,
hres, err := c.Do(req)
if err != nil {
metricDNSFwdDoHErrorTransport.Add(1)
return nil, err
}
defer hres.Body.Close()
if hres.StatusCode != 200 {
metricDNSFwdDoHErrorStatus.Add(1)
return nil, errors.New(hres.Status)
}
if ct := hres.Header.Get("Content-Type"); ct != dohType {
metricDNSFwdDoHErrorCT.Add(1)
return nil, fmt.Errorf("unexpected response Content-Type %q", ct)
}
return ioutil.ReadAll(hres.Body)
res, err := ioutil.ReadAll(hres.Body)
if err != nil {
metricDNSFwdDoHErrorBody.Add(1)
}
return res, err
}
// send sends packet to dst. It is best effort.
@@ -415,12 +423,15 @@ func (f *forwarder) sendDoH(ctx context.Context, urlBase string, c *http.Client,
// send expects the reply to have the same txid as txidOut.
func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDelay) ([]byte, error) {
if strings.HasPrefix(rr.name.Addr, "http://") {
metricDNSFwdErrorType.Add(1)
return nil, fmt.Errorf("http:// resolvers not supported yet")
}
if strings.HasPrefix(rr.name.Addr, "https://") {
metricDNSFwdErrorType.Add(1)
return nil, fmt.Errorf("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)
@@ -438,6 +449,7 @@ func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDe
f.logf("DoH error from %v: %v", ipp.IP(), err)
}
metricDNSFwdUDP.Add(1)
ln, err := f.packetListener(ipp.IP())
if err != nil {
return nil, err
@@ -453,11 +465,13 @@ func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDe
defer fq.closeOnCtxDone.Remove(conn)
if _, err := conn.WriteTo(fq.packet, ipp.UDPAddr()); err != nil {
metricDNSFwdUDPErrorWrite.Add(1)
if err := ctx.Err(); err != nil {
return nil, err
}
return nil, err
}
metricDNSFwdUDPWrote.Add(1)
// The 1 extra byte is to detect packet truncation.
out := make([]byte, maxResponseBytes+1)
@@ -469,6 +483,7 @@ func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDe
if packetWasTruncated(err) {
err = nil
} else {
metricDNSFwdUDPErrorRead.Add(1)
return nil, err
}
}
@@ -482,12 +497,14 @@ func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDe
out = out[:n]
txid := getTxID(out)
if txid != fq.txid {
metricDNSFwdUDPErrorTxID.Add(1)
return nil, errors.New("txid doesn't match")
}
rcode := getRCode(out)
// don't forward transient errors back to the client when the server fails
if rcode == dns.RCodeServerFailure {
f.logf("recv: response code indicating server failure: %d", rcode)
metricDNSFwdUDPErrorServer.Add(1)
return nil, errors.New("response code indicates server issue")
}
@@ -505,7 +522,7 @@ func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDe
}
clampEDNSSize(out, maxResponseBytes)
metricDNSFwdUDPSuccess.Add(1)
return out, nil
}
@@ -546,10 +563,30 @@ type forwardQuery struct {
// ...
}
// forward forwards the query to all upstream nameservers and returns the first response.
// forward forwards the query to all upstream nameservers and waits for
// the first response.
//
// It either sends to f.responses and returns nil, or returns a
// non-nil error (without sending to the channel).
func (f *forwarder) forward(query packet) error {
ctx, cancel := context.WithTimeout(f.ctx, responseTimeout)
defer cancel()
return f.forwardWithDestChan(ctx, query, f.responses)
}
// forward forwards the query to all upstream nameservers and waits
// for the first response.
//
// It either sends to responseChan and returns nil, or returns a
// non-nil error (without sending to the channel).
//
// If backupResolvers are specified, they're used in the case that no
// upstreams are available.
func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, responseChan chan<- packet, backupResolvers ...resolverAndDelay) error {
metricDNSFwd.Add(1)
domain, err := nameFromQuery(query.bs)
if err != nil {
metricDNSFwdErrorName.Add(1)
return err
}
@@ -558,6 +595,7 @@ func (f *forwarder) forward(query packet) error {
// when browsing for LAN devices. But even when filtering this
// out, playing on Sonos still works.
if hasRDNSBonjourPrefix(domain) {
metricDNSFwdDropBonjour.Add(1)
return nil
}
@@ -565,6 +603,10 @@ func (f *forwarder) forward(query packet) error {
resolvers := f.resolvers(domain)
if len(resolvers) == 0 {
resolvers = backupResolvers
}
if len(resolvers) == 0 {
metricDNSFwdErrorNoUpstream.Add(1)
return errNoUpstreams
}
@@ -575,9 +617,6 @@ func (f *forwarder) forward(query packet) error {
}
defer fq.closeOnCtxDone.Close()
ctx, cancel := context.WithTimeout(f.ctx, responseTimeout)
defer cancel()
resc := make(chan []byte, 1)
var (
mu sync.Mutex
@@ -615,14 +654,18 @@ func (f *forwarder) forward(query packet) error {
case v := <-resc:
select {
case <-ctx.Done():
metricDNSFwdErrorContext.Add(1)
return ctx.Err()
case f.responses <- packet{v, query.addr}:
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

@@ -8,6 +8,7 @@ package resolver
import (
"bufio"
"context"
"encoding/hex"
"errors"
"fmt"
@@ -23,6 +24,7 @@ import (
"inet.af/netaddr"
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
"tailscale.com/util/dnsname"
"tailscale.com/wgengine/monitor"
)
@@ -81,6 +83,14 @@ type Config struct {
// LocalDomains is a list of DNS name suffixes that should not be
// routed to upstream resolvers.
LocalDomains []dnsname.FQDN
// ExitNodeBackupResolvers are where the local node when
// acting as an exit node and serving a DNS proxy should
// forward DNS requests to in the case where there are no
// routes found. For example, for Linux systemd-resolved
// machines this is likely 127.0.0.53:53.
// If it's empty, there are no backups and the OS should
// be queried directly using its OS-level DNS APIs.
ExitNodeBackupResolvers []dnstype.Resolver
}
// WriteToBufioWriter write a debug version of c for logs to w, omitting
@@ -200,10 +210,11 @@ type Resolver struct {
wg sync.WaitGroup
// mu guards the following fields from being updated while used.
mu sync.Mutex
localDomains []dnsname.FQDN
hostToIP map[dnsname.FQDN][]netaddr.IP
ipToHost map[netaddr.IP]dnsname.FQDN
mu sync.Mutex
localDomains []dnsname.FQDN
hostToIP map[dnsname.FQDN][]netaddr.IP
ipToHost map[netaddr.IP]dnsname.FQDN
exitNodeBackupResolvers []dnstype.Resolver
}
type ForwardLinkSelector interface {
@@ -251,9 +262,16 @@ func (r *Resolver) SetConfig(cfg Config) error {
r.localDomains = cfg.LocalDomains
r.hostToIP = cfg.Hosts
r.ipToHost = reverse
r.exitNodeBackupResolvers = append([]dnstype.Resolver(nil), cfg.ExitNodeBackupResolvers...)
return nil
}
func (r *Resolver) exitNodeForwardResolvers() []dnstype.Resolver {
r.mu.Lock()
defer r.mu.Unlock()
return r.exitNodeBackupResolvers
}
// Close shuts down the resolver and ensures poll goroutines have exited.
// The Resolver cannot be used again after Close is called.
func (r *Resolver) Close() {
@@ -272,13 +290,16 @@ func (r *Resolver) Close() {
// It takes ownership of the payload and does not block.
// If the queue is full, the request will be dropped and an error will be returned.
func (r *Resolver) EnqueueRequest(bs []byte, from netaddr.IPPort) error {
metricDNSQueryLocal.Add(1)
select {
case <-r.closed:
metricDNSQueryErrorClosed.Add(1)
return ErrClosed
default:
}
if n := atomic.AddInt32(&r.activeQueriesAtomic, 1); n > maxActiveQueries() {
atomic.AddInt32(&r.activeQueriesAtomic, -1)
metricDNSQueryErrorQueue.Add(1)
return errFullQueue
}
go r.handleQuery(packet{bs, from})
@@ -298,14 +319,49 @@ func (r *Resolver) NextResponse() (packet []byte, to netaddr.IPPort, err error)
}
}
// HandleExitNodeDNSQuery handles a DNS query that arrived from a peer
// via the peerapi's DoH server. This is only used when the local
// node is being an exit node.
func (r *Resolver) HandleExitNodeDNSQuery(ctx context.Context, q []byte, from netaddr.IPPort) (res []byte, err error) {
metricDNSQueryForPeer.Add(1)
ch := make(chan packet, 1)
err = r.forwarder.forwardWithDestChan(ctx, packet{q, from}, ch)
if err == errNoUpstreams {
backup := r.exitNodeForwardResolvers()
if len(backup) > 0 {
var extra []resolverAndDelay
for _, v := range backup {
extra = append(extra, resolverAndDelay{name: v})
}
err = r.forwarder.forwardWithDestChan(ctx, packet{q, from}, ch, extra...)
}
}
if err != nil {
metricDNSQueryForPeerError.Add(1)
return nil, err
}
select {
case p, ok := <-ch:
if ok {
return p.bs, nil
}
panic("unexpected close chan")
default:
panic("unexpected unreadable chan")
}
}
// resolveLocal returns an IP for the given domain, if domain is in
// the local hosts map and has an IP corresponding to the requested
// typ (A, AAAA, ALL).
// Returns dns.RCodeRefused to indicate that the local map is not
// authoritative for domain.
func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netaddr.IP, dns.RCode) {
metricDNSResolveLocal.Add(1)
// Reject .onion domains per RFC 7686.
if dnsname.HasSuffix(domain.WithoutTrailingDot(), ".onion") {
metricDNSResolveLocalErrorOnion.Add(1)
return netaddr.IP{}, dns.RCodeNameError
}
@@ -319,6 +375,7 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netaddr.IP,
for _, suffix := range localDomains {
if suffix.Contains(domain) {
// We are authoritative for the queried domain.
metricDNSResolveLocalErrorMissing.Add(1)
return netaddr.IP{}, dns.RCodeNameError
}
}
@@ -336,30 +393,37 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netaddr.IP,
case dns.TypeA:
for _, ip := range addrs {
if ip.Is4() {
metricDNSResolveLocalOKA.Add(1)
return ip, dns.RCodeSuccess
}
}
metricDNSResolveLocalNoA.Add(1)
return netaddr.IP{}, dns.RCodeSuccess
case dns.TypeAAAA:
for _, ip := range addrs {
if ip.Is6() {
metricDNSResolveLocalOKAAAA.Add(1)
return ip, dns.RCodeSuccess
}
}
metricDNSResolveLocalNoAAAA.Add(1)
return netaddr.IP{}, dns.RCodeSuccess
case dns.TypeALL:
// Answer with whatever we've got.
// It could be IPv4, IPv6, or a zero addr.
// TODO: Return all available resolutions (A and AAAA, if we have them).
if len(addrs) == 0 {
metricDNSResolveLocalNoAll.Add(1)
return netaddr.IP{}, dns.RCodeSuccess
}
metricDNSResolveLocalOKAll.Add(1)
return addrs[0], dns.RCodeSuccess
// Leave some some record types explicitly unimplemented.
// These types relate to recursive resolution or special
// DNS semantics and might be implemented in the future.
case dns.TypeNS, dns.TypeSOA, dns.TypeAXFR, dns.TypeHINFO:
metricDNSResolveNotImplType.Add(1)
return netaddr.IP{}, dns.RCodeNotImplemented
// For everything except for the few types above that are explicitly not implemented, return no records.
@@ -369,6 +433,7 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netaddr.IP,
// dig -t TYPE9824 example.com
// and note that NOERROR is returned, despite that record type being made up.
default:
metricDNSResolveNoRecordType.Add(1)
// The name exists, but no records exist of the requested type.
return netaddr.IP{}, dns.RCodeSuccess
}
@@ -700,6 +765,7 @@ func (r *Resolver) respondReverse(query []byte, name dnsname.FQDN, resp *respons
return nil, errNotOurName
}
metricDNSMagicDNSSuccessReverse.Add(1)
return marshalResponse(resp)
}
@@ -716,8 +782,10 @@ func (r *Resolver) respond(query []byte) ([]byte, error) {
// We will not return this error: it is the sender's fault.
if err != nil {
if errors.Is(err, dns.ErrSectionDone) {
metricDNSErrorParseNoQ.Add(1)
r.logf("parseQuery(%02x): no DNS questions", query)
} else {
metricDNSErrorParseQuery.Add(1)
r.logf("parseQuery(%02x): %v", query, err)
}
resp := parser.response()
@@ -727,6 +795,7 @@ func (r *Resolver) respond(query []byte) ([]byte, error) {
rawName := parser.Question.Name.Data[:parser.Question.Name.Length]
name, err := dnsname.ToFQDN(rawNameToLower(rawName))
if err != nil {
metricDNSErrorNotFQDN.Add(1)
// DNS packet unexpectedly contains an invalid FQDN.
resp := parser.response()
resp.Header.RCode = dns.RCodeFormatError
@@ -750,3 +819,57 @@ func (r *Resolver) respond(query []byte) ([]byte, error) {
resp.IP = ip
return marshalResponse(resp)
}
var (
metricDNSQueryLocal = clientmetric.NewCounter("dns_query_local")
metricDNSQueryErrorClosed = clientmetric.NewCounter("dns_query_local_error_closed")
metricDNSQueryErrorQueue = clientmetric.NewCounter("dns_query_local_error_queue")
metricDNSErrorParseNoQ = clientmetric.NewCounter("dns_query_respond_error_no_question")
metricDNSErrorParseQuery = clientmetric.NewCounter("dns_query_respond_error_parse")
metricDNSErrorNotFQDN = clientmetric.NewCounter("dns_query_respond_error_not_fqdn")
metricDNSMagicDNSSuccessName = clientmetric.NewCounter("dns_query_magic_success_name")
metricDNSMagicDNSSuccessReverse = clientmetric.NewCounter("dns_query_magic_success_reverse")
metricDNSQueryForPeer = clientmetric.NewCounter("dns_query_peerapi")
metricDNSQueryForPeerError = clientmetric.NewCounter("dns_query_peerapi_error")
metricDNSFwd = clientmetric.NewCounter("dns_query_fwd")
metricDNSFwdDropBonjour = clientmetric.NewCounter("dns_query_fwd_drop_bonjour")
metricDNSFwdErrorName = clientmetric.NewCounter("dns_query_fwd_error_name")
metricDNSFwdErrorNoUpstream = clientmetric.NewCounter("dns_query_fwd_error_no_upstream")
metricDNSFwdSuccess = clientmetric.NewCounter("dns_query_fwd_success")
metricDNSFwdErrorContext = clientmetric.NewCounter("dns_query_fwd_error_context")
metricDNSFwdErrorContextGotError = clientmetric.NewCounter("dns_query_fwd_error_context_got_error")
metricDNSFwdErrorType = clientmetric.NewCounter("dns_query_fwd_error_type")
metricDNSFwdErrorParseAddr = clientmetric.NewCounter("dns_query_fwd_error_parse_addr")
metricDNSFwdUDP = clientmetric.NewCounter("dns_query_fwd_udp") // on entry
metricDNSFwdUDPWrote = clientmetric.NewCounter("dns_query_fwd_udp_wrote") // sent UDP packet
metricDNSFwdUDPErrorWrite = clientmetric.NewCounter("dns_query_fwd_udp_error_write")
metricDNSFwdUDPErrorServer = clientmetric.NewCounter("dns_query_fwd_udp_error_server")
metricDNSFwdUDPErrorTxID = clientmetric.NewCounter("dns_query_fwd_udp_error_txid")
metricDNSFwdUDPErrorRead = clientmetric.NewCounter("dns_query_fwd_udp_error_read")
metricDNSFwdUDPSuccess = clientmetric.NewCounter("dns_query_fwd_udp_success")
metricDNSFwdDoH = clientmetric.NewCounter("dns_query_fwd_doh")
metricDNSFwdDoHErrorStatus = clientmetric.NewCounter("dns_query_fwd_doh_error_status")
metricDNSFwdDoHErrorCT = clientmetric.NewCounter("dns_query_fwd_doh_error_content_type")
metricDNSFwdDoHErrorTransport = clientmetric.NewCounter("dns_query_fwd_doh_error_transport")
metricDNSFwdDoHErrorBody = clientmetric.NewCounter("dns_query_fwd_doh_error_body")
metricDNSResolveLocal = clientmetric.NewCounter("dns_resolve_local")
metricDNSResolveLocalErrorOnion = clientmetric.NewCounter("dns_resolve_local_error_onion")
metricDNSResolveLocalErrorMissing = clientmetric.NewCounter("dns_resolve_local_error_missing")
metricDNSResolveLocalErrorRefused = clientmetric.NewCounter("dns_resolve_local_error_refused")
metricDNSResolveLocalOKA = clientmetric.NewCounter("dns_resolve_local_ok_a")
metricDNSResolveLocalOKAAAA = clientmetric.NewCounter("dns_resolve_local_ok_aaaa")
metricDNSResolveLocalOKAll = clientmetric.NewCounter("dns_resolve_local_ok_all")
metricDNSResolveLocalNoA = clientmetric.NewCounter("dns_resolve_local_no_a")
metricDNSResolveLocalNoAAAA = clientmetric.NewCounter("dns_resolve_local_no_aaaa")
metricDNSResolveLocalNoAll = clientmetric.NewCounter("dns_resolve_local_no_all")
metricDNSResolveNotImplType = clientmetric.NewCounter("dns_resolve_local_not_impl_type")
metricDNSResolveNoRecordType = clientmetric.NewCounter("dns_resolve_local_no_record_type")
)

View File

@@ -345,9 +345,18 @@ func (s *State) String() string {
return sb.String()
}
// An InterfaceFilter indicates whether EqualFiltered should use i when deciding whether two States are equal.
// ips are all the IPPrefixes associated with i.
type InterfaceFilter func(i Interface, ips []netaddr.IPPrefix) bool
// An IPFilter indicates whether EqualFiltered should use ip when deciding whether two States are equal.
// ip is an ip address associated with some interface under consideration.
type IPFilter func(ip netaddr.IP) bool
// EqualFiltered reports whether s and s2 are equal,
// considering only interfaces in s for which filter returns true.
func (s *State) EqualFiltered(s2 *State, filter func(i Interface, ips []netaddr.IPPrefix) bool) bool {
// considering only interfaces in s for which filter returns true,
// and considering only IPs for those interfaces for which filterIP returns true.
func (s *State) EqualFiltered(s2 *State, useInterface InterfaceFilter, useIP IPFilter) bool {
if s == nil && s2 == nil {
return true
}
@@ -364,7 +373,7 @@ func (s *State) EqualFiltered(s2 *State, filter func(i Interface, ips []netaddr.
}
for iname, i := range s.Interface {
ips := s.InterfaceIPs[iname]
if !filter(i, ips) {
if !useInterface(i, ips) {
continue
}
i2, ok := s2.Interface[iname]
@@ -375,7 +384,7 @@ func (s *State) EqualFiltered(s2 *State, filter func(i Interface, ips []netaddr.
if !ok {
return false
}
if !interfacesEqual(i, i2) || !prefixesEqual(ips, ips2) {
if !interfacesEqual(i, i2) || !prefixesEqualFiltered(ips, ips2, useIP) {
return false
}
}
@@ -390,6 +399,21 @@ func interfacesEqual(a, b Interface) bool {
bytes.Equal([]byte(a.HardwareAddr), []byte(b.HardwareAddr))
}
func filteredIPPs(ipps []netaddr.IPPrefix, useIP IPFilter) []netaddr.IPPrefix {
// TODO: rewrite prefixesEqualFiltered to avoid making copies
x := make([]netaddr.IPPrefix, 0, len(ipps))
for _, ipp := range ipps {
if useIP(ipp.IP()) {
x = append(x, ipp)
}
}
return x
}
func prefixesEqualFiltered(a, b []netaddr.IPPrefix, useIP IPFilter) bool {
return prefixesEqual(filteredIPPs(a, useIP), filteredIPPs(b, useIP))
}
func prefixesEqual(a, b []netaddr.IPPrefix) bool {
if len(a) != len(b) {
return false
@@ -402,13 +426,24 @@ func prefixesEqual(a, b []netaddr.IPPrefix) bool {
return true
}
// FilterInteresting reports whether i is an interesting non-Tailscale interface.
func FilterInteresting(i Interface, ips []netaddr.IPPrefix) bool {
// UseInterestingInterfaces is an InterfaceFilter that reports whether i is an interesting interface.
// An interesting interface if it is (a) not owned by Tailscale and (b) routes interesting IP addresses.
// See UseInterestingIPs for the defition of an interesting IP address.
func UseInterestingInterfaces(i Interface, ips []netaddr.IPPrefix) bool {
return !isTailscaleInterface(i.Name, ips) && anyInterestingIP(ips)
}
// FilterAll always returns true, to use EqualFiltered against all interfaces.
func FilterAll(i Interface, ips []netaddr.IPPrefix) bool { return true }
// UseInterestingIPs is an IPFilter that reports whether ip is an interesting IP address.
// An IP address is interesting if it is neither a lopback not a link local unicast IP address.
func UseInterestingIPs(ip netaddr.IP) bool {
return isInterestingIP(ip)
}
// UseAllInterfaces is an InterfaceFilter that includes all interfaces.
func UseAllInterfaces(i Interface, ips []netaddr.IPPrefix) bool { return true }
// UseAllIPs is an IPFilter that includes all all IPs.
func UseAllIPs(ips netaddr.IP) bool { return true }
func (s *State) HasPAC() bool { return s != nil && s.PAC != "" }
@@ -594,10 +629,7 @@ func anyInterestingIP(pfxs []netaddr.IPPrefix) bool {
// should log in interfaces.State logging. We don't need to show
// localhost or link-local addresses.
func isInterestingIP(ip netaddr.IP) bool {
if ip.IsLoopback() || ip.IsLinkLocalUnicast() {
return false
}
return true
return !ip.IsLoopback() && !ip.IsLinkLocalUnicast()
}
var altNetInterfaces func() ([]Interface, error)

View File

@@ -6,6 +6,7 @@ package interfaces
import (
"encoding/json"
"net"
"testing"
"inet.af/netaddr"
@@ -28,7 +29,7 @@ func TestGetState(t *testing.T) {
t.Fatal(err)
}
if !st.EqualFiltered(st2, FilterAll) {
if !st.EqualFiltered(st2, UseAllInterfaces, UseAllIPs) {
// let's assume nobody was changing the system network interfaces between
// the two GetState calls.
t.Fatal("two States back-to-back were not equal")
@@ -68,3 +69,38 @@ func TestIsUsableV6(t *testing.T) {
}
}
}
func TestStateEqualFilteredIPFilter(t *testing.T) {
// s1 and s2 are identical, except that an "interesting" interface
// has gained an "uninteresting" IP address.
s1 := &State{
InterfaceIPs: map[string][]netaddr.IPPrefix{"x": {
netaddr.MustParseIPPrefix("42.0.0.0/8"),
netaddr.MustParseIPPrefix("169.254.0.0/16"), // link local unicast
}},
Interface: map[string]Interface{"x": {Interface: &net.Interface{Name: "x"}}},
}
s2 := &State{
InterfaceIPs: map[string][]netaddr.IPPrefix{"x": {
netaddr.MustParseIPPrefix("42.0.0.0/8"),
netaddr.MustParseIPPrefix("169.254.0.0/16"), // link local unicast
netaddr.MustParseIPPrefix("127.0.0.0/8"), // loopback (added)
}},
Interface: map[string]Interface{"x": {Interface: &net.Interface{Name: "x"}}},
}
// s1 and s2 are different...
if s1.EqualFiltered(s2, UseAllInterfaces, UseAllIPs) {
t.Errorf("%+v != %+v", s1, s2)
}
// ...and they look different if you only restrict to interesting interfaces...
if s1.EqualFiltered(s2, UseInterestingInterfaces, UseAllIPs) {
t.Errorf("%+v != %+v when restricting to interesting interfaces _but not_ IPs", s1, s2)
}
// ...but because the additional IP address is uninteresting, we should treat them as the same.
if !s1.EqualFiltered(s2, UseInterestingInterfaces, UseInterestingIPs) {
t.Errorf("%+v == %+v when restricting to interesting interfaces and IPs", s1, s2)
}
}

View File

@@ -798,7 +798,12 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
c.logf("unexpected PCP probe response: %+v", pres)
}
if pres, ok := parsePMPResponse(buf[:n]); ok {
if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr && pres.ResultCode == pmpCodeOK {
if pres.OpCode != pmpOpReply|pmpOpMapPublicAddr {
c.logf("unexpected PMP probe response opcode: %+v", pres)
continue
}
switch pres.ResultCode {
case pmpCodeOK:
c.logf("[v1] Got PMP response; IP: %v, epoch: %v", pres.PublicAddr, pres.SecondsSinceEpoch)
res.PMP = true
c.mu.Lock()
@@ -807,6 +812,10 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
c.pmpLastEpoch = pres.SecondsSinceEpoch
c.mu.Unlock()
continue
case pmpCodeNotAuthorized, pmpCodeNetworkFailure, pmpCodeOutOfResources:
// Normal failures.
c.logf("PMP probe failed due result code: %+v", pres)
continue
}
c.logf("unexpected PMP probe response: %+v", pres)
}

View File

@@ -190,7 +190,7 @@ type autoProxyOptions struct {
AutoConfigUrl *uint16
_ uintptr
_ uint32
FAutoLogonIfChallenged bool
FAutoLogonIfChallenged int32 // BOOL
}
// WINHTTP_PROXY_INFO

View File

@@ -26,7 +26,6 @@ import (
"tailscale.com/types/ipproto"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/pad32"
"tailscale.com/util/clientmetric"
"tailscale.com/wgengine/filter"
)
@@ -68,14 +67,17 @@ type FilterFunc func(*packet.Parsed, *Wrapper) filter.Response
// Wrapper augments a tun.Device with packet filtering and injection.
type Wrapper struct {
logf logger.Logf
logf logger.Logf
limitedLogf logger.Logf // aggressively rate-limited logf used for potentially high volume errors
// tdev is the underlying Wrapper device.
tdev tun.Device
isTAP bool // whether tdev is a TAP device
closeOnce sync.Once
_ pad32.Four
// lastActivityAtomic is read/written atomically.
// On 32 bit systems, if the fields above change,
// you might need to add a pad32.Four field here.
lastActivityAtomic mono.Time // time of last send or receive
destIPActivity atomic.Value // of map[netaddr.IP]func()
@@ -168,10 +170,12 @@ func Wrap(logf logger.Logf, tdev tun.Device) *Wrapper {
}
func wrap(logf logger.Logf, tdev tun.Device, isTAP bool) *Wrapper {
logf = logger.WithPrefix(logf, "tstun: ")
tun := &Wrapper{
logf: logger.WithPrefix(logf, "tstun: "),
isTAP: isTAP,
tdev: tdev,
logf: logf,
limitedLogf: logger.RateLimitedFn(logf, 1*time.Minute, 2, 10),
isTAP: isTAP,
tdev: tdev,
// bufferConsumed is conceptually a condition variable:
// a goroutine should not block when setting it, even with no listeners.
bufferConsumed: make(chan struct{}, 1),
@@ -421,7 +425,7 @@ func (t *Wrapper) filterOut(p *packet.Parsed) filter.Response {
// macOS in Network Extension mode might be.
if p.IPProto == ipproto.UDP && // disco is over UDP; avoid isSelfDisco call for TCP/etc
t.isSelfDisco(p) {
t.logf("[unexpected] received self disco out packet over tstun; dropping")
t.limitedLogf("[unexpected] received self disco out packet over tstun; dropping")
metricPacketOutDropSelfDisco.Add(1)
return filter.DropSilently
}
@@ -535,7 +539,7 @@ func (t *Wrapper) filterIn(buf []byte) filter.Response {
// macOS in Network Extension mode might be.
if p.IPProto == ipproto.UDP && // disco is over UDP; avoid isSelfDisco call for TCP/etc
t.isSelfDisco(p) {
t.logf("[unexpected] received self disco in packet over tstun; dropping")
t.limitedLogf("[unexpected] received self disco in packet over tstun; dropping")
metricPacketInDropSelfDisco.Add(1)
return filter.DropSilently
}

View File

@@ -495,7 +495,7 @@ func TestPeerAPIBypass(t *testing.T) {
func TestFilterDiscoLoop(t *testing.T) {
var memLog tstest.MemLogger
discoPub := key.DiscoPublicFromRaw32(mem.B([]byte{1: 1, 2: 2, 31: 0}))
tw := &Wrapper{logf: memLog.Logf}
tw := &Wrapper{logf: memLog.Logf, limitedLogf: memLog.Logf}
tw.SetDiscoKey(discoPub)
uh := packet.UDP4Header{
IP4Header: packet.IP4Header{

View File

@@ -32,8 +32,8 @@ main() {
# - VERSION_CODENAME: the codename of the OS release, if any (e.g. "buster")
. /etc/os-release
case "$ID" in
ubuntu)
OS="$ID"
ubuntu|pop|neon)
OS="ubuntu"
VERSION="$VERSION_CODENAME"
PACKAGETYPE="apt"
# Third-party keyrings became the preferred method of

View File

@@ -0,0 +1,75 @@
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package clientmetric
import (
"testing"
"time"
)
func TestDeltaEncBuf(t *testing.T) {
var enc deltaEncBuf
enc.writeName("one_one")
enc.writeValue(1, 1)
enc.writeName("two_zero")
enc.writeValue(2, 0)
enc.writeDelta(1, 63)
enc.writeDelta(2, 64)
enc.writeDelta(1, -65)
enc.writeDelta(2, -64)
got := enc.buf.String()
const want = "N0eone_oneS0202N10two_zeroS0400I027eI048001I028101I047f"
if got != want {
t.Errorf("error\n got %q\nwant %q\n", got, want)
}
}
func clearMetrics() {
mu.Lock()
defer mu.Unlock()
metrics = map[string]*Metric{}
numWireID = 0
lastDelta = time.Time{}
sorted = nil
lastLogVal = nil
unsorted = nil
}
func advanceTime() {
mu.Lock()
defer mu.Unlock()
lastDelta = time.Time{}
}
func TestEncodeLogTailMetricsDelta(t *testing.T) {
clearMetrics()
c1 := NewCounter("foo")
c2 := NewCounter("bar")
c1.Add(123)
if got, want := EncodeLogTailMetricsDelta(), "N06fooS02f601"; got != want {
t.Errorf("first = %q; want %q", got, want)
}
c2.Add(456)
advanceTime()
if got, want := EncodeLogTailMetricsDelta(), "N06barS049007"; got != want {
t.Errorf("second = %q; want %q", got, want)
}
advanceTime()
if got, want := EncodeLogTailMetricsDelta(), ""; got != want {
t.Errorf("with no changes = %q; want %q", got, want)
}
c1.Add(1)
c2.Add(2)
advanceTime()
if got, want := EncodeLogTailMetricsDelta(), "I0202I0404"; got != want {
t.Errorf("with increments = %q; want %q", got, want)
}
}

View File

@@ -103,7 +103,8 @@ func NewAllowAllForTest(logf logger.Logf) *Filter {
any6 := netaddr.IPPrefixFrom(netaddr.IPFrom16([16]byte{}), 0)
ms := []Match{
{
Srcs: []netaddr.IPPrefix{any4},
IPProto: []ipproto.Proto{ipproto.TCP, ipproto.UDP, ipproto.ICMPv4},
Srcs: []netaddr.IPPrefix{any4},
Dsts: []NetPortRange{
{
Net: any4,
@@ -115,7 +116,8 @@ func NewAllowAllForTest(logf logger.Logf) *Filter {
},
},
{
Srcs: []netaddr.IPPrefix{any6},
IPProto: []ipproto.Proto{ipproto.TCP, ipproto.UDP, ipproto.ICMPv6},
Srcs: []netaddr.IPPrefix{any6},
Dsts: []NetPortRange{
{
Net: any6,

View File

@@ -815,3 +815,13 @@ func TestMatchesFromFilterRules(t *testing.T) {
})
}
}
func TestNewAllowAllForTest(t *testing.T) {
f := NewAllowAllForTest(logger.Discard)
src := netaddr.MustParseIP("100.100.2.3")
dst := netaddr.MustParseIP("100.100.1.2")
res := f.CheckTCP(src, dst, 80)
if res.IsDrop() {
t.Fatalf("unexpected drop verdict: %v", res)
}
}

View File

@@ -17,7 +17,6 @@ import (
"math"
"math/rand"
"net"
"os"
"reflect"
"runtime"
"sort"
@@ -3129,18 +3128,6 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) {
ss.Addrs = append(ss.Addrs, ep.Addr.String())
}
ss.OS = version.OS()
if c.netMap != nil {
ss.HostName = c.netMap.Hostinfo.Hostname
ss.DNSName = c.netMap.Name
ss.UserID = c.netMap.User
if c.netMap.SelfNode != nil {
if c := c.netMap.SelfNode.Capabilities; len(c) > 0 {
ss.Capabilities = append([]string(nil), c...)
}
}
} else {
ss.HostName, _ = os.Hostname()
}
if c.derpMap != nil {
derpRegion, ok := c.derpMap.Regions[c.myDerp]
if ok {

View File

@@ -310,7 +310,7 @@ func (m *Mon) debounce() {
}
oldState := m.ifState
ifChanged := !curState.EqualFiltered(oldState, interfaces.FilterInteresting)
ifChanged := !curState.EqualFiltered(oldState, interfaces.UseInterestingInterfaces, interfaces.UseInterestingIPs)
if ifChanged {
m.gwValid = false
m.ifState = curState

View File

@@ -77,7 +77,7 @@ func (pm *pollingMon) Receive() (message, error) {
defer ticker.Stop()
base := pm.m.InterfaceState()
for {
if cur, err := pm.m.interfaceStateUncached(); err == nil && !cur.EqualFiltered(base, interfaces.FilterInteresting) {
if cur, err := pm.m.interfaceStateUncached(); err == nil && !cur.EqualFiltered(base, interfaces.UseInterestingInterfaces, interfaces.UseInterestingIPs) {
return unspecifiedMessage{}, nil
}
select {

View File

@@ -241,13 +241,21 @@ func (ns *Impl) addSubnetAddress(ip netaddr.IP) {
ns.mu.Unlock()
// Only register address into netstack for first concurrent connection.
if needAdd {
var pn tcpip.NetworkProtocolNumber
if ip.Is4() {
pn = ipv4.ProtocolNumber
} else if ip.Is6() {
pn = ipv6.ProtocolNumber
pa := tcpip.ProtocolAddress{
AddressWithPrefix: tcpip.AddressWithPrefix{
Address: tcpip.Address(ip.IPAddr().IP),
PrefixLen: int(ip.BitLen()),
},
}
ns.ipstack.AddAddress(nicID, pn, tcpip.Address(ip.IPAddr().IP))
if ip.Is4() {
pa.Protocol = ipv4.ProtocolNumber
} else if ip.Is6() {
pa.Protocol = ipv6.ProtocolNumber
}
ns.ipstack.AddProtocolAddress(nicID, pa, stack.AddressProperties{
PEB: stack.CanBePrimaryEndpoint, // zero value default
ConfigType: stack.AddressConfigStatic, // zero value default
})
}
}
@@ -318,12 +326,19 @@ func (ns *Impl) updateIPs(nm *netmap.NetworkMap) {
}
}
for ipp := range ipsToBeAdded {
var err tcpip.Error
if ipp.Address.To4() == "" {
err = ns.ipstack.AddAddressWithPrefix(nicID, ipv6.ProtocolNumber, ipp)
} else {
err = ns.ipstack.AddAddressWithPrefix(nicID, ipv4.ProtocolNumber, ipp)
pa := tcpip.ProtocolAddress{
AddressWithPrefix: ipp,
}
if ipp.Address.To4() == "" {
pa.Protocol = ipv6.ProtocolNumber
} else {
pa.Protocol = ipv4.ProtocolNumber
}
var err tcpip.Error
err = ns.ipstack.AddProtocolAddress(nicID, pa, stack.AddressProperties{
PEB: stack.CanBePrimaryEndpoint, // zero value default
ConfigType: stack.AddressConfigStatic, // zero value default
})
if err != nil {
ns.logf("netstack: could not register IP %s: %v", ipp, err)
} else {
@@ -572,8 +587,8 @@ func (ns *Impl) forwardTCP(client *gonet.TCPConn, clientRemoteIP netaddr.IP, wq
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
waitEntry, notifyCh := waiter.NewChannelEntry(nil)
wq.EventRegister(&waitEntry, waiter.EventHUp)
waitEntry, notifyCh := waiter.NewChannelEntry(waiter.EventHUp) // TODO(bradfitz): right EventMask?
wq.EventRegister(&waitEntry)
defer wq.EventUnregister(&waitEntry)
done := make(chan bool)
// netstack doesn't close the notification channel automatically if there was no

View File

@@ -99,7 +99,7 @@ type linuxRouter struct {
ipRuleFixLimiter *rate.Limiter
// Various feature checks for the network stack.
ipRuleAvailable bool
ipRuleAvailable bool // whether kernel was built with IP_MULTIPLE_TABLES
v6Available bool
v6NATAvailable bool
@@ -119,7 +119,7 @@ func newUserspaceRouter(logf logger.Logf, tunDev tun.Device, linkMon *monitor.Mo
return nil, err
}
v6err := checkIPv6()
v6err := checkIPv6(logf)
if v6err != nil {
logf("disabling tunneled IPv6 due to system IPv6 config: %v", v6err)
}
@@ -165,8 +165,13 @@ func newUserspaceRouterAdvanced(logf logger.Logf, tunname string, linkMon *monit
if r.useIPCommand() {
r.ipRuleAvailable = (cmd.run("ip", "rule") == nil)
} else {
// Pretend it is.
r.ipRuleAvailable = true
if rules, err := netlink.RuleList(netlink.FAMILY_V4); err != nil {
r.logf("error querying IP rules (does kernel have IP_MULTIPLE_TABLES?): %v", err)
r.logf("warning: running without policy routing")
} else {
r.logf("[v1] policy routing available; found %d rules", len(rules))
r.ipRuleAvailable = true
}
}
return r, nil
@@ -245,13 +250,13 @@ func (r *linuxRouter) Up() error {
return err
}
if err := r.addIPRules(); err != nil {
return err
return fmt.Errorf("adding IP rules: %w", err)
}
if err := r.setNetfilterMode(netfilterOff); err != nil {
return err
return fmt.Errorf("setting netfilter mode: %w", err)
}
if err := r.upInterface(); err != nil {
return err
return fmt.Errorf("bringing interface up: %w", err)
}
return nil
@@ -1487,7 +1492,7 @@ func cleanup(logf logger.Logf, interfaceName string) {
// missing. It does not check that IPv6 is currently functional or
// that there's a global address, just that the system would support
// IPv6 if it were on an IPv6 network.
func checkIPv6() error {
func checkIPv6(logf logger.Logf) error {
_, err := os.Stat("/proc/sys/net/ipv6")
if os.IsNotExist(err) {
return err
@@ -1519,7 +1524,7 @@ func checkIPv6() error {
}
}
if err := checkIPRuleSupportsV6(); err != nil {
if err := checkIPRuleSupportsV6(logf); err != nil {
return fmt.Errorf("kernel doesn't support IPv6 policy routing: %w", err)
}
@@ -1547,11 +1552,24 @@ func supportsV6NAT() bool {
return bytes.Contains(bs, []byte("nat\n"))
}
func checkIPRuleSupportsV6() error {
func checkIPRuleSupportsV6(logf logger.Logf) error {
// First try just a read-only operation to ideally avoid
// having to modify any state.
if rules, err := netlink.RuleList(netlink.FAMILY_V6); err != nil {
return fmt.Errorf("querying IPv6 policy routing rules: %w", err)
} else {
if len(rules) > 0 {
logf("[v1] kernel supports IPv6 policy routing (found %d rules)", len(rules))
return nil
}
}
// Try to actually create & delete one as a test.
rule := netlink.NewRule()
rule.Priority = 1234
rule.Mark = tailscaleBypassMarkNum
rule.Table = tailscaleRouteTable.num
rule.Family = netlink.FAMILY_V6
// First delete the rule unconditionally, and don't check for
// errors. This is just cleaning up anything that might be already
// there.

View File

@@ -793,7 +793,7 @@ func TestDebugListRules(t *testing.T) {
t.Run(famName[fam], func(t *testing.T) {
rules, err := netlink.RuleList(fam)
if err != nil {
t.Fatal(err)
t.Skipf("skip; RuleList fails with: %v", err)
}
for _, r := range rules {
t.Logf("Rule: %+v", r)
@@ -803,7 +803,7 @@ func TestDebugListRules(t *testing.T) {
}
func TestCheckIPRuleSupportsV6(t *testing.T) {
err := checkIPRuleSupportsV6()
err := checkIPRuleSupportsV6(t.Logf)
if err != nil && os.Getuid() != 0 {
t.Skipf("skipping, error when not root: %v", err)
}

View File

@@ -147,6 +147,20 @@ func (e *userspaceEngine) GetInternals() (_ *tstun.Wrapper, _ *magicsock.Conn, o
return e.tundev, e.magicConn, true
}
// ResolvingEngine is implemented by Engines that have DNS resolvers.
type ResolvingEngine interface {
GetResolver() (_ *resolver.Resolver, ok bool)
}
var (
_ ResolvingEngine = (*userspaceEngine)(nil)
_ ResolvingEngine = (*watchdogEngine)(nil)
)
func (e *userspaceEngine) GetResolver() (r *resolver.Resolver, ok bool) {
return e.dns.Resolver(), true
}
// BIRDClient handles communication with the BIRD Internet Routing Daemon.
type BIRDClient interface {
EnableProtocol(proto string) error
@@ -389,11 +403,11 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
e.logf("Bringing wireguard device up...")
if err := e.wgdev.Up(); err != nil {
return nil, fmt.Errorf("wgdev.Up(): %w", err)
return nil, fmt.Errorf("wgdev.Up: %w", err)
}
e.logf("Bringing router up...")
if err := e.router.Up(); err != nil {
return nil, err
return nil, fmt.Errorf("router.Up: %w", err)
}
// It's a little pointless to apply no-op settings here (they
@@ -401,7 +415,7 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
// router implementation early on.
e.logf("Clearing router settings...")
if err := e.router.Set(nil); err != nil {
return nil, err
return nil, fmt.Errorf("router.Set(nil): %w", err)
}
e.logf("Starting link monitor...")
e.linkMon.Start()

View File

@@ -15,6 +15,7 @@ import (
"inet.af/netaddr"
"tailscale.com/ipn/ipnstate"
"tailscale.com/net/dns"
"tailscale.com/net/dns/resolver"
"tailscale.com/net/tstun"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
@@ -139,6 +140,12 @@ func (e *watchdogEngine) GetInternals() (tw *tstun.Wrapper, c *magicsock.Conn, o
}
return
}
func (e *watchdogEngine) GetResolver() (r *resolver.Resolver, ok bool) {
if re, ok := e.wrap.(ResolvingEngine); ok {
return re.GetResolver()
}
return nil, false
}
func (e *watchdogEngine) Wait() {
e.wrap.Wait()
}