Compare commits
46 Commits
Xe/tailtls
...
bradfitz/e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4c7ee0c9f9 | ||
|
|
c2efe46f72 | ||
|
|
ff9727c9ff | ||
|
|
f8cef1ba08 | ||
|
|
6dc6ea9b37 | ||
|
|
78b0bd2957 | ||
|
|
097602b3ca | ||
|
|
db800ddeac | ||
|
|
33c541ae30 | ||
|
|
e121c2f724 | ||
|
|
25525b7754 | ||
|
|
9bb91cb977 | ||
|
|
259163dfe1 | ||
|
|
f56a7559ce | ||
|
|
d10cefdb9b | ||
|
|
9f00510833 | ||
|
|
955aa188b3 | ||
|
|
73beaaf360 | ||
|
|
b0d543f7a1 | ||
|
|
73beaf59fb | ||
|
|
a3b709f0c4 | ||
|
|
283ae702c1 | ||
|
|
6fd6fe11f2 | ||
|
|
027b46d0c1 | ||
|
|
0de1b74fbb | ||
|
|
ad5e04249b | ||
|
|
60510a6ae7 | ||
|
|
1ea270375a | ||
|
|
ca1b3fe235 | ||
|
|
9a217ec841 | ||
|
|
9feb483ad3 | ||
|
|
7d8feb2784 | ||
|
|
1a629a4715 | ||
|
|
e8db43e8fa | ||
|
|
937e96f43d | ||
|
|
f76a8d93da | ||
|
|
2ea765e5d8 | ||
|
|
def659d1ec | ||
|
|
946dfec98a | ||
|
|
9259377a7f | ||
|
|
88b8a09d37 | ||
|
|
6c82cebe57 | ||
|
|
4ef3fed100 | ||
|
|
cf9169e4be | ||
|
|
0350cf0438 | ||
|
|
5294125e7a |
7
.github/ISSUE_TEMPLATE/config.yml
vendored
7
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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
|
||||
9
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
9
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -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
26
.github/workflows/cifuzz.yml
vendored
Normal 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
|
||||
15
.github/workflows/linux-race.yml
vendored
15
.github/workflows/linux-race.yml
vendored
@@ -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: |
|
||||
|
||||
15
.github/workflows/linux.yml
vendored
15
.github/workflows/linux.yml
vendored
@@ -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: |
|
||||
|
||||
15
.github/workflows/linux32.yml
vendored
15
.github/workflows/linux32.yml
vendored
@@ -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: |
|
||||
|
||||
@@ -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
6
Dockerfile.base
Normal 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
|
||||
3
Makefile
3
Makefile
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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+
|
||||
|
||||
@@ -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+
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
6
go.mod
@@ -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
16
go.sum
@@ -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=
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
parameterNameRxStr = `^parameter/(.*)`
|
||||
parameterNameRxStr = `^parameter(/.*)`
|
||||
)
|
||||
|
||||
var parameterNameRx = regexp.MustCompile(parameterNameRxStr)
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ type autoProxyOptions struct {
|
||||
AutoConfigUrl *uint16
|
||||
_ uintptr
|
||||
_ uint32
|
||||
FAutoLogonIfChallenged bool
|
||||
FAutoLogonIfChallenged int32 // BOOL
|
||||
}
|
||||
|
||||
// WINHTTP_PROXY_INFO
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
75
util/clientmetric/clientmetric_test.go
Normal file
75
util/clientmetric/clientmetric_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user