Compare commits
135 Commits
Xe/tailtls
...
aaron/logl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96188ffd2f | ||
|
|
486059589b | ||
|
|
59f4f33f60 | ||
|
|
ac8e69b713 | ||
|
|
0f3b55c299 | ||
|
|
4691e012a9 | ||
|
|
e133bb570b | ||
|
|
adc97e9c4d | ||
|
|
d24a8f7b5a | ||
|
|
8dbda1a722 | ||
|
|
cced414c7d | ||
|
|
cab5c46481 | ||
|
|
63cd581c3f | ||
|
|
a5235e165c | ||
|
|
c8829b742b | ||
|
|
39ffa16853 | ||
|
|
b59e7669c1 | ||
|
|
21741e111b | ||
|
|
7b9c7bc42b | ||
|
|
affc4530a2 | ||
|
|
485bcdc951 | ||
|
|
878a20df29 | ||
|
|
a28d280b95 | ||
|
|
9f867ad2c5 | ||
|
|
c0701b130d | ||
|
|
656809e4ee | ||
|
|
e34ba3223c | ||
|
|
c18dc57861 | ||
|
|
ffb16cdffb | ||
|
|
d3d503d997 | ||
|
|
abc00e9c8d | ||
|
|
190b7a4cca | ||
|
|
0d8ef1ff35 | ||
|
|
329751c48e | ||
|
|
9ddef8cdbf | ||
|
|
9140f193bc | ||
|
|
05c1be3e47 | ||
|
|
e6e63c2305 | ||
|
|
c0984f88dc | ||
|
|
eeccbccd08 | ||
|
|
69de3bf7bf | ||
|
|
1813c2a162 | ||
|
|
0a9932f3b2 | ||
|
|
9c5c9d0a50 | ||
|
|
9f6249b26d | ||
|
|
de635ac0a8 | ||
|
|
003089820d | ||
|
|
03a323de4e | ||
|
|
a8f60cf6e8 | ||
|
|
f91481075d | ||
|
|
adc5997592 | ||
|
|
768baafcb5 | ||
|
|
43983a4a3b | ||
|
|
44d0c1ab06 | ||
|
|
8775c646be | ||
|
|
ad3d6e31f0 | ||
|
|
25eab78573 | ||
|
|
c7fb26acdb | ||
|
|
c37af58ea4 | ||
|
|
bf1d69f25b | ||
|
|
2075c39fd7 | ||
|
|
49a9e62d58 | ||
|
|
56c72d9cde | ||
|
|
d5405c66b7 | ||
|
|
3ae6f898cf | ||
|
|
16abd7e07c | ||
|
|
2a95ee4680 | ||
|
|
deb2f5e793 | ||
|
|
f93cf6fa03 | ||
|
|
b800663779 | ||
|
|
124363e0ca | ||
|
|
e16cb523aa | ||
|
|
a8cc519c70 | ||
|
|
fddf43f3d1 | ||
|
|
9787ec6f4a | ||
|
|
40f11c50a1 | ||
|
|
38d90fa330 | ||
|
|
999814e9e1 | ||
|
|
bb91cfeae7 | ||
|
|
3181bbb8e4 | ||
|
|
46a9782322 | ||
|
|
d89c61b812 | ||
|
|
341e1af873 | ||
|
|
b811a316bc | ||
|
|
6e584ffa33 | ||
|
|
a54d13294f | ||
|
|
135580a5a8 | ||
|
|
d9c21936c3 | ||
|
|
1e8b4e770a | ||
|
|
105c545366 | ||
|
|
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 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +1,2 @@
|
||||
go.mod filter=go-mod
|
||||
*.go diff=golang
|
||||
|
||||
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:
|
||||
|
||||
19
.github/dependabot.yml
vendored
19
.github/dependabot.yml
vendored
@@ -2,15 +2,20 @@
|
||||
# https://docs.github.com/en/code-security/supply-chain-security/keeping-your-dependencies-updated-automatically/configuration-options-for-dependency-updates
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
commit-message:
|
||||
prefix: "go.mod:"
|
||||
## Disabled between releases. We reenable it briefly after every
|
||||
## stable release, pull in all changes, and close it again so that
|
||||
## the tree remains more stable during development and the upstream
|
||||
## changes have time to soak before the next release.
|
||||
# - package-ecosystem: "gomod"
|
||||
# directory: "/"
|
||||
# schedule:
|
||||
# interval: "daily"
|
||||
# commit-message:
|
||||
# prefix: "go.mod:"
|
||||
# open-pull-requests-limit: 100
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "weekly"
|
||||
commit-message:
|
||||
prefix: ".github:"
|
||||
|
||||
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@v2.2.4
|
||||
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
|
||||
11
Makefile
11
Makefile
@@ -23,11 +23,18 @@ 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
|
||||
|
||||
staticcheck:
|
||||
go run honnef.co/go/tools/cmd/staticcheck -- $$(go list ./... | grep -v tempfork)
|
||||
|
||||
spk:
|
||||
go run github.com/tailscale/tailscale-synology@main --version=build -o tailscale.spk --source=.
|
||||
|
||||
pushspk: spk
|
||||
echo "Pushing SPKG to root@${SYNOHOST} (env var SYNOHOST) ..."
|
||||
scp tailscale.spk root@${SYNOHOST}:
|
||||
ssh root@${SYNOHOST} /usr/syno/bin/synopkg install tailscale.spk
|
||||
|
||||
@@ -8,11 +8,12 @@ Private WireGuard® networks made easy
|
||||
|
||||
This repository contains all the open source Tailscale client code and
|
||||
the `tailscaled` daemon and `tailscale` CLI tool. The `tailscaled`
|
||||
daemon runs primarily on Linux; it also works to varying degrees on
|
||||
FreeBSD, OpenBSD, Darwin, and Windows.
|
||||
daemon runs on Linux, Windows and [macOS](https://tailscale.com/kb/1065/macos-variants/), and to varying degrees on FreeBSD, OpenBSD, and Darwin. (The Tailscale iOS and Android apps use this repo's code, but this repo doesn't contain the mobile GUI code.)
|
||||
|
||||
The Android app is at https://github.com/tailscale/tailscale-android
|
||||
|
||||
The Synology package is at https://github.com/tailscale/tailscale-synology
|
||||
|
||||
## Using
|
||||
|
||||
We serve packages for a variety of distros at
|
||||
|
||||
@@ -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="v${VERSION_SHORT},v${VERSION_MINOR}" \
|
||||
--repos="tailscale/tailscale,ghcr.io/tailscale/tailscale" \
|
||||
--push
|
||||
|
||||
@@ -38,6 +38,9 @@ var (
|
||||
// TailscaledSocket is the tailscaled Unix socket. It's used by the TailscaledDialer.
|
||||
TailscaledSocket = paths.DefaultTailscaledSocket()
|
||||
|
||||
// TailscaledSocketSetExplicitly reports whether the user explicitly set TailscaledSocket.
|
||||
TailscaledSocketSetExplicitly bool
|
||||
|
||||
// TailscaledDialer is the DialContext func that connects to the local machine's
|
||||
// tailscaled or equivalent.
|
||||
TailscaledDialer = defaultDialer
|
||||
@@ -47,7 +50,8 @@ func defaultDialer(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
if addr != "local-tailscaled.sock:80" {
|
||||
return nil, fmt.Errorf("unexpected URL address %q", addr)
|
||||
}
|
||||
if TailscaledSocket == paths.DefaultTailscaledSocket() {
|
||||
// TODO: make this part of a safesocket.ConnectionStrategy
|
||||
if !TailscaledSocketSetExplicitly {
|
||||
// On macOS, when dialing from non-sandboxed program to sandboxed GUI running
|
||||
// a TCP server on a random port, find the random port. For HTTP connections,
|
||||
// we don't send the token. It gets added in an HTTP Basic-Auth header.
|
||||
@@ -56,7 +60,11 @@ func defaultDialer(ctx context.Context, network, addr string) (net.Conn, error)
|
||||
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
|
||||
}
|
||||
}
|
||||
return safesocket.Connect(TailscaledSocket, safesocket.WindowsLocalPort)
|
||||
s := safesocket.DefaultConnectionStrategy(TailscaledSocket)
|
||||
// The user provided a non-default tailscaled socket address.
|
||||
// Connect only to exactly what they provided.
|
||||
s.UseFallback(false)
|
||||
return safesocket.Connect(s)
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -90,6 +98,27 @@ func DoLocalRequest(req *http.Request) (*http.Response, error) {
|
||||
return tsClient.Do(req)
|
||||
}
|
||||
|
||||
func doLocalRequestNiceError(req *http.Request) (*http.Response, error) {
|
||||
res, err := DoLocalRequest(req)
|
||||
if err == nil {
|
||||
if server := res.Header.Get("Tailscale-Version"); server != "" && server != version.Long && onVersionMismatch != nil {
|
||||
onVersionMismatch(version.Long, server)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
if ue, ok := err.(*url.Error); ok {
|
||||
if oe, ok := ue.Err.(*net.OpError); ok && oe.Op == "dial" {
|
||||
path := req.URL.Path
|
||||
pathPrefix := path
|
||||
if i := strings.Index(path, "?"); i != -1 {
|
||||
pathPrefix = path[:i]
|
||||
}
|
||||
return nil, fmt.Errorf("Failed to connect to local Tailscale daemon for %s; %s Error: %w", pathPrefix, tailscaledConnectHint(), oe)
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
type errorJSON struct {
|
||||
Error string
|
||||
}
|
||||
@@ -140,23 +169,11 @@ func send(ctx context.Context, method, path string, wantStatus int, body io.Read
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := DoLocalRequest(req)
|
||||
res, err := doLocalRequestNiceError(req)
|
||||
if err != nil {
|
||||
if ue, ok := err.(*url.Error); ok {
|
||||
if oe, ok := ue.Err.(*net.OpError); ok && oe.Op == "dial" {
|
||||
pathPrefix := path
|
||||
if i := strings.Index(path, "?"); i != -1 {
|
||||
pathPrefix = path[:i]
|
||||
}
|
||||
return nil, fmt.Errorf("Failed to connect to local Tailscale daemon for %s; %s Error: %w", pathPrefix, tailscaledConnectHint(), oe)
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if server := res.Header.Get("Tailscale-Version"); server != "" && server != version.Long && onVersionMismatch != nil {
|
||||
onVersionMismatch(version.Long, server)
|
||||
}
|
||||
slurp, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -228,7 +245,7 @@ func Status(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return status(ctx, "")
|
||||
}
|
||||
|
||||
// StatusWithPeers returns the Tailscale daemon's status, without the peer info.
|
||||
// StatusWithoutPeers returns the Tailscale daemon's status, without the peer info.
|
||||
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return status(ctx, "?peers=false")
|
||||
}
|
||||
@@ -295,6 +312,30 @@ func FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
|
||||
return fts, nil
|
||||
}
|
||||
|
||||
// PushFile sends Taildrop file r to target.
|
||||
//
|
||||
// A size of -1 means unknown.
|
||||
// The name parameter is the original filename, not escaped.
|
||||
func PushFile(ctx context.Context, target tailcfg.StableNodeID, size int64, name string, r io.Reader) error {
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", "http://local-tailscaled.sock/localapi/v0/file-put/"+string(target)+"/"+url.PathEscape(name), r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if size != -1 {
|
||||
req.ContentLength = size
|
||||
}
|
||||
res, err := doLocalRequestNiceError(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.StatusCode == 200 {
|
||||
io.Copy(io.Discard, res.Body)
|
||||
return nil
|
||||
}
|
||||
all, _ := io.ReadAll(res.Body)
|
||||
return fmt.Errorf("%s: %s", res.Status, all)
|
||||
}
|
||||
|
||||
func CheckIPForwarding(ctx context.Context) error {
|
||||
body, err := get200(ctx, "/localapi/v0/check-ip-forwarding")
|
||||
if err != nil {
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"errors"
|
||||
"expvar"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@@ -36,6 +37,7 @@ import (
|
||||
var (
|
||||
dev = flag.Bool("dev", false, "run in localhost development mode")
|
||||
addr = flag.String("a", ":443", "server address")
|
||||
httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable")
|
||||
configPath = flag.String("c", "", "config file path")
|
||||
certMode = flag.String("certmode", "letsencrypt", "mode for getting a cert. possible options: manual, letsencrypt")
|
||||
certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443")
|
||||
@@ -235,24 +237,41 @@ func main() {
|
||||
cert.Certificate = append(cert.Certificate, s.MetaCert())
|
||||
return cert, nil
|
||||
}
|
||||
go func() {
|
||||
port80srv := &http.Server{
|
||||
Addr: net.JoinHostPort(listenHost, "80"),
|
||||
Handler: certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}),
|
||||
ReadTimeout: 30 * time.Second,
|
||||
// Crank up WriteTimeout a bit more than usually
|
||||
// necessary just so we can do long CPU profiles
|
||||
// and not hit net/http/pprof's "profile
|
||||
// duration exceeds server's WriteTimeout".
|
||||
WriteTimeout: 5 * time.Minute,
|
||||
}
|
||||
err := port80srv.ListenAndServe()
|
||||
if err != nil {
|
||||
if err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
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)
|
||||
})
|
||||
if *httpPort > -1 {
|
||||
go func() {
|
||||
port80srv := &http.Server{
|
||||
Addr: net.JoinHostPort(listenHost, fmt.Sprintf("%d", *httpPort)),
|
||||
Handler: certManager.HTTPHandler(tsweb.Port80Handler{Main: mux}),
|
||||
ReadTimeout: 30 * time.Second,
|
||||
// Crank up WriteTimeout a bit more than usually
|
||||
// necessary just so we can do long CPU profiles
|
||||
// and not hit net/http/pprof's "profile
|
||||
// duration exceeds server's WriteTimeout".
|
||||
WriteTimeout: 5 * time.Minute,
|
||||
}
|
||||
}
|
||||
}()
|
||||
err := port80srv.ListenAndServe()
|
||||
if err != nil {
|
||||
if err != http.ErrServerClosed {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
err = httpsrv.ListenAndServeTLS("", "")
|
||||
} else {
|
||||
log.Printf("derper: serving on %s", *addr)
|
||||
|
||||
@@ -164,6 +164,11 @@ change in the future.
|
||||
}
|
||||
|
||||
tailscale.TailscaledSocket = rootArgs.socket
|
||||
rootfs.Visit(func(f *flag.Flag) {
|
||||
if f.Name == "socket" {
|
||||
tailscale.TailscaledSocketSetExplicitly = true
|
||||
}
|
||||
})
|
||||
|
||||
err := rootCmd.Run(context.Background())
|
||||
if errors.Is(err, flag.ErrHelp) {
|
||||
@@ -191,7 +196,8 @@ var rootArgs struct {
|
||||
var gotSignal syncs.AtomicBool
|
||||
|
||||
func connect(ctx context.Context) (net.Conn, *ipn.BackendClient, context.Context, context.CancelFunc) {
|
||||
c, err := safesocket.Connect(rootArgs.socket, safesocket.WindowsLocalPort)
|
||||
s := safesocket.DefaultConnectionStrategy(rootArgs.socket)
|
||||
c, err := safesocket.Connect(s)
|
||||
if err != nil {
|
||||
if runtime.GOOS != "windows" && rootArgs.socket == "" {
|
||||
fatalf("--socket cannot be empty")
|
||||
|
||||
@@ -18,8 +18,10 @@ import (
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
// geese is a collection of gooses. It need not be complete.
|
||||
@@ -57,6 +59,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
curExitNodeIP netaddr.IP
|
||||
curUser string // os.Getenv("USER") on the client side
|
||||
goos string // empty means "linux"
|
||||
distro distro.Distro
|
||||
|
||||
want string
|
||||
}{
|
||||
@@ -313,6 +316,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
RouteAll: true,
|
||||
|
||||
// And assume this no-op accidental pre-1.8 value:
|
||||
NoSNAT: true,
|
||||
@@ -329,7 +333,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
|
||||
NetfilterMode: preftype.NetfilterNoDivert, // we never had this bug, but pretend it got set non-zero on Windows somehow
|
||||
},
|
||||
goos: "windows",
|
||||
goos: "openbsd",
|
||||
want: "", // not an error
|
||||
},
|
||||
{
|
||||
@@ -405,6 +409,21 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
},
|
||||
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.7",
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_and_allow_lan_omit_with_id_pref", // Isue 3480
|
||||
flags: []string{"--hostname=foo"},
|
||||
curExitNodeIP: netaddr.MustParseIP("100.2.3.4"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
|
||||
ExitNodeAllowLANAccess: true,
|
||||
ExitNodeID: "some_stable_id",
|
||||
},
|
||||
want: accidentalUpPrefix + " --hostname=foo --exit-node-allow-lan-access --exit-node=100.2.3.4",
|
||||
},
|
||||
{
|
||||
name: "ignore_login_server_synonym",
|
||||
flags: []string{"--login-server=https://controlplane.tailscale.com"},
|
||||
@@ -427,6 +446,38 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
},
|
||||
want: accidentalUpPrefix + " --netfilter-mode=off --accept-dns=false",
|
||||
},
|
||||
{
|
||||
// Issue 3176: on Synology, don't require --accept-routes=false because user
|
||||
// migth've had old an install, and we don't support --accept-routes anyway.
|
||||
name: "synology_permit_omit_accept_routes",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
RouteAll: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
goos: "linux",
|
||||
distro: distro.Synology,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
// Same test case as "synology_permit_omit_accept_routes" above, but
|
||||
// on non-Synology distro.
|
||||
name: "not_synology_dont_permit_omit_accept_routes",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
RouteAll: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
goos: "linux",
|
||||
distro: "", // not Synology
|
||||
want: accidentalUpPrefix + " --hostname=foo --accept-routes",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -447,6 +498,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
goos: goos,
|
||||
flagSet: flagSet,
|
||||
curExitNodeIP: tt.curExitNodeIP,
|
||||
distro: tt.distro,
|
||||
}); err != nil {
|
||||
got = err.Error()
|
||||
}
|
||||
@@ -495,6 +547,7 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
WantRunning: true,
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
RouteAll: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
},
|
||||
@@ -532,7 +585,7 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
args: upArgsT{
|
||||
exitNodeIP: "foo",
|
||||
},
|
||||
wantErr: `invalid IP address "foo" for --exit-node: ParseIP("foo"): unable to parse IP`,
|
||||
wantErr: `invalid value "foo" for --exit-node; must be IP or unique node name`,
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_allow_lan_without_exit_node",
|
||||
@@ -806,3 +859,133 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExitNodeIPOfArg(t *testing.T) {
|
||||
mustIP := netaddr.MustParseIP
|
||||
tests := []struct {
|
||||
name string
|
||||
arg string
|
||||
st *ipnstate.Status
|
||||
want netaddr.IP
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "ip_while_stopped_okay",
|
||||
arg: "1.2.3.4",
|
||||
st: &ipnstate.Status{
|
||||
BackendState: "Stopped",
|
||||
},
|
||||
want: mustIP("1.2.3.4"),
|
||||
},
|
||||
{
|
||||
name: "ip_not_found",
|
||||
arg: "1.2.3.4",
|
||||
st: &ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
},
|
||||
wantErr: `no node found in netmap with IP 1.2.3.4`,
|
||||
},
|
||||
{
|
||||
name: "ip_not_exit",
|
||||
arg: "1.2.3.4",
|
||||
st: &ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
|
||||
key.NewNode().Public(): {
|
||||
TailscaleIPs: []netaddr.IP{mustIP("1.2.3.4")},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: `node 1.2.3.4 is not advertising an exit node`,
|
||||
},
|
||||
{
|
||||
name: "ip",
|
||||
arg: "1.2.3.4",
|
||||
st: &ipnstate.Status{
|
||||
BackendState: "Running",
|
||||
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
|
||||
key.NewNode().Public(): {
|
||||
TailscaleIPs: []netaddr.IP{mustIP("1.2.3.4")},
|
||||
ExitNodeOption: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: mustIP("1.2.3.4"),
|
||||
},
|
||||
{
|
||||
name: "no_match",
|
||||
arg: "unknown",
|
||||
st: &ipnstate.Status{MagicDNSSuffix: ".foo"},
|
||||
wantErr: `invalid value "unknown" for --exit-node; must be IP or unique node name`,
|
||||
},
|
||||
{
|
||||
name: "name",
|
||||
arg: "skippy",
|
||||
st: &ipnstate.Status{
|
||||
MagicDNSSuffix: ".foo",
|
||||
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
|
||||
key.NewNode().Public(): {
|
||||
DNSName: "skippy.foo.",
|
||||
TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")},
|
||||
ExitNodeOption: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: mustIP("1.0.0.2"),
|
||||
},
|
||||
{
|
||||
name: "name_not_exit",
|
||||
arg: "skippy",
|
||||
st: &ipnstate.Status{
|
||||
MagicDNSSuffix: ".foo",
|
||||
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
|
||||
key.NewNode().Public(): {
|
||||
DNSName: "skippy.foo.",
|
||||
TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: `node "skippy" is not advertising an exit node`,
|
||||
},
|
||||
{
|
||||
name: "ambiguous",
|
||||
arg: "skippy",
|
||||
st: &ipnstate.Status{
|
||||
MagicDNSSuffix: ".foo",
|
||||
Peer: map[key.NodePublic]*ipnstate.PeerStatus{
|
||||
key.NewNode().Public(): {
|
||||
DNSName: "skippy.foo.",
|
||||
TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")},
|
||||
ExitNodeOption: true,
|
||||
},
|
||||
key.NewNode().Public(): {
|
||||
DNSName: "SKIPPY.foo.",
|
||||
TailscaleIPs: []netaddr.IP{mustIP("1.0.0.2")},
|
||||
ExitNodeOption: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: `ambiguous exit node name "skippy"`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := exitNodeIPOfArg(tt.arg, tt.st)
|
||||
if err != nil {
|
||||
if err.Error() == tt.wantErr {
|
||||
return
|
||||
}
|
||||
if tt.wantErr == "" {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Fatalf("error = %#q; want %#q", err, tt.wantErr)
|
||||
}
|
||||
if tt.wantErr != "" {
|
||||
t.Fatalf("got %v; want error %#q", got, tt.wantErr)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("got %v; want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,23 +25,23 @@ func fixTailscaledConnectError(origErr error) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to local Tailscaled process and failed to enumerate processes while looking for it")
|
||||
}
|
||||
found := false
|
||||
var foundProc ps.Process
|
||||
for _, proc := range procs {
|
||||
base := filepath.Base(proc.Executable())
|
||||
if base == "tailscaled" {
|
||||
found = true
|
||||
foundProc = proc
|
||||
break
|
||||
}
|
||||
if runtime.GOOS == "darwin" && base == "IPNExtension" {
|
||||
found = true
|
||||
foundProc = proc
|
||||
break
|
||||
}
|
||||
if runtime.GOOS == "windows" && strings.EqualFold(base, "tailscaled.exe") {
|
||||
found = true
|
||||
foundProc = proc
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
if foundProc == nil {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return fmt.Errorf("failed to connect to local tailscaled process; is the Tailscale service running?")
|
||||
@@ -52,5 +52,5 @@ func fixTailscaledConnectError(origErr error) error {
|
||||
}
|
||||
return fmt.Errorf("failed to connect to local tailscaled process; it doesn't appear to be running")
|
||||
}
|
||||
return fmt.Errorf("failed to connect to local tailscaled (which appears to be running). Got error: %w", origErr)
|
||||
return fmt.Errorf("failed to connect to local tailscaled (which appears to be running as %v, pid %v). Got error: %w", foundProc.Executable(), foundProc.Pid(), origErr)
|
||||
}
|
||||
|
||||
@@ -11,11 +11,9 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -30,6 +28,7 @@ import (
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -96,7 +95,7 @@ func runCp(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
peerAPIBase, isOffline, err := discoverPeerAPIBase(ctx, ip)
|
||||
stableID, isOffline, err := getTargetStableID(ctx, ip)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't send to %s: %v", target, err)
|
||||
}
|
||||
@@ -154,32 +153,21 @@ func runCp(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
dstURL := peerAPIBase + "/v0/put/" + url.PathEscape(name)
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", dstURL, fileContents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.ContentLength = contentLength
|
||||
if cpArgs.verbose {
|
||||
log.Printf("sending to %v ...", dstURL)
|
||||
log.Printf("sending %q to %v/%v/%v ...", name, target, ip, stableID)
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
err := tailscale.PushFile(ctx, stableID, contentLength, name, fileContents)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.StatusCode == 200 {
|
||||
io.Copy(ioutil.Discard, res.Body)
|
||||
res.Body.Close()
|
||||
continue
|
||||
if cpArgs.verbose {
|
||||
log.Printf("sent %q", name)
|
||||
}
|
||||
io.Copy(Stdout, res.Body)
|
||||
res.Body.Close()
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, isOffline bool, err error) {
|
||||
func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNodeID, isOffline bool, err error) {
|
||||
ip, err := netaddr.ParseIP(ipStr)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
@@ -195,7 +183,7 @@ func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, isOffl
|
||||
continue
|
||||
}
|
||||
isOffline = n.Online != nil && !*n.Online
|
||||
return ft.PeerAPIURL, isOffline, nil
|
||||
return n.StableID, isOffline, nil
|
||||
}
|
||||
}
|
||||
return "", false, fileTargetErrorDetail(ctx, ip)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -29,7 +29,22 @@ var statusCmd = &ffcli.Command{
|
||||
Name: "status",
|
||||
ShortUsage: "status [--active] [--web] [--json]",
|
||||
ShortHelp: "Show state of tailscaled and its connections",
|
||||
Exec: runStatus,
|
||||
LongHelp: strings.TrimSpace(`
|
||||
|
||||
JSON FORMAT
|
||||
|
||||
Warning: this format has changed between releases and might change more
|
||||
in the future.
|
||||
|
||||
For a description of the fields, see the "type Status" declaration at:
|
||||
|
||||
https://github.com/tailscale/tailscale/blob/main/ipn/ipnstate/ipnstate.go
|
||||
|
||||
(and be sure to select branch/tag that corresponds to the version
|
||||
of Tailscale you're running)
|
||||
|
||||
`),
|
||||
Exec: runStatus,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("status")
|
||||
fs.BoolVar(&statusArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
|
||||
@@ -145,11 +160,19 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
)
|
||||
relay := ps.Relay
|
||||
anyTraffic := ps.TxBytes != 0 || ps.RxBytes != 0
|
||||
var offline string
|
||||
if !ps.Online {
|
||||
offline = "; offline"
|
||||
}
|
||||
if !ps.Active {
|
||||
if ps.ExitNode {
|
||||
f("idle; exit node")
|
||||
f("idle; exit node" + offline)
|
||||
} else if ps.ExitNodeOption {
|
||||
f("idle; offers exit node" + offline)
|
||||
} else if anyTraffic {
|
||||
f("idle")
|
||||
f("idle" + offline)
|
||||
} else if !ps.Online {
|
||||
f("offline")
|
||||
} else {
|
||||
f("-")
|
||||
}
|
||||
@@ -157,12 +180,17 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
f("active; ")
|
||||
if ps.ExitNode {
|
||||
f("exit node; ")
|
||||
} else if ps.ExitNodeOption {
|
||||
f("offers exit node; ")
|
||||
}
|
||||
if relay != "" && ps.CurAddr == "" {
|
||||
f("relay %q", relay)
|
||||
} else if ps.CurAddr != "" {
|
||||
f("direct %s", ps.CurAddr)
|
||||
}
|
||||
if !ps.Online {
|
||||
f("; offline")
|
||||
}
|
||||
}
|
||||
if anyTraffic {
|
||||
f(", tx %d rx %d", ps.TxBytes, ps.RxBytes)
|
||||
|
||||
@@ -6,6 +6,8 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
@@ -28,6 +30,8 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
@@ -46,8 +50,10 @@ down").
|
||||
|
||||
If flags are specified, the flags must be the complete set of desired
|
||||
settings. An error is returned if any setting would be changed as a
|
||||
result of an unspecified flag's default value, unless the --reset
|
||||
flag is also used.
|
||||
result of an unspecified flag's default value, unless the --reset flag
|
||||
is also used. (The flags --authkey, --force-reauth, and --qr are not
|
||||
considered settings that need to be re-specified when modifying
|
||||
settings.)
|
||||
`),
|
||||
FlagSet: upFlagSet,
|
||||
Exec: runUp,
|
||||
@@ -60,20 +66,34 @@ func effectiveGOOS() string {
|
||||
return runtime.GOOS
|
||||
}
|
||||
|
||||
// acceptRouteDefault returns the CLI's default value of --accept-routes as
|
||||
// a function of the platform it's running on.
|
||||
func acceptRouteDefault(goos string) bool {
|
||||
switch goos {
|
||||
case "windows":
|
||||
return true
|
||||
case "darwin":
|
||||
return version.IsSandboxedMacOS()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
var upFlagSet = newUpFlagSet(effectiveGOOS(), &upArgs)
|
||||
|
||||
func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
||||
upf := newFlagSet("up")
|
||||
|
||||
upf.BoolVar(&upArgs.qr, "qr", false, "show QR code for login URLs")
|
||||
upf.BoolVar(&upArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
|
||||
upf.BoolVar(&upArgs.forceReauth, "force-reauth", false, "force reauthentication")
|
||||
upf.BoolVar(&upArgs.reset, "reset", false, "reset unspecified settings to their default values")
|
||||
|
||||
upf.StringVar(&upArgs.server, "login-server", ipn.DefaultControlURL, "base URL of control server")
|
||||
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", false, "accept routes advertised by other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.acceptRoutes, "accept-routes", acceptRouteDefault(goos), "accept routes advertised by other Tailscale nodes")
|
||||
upf.BoolVar(&upArgs.acceptDNS, "accept-dns", true, "accept DNS configuration from the admin panel")
|
||||
upf.BoolVar(&upArgs.singleRoutes, "host-routes", true, "install host routes to other Tailscale nodes")
|
||||
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale IP of the exit node for internet traffic, or empty string to not use an exit node")
|
||||
upf.StringVar(&upArgs.exitNodeIP, "exit-node", "", "Tailscale exit node (IP or base name) for internet traffic, or empty string to not use an exit node")
|
||||
upf.BoolVar(&upArgs.exitNodeAllowLANAccess, "exit-node-allow-lan-access", false, "Allow direct access to the local network when routing traffic via an exit node")
|
||||
upf.BoolVar(&upArgs.shieldsUp, "shields-up", false, "don't allow incoming connections")
|
||||
upf.StringVar(&upArgs.advertiseTags, "advertise-tags", "", "comma-separated ACL tags to request; each must start with \"tag:\" (e.g. \"tag:eng,tag:montreal,tag:ssh\")")
|
||||
@@ -121,6 +141,7 @@ type upArgsT struct {
|
||||
authKeyOrFile string // "secret" or "file:/path/to/secret"
|
||||
hostname string
|
||||
opUser string
|
||||
json bool
|
||||
}
|
||||
|
||||
func (a upArgsT) getAuthKey() (string, error) {
|
||||
@@ -138,6 +159,33 @@ func (a upArgsT) getAuthKey() (string, error) {
|
||||
|
||||
var upArgs upArgsT
|
||||
|
||||
// Fields output when `tailscale up --json` is used. Two JSON blocks will be output.
|
||||
//
|
||||
// When "tailscale up" is run it first outputs a block with AuthURL and QR populated,
|
||||
// providing the link for where to authenticate this client. BackendState would be
|
||||
// valid but boring, as it will almost certainly be "NeedsLogin". Error would be
|
||||
// populated if something goes badly wrong.
|
||||
//
|
||||
// When the client is authenticated by having someone visit the AuthURL, a second
|
||||
// JSON block will be output. The AuthURL and QR fields will not be present, the
|
||||
// BackendState and Error fields will give the result of the authentication.
|
||||
// Ex:
|
||||
// {
|
||||
// "AuthURL": "https://login.tailscale.com/a/0123456789abcdef",
|
||||
// "QR": "data:image/png;base64,0123...cdef"
|
||||
// "BackendState": "NeedsLogin"
|
||||
// }
|
||||
// {
|
||||
// "BackendState": "Running"
|
||||
// }
|
||||
//
|
||||
type upOutputJSON struct {
|
||||
AuthURL string `json:",omitempty"` // Authentication URL of the form https://login.tailscale.com/a/0123456789
|
||||
QR string `json:",omitempty"` // a DataURL (base64) PNG of a QR code AuthURL
|
||||
BackendState string `json:",omitempty"` // name of state like Running or NeedsMachineAuth
|
||||
Error string `json:",omitempty"` // description of an error
|
||||
}
|
||||
|
||||
func warnf(format string, args ...interface{}) {
|
||||
printf("Warning: "+format+"\n", args...)
|
||||
}
|
||||
@@ -190,6 +238,65 @@ func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]
|
||||
return routes, nil
|
||||
}
|
||||
|
||||
// peerWithTailscaleIP returns the peer in st with the provided
|
||||
// Tailscale IP.
|
||||
func peerWithTailscaleIP(st *ipnstate.Status, ip netaddr.IP) (ps *ipnstate.PeerStatus, ok bool) {
|
||||
for _, ps := range st.Peer {
|
||||
for _, ip2 := range ps.TailscaleIPs {
|
||||
if ip == ip2 {
|
||||
return ps, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// exitNodeIPOfArg maps from a user-provided CLI flag value to an IP
|
||||
// address they want to use as an exit node.
|
||||
func exitNodeIPOfArg(arg string, st *ipnstate.Status) (ip netaddr.IP, err error) {
|
||||
if arg == "" {
|
||||
return ip, errors.New("invalid use of exitNodeIPOfArg with empty string")
|
||||
}
|
||||
ip, err = netaddr.ParseIP(arg)
|
||||
if err == nil {
|
||||
// If we're online already and have a netmap, double check that the IP
|
||||
// address specified is valid.
|
||||
if st.BackendState == "Running" {
|
||||
ps, ok := peerWithTailscaleIP(st, ip)
|
||||
if !ok {
|
||||
return ip, fmt.Errorf("no node found in netmap with IP %v", ip)
|
||||
}
|
||||
if !ps.ExitNodeOption {
|
||||
return ip, fmt.Errorf("node %v is not advertising an exit node", ip)
|
||||
}
|
||||
}
|
||||
return ip, err
|
||||
}
|
||||
match := 0
|
||||
for _, ps := range st.Peer {
|
||||
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
|
||||
if !strings.EqualFold(arg, baseName) {
|
||||
continue
|
||||
}
|
||||
match++
|
||||
if len(ps.TailscaleIPs) == 0 {
|
||||
return ip, fmt.Errorf("node %q has no Tailscale IP?", arg)
|
||||
}
|
||||
if !ps.ExitNodeOption {
|
||||
return ip, fmt.Errorf("node %q is not advertising an exit node", arg)
|
||||
}
|
||||
ip = ps.TailscaleIPs[0]
|
||||
}
|
||||
switch match {
|
||||
case 0:
|
||||
return ip, fmt.Errorf("invalid value %q for --exit-node; must be IP or unique node name", arg)
|
||||
case 1:
|
||||
return ip, nil
|
||||
default:
|
||||
return ip, fmt.Errorf("ambiguous exit node name %q", arg)
|
||||
}
|
||||
}
|
||||
|
||||
// prefsFromUpArgs returns the ipn.Prefs for the provided args.
|
||||
//
|
||||
// Note that the parameters upArgs and warnf are named intentionally
|
||||
@@ -205,9 +312,9 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
var exitNodeIP netaddr.IP
|
||||
if upArgs.exitNodeIP != "" {
|
||||
var err error
|
||||
exitNodeIP, err = netaddr.ParseIP(upArgs.exitNodeIP)
|
||||
exitNodeIP, err = exitNodeIPOfArg(upArgs.exitNodeIP, st)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid IP address %q for --exit-node: %v", upArgs.exitNodeIP, err)
|
||||
return nil, err
|
||||
}
|
||||
} else if upArgs.exitNodeAllowLANAccess {
|
||||
return nil, fmt.Errorf("--exit-node-allow-lan-access can only be used with --exit-node")
|
||||
@@ -380,11 +487,12 @@ func runUp(ctx context.Context, args []string) error {
|
||||
|
||||
env := upCheckEnv{
|
||||
goos: effectiveGOOS(),
|
||||
distro: distro.Get(),
|
||||
user: os.Getenv("USER"),
|
||||
flagSet: upFlagSet,
|
||||
upArgs: upArgs,
|
||||
backendState: st.BackendState,
|
||||
curExitNodeIP: exitNodeIP(prefs, st),
|
||||
curExitNodeIP: exitNodeIP(curPrefs, st),
|
||||
}
|
||||
simpleUp, justEditMP, err := updatePrefs(prefs, curPrefs, env)
|
||||
if err != nil {
|
||||
@@ -435,10 +543,16 @@ func runUp(ctx context.Context, args []string) error {
|
||||
startLoginInteractive()
|
||||
case ipn.NeedsMachineAuth:
|
||||
printed = true
|
||||
fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
|
||||
if env.upArgs.json {
|
||||
printUpDoneJSON(ipn.NeedsMachineAuth, "")
|
||||
} else {
|
||||
fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
|
||||
}
|
||||
case ipn.Running:
|
||||
// Done full authentication process
|
||||
if printed {
|
||||
if env.upArgs.json {
|
||||
printUpDoneJSON(ipn.Running, "")
|
||||
} else if printed {
|
||||
// Only need to print an update if we printed the "please click" message earlier.
|
||||
fmt.Fprintf(Stderr, "Success.\n")
|
||||
}
|
||||
@@ -451,15 +565,33 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
if url := n.BrowseToURL; url != nil && printAuthURL(*url) {
|
||||
printed = true
|
||||
fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
|
||||
if upArgs.qr {
|
||||
if upArgs.json {
|
||||
js := &upOutputJSON{AuthURL: *url, BackendState: st.BackendState}
|
||||
|
||||
q, err := qrcode.New(*url, qrcode.Medium)
|
||||
if err != nil {
|
||||
log.Printf("QR code error: %v", err)
|
||||
} else {
|
||||
fmt.Fprintf(Stderr, "%s\n", q.ToString(false))
|
||||
if err == nil {
|
||||
png, err := q.PNG(128)
|
||||
if err == nil {
|
||||
js.QR = "data:image/png;base64," + base64.StdEncoding.EncodeToString(png)
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(js, "", "\t")
|
||||
if err != nil {
|
||||
log.Printf("upOutputJSON marshalling error: %v", err)
|
||||
} else {
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
} else {
|
||||
fmt.Fprintf(Stderr, "\nTo authenticate, visit:\n\n\t%s\n\n", *url)
|
||||
if upArgs.qr {
|
||||
q, err := qrcode.New(*url, qrcode.Medium)
|
||||
if err != nil {
|
||||
log.Printf("QR code error: %v", err)
|
||||
} else {
|
||||
fmt.Fprintf(Stderr, "%s\n", q.ToString(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -546,6 +678,16 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
func printUpDoneJSON(state ipn.State, errorString string) {
|
||||
js := &upOutputJSON{BackendState: state.String(), Error: errorString}
|
||||
data, err := json.MarshalIndent(js, "", " ")
|
||||
if err != nil {
|
||||
log.Printf("printUpDoneJSON marshalling error: %v", err)
|
||||
} else {
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
prefsOfFlag = map[string][]string{} // "exit-node" => ExitNodeIP, ExitNodeID
|
||||
)
|
||||
@@ -588,7 +730,7 @@ func addPrefFlagMapping(flagName string, prefNames ...string) {
|
||||
// correspond to an ipn.Pref.
|
||||
func preflessFlag(flagName string) bool {
|
||||
switch flagName {
|
||||
case "authkey", "force-reauth", "reset", "qr":
|
||||
case "authkey", "force-reauth", "reset", "qr", "json":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -622,6 +764,7 @@ type upCheckEnv struct {
|
||||
upArgs upArgsT
|
||||
backendState string
|
||||
curExitNodeIP netaddr.IP
|
||||
distro distro.Distro
|
||||
}
|
||||
|
||||
// checkForAccidentalSettingReverts (the "up checker") checks for
|
||||
@@ -672,6 +815,10 @@ func checkForAccidentalSettingReverts(newPrefs, curPrefs *ipn.Prefs, env upCheck
|
||||
if flagName == "login-server" && ipn.IsLoginServerSynonym(valCur) && ipn.IsLoginServerSynonym(valNew) {
|
||||
continue
|
||||
}
|
||||
if flagName == "accept-routes" && valNew == false && env.goos == "linux" && env.distro == distro.Synology {
|
||||
// Issue 3176. Old prefs had 'RouteAll: true' on disk, so ignore that.
|
||||
continue
|
||||
}
|
||||
missing = append(missing, fmtFlagValueArg(flagName, valCur))
|
||||
}
|
||||
if len(missing) == 0 {
|
||||
|
||||
@@ -3,6 +3,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
|
||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
|
||||
L github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
|
||||
@@ -72,7 +73,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/types/persist from tailscale.com/ipn
|
||||
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/structs from tailscale.com/ipn+
|
||||
tailscale.com/util/clientmetric from tailscale.com/net/netcheck
|
||||
tailscale.com/util/clientmetric from tailscale.com/net/netcheck+
|
||||
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
|
||||
W tailscale.com/util/endian from tailscale.com/net/netns
|
||||
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
|
||||
@@ -91,7 +92,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/poly1305 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
golang.org/x/net/dns/dnsmessage from net
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from net/http+
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
@@ -103,7 +104,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+
|
||||
|
||||
@@ -25,7 +25,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/aws/aws-sdk-go-v2/credentials/stscreds from github.com/aws/aws-sdk-go-v2/config
|
||||
L github.com/aws/aws-sdk-go-v2/feature/ec2/imds from github.com/aws/aws-sdk-go-v2/config+
|
||||
L github.com/aws/aws-sdk-go-v2/feature/ec2/imds/internal/config from github.com/aws/aws-sdk-go-v2/feature/ec2/imds
|
||||
L github.com/aws/aws-sdk-go-v2/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/ssm/internal/endpoints+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/configsources from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 from github.com/aws/aws-sdk-go-v2/service/ssm/internal/endpoints+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/ini from github.com/aws/aws-sdk-go-v2/config
|
||||
L github.com/aws/aws-sdk-go-v2/internal/rand from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/aws-sdk-go-v2/internal/sdk from github.com/aws/aws-sdk-go-v2/aws+
|
||||
@@ -62,6 +63,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
|
||||
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
|
||||
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
github.com/google/btree from inet.af/netstack/tcpip/header+
|
||||
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
|
||||
L github.com/insomniacslk/dhcp/iana from github.com/insomniacslk/dhcp/dhcpv4
|
||||
@@ -116,10 +118,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 +135,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 +145,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+
|
||||
@@ -172,7 +181,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/ipn/store/aws from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/kube from tailscale.com/ipn
|
||||
tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver
|
||||
W tailscale.com/log/filelogger from tailscale.com/logpolicy
|
||||
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
||||
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/logtail from tailscale.com/logpolicy+
|
||||
@@ -186,16 +195,17 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/flowtrack from tailscale.com/net/packet+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/net/netknob from tailscale.com/ipn/localapi+
|
||||
tailscale.com/net/netknob from tailscale.com/logpolicy+
|
||||
tailscale.com/net/netns from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/net/packet from tailscale.com/net/tstun+
|
||||
tailscale.com/net/portmapper from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/socks5 from tailscale.com/net/socks5/tssocks
|
||||
tailscale.com/net/socks5/tssocks from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/net/tsdial from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
|
||||
💣 tailscale.com/paths from tailscale.com/client/tailscale+
|
||||
@@ -217,7 +227,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+
|
||||
@@ -243,7 +253,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/magicsock from tailscale.com/wgengine+
|
||||
tailscale.com/wgengine/monitor from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/wgengine/router from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/wgcfg/nmcfg from tailscale.com/ipn/ipnlocal
|
||||
@@ -266,7 +276,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
golang.org/x/net/http/httpguts from net/http+
|
||||
golang.org/x/net/http/httpproxy from net/http
|
||||
golang.org/x/net/http2/hpack from net/http
|
||||
golang.org/x/net/http2 from golang.org/x/net/http2/h2c+
|
||||
golang.org/x/net/http2/h2c from tailscale.com/ipn/ipnlocal
|
||||
golang.org/x/net/http2/hpack from net/http+
|
||||
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
|
||||
golang.org/x/net/ipv4 from golang.zx2c4.com/wireguard/device
|
||||
golang.org/x/net/ipv6 from golang.zx2c4.com/wireguard/device+
|
||||
@@ -357,7 +369,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
path from github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds+
|
||||
path/filepath from crypto/x509+
|
||||
reflect from crypto/x509+
|
||||
regexp from github.com/aws/aws-sdk-go-v2/internal/endpoints+
|
||||
regexp from github.com/aws/aws-sdk-go-v2/internal/endpoints/v2+
|
||||
regexp/syntax from regexp
|
||||
runtime/debug from github.com/klauspost/compress/zstd+
|
||||
runtime/pprof from net/http/pprof+
|
||||
|
||||
@@ -28,13 +28,16 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/socks5/tssocks"
|
||||
"tailscale.com/net/proxymux"
|
||||
"tailscale.com/net/socks5"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
@@ -175,8 +178,7 @@ func main() {
|
||||
osshare.SetFileSharingEnabled(false, logger.Discard)
|
||||
|
||||
if err != nil {
|
||||
// No need to log; the func already did
|
||||
os.Exit(1)
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,40 +300,55 @@ func run() error {
|
||||
|
||||
linkMon, err := monitor.New(logf)
|
||||
if err != nil {
|
||||
log.Fatalf("creating link monitor: %v", err)
|
||||
return fmt.Errorf("monitor.New: %w", err)
|
||||
}
|
||||
pol.Logtail.SetLinkMonitor(linkMon)
|
||||
|
||||
socksListener := mustStartTCPListener("SOCKS5", args.socksAddr)
|
||||
httpProxyListener := mustStartTCPListener("HTTP proxy", args.httpProxyAddr)
|
||||
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
|
||||
|
||||
e, useNetstack, err := createEngine(logf, linkMon)
|
||||
dialer := new(tsdial.Dialer) // mutated below (before used)
|
||||
e, useNetstack, err := createEngine(logf, linkMon, dialer)
|
||||
if err != nil {
|
||||
logf("wgengine.New: %v", err)
|
||||
return err
|
||||
return fmt.Errorf("createEngine: %w", err)
|
||||
}
|
||||
if _, ok := e.(wgengine.ResolvingEngine).GetResolver(); !ok {
|
||||
panic("internal error: exit node resolver not wired up")
|
||||
}
|
||||
|
||||
ns, err := newNetstack(logf, e)
|
||||
ns, err := newNetstack(logf, dialer, e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("newNetstack: %w", err)
|
||||
}
|
||||
ns.ProcessLocalIPs = useNetstack
|
||||
ns.ProcessSubnets = useNetstack || wrapNetstack
|
||||
if err := ns.Start(); err != nil {
|
||||
log.Fatalf("failed to start netstack: %v", err)
|
||||
return fmt.Errorf("failed to start netstack: %w", err)
|
||||
}
|
||||
|
||||
if useNetstack {
|
||||
dialer.UseNetstackForIP = func(ip netaddr.IP) bool {
|
||||
_, ok := e.PeerForIP(ip)
|
||||
return ok
|
||||
}
|
||||
dialer.NetstackDialTCP = func(ctx context.Context, dst netaddr.IPPort) (net.Conn, error) {
|
||||
return ns.DialContextTCP(ctx, dst)
|
||||
}
|
||||
}
|
||||
|
||||
if socksListener != nil || httpProxyListener != nil {
|
||||
srv := tssocks.NewServer(logger.WithPrefix(logf, "socks5: "), e, ns)
|
||||
if httpProxyListener != nil {
|
||||
hs := &http.Server{Handler: httpProxyHandler(srv.Dialer)}
|
||||
hs := &http.Server{Handler: httpProxyHandler(dialer.UserDial)}
|
||||
go func() {
|
||||
log.Fatalf("HTTP proxy exited: %v", hs.Serve(httpProxyListener))
|
||||
}()
|
||||
}
|
||||
if socksListener != nil {
|
||||
ss := &socks5.Server{
|
||||
Logf: logger.WithPrefix(logf, "socks5: "),
|
||||
Dialer: dialer.UserDial,
|
||||
}
|
||||
go func() {
|
||||
log.Fatalf("SOCKS5 server exited: %v", srv.Serve(socksListener))
|
||||
log.Fatalf("SOCKS5 server exited: %v", ss.Serve(socksListener))
|
||||
}()
|
||||
}
|
||||
}
|
||||
@@ -361,12 +378,11 @@ func run() error {
|
||||
|
||||
store, err := ipnserver.StateStore(statePathOrDefault(), logf)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("ipnserver.StateStore: %w", err)
|
||||
}
|
||||
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, nil, opts)
|
||||
srv, err := ipnserver.New(logf, pol.PublicID.String(), store, e, dialer, nil, opts)
|
||||
if err != nil {
|
||||
logf("ipnserver.New: %v", err)
|
||||
return err
|
||||
return fmt.Errorf("ipnserver.New: %w", err)
|
||||
}
|
||||
|
||||
if debugMux != nil {
|
||||
@@ -381,21 +397,20 @@ func run() error {
|
||||
err = srv.Run(ctx, ln)
|
||||
// Cancelation is not an error: it is the only way to stop ipnserver.
|
||||
if err != nil && err != context.Canceled {
|
||||
logf("ipnserver.Run: %v", err)
|
||||
return err
|
||||
return fmt.Errorf("ipnserver.Run: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createEngine(logf logger.Logf, linkMon *monitor.Mon) (e wgengine.Engine, useNetstack bool, err error) {
|
||||
func createEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer) (e wgengine.Engine, useNetstack bool, err error) {
|
||||
if args.tunname == "" {
|
||||
return nil, false, errors.New("no --tun value specified")
|
||||
}
|
||||
var errs []error
|
||||
for _, name := range strings.Split(args.tunname, ",") {
|
||||
logf("wgengine.NewUserspaceEngine(tun %q) ...", name)
|
||||
e, useNetstack, err = tryEngine(logf, linkMon, name)
|
||||
e, useNetstack, err = tryEngine(logf, linkMon, dialer, name)
|
||||
if err == nil {
|
||||
return e, useNetstack, nil
|
||||
}
|
||||
@@ -427,10 +442,11 @@ func shouldWrapNetstack() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, name string) (e wgengine.Engine, useNetstack bool, err error) {
|
||||
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, name string) (e wgengine.Engine, useNetstack bool, err error) {
|
||||
conf := wgengine.Config{
|
||||
ListenPort: args.port,
|
||||
LinkMonitor: linkMon,
|
||||
Dialer: dialer,
|
||||
}
|
||||
|
||||
useNetstack = name == "userspace-networking"
|
||||
@@ -440,14 +456,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 +475,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
|
||||
@@ -504,26 +520,54 @@ func runDebugServer(mux *http.ServeMux, addr string) {
|
||||
}
|
||||
}
|
||||
|
||||
func newNetstack(logf logger.Logf, e wgengine.Engine) (*netstack.Impl, error) {
|
||||
func newNetstack(logf logger.Logf, dialer *tsdial.Dialer, e wgengine.Engine) (*netstack.Impl, error) {
|
||||
tunDev, magicConn, ok := e.(wgengine.InternalsGetter).GetInternals()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%T is not a wgengine.InternalsGetter", e)
|
||||
}
|
||||
return netstack.Create(logf, tunDev, e, magicConn)
|
||||
return netstack.Create(logf, tunDev, e, magicConn, dialer)
|
||||
}
|
||||
|
||||
func mustStartTCPListener(name, addr string) net.Listener {
|
||||
if addr == "" {
|
||||
return nil
|
||||
// mustStartProxyListeners creates listeners for local SOCKS and HTTP
|
||||
// proxies, if the respective addresses are not empty. socksAddr and
|
||||
// httpAddr can be the same, in which case socksListener will receive
|
||||
// connections that look like they're speaking SOCKS and httpListener
|
||||
// will receive everything else.
|
||||
//
|
||||
// socksListener and httpListener can be nil, if their respective
|
||||
// addrs are empty.
|
||||
func mustStartProxyListeners(socksAddr, httpAddr string) (socksListener, httpListener net.Listener) {
|
||||
if socksAddr == httpAddr && socksAddr != "" && !strings.HasSuffix(socksAddr, ":0") {
|
||||
ln, err := net.Listen("tcp", socksAddr)
|
||||
if err != nil {
|
||||
log.Fatalf("proxy listener: %v", err)
|
||||
}
|
||||
return proxymux.SplitSOCKSAndHTTP(ln)
|
||||
}
|
||||
ln, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
log.Fatalf("%v listener: %v", name, err)
|
||||
|
||||
var err error
|
||||
if socksAddr != "" {
|
||||
socksListener, err = net.Listen("tcp", socksAddr)
|
||||
if err != nil {
|
||||
log.Fatalf("SOCKS5 listener: %v", err)
|
||||
}
|
||||
if strings.HasSuffix(socksAddr, ":0") {
|
||||
// Log kernel-selected port number so integration tests
|
||||
// can find it portably.
|
||||
log.Printf("SOCKS5 listening on %v", socksListener.Addr())
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(addr, ":0") {
|
||||
// Log kernel-selected port number so integration tests
|
||||
// can find it portably.
|
||||
log.Printf("%v listening on %v", name, ln.Addr())
|
||||
if httpAddr != "" {
|
||||
httpListener, err = net.Listen("tcp", httpAddr)
|
||||
if err != nil {
|
||||
log.Fatalf("HTTP proxy listener: %v", err)
|
||||
}
|
||||
if strings.HasSuffix(httpAddr, ":0") {
|
||||
// Log kernel-selected port number so integration tests
|
||||
// can find it portably.
|
||||
log.Printf("HTTP proxy listening on %v", httpListener.Addr())
|
||||
}
|
||||
}
|
||||
return ln
|
||||
|
||||
return socksListener, httpListener
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -39,6 +40,7 @@ import (
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wf"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
@@ -78,7 +80,10 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
|
||||
// Make a logger without a date prefix, as filelogger
|
||||
// and logtail both already add their own. All we really want
|
||||
// from the log package is the automatic newline.
|
||||
logger := log.New(os.Stderr, "", 0)
|
||||
// We start with log.Default().Writer(), which is the logtail
|
||||
// writer that logpolicy already installed as the global
|
||||
// output.
|
||||
logger := log.New(log.Default().Writer(), "", 0)
|
||||
ipnserver.BabysitProc(ctx, args, logger.Printf)
|
||||
}()
|
||||
|
||||
@@ -114,6 +119,9 @@ func beWindowsSubprocess() bool {
|
||||
}
|
||||
logid := os.Args[2]
|
||||
|
||||
// Remove the date/time prefix; the logtail + file logggers add it.
|
||||
log.SetFlags(0)
|
||||
|
||||
log.Printf("Program starting: v%v: %#v", version.Long, os.Args)
|
||||
log.Printf("subproc mode: logid=%v", logid)
|
||||
|
||||
@@ -177,6 +185,12 @@ func beFirewallKillswitch() bool {
|
||||
func startIPNServer(ctx context.Context, logid string) error {
|
||||
var logf logger.Logf = log.Printf
|
||||
|
||||
linkMon, err := monitor.New(logf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dialer := new(tsdial.Dialer)
|
||||
|
||||
getEngineRaw := func() (wgengine.Engine, error) {
|
||||
dev, devName, err := tstun.New(logf, "Tailscale")
|
||||
if err != nil {
|
||||
@@ -197,17 +211,19 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
return nil, fmt.Errorf("DNS: %w", err)
|
||||
}
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
Tun: dev,
|
||||
Router: r,
|
||||
DNS: d,
|
||||
ListenPort: 41641,
|
||||
Tun: dev,
|
||||
Router: r,
|
||||
DNS: d,
|
||||
ListenPort: 41641,
|
||||
LinkMonitor: linkMon,
|
||||
Dialer: dialer,
|
||||
})
|
||||
if err != nil {
|
||||
r.Close()
|
||||
dev.Close()
|
||||
return nil, fmt.Errorf("engine: %w", err)
|
||||
}
|
||||
ns, err := newNetstack(logf, eng)
|
||||
ns, err := newNetstack(logf, dialer, eng)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("newNetstack: %w", err)
|
||||
}
|
||||
@@ -287,7 +303,7 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
return fmt.Errorf("safesocket.Listen: %v", err)
|
||||
}
|
||||
|
||||
err = ipnserver.Run(ctx, logf, ln, store, logid, getEngine, ipnServerOpts())
|
||||
err = ipnserver.Run(ctx, logf, ln, store, linkMon, dialer, logid, getEngine, ipnServerOpts())
|
||||
if err != nil {
|
||||
logf("ipnserver.Run: %v", err)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -339,11 +339,9 @@ func (c *Auto) authRoutine() {
|
||||
continue
|
||||
}
|
||||
if url != "" {
|
||||
if goal.url != "" {
|
||||
err = fmt.Errorf("[unexpected] server required a new URL?")
|
||||
report(err, "WaitLoginURL")
|
||||
}
|
||||
|
||||
// goal.url ought to be empty here.
|
||||
// However, not all control servers get this right,
|
||||
// and logging about it here just generates noise.
|
||||
c.mu.Lock()
|
||||
c.loginGoal = &LoginGoal{
|
||||
wantLoggedIn: true,
|
||||
|
||||
@@ -139,6 +139,9 @@ func TestNoReuse(t *testing.T) {
|
||||
t.Fatalf("server wire traffic seen twice")
|
||||
}
|
||||
packets[serverWire] = true
|
||||
|
||||
server.Close()
|
||||
client.Close()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
251
go.mod
251
go.mod
@@ -7,208 +7,257 @@ require (
|
||||
github.com/akutz/memconn v0.1.0
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||
github.com/aws/aws-sdk-go v1.38.52
|
||||
github.com/aws/aws-sdk-go-v2 v1.9.2
|
||||
github.com/aws/aws-sdk-go-v2/config v1.8.3
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.12.0
|
||||
github.com/aws/aws-sdk-go-v2 v1.11.2
|
||||
github.com/aws/aws-sdk-go-v2/config v1.11.0
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.7.4
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.21.0
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.17.1
|
||||
github.com/coreos/go-iptables v0.6.0
|
||||
github.com/creack/pty v1.1.17
|
||||
github.com/dave/jennifer v1.4.1
|
||||
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/go-ole/go-ole v1.2.6
|
||||
github.com/godbus/dbus/v5 v5.0.6
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
|
||||
github.com/google/go-cmp v0.5.6
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/goreleaser/nfpm v1.10.3
|
||||
github.com/iancoleman/strcase v0.2.0
|
||||
github.com/insomniacslk/dhcp v0.0.0-20210621130208-1cac67f12b1e
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20210525051524-4cc836578190
|
||||
github.com/insomniacslk/dhcp v0.0.0-20211026125128-ad197bcd36fd
|
||||
github.com/jsimonetti/rtnetlink v0.0.0-20211203074127-fd9a11f42291
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
|
||||
github.com/klauspost/compress v1.13.6
|
||||
github.com/mdlayher/netlink v1.4.1
|
||||
github.com/mdlayher/netlink v1.4.2
|
||||
github.com/mdlayher/sdnotify v0.0.0-20210228150836-ea3ec207d697
|
||||
github.com/miekg/dns v1.1.43
|
||||
github.com/mitchellh/go-ps v1.0.0
|
||||
github.com/pborman/getopt v1.1.0
|
||||
github.com/peterbourgon/ff/v3 v3.1.0
|
||||
github.com/peterbourgon/ff/v3 v3.1.2
|
||||
github.com/pkg/sftp v1.13.4
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/tailscale/certstore v0.0.0-20210528134328-066c94b793d3
|
||||
github.com/tailscale/depaware v0.0.0-20201214215404-77d1e9757027
|
||||
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502
|
||||
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
|
||||
github.com/tailscale/hujson v0.0.0-20200924210142-dde312d0d6a2
|
||||
github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83
|
||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
github.com/ulikunitz/xz v0.5.10 // indirect
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211101163509-b10eb8fe5cf6
|
||||
go4.org/mem v0.0.0-20201119185036-c04c5a6ff174
|
||||
golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa
|
||||
golang.org/x/net v0.0.0-20211111083644-e5c967477495
|
||||
go4.org/mem v0.0.0-20210711025021-927187094b94
|
||||
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e
|
||||
golang.org/x/net v0.0.0-20211205041911-012df41ee64c
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/sys v0.0.0-20211110154304-99a53858aa08
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56
|
||||
golang.org/x/time v0.0.0-20210611083556-38a9dc6acbc6
|
||||
golang.org/x/tools v0.1.7
|
||||
golang.org/x/sys v0.0.0-20211205182925-97ca703d548d
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
|
||||
golang.org/x/tools v0.1.8
|
||||
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/peercred v0.0.0-20210318190834-4259e17bb763
|
||||
inet.af/wf v0.0.0-20210516214145-a5343001b756
|
||||
inet.af/netstack v0.0.0-20211120045802-8aa80cf23d3c
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a
|
||||
inet.af/wf v0.0.0-20211204062712-86aaea0a7310
|
||||
nhooyr.io/websocket v1.8.7
|
||||
)
|
||||
|
||||
require (
|
||||
4d63.com/gochecknoglobals v0.0.0-20201008074935-acfc0b28355a // indirect
|
||||
github.com/BurntSushi/toml v0.3.1 // indirect
|
||||
4d63.com/gochecknoglobals v0.1.0 // indirect
|
||||
github.com/Antonboom/errname v0.1.5 // indirect
|
||||
github.com/Antonboom/nilnil v0.1.0 // indirect
|
||||
github.com/BurntSushi/toml v0.4.1 // indirect
|
||||
github.com/Djarvur/go-err113 v0.1.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.0 // indirect
|
||||
github.com/Masterminds/goutils v1.1.1 // indirect
|
||||
github.com/Masterminds/semver v1.5.0 // indirect
|
||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
|
||||
github.com/Microsoft/go-winio v0.4.16 // indirect
|
||||
github.com/Microsoft/go-winio v0.5.1 // indirect
|
||||
github.com/OpenPeeDeeP/depguard v1.0.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.4.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.6.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.2.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.3.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.4.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.7.2 // indirect
|
||||
github.com/aws/smithy-go v1.8.0 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 // indirect
|
||||
github.com/acomagu/bufpipe v1.0.3 // indirect
|
||||
github.com/alexkohler/prealloc v1.0.0 // indirect
|
||||
github.com/ashanbrown/forbidigo v1.2.0 // indirect
|
||||
github.com/ashanbrown/makezero v0.0.0-20210520155254-b6261585ddde // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.0.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.6.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.5.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.9.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.6.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.11.1 // indirect
|
||||
github.com/aws/smithy-go v1.9.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bkielbasa/cyclop v1.2.0 // indirect
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
|
||||
github.com/bombsimon/wsl/v3 v3.1.0 // indirect
|
||||
github.com/blizzy78/varnamelen v0.5.0 // indirect
|
||||
github.com/bombsimon/wsl/v3 v3.3.0 // indirect
|
||||
github.com/breml/bidichk v0.2.1 // indirect
|
||||
github.com/butuzov/ireturn v0.1.1 // indirect
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e // indirect
|
||||
github.com/daixiang0/gci v0.2.7 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/charithe/durationcheck v0.0.9 // indirect
|
||||
github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af // indirect
|
||||
github.com/daixiang0/gci v0.2.9 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/denis-tingajkin/go-header v0.3.1 // indirect
|
||||
github.com/denis-tingajkin/go-header v0.4.2 // indirect
|
||||
github.com/emirpasic/gods v1.12.0 // indirect
|
||||
github.com/fatih/color v1.10.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.4.9 // indirect
|
||||
github.com/go-critic/go-critic v0.5.2 // indirect
|
||||
github.com/esimonov/ifshort v1.0.3 // indirect
|
||||
github.com/ettle/strcase v0.1.1 // indirect
|
||||
github.com/fatih/color v1.13.0 // indirect
|
||||
github.com/fatih/structtag v1.2.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.5.1 // indirect
|
||||
github.com/fzipp/gocyclo v0.3.1 // indirect
|
||||
github.com/go-critic/go-critic v0.6.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.0.0 // indirect
|
||||
github.com/go-git/go-git/v5 v5.2.0 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.3.1 // indirect
|
||||
github.com/go-git/go-git/v5 v5.4.2 // indirect
|
||||
github.com/go-toolsmith/astcast v1.0.0 // indirect
|
||||
github.com/go-toolsmith/astcopy v1.0.0 // indirect
|
||||
github.com/go-toolsmith/astequal v1.0.0 // indirect
|
||||
github.com/go-toolsmith/astequal v1.0.1 // indirect
|
||||
github.com/go-toolsmith/astfmt v1.0.0 // indirect
|
||||
github.com/go-toolsmith/astp v1.0.0 // indirect
|
||||
github.com/go-toolsmith/strparse v1.0.0 // indirect
|
||||
github.com/go-toolsmith/typep v1.0.2 // indirect
|
||||
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b // indirect
|
||||
github.com/go-xmlfmt/xmlfmt v0.0.0-20211206191508-7fd73a941850 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/gofrs/flock v0.8.0 // indirect
|
||||
github.com/gofrs/flock v0.8.1 // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 // indirect
|
||||
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect
|
||||
github.com/golangci/errcheck v0.0.0-20181223084120-ef45e06d44b6 // indirect
|
||||
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 // indirect
|
||||
github.com/golangci/gocyclo v0.0.0-20180528144436-0a533e8fa43d // indirect
|
||||
github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a // indirect
|
||||
github.com/golangci/golangci-lint v1.33.0 // indirect
|
||||
github.com/golangci/ineffassign v0.0.0-20190609212857-42439a7714cc // indirect
|
||||
github.com/golangci/golangci-lint v1.43.0 // indirect
|
||||
github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 // indirect
|
||||
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca // indirect
|
||||
github.com/golangci/misspell v0.3.5 // indirect
|
||||
github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21 // indirect
|
||||
github.com/golangci/revgrep v0.0.0-20180812185044-276a5c0a1039 // indirect
|
||||
github.com/golangci/revgrep v0.0.0-20210930125155-c22e5001d4f2 // indirect
|
||||
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect
|
||||
github.com/google/btree v1.0.1 // indirect
|
||||
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f // indirect
|
||||
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 // indirect
|
||||
github.com/google/rpmpack v0.0.0-20201206194719-59e495f2b7e1 // indirect
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20210914165742-4cc7213b9bc8 // indirect
|
||||
github.com/goreleaser/chglog v0.1.2 // indirect
|
||||
github.com/goreleaser/fileglob v0.3.1 // indirect
|
||||
github.com/gostaticanalysis/analysisutil v0.6.1 // indirect
|
||||
github.com/gostaticanalysis/comment v1.4.1 // indirect
|
||||
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
|
||||
github.com/gostaticanalysis/comment v1.4.2 // indirect
|
||||
github.com/gostaticanalysis/forcetypeassert v0.1.0 // indirect
|
||||
github.com/gostaticanalysis/nilerr v0.1.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/huandu/xstrings v1.3.2 // indirect
|
||||
github.com/imdario/mergo v0.3.11 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/jgautheron/goconst v0.0.0-20201117150253-ccae5bf973f3 // indirect
|
||||
github.com/jingyugao/rowserrcheck v0.0.0-20191204022205-72ab7603b68a // indirect
|
||||
github.com/jgautheron/goconst v1.5.1 // indirect
|
||||
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
|
||||
github.com/jirfag/go-printf-func-name v0.0.0-20200119135958-7558a9eaa5af // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/josharian/native v0.0.0-20200817173448-b6b71def0850 // indirect
|
||||
github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 // indirect
|
||||
github.com/julz/importas v0.0.0-20210922140945-27e0a5d4dee2 // indirect
|
||||
github.com/kevinburke/ssh_config v1.1.0 // indirect
|
||||
github.com/kisielk/errcheck v1.6.0 // indirect
|
||||
github.com/kisielk/gotool v1.0.0 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/kr/pretty v0.3.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/kunwardeep/paralleltest v1.0.2 // indirect
|
||||
github.com/kulti/thelper v0.4.0 // indirect
|
||||
github.com/kunwardeep/paralleltest v1.0.3 // indirect
|
||||
github.com/kyoh86/exportloopref v0.1.8 // indirect
|
||||
github.com/magiconair/properties v1.8.4 // indirect
|
||||
github.com/ldez/gomoddirectives v0.2.2 // indirect
|
||||
github.com/ldez/tagliatelle v0.2.0 // indirect
|
||||
github.com/magiconair/properties v1.8.5 // indirect
|
||||
github.com/maratori/testpackage v1.0.1 // indirect
|
||||
github.com/matoous/godox v0.0.0-20200801072554-4fb83dc2941e // indirect
|
||||
github.com/mattn/go-colorable v0.1.8 // indirect
|
||||
github.com/mattn/go-isatty v0.0.12 // indirect
|
||||
github.com/mbilski/exhaustivestruct v1.1.0 // indirect
|
||||
github.com/mdlayher/socket v0.0.0-20210307095302-262dc9984e00 // indirect
|
||||
github.com/mitchellh/copystructure v1.0.0 // indirect
|
||||
github.com/matoous/godox v0.0.0-20210227103229-6504466cf951 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||
github.com/mbilski/exhaustivestruct v1.2.0 // indirect
|
||||
github.com/mdlayher/socket v0.0.0-20211102153432-57e3fa563ecb // indirect
|
||||
github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517 // indirect
|
||||
github.com/mgechev/revive v1.1.2 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moricho/tparallel v0.2.1 // indirect
|
||||
github.com/nakabonne/nestif v0.3.0 // indirect
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20180912185939-ae427f1e4c1d // indirect
|
||||
github.com/nishanths/exhaustive v0.1.0 // indirect
|
||||
github.com/nakabonne/nestif v0.3.1 // indirect
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect
|
||||
github.com/nishanths/exhaustive v0.7.11 // indirect
|
||||
github.com/nishanths/predeclared v0.2.1 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
|
||||
github.com/pelletier/go-toml v1.8.1 // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||
github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d // indirect
|
||||
github.com/pkg/diff v0.0.0-20200914180035-5b29258ca4f7 // indirect
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/polyfloyd/go-errorlint v0.0.0-20201127212506-19bd8db6546f // indirect
|
||||
github.com/quasilyte/go-ruleguard v0.2.1 // indirect
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20200805063351-8f842688393c // indirect
|
||||
github.com/rogpeppe/go-internal v1.6.2 // indirect
|
||||
github.com/ryancurrah/gomodguard v1.1.0 // indirect
|
||||
github.com/polyfloyd/go-errorlint v0.0.0-20211125173453-6d6d39c5bb8b // indirect
|
||||
github.com/prometheus/client_golang v1.11.0 // indirect
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.32.1 // indirect
|
||||
github.com/prometheus/procfs v0.7.3 // indirect
|
||||
github.com/quasilyte/go-ruleguard v0.3.13 // indirect
|
||||
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect
|
||||
github.com/ryancurrah/gomodguard v1.2.3 // indirect
|
||||
github.com/ryanrolds/sqlclosecheck v0.3.0 // indirect
|
||||
github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect
|
||||
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b // indirect
|
||||
github.com/securego/gosec/v2 v2.5.0 // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/securego/gosec/v2 v2.9.3 // indirect
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect
|
||||
github.com/sirupsen/logrus v1.7.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/sivchari/tenv v1.4.7 // indirect
|
||||
github.com/sonatard/noctx v0.0.1 // indirect
|
||||
github.com/sourcegraph/go-diff v0.6.1 // indirect
|
||||
github.com/spf13/afero v1.5.1 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/spf13/cobra v1.1.1 // indirect
|
||||
github.com/spf13/afero v1.6.0 // indirect
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
github.com/spf13/cobra v1.2.1 // indirect
|
||||
github.com/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.7.1 // indirect
|
||||
github.com/ssgreg/nlreturn/v2 v2.1.0 // indirect
|
||||
github.com/spf13/viper v1.9.0 // indirect
|
||||
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
|
||||
github.com/stretchr/objx v0.3.0 // indirect
|
||||
github.com/stretchr/testify v1.7.0 // indirect
|
||||
github.com/subosito/gotenv v1.2.0 // indirect
|
||||
github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b // indirect
|
||||
github.com/tetafro/godot v1.3.2 // indirect
|
||||
github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94 // indirect
|
||||
github.com/tomarrell/wrapcheck v0.0.0-20201130113247-1683564d9756 // indirect
|
||||
github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa // indirect
|
||||
github.com/sylvia7788/contextcheck v1.0.4 // indirect
|
||||
github.com/tdakkota/asciicheck v0.1.1 // indirect
|
||||
github.com/tetafro/godot v1.4.11 // indirect
|
||||
github.com/timakin/bodyclose v0.0.0-20210704033933-f49887972144 // indirect
|
||||
github.com/tomarrell/wrapcheck/v2 v2.4.0 // indirect
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.4.0 // indirect
|
||||
github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect
|
||||
github.com/ultraware/funlen v0.0.3 // indirect
|
||||
github.com/ultraware/whitespace v0.0.4 // indirect
|
||||
github.com/uudashr/gocognit v1.0.1 // indirect
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.0 // indirect
|
||||
github.com/uudashr/gocognit v1.0.5 // indirect
|
||||
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.1 // indirect
|
||||
github.com/yeya24/promlinter v0.1.0 // indirect
|
||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect
|
||||
golang.org/x/mod v0.4.2 // indirect
|
||||
golang.org/x/mod v0.5.1 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 // indirect
|
||||
gopkg.in/ini.v1 v1.62.0 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
gopkg.in/ini.v1 v1.66.2 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 // indirect
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb // indirect
|
||||
mvdan.cc/gofumpt v0.0.0-20201129102820-5c11c50e9475 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
mvdan.cc/gofumpt v0.2.0 // indirect
|
||||
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect
|
||||
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect
|
||||
mvdan.cc/unparam v0.0.0-20200501210554-b37ab49443f7 // indirect
|
||||
software.sslmate.com/src/go-pkcs12 v0.0.0-20180114231543-2291e8f0f237 // indirect
|
||||
mvdan.cc/unparam v0.0.0-20211002134041-24922b6997ca // indirect
|
||||
software.sslmate.com/src/go-pkcs12 v0.0.0-20210415151418-c5206de65a78 // indirect
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -232,32 +232,11 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "android_does_need_fallbacks",
|
||||
os: "android",
|
||||
nm: &netmap.NetworkMap{
|
||||
DNS: tailcfg.DNSConfig{
|
||||
FallbackResolvers: []dnstype.Resolver{
|
||||
{Addr: "8.8.4.4"},
|
||||
},
|
||||
Routes: map[string][]dnstype.Resolver{
|
||||
"foo.com.": {{Addr: "1.2.3.4"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
prefs: &ipn.Prefs{
|
||||
CorpDNS: true,
|
||||
},
|
||||
want: &dns.Config{
|
||||
Hosts: map[dnsname.FQDN][]netaddr.IP{},
|
||||
DefaultResolvers: []dnstype.Resolver{
|
||||
{Addr: "8.8.4.4:53"},
|
||||
},
|
||||
Routes: map[dnsname.FQDN][]dnstype.Resolver{
|
||||
"foo.com.": {{Addr: "1.2.3.4:53"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Prior to fixing https://github.com/tailscale/tailscale/issues/2116,
|
||||
// Android had cases where it needed FallbackResolvers. This was the
|
||||
// negative test for the case where Override-local-DNS was set, so the
|
||||
// fallback resolvers did not need to be used. This test is still valid
|
||||
// so we keep it, but the fallback test has been removed.
|
||||
name: "android_does_NOT_need_fallbacks",
|
||||
os: "android",
|
||||
nm: &netmap.NetworkMap{
|
||||
@@ -344,3 +323,48 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowExitNodeDNSProxyToServeName(t *testing.T) {
|
||||
b := &LocalBackend{}
|
||||
if b.allowExitNodeDNSProxyToServeName("google.com") {
|
||||
t.Fatal("unexpected true on backend with nil NetMap")
|
||||
}
|
||||
|
||||
b.netMap = &netmap.NetworkMap{
|
||||
DNS: tailcfg.DNSConfig{
|
||||
ExitNodeFilteredSet: []string{
|
||||
".ts.net",
|
||||
"some.exact.bad",
|
||||
},
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
want bool
|
||||
}{
|
||||
// Allow by default:
|
||||
{"google.com", true},
|
||||
{"GOOGLE.com", true},
|
||||
|
||||
// Rejected by suffix:
|
||||
{"foo.TS.NET", false},
|
||||
{"foo.ts.net", false},
|
||||
|
||||
// Suffix doesn't match
|
||||
{"ts.net", true},
|
||||
|
||||
// Rejected by exact match:
|
||||
{"some.exact.bad", false},
|
||||
{"SOME.EXACT.BAD", false},
|
||||
|
||||
// But a prefix is okay.
|
||||
{"prefix-okay.some.exact.bad", true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := b.allowExitNodeDNSProxyToServeName(tt.name)
|
||||
if got != tt.want {
|
||||
t.Errorf("for %q = %v; want %v", tt.name, got, tt.want)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
@@ -36,6 +35,7 @@ import (
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/portlist"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -88,6 +88,7 @@ type LocalBackend struct {
|
||||
statsLogf logger.Logf // for printing peers stats on change
|
||||
e wgengine.Engine
|
||||
store ipn.StateStore
|
||||
dialer *tsdial.Dialer // non-nil
|
||||
backendLogID string
|
||||
unregisterLinkMon func()
|
||||
unregisterHealthWatch func()
|
||||
@@ -100,6 +101,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.
|
||||
@@ -139,7 +142,11 @@ type LocalBackend struct {
|
||||
// same as the Network Extension lifetime and we can thus avoid
|
||||
// double-copying files by writing them to the right location
|
||||
// immediately.
|
||||
directFileRoot string
|
||||
// It's also used on Synology & TrueNAS, but in that case DoFinalRename
|
||||
// is also set true, which moves the *.partial file to its final
|
||||
// name on completion.
|
||||
directFileRoot string
|
||||
directFileDoFinalRename bool // false on macOS, true on Synology & TrueNAS
|
||||
|
||||
// statusLock must be held before calling statusChanged.Wait() or
|
||||
// statusChanged.Broadcast().
|
||||
@@ -153,16 +160,18 @@ type clientGen func(controlclient.Options) (controlclient.Client, error)
|
||||
|
||||
// NewLocalBackend returns a new LocalBackend that is ready to run,
|
||||
// but is not actually running.
|
||||
func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wgengine.Engine) (*LocalBackend, error) {
|
||||
//
|
||||
// If dialer is nil, a new one is made.
|
||||
func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, dialer *tsdial.Dialer, e wgengine.Engine) (*LocalBackend, error) {
|
||||
if e == nil {
|
||||
panic("ipn.NewLocalBackend: wgengine must not be nil")
|
||||
panic("ipn.NewLocalBackend: engine must not be nil")
|
||||
}
|
||||
if dialer == nil {
|
||||
dialer = new(tsdial.Dialer)
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -177,11 +186,16 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge
|
||||
statsLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
|
||||
e: e,
|
||||
store: store,
|
||||
dialer: dialer,
|
||||
backendLogID: logid,
|
||||
state: ipn.NoState,
|
||||
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)
|
||||
|
||||
@@ -208,6 +222,11 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, e wge
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// Dialer returns the backend's dialer.
|
||||
func (b *LocalBackend) Dialer() *tsdial.Dialer {
|
||||
return b.dialer
|
||||
}
|
||||
|
||||
// SetDirectFileRoot sets the directory to download files to directly,
|
||||
// without buffering them through an intermediate daemon-owned
|
||||
// tailcfg.UserID-specific directory.
|
||||
@@ -219,6 +238,17 @@ func (b *LocalBackend) SetDirectFileRoot(dir string) {
|
||||
b.directFileRoot = dir
|
||||
}
|
||||
|
||||
// SetDirectFileDoFinalRename sets whether the peerapi file server should rename
|
||||
// a received "name.partial" file to "name" when the download is complete.
|
||||
//
|
||||
// This only applies when SetDirectFileRoot is non-empty.
|
||||
// The default is false.
|
||||
func (b *LocalBackend) SetDirectFileDoFinalRename(v bool) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.directFileDoFinalRename = v
|
||||
}
|
||||
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) maybePauseControlClientLocked() {
|
||||
if b.cc == nil {
|
||||
@@ -350,8 +380,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)
|
||||
@@ -377,33 +417,30 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
|
||||
if p.LastSeen != nil {
|
||||
lastSeen = *p.LastSeen
|
||||
}
|
||||
var tailAddr4 string
|
||||
var tailscaleIPs = make([]netaddr.IP, 0, len(p.Addresses))
|
||||
for _, addr := range p.Addresses {
|
||||
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.IP()) {
|
||||
if addr.IP().Is4() && tailAddr4 == "" {
|
||||
// The peer struct previously only allowed a single
|
||||
// Tailscale IP address. For compatibility for a few releases starting
|
||||
// with 1.8, keep it pulled out as IPv4-only for a bit.
|
||||
tailAddr4 = addr.IP().String()
|
||||
}
|
||||
tailscaleIPs = append(tailscaleIPs, addr.IP())
|
||||
}
|
||||
}
|
||||
exitNodeOption := tsaddr.PrefixesContainsFunc(p.AllowedIPs, func(r netaddr.IPPrefix) bool {
|
||||
return r.Bits() == 0
|
||||
})
|
||||
sb.AddPeer(p.Key, &ipnstate.PeerStatus{
|
||||
InNetworkMap: true,
|
||||
ID: p.StableID,
|
||||
UserID: p.User,
|
||||
TailAddrDeprecated: tailAddr4,
|
||||
TailscaleIPs: tailscaleIPs,
|
||||
HostName: p.Hostinfo.Hostname,
|
||||
DNSName: p.Name,
|
||||
OS: p.Hostinfo.OS,
|
||||
KeepAlive: p.KeepAlive,
|
||||
Created: p.Created,
|
||||
LastSeen: lastSeen,
|
||||
ShareeNode: p.Hostinfo.ShareeNode,
|
||||
ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID,
|
||||
InNetworkMap: true,
|
||||
ID: p.StableID,
|
||||
UserID: p.User,
|
||||
TailscaleIPs: tailscaleIPs,
|
||||
HostName: p.Hostinfo.Hostname,
|
||||
DNSName: p.Name,
|
||||
OS: p.Hostinfo.OS,
|
||||
KeepAlive: p.KeepAlive,
|
||||
Created: p.Created,
|
||||
LastSeen: lastSeen,
|
||||
Online: p.Online != nil && *p.Online,
|
||||
ShareeNode: p.Hostinfo.ShareeNode,
|
||||
ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID,
|
||||
ExitNodeOption: exitNodeOption,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -588,6 +625,11 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
// findExitNodeIDLocked updates b.prefs to reference an exit node by ID,
|
||||
// rather than by IP. It returns whether prefs was mutated.
|
||||
func (b *LocalBackend) findExitNodeIDLocked(nm *netmap.NetworkMap) (prefsChanged bool) {
|
||||
if nm == nil {
|
||||
// No netmap, can't resolve anything.
|
||||
return false
|
||||
}
|
||||
|
||||
// If we have a desired IP on file, try to find the corresponding
|
||||
// node.
|
||||
if b.prefs.ExitNodeIP.IsZero() {
|
||||
@@ -1001,20 +1043,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"),
|
||||
@@ -1242,7 +1289,7 @@ func (b *LocalBackend) send(n ipn.Notify) {
|
||||
return
|
||||
}
|
||||
|
||||
if apiSrv != nil && apiSrv.hasFilesWaiting() {
|
||||
if apiSrv.hasFilesWaiting() {
|
||||
n.FilesWaiting = &empty.Message{}
|
||||
}
|
||||
|
||||
@@ -1651,7 +1698,7 @@ func (b *LocalBackend) SetPrefs(newp *ipn.Prefs) {
|
||||
}
|
||||
|
||||
// setPrefsLockedOnEntry requires b.mu be held to call it, but it
|
||||
// unlocks b.mu when done.
|
||||
// unlocks b.mu when done. newp ownership passes to this function.
|
||||
func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
||||
netMap := b.netMap
|
||||
stateKey := b.stateKey
|
||||
@@ -1659,6 +1706,10 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
|
||||
oldp := b.prefs
|
||||
newp.Persist = oldp.Persist // caller isn't allowed to override this
|
||||
b.prefs = newp
|
||||
// findExitNodeIDLocked returns whether it updated b.prefs, but
|
||||
// everything in this function treats b.prefs as completely new
|
||||
// anyway. No-op if no exit node resolution is needed.
|
||||
b.findExitNodeIDLocked(netMap)
|
||||
b.inServerMode = newp.ForceDaemon
|
||||
// We do this to avoid holding the lock while doing everything else.
|
||||
newp = b.prefs.Clone()
|
||||
@@ -1694,7 +1745,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1736,15 +1787,24 @@ func (b *LocalBackend) getPeerAPIPortForTSMPPing(ip netaddr.IP) (port uint16, ok
|
||||
|
||||
func (b *LocalBackend) peerAPIServicesLocked() (ret []tailcfg.Service) {
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
proto := tailcfg.ServiceProto("peerapi4")
|
||||
proto := tailcfg.PeerAPI4
|
||||
if pln.ip.Is6() {
|
||||
proto = "peerapi6"
|
||||
proto = tailcfg.PeerAPI6
|
||||
}
|
||||
ret = append(ret, tailcfg.Service{
|
||||
Proto: proto,
|
||||
Port: uint16(pln.port),
|
||||
})
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "linux", "freebsd", "openbsd", "illumos", "darwin", "windows":
|
||||
// These are the platforms currently supported by
|
||||
// net/dns/resolver/tsdns.go:Resolver.HandleExitNodeDNSQuery.
|
||||
ret = append(ret, tailcfg.Service{
|
||||
Proto: tailcfg.PeerAPIDNS,
|
||||
Port: 1, // version
|
||||
})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
@@ -1845,6 +1905,15 @@ func (b *LocalBackend) authReconfig() {
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the dialer updated about whether we're supposed to use
|
||||
// an exit node's DNS server (so SOCKS5/HTTP outgoing dials
|
||||
// can use it for name resolution)
|
||||
if dohURL, ok := exitNodeCanProxyDNS(nm, prefs.ExitNodeID); ok {
|
||||
b.dialer.SetExitDNSDoH(dohURL)
|
||||
} else {
|
||||
b.dialer.SetExitDNSDoH("")
|
||||
}
|
||||
|
||||
cfg, err := nmcfg.WGCfg(nm, b.logf, flags, prefs.ExitNodeID)
|
||||
if err != nil {
|
||||
b.logf("wgcfg: %v", err)
|
||||
@@ -1943,12 +2012,32 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
|
||||
return dcfg
|
||||
}
|
||||
|
||||
for _, dom := range nm.DNS.Domains {
|
||||
fqdn, err := dnsname.ToFQDN(dom)
|
||||
if err != nil {
|
||||
logf("[unexpected] non-FQDN search domain %q", dom)
|
||||
}
|
||||
dcfg.SearchDomains = append(dcfg.SearchDomains, fqdn)
|
||||
}
|
||||
if nm.DNS.Proxied { // actually means "enable MagicDNS"
|
||||
for _, dom := range magicDNSRootDomains(nm) {
|
||||
dcfg.Routes[dom] = nil // resolve internally with dcfg.Hosts
|
||||
}
|
||||
}
|
||||
|
||||
addDefault := func(resolvers []dnstype.Resolver) {
|
||||
for _, r := range resolvers {
|
||||
dcfg.DefaultResolvers = append(dcfg.DefaultResolvers, normalizeResolver(r))
|
||||
}
|
||||
}
|
||||
|
||||
// If we're using an exit node and that exit node is new enough (1.19.x+)
|
||||
// to run a DoH DNS proxy, then send all our DNS traffic through it.
|
||||
if dohURL, ok := exitNodeCanProxyDNS(nm, prefs.ExitNodeID); ok {
|
||||
addDefault([]dnstype.Resolver{{Addr: dohURL}})
|
||||
return dcfg
|
||||
}
|
||||
|
||||
addDefault(nm.DNS.Resolvers)
|
||||
for suffix, resolvers := range nm.DNS.Routes {
|
||||
fqdn, err := dnsname.ToFQDN(suffix)
|
||||
@@ -1970,18 +2059,6 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
|
||||
dcfg.Routes[fqdn] = append(dcfg.Routes[fqdn], normalizeResolver(r))
|
||||
}
|
||||
}
|
||||
for _, dom := range nm.DNS.Domains {
|
||||
fqdn, err := dnsname.ToFQDN(dom)
|
||||
if err != nil {
|
||||
logf("[unexpected] non-FQDN search domain %q", dom)
|
||||
}
|
||||
dcfg.SearchDomains = append(dcfg.SearchDomains, fqdn)
|
||||
}
|
||||
if nm.DNS.Proxied { // actually means "enable MagicDNS"
|
||||
for _, dom := range magicDNSRootDomains(nm) {
|
||||
dcfg.Routes[dom] = nil // resolve internally with dcfg.Hosts
|
||||
}
|
||||
}
|
||||
|
||||
// Set FallbackResolvers as the default resolvers in the
|
||||
// scenarios that can't handle a purely split-DNS config. See
|
||||
@@ -2005,9 +2082,6 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
|
||||
addDefault(nm.DNS.FallbackResolvers)
|
||||
case len(dcfg.Routes) == 0:
|
||||
// No settings requiring split DNS, no problem.
|
||||
case versionOS == "android":
|
||||
// We don't support split DNS at all on Android yet.
|
||||
addDefault(nm.DNS.FallbackResolvers)
|
||||
}
|
||||
|
||||
return dcfg
|
||||
@@ -2054,7 +2128,7 @@ func (b *LocalBackend) fileRootLocked(uid tailcfg.UserID) string {
|
||||
}
|
||||
varRoot := b.TailscaleVarRoot()
|
||||
if varRoot == "" {
|
||||
b.logf("peerapi disabled; no state directory")
|
||||
b.logf("Taildrop disabled; no state directory")
|
||||
return ""
|
||||
}
|
||||
baseDir := fmt.Sprintf("%s-uid-%d",
|
||||
@@ -2062,7 +2136,7 @@ func (b *LocalBackend) fileRootLocked(uid tailcfg.UserID) string {
|
||||
uid)
|
||||
dir := filepath.Join(varRoot, "files", baseDir)
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
b.logf("peerapi disabled; error making directory: %v", err)
|
||||
b.logf("Taildrop disabled; error making directory: %v", err)
|
||||
return ""
|
||||
}
|
||||
return dir
|
||||
@@ -2125,22 +2199,20 @@ func (b *LocalBackend) initPeerAPIListener() {
|
||||
|
||||
fileRoot := b.fileRootLocked(selfNode.User)
|
||||
if fileRoot == "" {
|
||||
return
|
||||
}
|
||||
|
||||
var tunName string
|
||||
if ge, ok := b.e.(wgengine.InternalsGetter); ok {
|
||||
if tunWrap, _, ok := ge.GetInternals(); ok {
|
||||
tunName, _ = tunWrap.Name()
|
||||
}
|
||||
b.logf("peerapi starting without Taildrop directory configured")
|
||||
}
|
||||
|
||||
ps := &peerAPIServer{
|
||||
b: b,
|
||||
rootDir: fileRoot,
|
||||
tunName: tunName,
|
||||
selfNode: selfNode,
|
||||
directFileMode: b.directFileRoot != "",
|
||||
b: b,
|
||||
rootDir: fileRoot,
|
||||
selfNode: selfNode,
|
||||
directFileMode: b.directFileRoot != "",
|
||||
directFileDoFinalRename: b.directFileDoFinalRename,
|
||||
}
|
||||
if re, ok := b.e.(wgengine.ResolvingEngine); ok {
|
||||
if r, ok := re.GetResolver(); ok {
|
||||
ps.resolver = r
|
||||
}
|
||||
}
|
||||
b.peerAPIServer = ps
|
||||
|
||||
@@ -2618,6 +2690,7 @@ func hasCapability(nm *netmap.NetworkMap, cap string) bool {
|
||||
}
|
||||
|
||||
func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
b.dialer.SetNetMap(nm)
|
||||
var login string
|
||||
if nm != nil {
|
||||
login = nm.UserProfiles[nm.User].LoginName
|
||||
@@ -2722,9 +2795,6 @@ func (b *LocalBackend) WaitingFiles() ([]apitype.WaitingFile, error) {
|
||||
b.mu.Lock()
|
||||
apiSrv := b.peerAPIServer
|
||||
b.mu.Unlock()
|
||||
if apiSrv == nil {
|
||||
return nil, errors.New("peerapi disabled")
|
||||
}
|
||||
return apiSrv.WaitingFiles()
|
||||
}
|
||||
|
||||
@@ -2732,9 +2802,6 @@ func (b *LocalBackend) DeleteFile(name string) error {
|
||||
b.mu.Lock()
|
||||
apiSrv := b.peerAPIServer
|
||||
b.mu.Unlock()
|
||||
if apiSrv == nil {
|
||||
return errors.New("peerapi disabled")
|
||||
}
|
||||
return apiSrv.DeleteFile(name)
|
||||
}
|
||||
|
||||
@@ -2742,9 +2809,6 @@ func (b *LocalBackend) OpenFile(name string) (rc io.ReadCloser, size int64, err
|
||||
b.mu.Lock()
|
||||
apiSrv := b.peerAPIServer
|
||||
b.mu.Unlock()
|
||||
if apiSrv == nil {
|
||||
return nil, 0, errors.New("peerapi disabled")
|
||||
}
|
||||
return apiSrv.OpenFile(name)
|
||||
}
|
||||
|
||||
@@ -2858,9 +2922,9 @@ func peerAPIBase(nm *netmap.NetworkMap, peer *tailcfg.Node) string {
|
||||
var p4, p6 uint16
|
||||
for _, s := range peer.Hostinfo.Services {
|
||||
switch s.Proto {
|
||||
case "peerapi4":
|
||||
case tailcfg.PeerAPI4:
|
||||
p4 = s.Port
|
||||
case "peerapi6":
|
||||
case tailcfg.PeerAPI6:
|
||||
p6 = s.Port
|
||||
}
|
||||
}
|
||||
@@ -2894,48 +2958,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()
|
||||
if err != nil {
|
||||
return fmt.Errorf("couldn't check %s (%v)%s", key, err, suffix)
|
||||
// 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
|
||||
}
|
||||
on, err := strconv.ParseBool(string(bytes.TrimSpace(bs)))
|
||||
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 parse %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 !on {
|
||||
return fmt.Errorf("%s is disabled.%s", key, suffix)
|
||||
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
|
||||
}
|
||||
|
||||
// peerDialControlFunc is non-nil on platforms that require a way to
|
||||
// bind to dial out to other peers.
|
||||
var peerDialControlFunc func(*LocalBackend) func(network, address string, c syscall.RawConn) error
|
||||
|
||||
// PeerDialControlFunc returns a net.Dialer.Control func (possibly nil) to use to
|
||||
// dial other Tailscale peers from the current environment.
|
||||
func (b *LocalBackend) PeerDialControlFunc() func(network, address string, c syscall.RawConn) error {
|
||||
if peerDialControlFunc != nil {
|
||||
return peerDialControlFunc(b)
|
||||
// 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 nil, fmt.Errorf("couldn't parse %s (%v)", k, err)
|
||||
}
|
||||
if !on {
|
||||
disabled = append(disabled, k)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return disabled, nil
|
||||
}
|
||||
|
||||
// DERPMap returns the current DERPMap in use, or nil if not connected.
|
||||
@@ -2947,3 +3060,77 @@ 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
|
||||
}
|
||||
|
||||
// allowExitNodeDNSProxyToServeName reports whether the Exit Node DNS
|
||||
// proxy is allowed to serve responses for the provided DNS name.
|
||||
func (b *LocalBackend) allowExitNodeDNSProxyToServeName(name string) bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
nm := b.netMap
|
||||
if nm == nil {
|
||||
return false
|
||||
}
|
||||
name = strings.ToLower(name)
|
||||
for _, bad := range nm.DNS.ExitNodeFilteredSet {
|
||||
if bad == "" {
|
||||
// Invalid, ignore.
|
||||
continue
|
||||
}
|
||||
if bad[0] == '.' {
|
||||
// Entries beginning with a dot are suffix matches.
|
||||
if dnsname.HasSuffix(name, bad) {
|
||||
return false
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Otherwise entries are exact matches. They're
|
||||
// guaranteed to be lowercase already.
|
||||
if name == bad {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// exitNodeCanProxyDNS reports the DoH base URL ("http://foo/dns-query") without query parameters
|
||||
// to exitNodeID's DoH service, if available.
|
||||
//
|
||||
// If exitNodeID is the zero valid, it returns "", false.
|
||||
func exitNodeCanProxyDNS(nm *netmap.NetworkMap, exitNodeID tailcfg.StableNodeID) (dohURL string, ok bool) {
|
||||
if exitNodeID.IsZero() {
|
||||
return "", false
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
if p.StableID != exitNodeID {
|
||||
continue
|
||||
}
|
||||
for _, s := range p.Hostinfo.Services {
|
||||
if s.Proto == tailcfg.PeerAPIDNS && s.Port >= 1 {
|
||||
return peerAPIBase(nm, p) + "/dns-query", true
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
@@ -92,14 +92,14 @@ func TestNetworkMapCompare(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"Node names identical",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"Node names differ",
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{Name: "B"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "A"}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{Name: "B"}}},
|
||||
false,
|
||||
},
|
||||
{
|
||||
@@ -117,8 +117,8 @@ func TestNetworkMapCompare(t *testing.T) {
|
||||
{
|
||||
"Node Users differ",
|
||||
// User field is not checked.
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 0}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{&tailcfg.Node{User: 1}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{User: 0}}},
|
||||
&netmap.NetworkMap{Peers: []*tailcfg.Node{{User: 1}}},
|
||||
true,
|
||||
},
|
||||
}
|
||||
@@ -445,7 +445,7 @@ func TestLazyMachineKeyGeneration(t *testing.T) {
|
||||
t.Fatalf("NewFakeUserspaceEngine: %v", err)
|
||||
}
|
||||
t.Cleanup(eng.Close)
|
||||
lb, err := NewLocalBackend(logf, "logid", store, eng)
|
||||
lb, err := NewLocalBackend(logf, "logid", store, nil, eng)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLocalBackend: %v", err)
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ func TestLocalLogLines(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(e.Close)
|
||||
|
||||
lb, err := NewLocalBackend(logf, idA.String(), store, e)
|
||||
lb, err := NewLocalBackend(logf, idA.String(), store, nil, e)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ package ipnlocal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -28,34 +29,48 @@ 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
|
||||
|
||||
// addH2C is non-nil on platforms where we want to add H2C
|
||||
// ("cleartext" HTTP/2) support to the peerAPI.
|
||||
var addH2C func(*http.Server)
|
||||
|
||||
type peerAPIServer struct {
|
||||
b *LocalBackend
|
||||
rootDir string
|
||||
tunName string
|
||||
rootDir string // empty means file receiving unavailable
|
||||
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
|
||||
// the frontend retrieve it over localapi HTTP and write it
|
||||
// somewhere itself. This is used on GUI macOS version.
|
||||
// somewhere itself. This is used on the GUI macOS versions
|
||||
// and on Synology.
|
||||
// In directFileMode, the peerapi doesn't do the final rename
|
||||
// from "foo.jpg.partial" to "foo.jpg".
|
||||
// from "foo.jpg.partial" to "foo.jpg" unless
|
||||
// directFileDoFinalRename is set.
|
||||
directFileMode bool
|
||||
|
||||
// directFileDoFinalRename is whether in directFileMode we
|
||||
// additionally move the *.direct file to its final name after
|
||||
// it's received.
|
||||
directFileDoFinalRename bool
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -72,6 +87,10 @@ const (
|
||||
deletedSuffix = ".deleted"
|
||||
)
|
||||
|
||||
func (s *peerAPIServer) canReceiveFiles() bool {
|
||||
return s != nil && s.rootDir != ""
|
||||
}
|
||||
|
||||
func validFilenameRune(r rune) bool {
|
||||
switch r {
|
||||
case '/':
|
||||
@@ -118,7 +137,7 @@ func (s *peerAPIServer) diskPath(baseName string) (fullPath string, ok bool) {
|
||||
// hasFilesWaiting reports whether any files are buffered in the
|
||||
// tailscaled daemon storage.
|
||||
func (s *peerAPIServer) hasFilesWaiting() bool {
|
||||
if s.rootDir == "" || s.directFileMode {
|
||||
if s == nil || s.rootDir == "" || s.directFileMode {
|
||||
return false
|
||||
}
|
||||
if s.knownEmpty.Get() {
|
||||
@@ -178,8 +197,11 @@ func (s *peerAPIServer) hasFilesWaiting() bool {
|
||||
// As a side effect, it also does any lazy deletion of files as
|
||||
// required by Windows.
|
||||
func (s *peerAPIServer) WaitingFiles() (ret []apitype.WaitingFile, err error) {
|
||||
if s == nil {
|
||||
return nil, errNilPeerAPIServer
|
||||
}
|
||||
if s.rootDir == "" {
|
||||
return nil, errors.New("peerapi disabled; no storage configured")
|
||||
return nil, errNoTaildrop
|
||||
}
|
||||
if s.directFileMode {
|
||||
return nil, nil
|
||||
@@ -243,6 +265,11 @@ func (s *peerAPIServer) WaitingFiles() (ret []apitype.WaitingFile, err error) {
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
var (
|
||||
errNilPeerAPIServer = errors.New("peerapi unavailable; not listening")
|
||||
errNoTaildrop = errors.New("Taildrop disabled; no storage directory")
|
||||
)
|
||||
|
||||
// tryDeleteAgain tries to delete path (and path+deletedSuffix) after
|
||||
// it failed earlier. This happens on Windows when various anti-virus
|
||||
// tools hook into filesystem operations and have the file open still
|
||||
@@ -258,8 +285,11 @@ func tryDeleteAgain(fullPath string) {
|
||||
}
|
||||
|
||||
func (s *peerAPIServer) DeleteFile(baseName string) error {
|
||||
if s == nil {
|
||||
return errNilPeerAPIServer
|
||||
}
|
||||
if s.rootDir == "" {
|
||||
return errors.New("peerapi disabled; no storage configured")
|
||||
return errNoTaildrop
|
||||
}
|
||||
if s.directFileMode {
|
||||
return errors.New("deletes not allowed in direct mode")
|
||||
@@ -324,8 +354,11 @@ func touchFile(path string) error {
|
||||
}
|
||||
|
||||
func (s *peerAPIServer) OpenFile(baseName string) (rc io.ReadCloser, size int64, err error) {
|
||||
if s == nil {
|
||||
return nil, 0, errNilPeerAPIServer
|
||||
}
|
||||
if s.rootDir == "" {
|
||||
return nil, 0, errors.New("peerapi disabled; no storage configured")
|
||||
return nil, 0, errNoTaildrop
|
||||
}
|
||||
if s.directFileMode {
|
||||
return nil, 0, errors.New("opens not allowed in direct mode")
|
||||
@@ -358,7 +391,7 @@ func (s *peerAPIServer) listen(ip netaddr.IP, ifState *interfaces.State) (ln net
|
||||
// On iOS/macOS, this sets the lc.Control hook to
|
||||
// setsockopt the interface index to bind to, to get
|
||||
// out of the network sandbox.
|
||||
if err := initListenConfig(&lc, ip, ifState, s.tunName); err != nil {
|
||||
if err := initListenConfig(&lc, ip, ifState, s.b.dialer.TUNName()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if runtime.GOOS == "darwin" || runtime.GOOS == "ios" {
|
||||
@@ -463,6 +496,9 @@ func (pln *peerAPIListener) serve() {
|
||||
httpServer := &http.Server{
|
||||
Handler: h,
|
||||
}
|
||||
if addH2C != nil {
|
||||
addH2C(httpServer)
|
||||
}
|
||||
go httpServer.Serve(&oneConnListener{Listener: pln.ln, conn: c})
|
||||
}
|
||||
}
|
||||
@@ -503,6 +539,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)
|
||||
@@ -599,7 +639,7 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
if h.ps.rootDir == "" {
|
||||
http.Error(w, "no rootdir", http.StatusInternalServerError)
|
||||
http.Error(w, errNoTaildrop.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rawPath := r.URL.EscapedPath()
|
||||
@@ -671,7 +711,7 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if h.ps.directFileMode {
|
||||
if h.ps.directFileMode && !h.ps.directFileDoFinalRename {
|
||||
if inFile != nil { // non-zero length; TODO: notify even for zero length
|
||||
inFile.markAndNotifyDone()
|
||||
}
|
||||
@@ -749,3 +789,218 @@ 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, h.ps.b.allowExitNodeDNSProxyToServeName)
|
||||
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(res)))
|
||||
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()})
|
||||
j = append(j, '\n')
|
||||
w.Write(j)
|
||||
return
|
||||
}
|
||||
}()
|
||||
var p dnsmessage.Parser
|
||||
hdr, err := p.Start(res)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hdr.RCode != dnsmessage.RCodeSuccess {
|
||||
return fmt.Errorf("DNS RCode = %v", hdr.RCode)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
22
ipn/ipnlocal/peerapi_h2c.go
Normal file
22
ipn/ipnlocal/peerapi_h2c.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !ios && !android
|
||||
// +build !ios,!android
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
)
|
||||
|
||||
func init() {
|
||||
addH2C = func(s *http.Server) {
|
||||
h2s := &http2.Server{}
|
||||
s.Handler = h2c.NewHandler(s.Handler, h2s)
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,8 @@
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
@@ -21,7 +19,6 @@ import (
|
||||
|
||||
func init() {
|
||||
initListenConfig = initListenConfigNetworkExtension
|
||||
peerDialControlFunc = peerDialControlFuncNetworkExtension
|
||||
}
|
||||
|
||||
// initListenConfigNetworkExtension configures nc for listening on IP
|
||||
@@ -34,24 +31,3 @@ func initListenConfigNetworkExtension(nc *net.ListenConfig, ip netaddr.IP, st *i
|
||||
}
|
||||
return netns.SetListenConfigInterfaceIndex(nc, tunIf.Index)
|
||||
}
|
||||
|
||||
func peerDialControlFuncNetworkExtension(b *LocalBackend) func(network, address string, c syscall.RawConn) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
st := b.prevIfState
|
||||
pas := b.peerAPIServer
|
||||
index := -1
|
||||
if st != nil && pas != nil && pas.tunName != "" {
|
||||
if tunIf, ok := st.Interface[pas.tunName]; ok {
|
||||
index = tunIf.Index
|
||||
}
|
||||
}
|
||||
var lc net.ListenConfig
|
||||
netns.SetListenConfigInterfaceIndex(&lc, index)
|
||||
return func(network, address string, c syscall.RawConn) error {
|
||||
if index == -1 {
|
||||
return errors.New("failed to find TUN interface to bind to")
|
||||
}
|
||||
return lc.Control(network, address, c)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -174,7 +179,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
req: httptest.NewRequest("PUT", "/v0/put/foo", nil),
|
||||
checks: checks(
|
||||
httpStatus(http.StatusInternalServerError),
|
||||
bodyContains("no rootdir"),
|
||||
bodyContains("Taildrop disabled; no storage directory"),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -284,7 +284,7 @@ func TestStateMachine(t *testing.T) {
|
||||
t.Cleanup(e.Close)
|
||||
|
||||
cc := newMockControl(t)
|
||||
b, err := NewLocalBackend(logf, "logid", store, e)
|
||||
b, err := NewLocalBackend(logf, "logid", store, nil, e)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLocalBackend: %v", err)
|
||||
}
|
||||
@@ -941,7 +941,7 @@ func TestWGEngineStatusRace(t *testing.T) {
|
||||
eng, err := wgengine.NewFakeUserspaceEngine(logf, 0)
|
||||
c.Assert(err, qt.IsNil)
|
||||
t.Cleanup(eng.Close)
|
||||
b, err := NewLocalBackend(logf, "logid", new(ipn.MemoryStore), eng)
|
||||
b, err := NewLocalBackend(logf, "logid", new(ipn.MemoryStore), nil, eng)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
cc := newMockControl(t)
|
||||
|
||||
@@ -35,9 +35,9 @@ import (
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/localapi"
|
||||
"tailscale.com/ipn/store/aws"
|
||||
"tailscale.com/log/filelogger"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/netstat"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/smallzstd"
|
||||
@@ -48,6 +48,7 @@ import (
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
// Options is the configuration of the Tailscale node agent.
|
||||
@@ -651,7 +652,7 @@ func StateStore(path string, logf logger.Logf) (ipn.StateStore, error) {
|
||||
// The getEngine func is called repeatedly, once per connection, until it returns an engine successfully.
|
||||
//
|
||||
// Deprecated: use New and Server.Run instead.
|
||||
func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.StateStore, logid string, getEngine func() (wgengine.Engine, error), opts Options) error {
|
||||
func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.StateStore, linkMon *monitor.Mon, dialer *tsdial.Dialer, logid string, getEngine func() (wgengine.Engine, error), opts Options) error {
|
||||
getEngine = getEngineUntilItWorksWrapper(getEngine)
|
||||
runDone := make(chan struct{})
|
||||
defer close(runDone)
|
||||
@@ -735,7 +736,7 @@ func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.State
|
||||
}
|
||||
}
|
||||
|
||||
server, err := New(logf, logid, store, eng, serverModeUser, opts)
|
||||
server, err := New(logf, logid, store, eng, dialer, serverModeUser, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -748,8 +749,8 @@ func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.State
|
||||
// New returns a new Server.
|
||||
//
|
||||
// To start it, use the Server.Run method.
|
||||
func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engine, serverModeUser *user.User, opts Options) (*Server, error) {
|
||||
b, err := ipnlocal.NewLocalBackend(logf, logid, store, eng)
|
||||
func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engine, dialer *tsdial.Dialer, serverModeUser *user.User, opts Options) (*Server, error) {
|
||||
b, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, eng)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("NewLocalBackend: %v", err)
|
||||
}
|
||||
@@ -758,6 +759,22 @@ func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engi
|
||||
return smallzstd.NewDecoder(nil)
|
||||
})
|
||||
|
||||
dg := distro.Get()
|
||||
switch dg {
|
||||
case distro.Synology, distro.TrueNAS:
|
||||
// See if they have a "Taildrop" share.
|
||||
// See https://github.com/tailscale/tailscale/issues/2179#issuecomment-982821319
|
||||
path, err := findTaildropDir(dg)
|
||||
if err != nil {
|
||||
logf("%s Taildrop support: %v", dg, err)
|
||||
} else {
|
||||
logf("%s Taildrop: using %v", dg, path)
|
||||
b.SetDirectFileRoot(path)
|
||||
b.SetDirectFileDoFinalRename(true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if opts.AutostartStateKey == "" {
|
||||
autoStartKey, err := store.ReadState(ipn.ServerModeStartKey)
|
||||
if err != nil && err != ipn.ErrStateNotExist {
|
||||
@@ -851,14 +868,6 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
panic("cannot determine executable: " + err.Error())
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
if len(args) != 2 && args[0] != "/subproc" {
|
||||
panic(fmt.Sprintf("unexpected arguments %q", args))
|
||||
}
|
||||
logID := args[1]
|
||||
logf = filelogger.New("tailscale-service", logID, logf)
|
||||
}
|
||||
|
||||
var proc struct {
|
||||
mu sync.Mutex
|
||||
p *os.Process
|
||||
@@ -1112,3 +1121,50 @@ func (ln *listenerWithReadyConn) Accept() (net.Conn, error) {
|
||||
}
|
||||
return ln.Listener.Accept()
|
||||
}
|
||||
|
||||
func findTaildropDir(dg distro.Distro) (string, error) {
|
||||
const name = "Taildrop"
|
||||
switch dg {
|
||||
case distro.Synology:
|
||||
return findSynologyTaildropDir(name)
|
||||
case distro.TrueNAS:
|
||||
return findTrueNASTaildropDir(name)
|
||||
}
|
||||
return "", fmt.Errorf("%s is an unsupported distro for Taildrop dir", dg)
|
||||
}
|
||||
|
||||
// findSynologyTaildropDir looks for the first volume containing a
|
||||
// "Taildrop" directory. We'd run "synoshare --get Taildrop" command
|
||||
// but on DSM7 at least, we lack permissions to run that.
|
||||
func findSynologyTaildropDir(name string) (dir string, err error) {
|
||||
for i := 1; i <= 16; i++ {
|
||||
dir = fmt.Sprintf("/volume%v/%s", i, name)
|
||||
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
|
||||
return dir, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("shared folder %q not found", name)
|
||||
}
|
||||
|
||||
// findTrueNASTaildropDir returns the first matching directory of
|
||||
// /mnt/{name} or /mnt/*/{name}
|
||||
func findTrueNASTaildropDir(name string) (dir string, err error) {
|
||||
// If we're running in a jail, a mount point could just be added at /mnt/Taildrop
|
||||
dir = fmt.Sprintf("/mnt/%s", name)
|
||||
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
|
||||
return dir, nil
|
||||
}
|
||||
|
||||
// but if running on the host, it may be something like /mnt/Primary/Taildrop
|
||||
fis, err := ioutil.ReadDir("/mnt")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading /mnt: %w", err)
|
||||
}
|
||||
for _, fi := range fis {
|
||||
dir = fmt.Sprintf("/mnt/%s/%s", fi.Name(), name)
|
||||
if fi, err := os.Stat(dir); err == nil && fi.IsDir() {
|
||||
return dir, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("shared folder %q not found", name)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnserver"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
@@ -32,10 +33,11 @@ func TestRunMultipleAccepts(t *testing.T) {
|
||||
t.Logf(format, args...)
|
||||
}
|
||||
|
||||
s := safesocket.DefaultConnectionStrategy(socketPath)
|
||||
connect := func() {
|
||||
for i := 1; i <= 2; i++ {
|
||||
logf("connect %d ...", i)
|
||||
c, err := safesocket.Connect(socketPath, 0)
|
||||
c, err := safesocket.Connect(s)
|
||||
if err != nil {
|
||||
t.Fatalf("safesocket.Connect: %v\n", err)
|
||||
}
|
||||
@@ -72,6 +74,6 @@ func TestRunMultipleAccepts(t *testing.T) {
|
||||
}
|
||||
defer ln.Close()
|
||||
|
||||
err = ipnserver.Run(ctx, logTriggerTestf, ln, store, "dummy_logid", ipnserver.FixedEngine(eng), opts)
|
||||
err = ipnserver.Run(ctx, logTriggerTestf, ln, store, nil /* mon */, new(tsdial.Dialer), "dummy_logid", ipnserver.FixedEngine(eng), opts)
|
||||
t.Logf("ipnserver.Run = %v", err)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -83,22 +88,23 @@ type PeerStatus struct {
|
||||
OS string // HostInfo.OS
|
||||
UserID tailcfg.UserID
|
||||
|
||||
TailAddrDeprecated string `json:"TailAddr"` // Tailscale IP
|
||||
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
||||
TailscaleIPs []netaddr.IP // Tailscale IP(s) assigned to this node
|
||||
|
||||
// Endpoints:
|
||||
Addrs []string
|
||||
CurAddr string // one of Addrs, or unique if roaming
|
||||
Relay string // DERP region
|
||||
|
||||
RxBytes int64
|
||||
TxBytes int64
|
||||
Created time.Time // time registered with tailcontrol
|
||||
LastWrite time.Time // time last packet sent
|
||||
LastSeen time.Time // last seen to tailcontrol
|
||||
LastHandshake time.Time // with local wireguard
|
||||
KeepAlive bool
|
||||
ExitNode bool // true if this is the currently selected exit node.
|
||||
RxBytes int64
|
||||
TxBytes int64
|
||||
Created time.Time // time registered with tailcontrol
|
||||
LastWrite time.Time // time last packet sent
|
||||
LastSeen time.Time // last seen to tailcontrol; only present if offline
|
||||
LastHandshake time.Time // with local wireguard
|
||||
Online bool // whether node is connected to the control plane
|
||||
KeepAlive bool
|
||||
ExitNode bool // true if this is the currently selected exit node.
|
||||
ExitNodeOption bool // true if this node can be an exit node (offered && approved)
|
||||
|
||||
// Active is whether the node was recently active. The
|
||||
// definition is somewhat undefined but has historically and
|
||||
@@ -237,9 +243,6 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
|
||||
if v := st.UserID; v != 0 {
|
||||
e.UserID = v
|
||||
}
|
||||
if v := st.TailAddrDeprecated; v != "" {
|
||||
e.TailAddrDeprecated = v
|
||||
}
|
||||
if v := st.TailscaleIPs; v != nil {
|
||||
e.TailscaleIPs = v
|
||||
}
|
||||
@@ -270,6 +273,9 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
|
||||
if v := st.LastWrite; !v.IsZero() {
|
||||
e.LastWrite = v
|
||||
}
|
||||
if st.Online {
|
||||
e.Online = true
|
||||
}
|
||||
if st.InNetworkMap {
|
||||
e.InNetworkMap = true
|
||||
}
|
||||
@@ -285,6 +291,9 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
|
||||
if st.ExitNode {
|
||||
e.ExitNode = true
|
||||
}
|
||||
if st.ExitNodeOption {
|
||||
e.ExitNodeOption = true
|
||||
}
|
||||
if st.ShareeNode {
|
||||
e.ShareeNode = true
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
@@ -20,7 +19,6 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
@@ -28,7 +26,6 @@ import (
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/netknob"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
@@ -376,6 +373,25 @@ func (h *Handler) serveFileTargets(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(fts)
|
||||
}
|
||||
|
||||
// serveFilePut sends a file to another node.
|
||||
//
|
||||
// It's sometimes possible for clients to do this themselves, without
|
||||
// tailscaled, except in the case of tailscaled running in
|
||||
// userspace-networking ("netstack") mode, in which case tailscaled
|
||||
// needs to a do a netstack dial out.
|
||||
//
|
||||
// Instead, the CLI also goes through tailscaled so it doesn't need to be
|
||||
// aware of the network mode in use.
|
||||
//
|
||||
// macOS/iOS have always used this localapi method to simplify the GUI
|
||||
// clients.
|
||||
//
|
||||
// The Windows client currently (2021-11-30) uses the peerapi (/v0/put/)
|
||||
// directly, as the Windows GUI always runs in tun mode anyway.
|
||||
//
|
||||
// URL format:
|
||||
//
|
||||
// * PUT /localapi/v0/file-put/:stableID/:escaped-filename
|
||||
func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "file access denied", http.StatusForbidden)
|
||||
@@ -423,7 +439,7 @@ func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
|
||||
outReq.ContentLength = r.ContentLength
|
||||
|
||||
rp := httputil.NewSingleHostReverseProxy(dstURL)
|
||||
rp.Transport = getDialPeerTransport(h.b)
|
||||
rp.Transport = h.b.Dialer().PeerAPITransport()
|
||||
rp.ServeHTTP(w, outReq)
|
||||
}
|
||||
|
||||
@@ -457,26 +473,6 @@ func (h *Handler) serveDERPMap(w http.ResponseWriter, r *http.Request) {
|
||||
e.Encode(h.b.DERPMap())
|
||||
}
|
||||
|
||||
var dialPeerTransportOnce struct {
|
||||
sync.Once
|
||||
v *http.Transport
|
||||
}
|
||||
|
||||
func getDialPeerTransport(b *ipnlocal.LocalBackend) *http.Transport {
|
||||
dialPeerTransportOnce.Do(func() {
|
||||
t := http.DefaultTransport.(*http.Transport).Clone()
|
||||
t.Dial = nil
|
||||
dialer := net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: netknob.PlatformTCPKeepAlive(),
|
||||
Control: b.PeerDialControlFunc(),
|
||||
}
|
||||
t.DialContext = dialer.DialContext
|
||||
dialPeerTransportOnce.v = t
|
||||
})
|
||||
return dialPeerTransportOnce.v
|
||||
}
|
||||
|
||||
func defBool(a string, def bool) bool {
|
||||
if a == "" {
|
||||
return def
|
||||
|
||||
@@ -14,7 +14,8 @@ import (
|
||||
// system (a version.OS value) is an interesting enough port to report
|
||||
// to our peer nodes for discovery purposes.
|
||||
func IsInterestingService(s tailcfg.Service, os string) bool {
|
||||
if s.Proto == "peerapi4" || s.Proto == "peerapi6" {
|
||||
switch s.Proto {
|
||||
case tailcfg.PeerAPI4, tailcfg.PeerAPI6, tailcfg.PeerAPIDNS:
|
||||
return true
|
||||
}
|
||||
if s.Proto != tailcfg.TCP {
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
parameterNameRxStr = `^parameter/(.*)`
|
||||
parameterNameRxStr = `^parameter(/.*)`
|
||||
)
|
||||
|
||||
var parameterNameRx = regexp.MustCompile(parameterNameRxStr)
|
||||
|
||||
@@ -9,6 +9,7 @@ package filelogger
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
@@ -26,30 +27,30 @@ const (
|
||||
maxFiles = 50
|
||||
)
|
||||
|
||||
// New returns a logf wrapper that appends to local disk log
|
||||
// New returns a Writer that appends to local disk log
|
||||
// files on Windows, rotating old log files as needed to stay under
|
||||
// file count & byte limits.
|
||||
func New(fileBasePrefix, logID string, logf logger.Logf) logger.Logf {
|
||||
func New(fileBasePrefix, logID string, inner *log.Logger) io.Writer {
|
||||
if runtime.GOOS != "windows" {
|
||||
panic("not yet supported on any platform except Windows")
|
||||
}
|
||||
if logf == nil {
|
||||
panic("nil logf")
|
||||
if inner == nil {
|
||||
panic("nil inner logger")
|
||||
}
|
||||
dir := filepath.Join(os.Getenv("ProgramData"), "Tailscale", "Logs")
|
||||
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
log.Printf("failed to create local log directory; not writing logs to disk: %v", err)
|
||||
return logf
|
||||
inner.Printf("failed to create local log directory; not writing logs to disk: %v", err)
|
||||
return inner.Writer()
|
||||
}
|
||||
logf("local disk logdir: %v", dir)
|
||||
inner.Printf("local disk logdir: %v", dir)
|
||||
lfw := &logFileWriter{
|
||||
fileBasePrefix: fileBasePrefix,
|
||||
logID: logID,
|
||||
dir: dir,
|
||||
wrappedLogf: logf,
|
||||
wrappedLogf: inner.Printf,
|
||||
}
|
||||
return lfw.Logf
|
||||
return logger.FuncWriter(lfw.Logf)
|
||||
}
|
||||
|
||||
// logFileWriter is the state for the log writer & rotator.
|
||||
|
||||
@@ -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{
|
||||
@@ -525,7 +525,7 @@ func New(collection string) *Policy {
|
||||
}
|
||||
lw := logtail.NewLogger(c, log.Printf)
|
||||
log.SetFlags(0) // other logflags are set on console, not here
|
||||
log.SetOutput(lw)
|
||||
log.SetOutput(maybeWrapForPlatform(lw, cmdName, newc.PublicID.String()))
|
||||
|
||||
log.Printf("Program starting: v%v, Go %v: %#v",
|
||||
version.Long,
|
||||
@@ -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()
|
||||
|
||||
|
||||
16
logpolicy/logpolicy_notwindows.go
Normal file
16
logpolicy/logpolicy_notwindows.go
Normal file
@@ -0,0 +1,16 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package logpolicy
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
func maybeWrapForPlatform(lw io.Writer, cmdName, logID string) io.Writer {
|
||||
return lw
|
||||
}
|
||||
26
logpolicy/logpolicy_windows.go
Normal file
26
logpolicy/logpolicy_windows.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package logpolicy
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
|
||||
"golang.org/x/sys/windows/svc"
|
||||
"tailscale.com/log/filelogger"
|
||||
)
|
||||
|
||||
func maybeWrapForPlatform(lw io.Writer, cmdName, logID string) io.Writer {
|
||||
if cmdName != "tailscaled" {
|
||||
return lw
|
||||
}
|
||||
|
||||
isSvc, err := svc.IsWindowsService()
|
||||
if err != nil || !isSvc {
|
||||
return lw
|
||||
}
|
||||
|
||||
return filelogger.New("tailscale-service", logID, log.New(lw, "", 0))
|
||||
}
|
||||
@@ -7,6 +7,7 @@ package dns
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -50,10 +52,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 +72,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)
|
||||
@@ -121,12 +134,20 @@ func isResolvedRunning() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// is-active exits with code 3 if the service is not active.
|
||||
err = exec.Command("systemctl", "is-active", "systemd-resolved.service").Run()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
err = exec.CommandContext(ctx, "systemctl", "is-active", "systemd-resolved.service").Run()
|
||||
|
||||
// is-active exits with code 3 if the service is not active.
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func restartResolved() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
return exec.CommandContext(ctx, "systemctl", "restart", "systemd-resolved.service").Run()
|
||||
}
|
||||
|
||||
// directManager is an OSConfigurator which replaces /etc/resolv.conf with a file
|
||||
// generated from the given configuration, creating a backup of its old state.
|
||||
//
|
||||
@@ -383,7 +404,12 @@ func (m *directManager) Close() error {
|
||||
}
|
||||
|
||||
if isResolvedRunning() && !runningAsGUIDesktopUser() {
|
||||
exec.Command("systemctl", "restart", "systemd-resolved.service").Run() // Best-effort.
|
||||
m.logf("restarting systemd-resolved...")
|
||||
if err := restartResolved(); err != nil {
|
||||
m.logf("restart of systemd-resolved failed: %v", err)
|
||||
} else {
|
||||
m.logf("restarted systemd-resolved")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ func TestParseIni(t *testing.T) {
|
||||
[network] # trailing comment
|
||||
generateResolvConf = false # trailing comment`,
|
||||
want: map[string]map[string]string{
|
||||
"automount": map[string]string{"enabled": "true", "root": "/mnt/"},
|
||||
"network": map[string]string{"generateResolvConf": "false"},
|
||||
"automount": {"enabled": "true", "root": "/mnt/"},
|
||||
"network": {"generateResolvConf": "false"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -10,8 +10,10 @@ import (
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dns/resolver"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
@@ -35,22 +37,26 @@ type Manager struct {
|
||||
|
||||
resolver *resolver.Resolver
|
||||
os OSConfigurator
|
||||
|
||||
config Config
|
||||
}
|
||||
|
||||
// NewManagers created a new manager from the given config.
|
||||
func NewManager(logf logger.Logf, oscfg OSConfigurator, linkMon *monitor.Mon, linkSel resolver.ForwardLinkSelector) *Manager {
|
||||
func NewManager(logf logger.Logf, oscfg OSConfigurator, linkMon *monitor.Mon, dialer *tsdial.Dialer, linkSel resolver.ForwardLinkSelector) *Manager {
|
||||
if dialer == nil {
|
||||
panic("nil Dialer")
|
||||
}
|
||||
logf = logger.WithPrefix(logf, "dns: ")
|
||||
m := &Manager{
|
||||
logf: logf,
|
||||
resolver: resolver.New(logf, linkMon, linkSel),
|
||||
resolver: resolver.New(logf, linkMon, linkSel, dialer),
|
||||
os: oscfg,
|
||||
}
|
||||
m.logf("using %T", m.os)
|
||||
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)
|
||||
@@ -70,8 +76,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 +169,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
|
||||
@@ -225,7 +234,7 @@ func Cleanup(logf logger.Logf, interfaceName string) {
|
||||
logf("creating dns cleanup: %v", err)
|
||||
return
|
||||
}
|
||||
dns := NewManager(logf, oscfg, nil, nil)
|
||||
dns := NewManager(logf, oscfg, nil, new(tsdial.Dialer), nil)
|
||||
if err := dns.Down(); err != nil {
|
||||
logf("dns down: %v", err)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/dns/resolver"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
@@ -398,7 +399,7 @@ func TestManager(t *testing.T) {
|
||||
SplitDNS: test.split,
|
||||
BaseConfig: test.bs,
|
||||
}
|
||||
m := NewManager(t.Logf, &f, nil, nil)
|
||||
m := NewManager(t.Logf, &f, nil, new(tsdial.Dialer), nil)
|
||||
m.resolver.TestOnlySetHook(f.SetResolver)
|
||||
|
||||
if err := m.Set(test.in); err != nil {
|
||||
|
||||
@@ -13,10 +13,12 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"golang.org/x/sys/unix"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
@@ -40,6 +42,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 +108,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 +130,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 +227,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 +246,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 +258,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 +268,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 +283,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 +298,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)
|
||||
}
|
||||
|
||||
@@ -234,13 +333,20 @@ func (m *resolvedManager) GetBaseConfig() (OSConfig, error) {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
@@ -159,7 +160,8 @@ type resolverAndDelay struct {
|
||||
type forwarder struct {
|
||||
logf logger.Logf
|
||||
linkMon *monitor.Mon
|
||||
linkSel ForwardLinkSelector
|
||||
linkSel ForwardLinkSelector // TODO(bradfitz): remove this when tsdial.Dialer absords it
|
||||
dialer *tsdial.Dialer
|
||||
dohSem chan struct{}
|
||||
|
||||
ctx context.Context // good until Close
|
||||
@@ -205,11 +207,12 @@ func maxDoHInFlight(goos string) int {
|
||||
return 1000
|
||||
}
|
||||
|
||||
func newForwarder(logf logger.Logf, responses chan packet, linkMon *monitor.Mon, linkSel ForwardLinkSelector) *forwarder {
|
||||
func newForwarder(logf logger.Logf, responses chan packet, linkMon *monitor.Mon, linkSel ForwardLinkSelector, dialer *tsdial.Dialer) *forwarder {
|
||||
f := &forwarder{
|
||||
logf: logger.WithPrefix(logf, "forward: "),
|
||||
linkMon: linkMon,
|
||||
linkSel: linkSel,
|
||||
dialer: dialer,
|
||||
responses: responses,
|
||||
dohSem: make(chan struct{}, maxDoHInFlight(runtime.GOOS)),
|
||||
}
|
||||
@@ -385,6 +388,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 +402,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 +426,14 @@ 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://") {
|
||||
return nil, fmt.Errorf("http:// resolvers not supported yet")
|
||||
return f.sendDoH(ctx, rr.name.Addr, f.dialer.PeerAPIHTTPClient(), fq.packet)
|
||||
}
|
||||
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 +451,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 +467,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 +485,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 +499,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 +524,7 @@ func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDe
|
||||
}
|
||||
|
||||
clampEDNSSize(out, maxResponseBytes)
|
||||
|
||||
metricDNSFwdUDPSuccess.Add(1)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -546,10 +565,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 resolvers is non-empty, it's used explicitly (notably, for exit
|
||||
// node DNS proxy queries), otherwise f.resolvers is used.
|
||||
func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, responseChan chan<- packet, resolvers ...resolverAndDelay) error {
|
||||
metricDNSFwd.Add(1)
|
||||
domain, err := nameFromQuery(query.bs)
|
||||
if err != nil {
|
||||
metricDNSFwdErrorName.Add(1)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -558,14 +597,18 @@ 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
|
||||
}
|
||||
|
||||
clampEDNSSize(query.bs, maxResponseBytes)
|
||||
|
||||
resolvers := f.resolvers(domain)
|
||||
if len(resolvers) == 0 {
|
||||
return errNoUpstreams
|
||||
resolvers = f.resolvers(domain)
|
||||
if len(resolvers) == 0 {
|
||||
metricDNSFwdErrorNoUpstream.Add(1)
|
||||
return errNoUpstreams
|
||||
}
|
||||
}
|
||||
|
||||
fq := &forwardQuery{
|
||||
@@ -575,9 +618,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 +655,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,10 +8,14 @@ package resolver
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -19,11 +23,16 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
dns "golang.org/x/net/dns/dnsmessage"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/lineread"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
@@ -184,6 +193,7 @@ func WriteRoutes(w *bufio.Writer, routes map[dnsname.FQDN][]dnstype.Resolver) {
|
||||
type Resolver struct {
|
||||
logf logger.Logf
|
||||
linkMon *monitor.Mon // or nil
|
||||
dialer *tsdial.Dialer // non-nil
|
||||
saveConfigForTests func(cfg Config) // used in tests to capture resolver config
|
||||
// forwarder forwards requests to upstream nameservers.
|
||||
forwarder *forwarder
|
||||
@@ -215,7 +225,10 @@ type ForwardLinkSelector interface {
|
||||
|
||||
// New returns a new resolver.
|
||||
// linkMon optionally specifies a link monitor to use for socket rebinding.
|
||||
func New(logf logger.Logf, linkMon *monitor.Mon, linkSel ForwardLinkSelector) *Resolver {
|
||||
func New(logf logger.Logf, linkMon *monitor.Mon, linkSel ForwardLinkSelector, dialer *tsdial.Dialer) *Resolver {
|
||||
if dialer == nil {
|
||||
panic("nil Dialer")
|
||||
}
|
||||
r := &Resolver{
|
||||
logf: logger.WithPrefix(logf, "resolver: "),
|
||||
linkMon: linkMon,
|
||||
@@ -224,8 +237,9 @@ func New(logf logger.Logf, linkMon *monitor.Mon, linkSel ForwardLinkSelector) *R
|
||||
closed: make(chan struct{}),
|
||||
hostToIP: map[dnsname.FQDN][]netaddr.IP{},
|
||||
ipToHost: map[netaddr.IP]dnsname.FQDN{},
|
||||
dialer: dialer,
|
||||
}
|
||||
r.forwarder = newForwarder(r.logf, r.responses, linkMon, linkSel)
|
||||
r.forwarder = newForwarder(r.logf, r.responses, linkMon, linkSel, dialer)
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -272,13 +286,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 +315,259 @@ func (r *Resolver) NextResponse() (packet []byte, to netaddr.IPPort, err error)
|
||||
}
|
||||
}
|
||||
|
||||
// parseExitNodeQuery parses a DNS request packet.
|
||||
// It returns nil if it's malformed or lacking a question.
|
||||
func parseExitNodeQuery(q []byte) *response {
|
||||
p := dnsParserPool.Get().(*dnsParser)
|
||||
defer dnsParserPool.Put(p)
|
||||
p.zeroParser()
|
||||
defer p.zeroParser()
|
||||
if err := p.parseQuery(q); err != nil {
|
||||
return nil
|
||||
}
|
||||
return p.response()
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// The provided allowName callback is whether a DNS query for a name
|
||||
// (as found by parsing q) is allowed.
|
||||
//
|
||||
// In most (all?) cases, err will be nil. A bogus DNS query q will
|
||||
// still result in a response DNS packet (saying there's a failure)
|
||||
// and a nil error.
|
||||
// TODO: figure out if we even need an error result.
|
||||
func (r *Resolver) HandleExitNodeDNSQuery(ctx context.Context, q []byte, from netaddr.IPPort, allowName func(name string) bool) (res []byte, err error) {
|
||||
metricDNSExitProxyQuery.Add(1)
|
||||
ch := make(chan packet, 1)
|
||||
|
||||
resp := parseExitNodeQuery(q)
|
||||
if resp == nil {
|
||||
return nil, errors.New("bad query")
|
||||
}
|
||||
name := resp.Question.Name.String()
|
||||
if !allowName(name) {
|
||||
metricDNSExitProxyErrorName.Add(1)
|
||||
resp.Header.RCode = dns.RCodeRefused
|
||||
return marshalResponse(resp)
|
||||
}
|
||||
|
||||
switch runtime.GOOS {
|
||||
default:
|
||||
return nil, errors.New("unsupported exit node OS")
|
||||
case "windows":
|
||||
// TODO: use DnsQueryEx and write to ch.
|
||||
// See https://docs.microsoft.com/en-us/windows/win32/api/windns/nf-windns-dnsqueryex.
|
||||
// For now just use the net package:
|
||||
return handleExitNodeDNSQueryWithNetPkg(ctx, nil, resp)
|
||||
case "darwin":
|
||||
// /etc/resolv.conf is a lie and only says one upstream DNS
|
||||
// but for now that's probably good enough. Later we'll
|
||||
// want to blend in everything from scutil --dns.
|
||||
fallthrough
|
||||
case "linux", "freebsd", "openbsd", "illumos":
|
||||
nameserver, err := stubResolverForOS()
|
||||
if err != nil {
|
||||
r.logf("stubResolverForOS: %v", err)
|
||||
metricDNSExitProxyErrorResolvConf.Add(1)
|
||||
return nil, err
|
||||
}
|
||||
// TODO: more than 1 resolver from /etc/resolv.conf?
|
||||
|
||||
var resolvers []resolverAndDelay
|
||||
if nameserver == tsaddr.TailscaleServiceIP() {
|
||||
// If resolv.conf says 100.100.100.100, it's coming right back to us anyway
|
||||
// so avoid the loop through the kernel and just do what we
|
||||
// would've done anyway. By not passing any resolvers, the forwarder
|
||||
// will use its default ones from our DNS config.
|
||||
} else {
|
||||
resolvers = []resolverAndDelay{{
|
||||
name: dnstype.Resolver{Addr: net.JoinHostPort(nameserver.String(), "53")},
|
||||
}}
|
||||
}
|
||||
|
||||
err = r.forwarder.forwardWithDestChan(ctx, packet{q, from}, ch, resolvers...)
|
||||
if err != nil {
|
||||
metricDNSExitProxyErrorForward.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")
|
||||
}
|
||||
}
|
||||
|
||||
// handleExitNodeDNSQueryWithNetPkg takes a DNS query message in q and
|
||||
// return a reply (for the ExitDNS DoH service) using the net package's
|
||||
// native APIs. This is only used on Windows for now.
|
||||
//
|
||||
// If resolver is nil, the net.Resolver zero value is used.
|
||||
//
|
||||
// response contains the pre-serialized response, which notably
|
||||
// includes the original question and its header.
|
||||
func handleExitNodeDNSQueryWithNetPkg(ctx context.Context, resolver *net.Resolver, resp *response) (res []byte, err error) {
|
||||
if resp.Question.Class != dns.ClassINET {
|
||||
return nil, errors.New("unsupported class")
|
||||
}
|
||||
|
||||
r := resolver
|
||||
if r == nil {
|
||||
r = new(net.Resolver)
|
||||
}
|
||||
name := resp.Question.Name.String()
|
||||
|
||||
handleError := func(err error) (res []byte, _ error) {
|
||||
if isGoNoSuchHostError(err) {
|
||||
resp.Header.RCode = dns.RCodeNameError
|
||||
return marshalResponse(resp)
|
||||
}
|
||||
// TODO: map other errors to RCodeServerFailure?
|
||||
// Or I guess our caller should do that?
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp.Header.RCode = dns.RCodeSuccess // unless changed below
|
||||
|
||||
switch resp.Question.Type {
|
||||
case dns.TypeA, dns.TypeAAAA:
|
||||
network := "ip4"
|
||||
if resp.Question.Type == dns.TypeAAAA {
|
||||
network = "ip6"
|
||||
}
|
||||
ips, err := r.LookupIP(ctx, network, name)
|
||||
if err != nil {
|
||||
return handleError(err)
|
||||
}
|
||||
for _, stdIP := range ips {
|
||||
if ip, ok := netaddr.FromStdIP(stdIP); ok {
|
||||
resp.IPs = append(resp.IPs, ip)
|
||||
}
|
||||
}
|
||||
case dns.TypeTXT:
|
||||
strs, err := r.LookupTXT(ctx, name)
|
||||
if err != nil {
|
||||
return handleError(err)
|
||||
}
|
||||
resp.TXT = strs
|
||||
case dns.TypePTR:
|
||||
ipStr, ok := unARPA(name)
|
||||
if !ok {
|
||||
// TODO: is this RCodeFormatError?
|
||||
return nil, errors.New("bogus PTR name")
|
||||
}
|
||||
addrs, err := r.LookupAddr(ctx, ipStr)
|
||||
if err != nil {
|
||||
return handleError(err)
|
||||
}
|
||||
if len(addrs) > 0 {
|
||||
resp.Name, _ = dnsname.ToFQDN(addrs[0])
|
||||
}
|
||||
case dns.TypeCNAME:
|
||||
cname, err := r.LookupCNAME(ctx, name)
|
||||
if err != nil {
|
||||
return handleError(err)
|
||||
}
|
||||
resp.CNAME = cname
|
||||
case dns.TypeSRV:
|
||||
// Thanks, Go: "To accommodate services publishing SRV
|
||||
// records under non-standard names, if both service
|
||||
// and proto are empty strings, LookupSRV looks up
|
||||
// name directly."
|
||||
_, srvs, err := r.LookupSRV(ctx, "", "", name)
|
||||
if err != nil {
|
||||
return handleError(err)
|
||||
}
|
||||
resp.SRVs = srvs
|
||||
case dns.TypeNS:
|
||||
nss, err := r.LookupNS(ctx, name)
|
||||
if err != nil {
|
||||
return handleError(err)
|
||||
}
|
||||
resp.NSs = nss
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported record type %v", resp.Question.Type)
|
||||
}
|
||||
return marshalResponse(resp)
|
||||
}
|
||||
|
||||
func isGoNoSuchHostError(err error) bool {
|
||||
if de, ok := err.(*net.DNSError); ok {
|
||||
return de.IsNotFound
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type resolvConfCache struct {
|
||||
mod time.Time
|
||||
size int64
|
||||
ip netaddr.IP
|
||||
// TODO: inode/dev?
|
||||
}
|
||||
|
||||
// resolvConfCacheValue contains the most recent stat metadata and parsed
|
||||
// version of /etc/resolv.conf.
|
||||
var resolvConfCacheValue atomic.Value // of resolvConfCache
|
||||
|
||||
var errEmptyResolvConf = errors.New("resolv.conf has no nameservers")
|
||||
|
||||
// stubResolverForOS returns the IP address of the first nameserver in
|
||||
// /etc/resolv.conf.
|
||||
func stubResolverForOS() (ip netaddr.IP, err error) {
|
||||
fi, err := os.Stat("/etc/resolv.conf")
|
||||
if err != nil {
|
||||
return netaddr.IP{}, err
|
||||
}
|
||||
cur := resolvConfCache{
|
||||
mod: fi.ModTime(),
|
||||
size: fi.Size(),
|
||||
}
|
||||
if c, ok := resolvConfCacheValue.Load().(resolvConfCache); ok && c.mod == cur.mod && c.size == cur.size {
|
||||
return c.ip, nil
|
||||
}
|
||||
err = lineread.File("/etc/resolv.conf", func(line []byte) error {
|
||||
if !ip.IsZero() {
|
||||
return nil
|
||||
}
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 || line[0] == '#' {
|
||||
return nil
|
||||
}
|
||||
if mem.HasPrefix(mem.B(line), mem.S("nameserver ")) {
|
||||
s := strings.TrimSpace(strings.TrimPrefix(string(line), "nameserver "))
|
||||
ip, err = netaddr.ParseIP(s)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return netaddr.IP{}, err
|
||||
}
|
||||
if !ip.IsValid() {
|
||||
return netaddr.IP{}, errEmptyResolvConf
|
||||
}
|
||||
cur.ip = ip
|
||||
resolvConfCacheValue.Store(cur)
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
// 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 +581,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 +599,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 +639,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
|
||||
}
|
||||
@@ -434,10 +705,27 @@ func (r *Resolver) handleQuery(pkt packet) {
|
||||
type response struct {
|
||||
Header dns.Header
|
||||
Question dns.Question
|
||||
|
||||
// Name is the response to a PTR query.
|
||||
Name dnsname.FQDN
|
||||
// IP is the response to an A, AAAA, or ALL query.
|
||||
IP netaddr.IP
|
||||
|
||||
// IP and IPs are the responses to an A, AAAA, or ALL query.
|
||||
// Either/both/neither can be populated.
|
||||
IP netaddr.IP
|
||||
IPs []netaddr.IP
|
||||
|
||||
// TXT is the response to a TXT query.
|
||||
// Each one is its own RR with one string.
|
||||
TXT []string
|
||||
|
||||
// CNAME is the response to a CNAME query.
|
||||
CNAME string
|
||||
|
||||
// SRVs are the responses to a SRV query.
|
||||
SRVs []*net.SRV
|
||||
|
||||
// NSs are the responses to an NS query.
|
||||
NSs []*net.NS
|
||||
}
|
||||
|
||||
var dnsParserPool = &sync.Pool{
|
||||
@@ -468,6 +756,7 @@ func (p *dnsParser) zeroParser() { p.parser = dns.Parser{} }
|
||||
// p.Question.
|
||||
func (p *dnsParser) parseQuery(query []byte) error {
|
||||
defer p.zeroParser()
|
||||
p.zeroParser()
|
||||
var err error
|
||||
p.Header, err = p.parser.Start(query)
|
||||
if err != nil {
|
||||
@@ -512,6 +801,16 @@ func marshalAAAARecord(name dns.Name, ip netaddr.IP, builder *dns.Builder) error
|
||||
return builder.AAAAResource(answerHeader, answer)
|
||||
}
|
||||
|
||||
func marshalIP(name dns.Name, ip netaddr.IP, builder *dns.Builder) error {
|
||||
if ip.Is4() {
|
||||
return marshalARecord(name, ip, builder)
|
||||
}
|
||||
if ip.Is6() {
|
||||
return marshalAAAARecord(name, ip, builder)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// marshalPTRRecord serializes a PTR record into an active builder.
|
||||
// The caller may continue using the builder following the call.
|
||||
func marshalPTRRecord(queryName dns.Name, name dnsname.FQDN, builder *dns.Builder) error {
|
||||
@@ -531,6 +830,83 @@ func marshalPTRRecord(queryName dns.Name, name dnsname.FQDN, builder *dns.Builde
|
||||
return builder.PTRResource(answerHeader, answer)
|
||||
}
|
||||
|
||||
func marshalTXT(queryName dns.Name, txts []string, builder *dns.Builder) error {
|
||||
for _, txt := range txts {
|
||||
if err := builder.TXTResource(dns.ResourceHeader{
|
||||
Name: queryName,
|
||||
Type: dns.TypeTXT,
|
||||
Class: dns.ClassINET,
|
||||
TTL: uint32(defaultTTL / time.Second),
|
||||
}, dns.TXTResource{
|
||||
TXT: []string{txt},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func marshalCNAME(queryName dns.Name, cname string, builder *dns.Builder) error {
|
||||
if cname == "" {
|
||||
return nil
|
||||
}
|
||||
name, err := dns.NewName(cname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return builder.CNAMEResource(dns.ResourceHeader{
|
||||
Name: queryName,
|
||||
Type: dns.TypeCNAME,
|
||||
Class: dns.ClassINET,
|
||||
TTL: uint32(defaultTTL / time.Second),
|
||||
}, dns.CNAMEResource{
|
||||
CNAME: name,
|
||||
})
|
||||
}
|
||||
|
||||
func marshalNS(queryName dns.Name, nss []*net.NS, builder *dns.Builder) error {
|
||||
for _, ns := range nss {
|
||||
name, err := dns.NewName(ns.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = builder.NSResource(dns.ResourceHeader{
|
||||
Name: queryName,
|
||||
Type: dns.TypeNS,
|
||||
Class: dns.ClassINET,
|
||||
TTL: uint32(defaultTTL / time.Second),
|
||||
}, dns.NSResource{NS: name})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func marshalSRV(queryName dns.Name, srvs []*net.SRV, builder *dns.Builder) error {
|
||||
for _, s := range srvs {
|
||||
srvName, err := dns.NewName(s.Target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = builder.SRVResource(dns.ResourceHeader{
|
||||
Name: queryName,
|
||||
Type: dns.TypeSRV,
|
||||
Class: dns.ClassINET,
|
||||
TTL: uint32(defaultTTL / time.Second),
|
||||
}, dns.SRVResource{
|
||||
Target: srvName,
|
||||
Priority: s.Priority,
|
||||
Port: s.Port,
|
||||
Weight: s.Weight,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// marshalResponse serializes the DNS response into a new buffer.
|
||||
func marshalResponse(resp *response) ([]byte, error) {
|
||||
resp.Header.Response = true
|
||||
@@ -541,6 +917,14 @@ func marshalResponse(resp *response) ([]byte, error) {
|
||||
|
||||
builder := dns.NewBuilder(nil, resp.Header)
|
||||
|
||||
// TODO(bradfitz): I'm not sure why this wasn't enabled
|
||||
// before, but for now (2021-12-09) enable it at least when
|
||||
// there's more than 1 record (which was never the case
|
||||
// before), where it really helps.
|
||||
if len(resp.IPs) > 1 {
|
||||
builder.EnableCompression()
|
||||
}
|
||||
|
||||
isSuccess := resp.Header.RCode == dns.RCodeSuccess
|
||||
|
||||
if resp.Question.Type != 0 || isSuccess {
|
||||
@@ -567,13 +951,24 @@ func marshalResponse(resp *response) ([]byte, error) {
|
||||
|
||||
switch resp.Question.Type {
|
||||
case dns.TypeA, dns.TypeAAAA, dns.TypeALL:
|
||||
if resp.IP.Is4() {
|
||||
err = marshalARecord(resp.Question.Name, resp.IP, &builder)
|
||||
} else if resp.IP.Is6() {
|
||||
err = marshalAAAARecord(resp.Question.Name, resp.IP, &builder)
|
||||
if err := marshalIP(resp.Question.Name, resp.IP, &builder); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, ip := range resp.IPs {
|
||||
if err := marshalIP(resp.Question.Name, ip, &builder); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
case dns.TypePTR:
|
||||
err = marshalPTRRecord(resp.Question.Name, resp.Name, &builder)
|
||||
case dns.TypeTXT:
|
||||
err = marshalTXT(resp.Question.Name, resp.TXT, &builder)
|
||||
case dns.TypeCNAME:
|
||||
err = marshalCNAME(resp.Question.Name, resp.CNAME, &builder)
|
||||
case dns.TypeSRV:
|
||||
err = marshalSRV(resp.Question.Name, resp.SRVs, &builder)
|
||||
case dns.TypeNS:
|
||||
err = marshalNS(resp.Question.Name, resp.NSs, &builder)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -700,6 +1095,7 @@ func (r *Resolver) respondReverse(query []byte, name dnsname.FQDN, resp *respons
|
||||
return nil, errNotOurName
|
||||
}
|
||||
|
||||
metricDNSMagicDNSSuccessReverse.Add(1)
|
||||
return marshalResponse(resp)
|
||||
}
|
||||
|
||||
@@ -716,8 +1112,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 +1125,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 +1149,90 @@ func (r *Resolver) respond(query []byte) ([]byte, error) {
|
||||
resp.IP = ip
|
||||
return marshalResponse(resp)
|
||||
}
|
||||
|
||||
// unARPA maps from "4.4.8.8.in-addr.arpa." to "8.8.4.4", etc.
|
||||
func unARPA(a string) (ipStr string, ok bool) {
|
||||
const suf4 = ".in-addr.arpa."
|
||||
if strings.HasSuffix(a, suf4) {
|
||||
s := strings.TrimSuffix(a, suf4)
|
||||
// Parse and reverse octets.
|
||||
ip, err := netaddr.ParseIP(s)
|
||||
if err != nil || !ip.Is4() {
|
||||
return "", false
|
||||
}
|
||||
a4 := ip.As4()
|
||||
return netaddr.IPv4(a4[3], a4[2], a4[1], a4[0]).String(), true
|
||||
}
|
||||
const suf6 = ".ip6.arpa."
|
||||
if len(a) == len("e.0.0.2.0.0.0.0.0.0.0.0.0.0.0.0.b.0.8.0.a.0.0.4.0.b.8.f.7.0.6.2.ip6.arpa.") &&
|
||||
strings.HasSuffix(a, suf6) {
|
||||
var hx [32]byte
|
||||
var a16 [16]byte
|
||||
for i := range hx {
|
||||
hx[31-i] = a[i*2]
|
||||
if a[i*2+1] != '.' {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
hex.Decode(a16[:], hx[:])
|
||||
return netaddr.IPFrom16(a16).String(), true
|
||||
}
|
||||
return "", false
|
||||
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
metricDNSExitProxyQuery = clientmetric.NewCounter("dns_exit_node_query")
|
||||
metricDNSExitProxyErrorName = clientmetric.NewCounter("dns_exit_node_error_name")
|
||||
metricDNSExitProxyErrorForward = clientmetric.NewCounter("dns_exit_node_error_forward")
|
||||
metricDNSExitProxyErrorResolvConf = clientmetric.NewCounter("dns_exit_node_error_resolvconf")
|
||||
|
||||
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")
|
||||
)
|
||||
|
||||
@@ -6,6 +6,7 @@ package resolver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -179,6 +180,129 @@ var resolveToNXDOMAIN = dns.HandlerFunc(func(w dns.ResponseWriter, req *dns.Msg)
|
||||
w.WriteMsg(m)
|
||||
})
|
||||
|
||||
// weirdoGoCNAMEHandler returns a DNS handler that satisfies
|
||||
// Go's weird Resolver.LookupCNAME (read its godoc carefully!).
|
||||
//
|
||||
// This doesn't even return a CNAME record, because that's not
|
||||
// what Go looks for.
|
||||
func weirdoGoCNAMEHandler(target string) dns.HandlerFunc {
|
||||
return func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(req)
|
||||
question := req.Question[0]
|
||||
|
||||
switch question.Qtype {
|
||||
case dns.TypeA:
|
||||
m.Answer = append(m.Answer, &dns.CNAME{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: target,
|
||||
Rrtype: dns.TypeCNAME,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 600,
|
||||
},
|
||||
Target: target,
|
||||
})
|
||||
case dns.TypeAAAA:
|
||||
m.Answer = append(m.Answer, &dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: target,
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 600,
|
||||
},
|
||||
AAAA: net.ParseIP("1::2"),
|
||||
})
|
||||
}
|
||||
w.WriteMsg(m)
|
||||
}
|
||||
}
|
||||
|
||||
// dnsHandler returns a handler that replies with the answers/options
|
||||
// provided.
|
||||
//
|
||||
// Types supported: netaddr.IP.
|
||||
func dnsHandler(answers ...interface{}) dns.HandlerFunc {
|
||||
return func(w dns.ResponseWriter, req *dns.Msg) {
|
||||
m := new(dns.Msg)
|
||||
m.SetReply(req)
|
||||
if len(req.Question) != 1 {
|
||||
panic("not a single-question request")
|
||||
}
|
||||
m.RecursionAvailable = true // to stop net package's errLameReferral on empty replies
|
||||
|
||||
question := req.Question[0]
|
||||
for _, a := range answers {
|
||||
switch a := a.(type) {
|
||||
default:
|
||||
panic(fmt.Sprintf("unsupported dnsHandler arg %T", a))
|
||||
case netaddr.IP:
|
||||
ip := a
|
||||
if ip.Is4() {
|
||||
m.Answer = append(m.Answer, &dns.A{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
A: ip.IPAddr().IP,
|
||||
})
|
||||
} else if ip.Is6() {
|
||||
m.Answer = append(m.Answer, &dns.AAAA{
|
||||
Hdr: dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeAAAA,
|
||||
Class: dns.ClassINET,
|
||||
},
|
||||
AAAA: ip.IPAddr().IP,
|
||||
})
|
||||
}
|
||||
case dns.PTR:
|
||||
ptr := a
|
||||
ptr.Hdr = dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypePTR,
|
||||
Class: dns.ClassINET,
|
||||
}
|
||||
m.Answer = append(m.Answer, &ptr)
|
||||
case dns.CNAME:
|
||||
c := a
|
||||
c.Hdr = dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeCNAME,
|
||||
Class: dns.ClassINET,
|
||||
Ttl: 600,
|
||||
}
|
||||
m.Answer = append(m.Answer, &c)
|
||||
case dns.TXT:
|
||||
txt := a
|
||||
txt.Hdr = dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeTXT,
|
||||
Class: dns.ClassINET,
|
||||
}
|
||||
m.Answer = append(m.Answer, &txt)
|
||||
case dns.SRV:
|
||||
srv := a
|
||||
srv.Hdr = dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeSRV,
|
||||
Class: dns.ClassINET,
|
||||
}
|
||||
m.Answer = append(m.Answer, &srv)
|
||||
case dns.NS:
|
||||
rr := a
|
||||
rr.Hdr = dns.RR_Header{
|
||||
Name: question.Name,
|
||||
Rrtype: dns.TypeNS,
|
||||
Class: dns.ClassINET,
|
||||
}
|
||||
m.Answer = append(m.Answer, &rr)
|
||||
}
|
||||
}
|
||||
w.WriteMsg(m)
|
||||
}
|
||||
}
|
||||
|
||||
func serveDNS(tb testing.TB, addr string, records ...interface{}) *dns.Server {
|
||||
if len(records)%2 != 0 {
|
||||
panic("must have an even number of record values")
|
||||
|
||||
@@ -6,18 +6,25 @@ package resolver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
miekdns "github.com/miekg/dns"
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
dns "golang.org/x/net/dns/dnsmessage"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/util/dnsname"
|
||||
@@ -34,14 +41,16 @@ var (
|
||||
|
||||
var dnsCfg = Config{
|
||||
Hosts: map[dnsname.FQDN][]netaddr.IP{
|
||||
"test1.ipn.dev.": []netaddr.IP{testipv4},
|
||||
"test2.ipn.dev.": []netaddr.IP{testipv6},
|
||||
"test1.ipn.dev.": {testipv4},
|
||||
"test2.ipn.dev.": {testipv6},
|
||||
},
|
||||
LocalDomains: []dnsname.FQDN{"ipn.dev.", "3.2.1.in-addr.arpa.", "1.0.0.0.ip6.arpa."},
|
||||
}
|
||||
|
||||
const noEdns = 0
|
||||
|
||||
const dnsHeaderLen = 12
|
||||
|
||||
func dnspacket(domain dnsname.FQDN, tp dns.Type, ednsSize uint16) []byte {
|
||||
var dnsHeader dns.Header
|
||||
question := dns.Question{
|
||||
@@ -308,7 +317,7 @@ func TestRDNSNameToIPv6(t *testing.T) {
|
||||
}
|
||||
|
||||
func newResolver(t testing.TB) *Resolver {
|
||||
return New(t.Logf, nil /* no link monitor */, nil /* no link selector */)
|
||||
return New(t.Logf, nil /* no link monitor */, nil /* no link selector */, new(tsdial.Dialer))
|
||||
}
|
||||
|
||||
func TestResolveLocal(t *testing.T) {
|
||||
@@ -1062,7 +1071,7 @@ func TestForwardLinkSelection(t *testing.T) {
|
||||
return "special"
|
||||
}
|
||||
return ""
|
||||
}))
|
||||
}), new(tsdial.Dialer))
|
||||
|
||||
// Test non-special IP.
|
||||
if got, err := fwd.packetListener(netaddr.IP{}); err != nil {
|
||||
@@ -1092,3 +1101,383 @@ func TestForwardLinkSelection(t *testing.T) {
|
||||
type linkSelFunc func(ip netaddr.IP) string
|
||||
|
||||
func (f linkSelFunc) PickLink(ip netaddr.IP) string { return f(ip) }
|
||||
|
||||
func TestHandleExitNodeDNSQueryWithNetPkg(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("skipping test on Windows; waiting for golang.org/issue/33097")
|
||||
}
|
||||
|
||||
records := []interface{}{
|
||||
"no-records.test.",
|
||||
dnsHandler(),
|
||||
|
||||
"one-a.test.",
|
||||
dnsHandler(netaddr.MustParseIP("1.2.3.4")),
|
||||
|
||||
"two-a.test.",
|
||||
dnsHandler(netaddr.MustParseIP("1.2.3.4"), netaddr.MustParseIP("5.6.7.8")),
|
||||
|
||||
"one-aaaa.test.",
|
||||
dnsHandler(netaddr.MustParseIP("1::2")),
|
||||
|
||||
"two-aaaa.test.",
|
||||
dnsHandler(netaddr.MustParseIP("1::2"), netaddr.MustParseIP("3::4")),
|
||||
|
||||
"nx-domain.test.",
|
||||
resolveToNXDOMAIN,
|
||||
|
||||
"4.3.2.1.in-addr.arpa.",
|
||||
dnsHandler(miekdns.PTR{Ptr: "foo.com."}),
|
||||
|
||||
"cname.test.",
|
||||
weirdoGoCNAMEHandler("the-target.foo."),
|
||||
|
||||
"txt.test.",
|
||||
dnsHandler(
|
||||
miekdns.TXT{Txt: []string{"txt1=one"}},
|
||||
miekdns.TXT{Txt: []string{"txt2=two"}},
|
||||
miekdns.TXT{Txt: []string{"txt3=three"}},
|
||||
),
|
||||
|
||||
"srv.test.",
|
||||
dnsHandler(
|
||||
miekdns.SRV{
|
||||
Priority: 1,
|
||||
Weight: 2,
|
||||
Port: 3,
|
||||
Target: "foo.com.",
|
||||
},
|
||||
miekdns.SRV{
|
||||
Priority: 4,
|
||||
Weight: 5,
|
||||
Port: 6,
|
||||
Target: "bar.com.",
|
||||
},
|
||||
),
|
||||
|
||||
"ns.test.",
|
||||
dnsHandler(miekdns.NS{Ns: "ns1.foo."}, miekdns.NS{Ns: "ns2.bar."}),
|
||||
}
|
||||
v4server := serveDNS(t, "127.0.0.1:0", records...)
|
||||
defer v4server.Shutdown()
|
||||
|
||||
// backendResolver is the resolver between
|
||||
// handleExitNodeDNSQueryWithNetPkg and its upstream resolver,
|
||||
// which in this test's case is the miekg/dns test DNS server
|
||||
// (v4server).
|
||||
backResolver := &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
var d net.Dialer
|
||||
return d.DialContext(ctx, "udp", v4server.PacketConn.LocalAddr().String())
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("no_such_host", func(t *testing.T) {
|
||||
res, err := handleExitNodeDNSQueryWithNetPkg(context.Background(), backResolver, &response{
|
||||
Header: dnsmessage.Header{
|
||||
ID: 123,
|
||||
Response: true,
|
||||
OpCode: 0, // query
|
||||
},
|
||||
Question: dnsmessage.Question{
|
||||
Name: dnsmessage.MustNewName("nx-domain.test."),
|
||||
Type: dnsmessage.TypeA,
|
||||
Class: dnsmessage.ClassINET,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(res) < dnsHeaderLen {
|
||||
t.Fatal("short reply")
|
||||
}
|
||||
rcode := dns.RCode(res[3] & 0x0f)
|
||||
if rcode != dns.RCodeNameError {
|
||||
t.Errorf("RCode = %v; want dns.RCodeNameError", rcode)
|
||||
t.Logf("Response was: %q", res)
|
||||
}
|
||||
})
|
||||
|
||||
matchPacked := func(want string) func(t testing.TB, got []byte) {
|
||||
return func(t testing.TB, got []byte) {
|
||||
if string(got) == want {
|
||||
return
|
||||
}
|
||||
t.Errorf("unexpected reply.\n got: %q\nwant: %q\n", got, want)
|
||||
t.Errorf("\nin hex:\n got: % 2x\nwant: % 2x\n", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
Type dnsmessage.Type
|
||||
Name string
|
||||
Check func(t testing.TB, got []byte)
|
||||
}{
|
||||
{
|
||||
Type: dnsmessage.TypeA,
|
||||
Name: "one-a.test.",
|
||||
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x01\x00\x00\x00\x00\x05one-a\x04test\x00\x00\x01\x00\x01\x05one-a\x04test\x00\x00\x01\x00\x01\x00\x00\x02X\x00\x04\x01\x02\x03\x04"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypeA,
|
||||
Name: "two-a.test.",
|
||||
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x02\x00\x00\x00\x00\x05two-a\x04test\x00\x00\x01\x00\x01\xc0\f\x00\x01\x00\x01\x00\x00\x02X\x00\x04\x01\x02\x03\x04\xc0\f\x00\x01\x00\x01\x00\x00\x02X\x00\x04\x05\x06\a\b"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypeAAAA,
|
||||
Name: "one-aaaa.test.",
|
||||
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x01\x00\x00\x00\x00\bone-aaaa\x04test\x00\x00\x1c\x00\x01\bone-aaaa\x04test\x00\x00\x1c\x00\x01\x00\x00\x02X\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypeAAAA,
|
||||
Name: "two-aaaa.test.",
|
||||
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x02\x00\x00\x00\x00\btwo-aaaa\x04test\x00\x00\x1c\x00\x01\xc0\f\x00\x1c\x00\x01\x00\x00\x02X\x00\x10\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\xc0\f\x00\x1c\x00\x01\x00\x00\x02X\x00\x10\x00\x03\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypePTR,
|
||||
Name: "4.3.2.1.in-addr.arpa.",
|
||||
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x01\x00\x00\x00\x00\x014\x013\x012\x011\ain-addr\x04arpa\x00\x00\f\x00\x01\x014\x013\x012\x011\ain-addr\x04arpa\x00\x00\f\x00\x01\x00\x00\x02X\x00\t\x03foo\x03com\x00"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypeCNAME,
|
||||
Name: "cname.test.",
|
||||
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x01\x00\x00\x00\x00\x05cname\x04test\x00\x00\x05\x00\x01\x05cname\x04test\x00\x00\x05\x00\x01\x00\x00\x02X\x00\x10\nthe-target\x03foo\x00"),
|
||||
},
|
||||
|
||||
// No records of various types
|
||||
{
|
||||
Type: dnsmessage.TypeA,
|
||||
Name: "no-records.test.",
|
||||
Check: matchPacked("\x00{\x84\x03\x00\x01\x00\x00\x00\x00\x00\x00\nno-records\x04test\x00\x00\x01\x00\x01"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypeAAAA,
|
||||
Name: "no-records.test.",
|
||||
Check: matchPacked("\x00{\x84\x03\x00\x01\x00\x00\x00\x00\x00\x00\nno-records\x04test\x00\x00\x1c\x00\x01"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypeCNAME,
|
||||
Name: "no-records.test.",
|
||||
Check: matchPacked("\x00{\x84\x03\x00\x01\x00\x00\x00\x00\x00\x00\nno-records\x04test\x00\x00\x05\x00\x01"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypeSRV,
|
||||
Name: "no-records.test.",
|
||||
Check: matchPacked("\x00{\x84\x03\x00\x01\x00\x00\x00\x00\x00\x00\nno-records\x04test\x00\x00!\x00\x01"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypeTXT,
|
||||
Name: "txt.test.",
|
||||
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x03\x00\x00\x00\x00\x03txt\x04test\x00\x00\x10\x00\x01\x03txt\x04test\x00\x00\x10\x00\x01\x00\x00\x02X\x00\t\btxt1=one\x03txt\x04test\x00\x00\x10\x00\x01\x00\x00\x02X\x00\t\btxt2=two\x03txt\x04test\x00\x00\x10\x00\x01\x00\x00\x02X\x00\v\ntxt3=three"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypeSRV,
|
||||
Name: "srv.test.",
|
||||
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x02\x00\x00\x00\x00\x03srv\x04test\x00\x00!\x00\x01\x03srv\x04test\x00\x00!\x00\x01\x00\x00\x02X\x00\x0f\x00\x01\x00\x02\x00\x03\x03foo\x03com\x00\x03srv\x04test\x00\x00!\x00\x01\x00\x00\x02X\x00\x0f\x00\x04\x00\x05\x00\x06\x03bar\x03com\x00"),
|
||||
},
|
||||
{
|
||||
Type: dnsmessage.TypeNS,
|
||||
Name: "ns.test.",
|
||||
Check: matchPacked("\x00{\x84\x00\x00\x01\x00\x02\x00\x00\x00\x00\x02ns\x04test\x00\x00\x02\x00\x01\x02ns\x04test\x00\x00\x02\x00\x01\x00\x00\x02X\x00\t\x03ns1\x03foo\x00\x02ns\x04test\x00\x00\x02\x00\x01\x00\x00\x02X\x00\t\x03ns2\x03bar\x00"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(fmt.Sprintf("%v_%v", tt.Type, strings.Trim(tt.Name, ".")), func(t *testing.T) {
|
||||
got, err := handleExitNodeDNSQueryWithNetPkg(context.Background(), backResolver, &response{
|
||||
Header: dnsmessage.Header{
|
||||
ID: 123,
|
||||
Response: true,
|
||||
OpCode: 0, // query
|
||||
},
|
||||
Question: dnsmessage.Question{
|
||||
Name: dnsmessage.MustNewName(tt.Name),
|
||||
Type: tt.Type,
|
||||
Class: dnsmessage.ClassINET,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) < dnsHeaderLen {
|
||||
t.Errorf("short record")
|
||||
}
|
||||
if tt.Check != nil {
|
||||
tt.Check(t, got)
|
||||
if t.Failed() {
|
||||
t.Errorf("Got: %q\nIn hex: % 02x", got, got)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
wrapRes := newWrapResolver(backResolver)
|
||||
ctx := context.Background()
|
||||
|
||||
t.Run("wrap_ip_a", func(t *testing.T) {
|
||||
ips, err := wrapRes.LookupIP(ctx, "ip", "two-a.test.")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := ips, []net.IP{
|
||||
net.ParseIP("1.2.3.4").To4(),
|
||||
net.ParseIP("5.6.7.8").To4(),
|
||||
}; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("LookupIP = %v; want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrap_ip_aaaa", func(t *testing.T) {
|
||||
ips, err := wrapRes.LookupIP(ctx, "ip", "two-aaaa.test.")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := ips, []net.IP{
|
||||
net.ParseIP("1::2"),
|
||||
net.ParseIP("3::4"),
|
||||
}; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("LookupIP(v6) = %v; want %v", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrap_ip_nx", func(t *testing.T) {
|
||||
ips, err := wrapRes.LookupIP(ctx, "ip", "nx-domain.test.")
|
||||
if !isGoNoSuchHostError(err) {
|
||||
t.Errorf("no NX domain = (%v, %v); want no host error", ips, err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrap_srv", func(t *testing.T) {
|
||||
_, srvs, err := wrapRes.LookupSRV(ctx, "", "", "srv.test.")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := srvs, []*net.SRV{
|
||||
{
|
||||
Target: "foo.com.",
|
||||
Priority: 1,
|
||||
Weight: 2,
|
||||
Port: 3,
|
||||
},
|
||||
{
|
||||
Target: "bar.com.",
|
||||
Priority: 4,
|
||||
Weight: 5,
|
||||
Port: 6,
|
||||
},
|
||||
}; !reflect.DeepEqual(got, want) {
|
||||
jgot, _ := json.Marshal(got)
|
||||
jwant, _ := json.Marshal(want)
|
||||
t.Errorf("SRV = %s; want %s", jgot, jwant)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrap_txt", func(t *testing.T) {
|
||||
txts, err := wrapRes.LookupTXT(ctx, "txt.test.")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := txts, []string{"txt1=one", "txt2=two", "txt3=three"}; !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("TXT = %q; want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrap_ns", func(t *testing.T) {
|
||||
nss, err := wrapRes.LookupNS(ctx, "ns.test.")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := nss, []*net.NS{
|
||||
{Host: "ns1.foo."},
|
||||
{Host: "ns2.bar."},
|
||||
}; !reflect.DeepEqual(got, want) {
|
||||
jgot, _ := json.Marshal(got)
|
||||
jwant, _ := json.Marshal(want)
|
||||
t.Errorf("NS = %s; want %s", jgot, jwant)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// newWrapResolver returns a resolver that uses r (via handleExitNodeDNSQueryWithNetPkg)
|
||||
// to make DNS requests.
|
||||
func newWrapResolver(r *net.Resolver) *net.Resolver {
|
||||
if runtime.GOOS == "windows" {
|
||||
panic("doesn't work on Windows") // golang.org/issue/33097
|
||||
}
|
||||
return &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return &wrapResolverConn{ctx: ctx, r: r}, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type wrapResolverConn struct {
|
||||
ctx context.Context
|
||||
r *net.Resolver
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
var _ net.PacketConn = (*wrapResolverConn)(nil)
|
||||
|
||||
func (*wrapResolverConn) Close() error { return nil }
|
||||
func (*wrapResolverConn) LocalAddr() net.Addr { return fakeAddr{} }
|
||||
func (*wrapResolverConn) RemoteAddr() net.Addr { return fakeAddr{} }
|
||||
func (*wrapResolverConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (*wrapResolverConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (*wrapResolverConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
|
||||
func (a *wrapResolverConn) Read(p []byte) (n int, err error) {
|
||||
n, _, err = a.ReadFrom(p)
|
||||
return
|
||||
}
|
||||
|
||||
func (a *wrapResolverConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
n, err = a.buf.Read(p)
|
||||
return n, fakeAddr{}, err
|
||||
}
|
||||
|
||||
func (a *wrapResolverConn) Write(packet []byte) (n int, err error) {
|
||||
return a.WriteTo(packet, fakeAddr{})
|
||||
}
|
||||
|
||||
func (a *wrapResolverConn) WriteTo(q []byte, _ net.Addr) (n int, err error) {
|
||||
resp := parseExitNodeQuery(q)
|
||||
if resp == nil {
|
||||
return 0, errors.New("bad query")
|
||||
}
|
||||
res, err := handleExitNodeDNSQueryWithNetPkg(context.Background(), a.r, resp)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
a.buf.Write(res)
|
||||
return len(q), nil
|
||||
}
|
||||
|
||||
type fakeAddr struct{}
|
||||
|
||||
func (fakeAddr) Network() string { return "unused" }
|
||||
func (fakeAddr) String() string { return "unused-todoAddr" }
|
||||
|
||||
func TestUnARPA(t *testing.T) {
|
||||
tests := []struct {
|
||||
in, want string
|
||||
}{
|
||||
{"", ""},
|
||||
{"bad", ""},
|
||||
{"4.4.8.8.in-addr.arpa.", "8.8.4.4"},
|
||||
{".in-addr.arpa.", ""},
|
||||
{"e.0.0.2.0.0.0.0.0.0.0.0.0.0.0.0.b.0.8.0.a.0.0.4.0.b.8.f.7.0.6.2.ip6.arpa.", "2607:f8b0:400a:80b::200e"},
|
||||
{".ip6.arpa.", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got, ok := unARPA(tt.in)
|
||||
if ok != (got != "") {
|
||||
t.Errorf("inconsistent results for %q: (%q, %v)", tt.in, got, ok)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("unARPA(%q) = %q; want %q", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
56
net/dns/utf.go
Normal file
56
net/dns/utf.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package dns
|
||||
|
||||
// This code is only used in Windows builds, but is in an
|
||||
// OS-independent file so tests can run all the time.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"unicode/utf16"
|
||||
)
|
||||
|
||||
// maybeUnUTF16 tries to detect whether bs contains UTF-16, and if so
|
||||
// translates it to regular UTF-8.
|
||||
//
|
||||
// Some of wsl.exe's output get printed as UTF-16, which breaks a
|
||||
// bunch of things. Try to detect this by looking for a zero byte in
|
||||
// the first few bytes of output (which will appear if any of those
|
||||
// codepoints are basic ASCII - very likely). From that we can infer
|
||||
// that UTF-16 is being printed, and the byte order in use, and we
|
||||
// decode that back to UTF-8.
|
||||
//
|
||||
// https://github.com/microsoft/WSL/issues/4607
|
||||
func maybeUnUTF16(bs []byte) []byte {
|
||||
if len(bs)%2 != 0 {
|
||||
// Can't be complete UTF-16.
|
||||
return bs
|
||||
}
|
||||
checkLen := 20
|
||||
if len(bs) < checkLen {
|
||||
checkLen = len(bs)
|
||||
}
|
||||
zeroOff := bytes.IndexByte(bs[:checkLen], 0)
|
||||
if zeroOff == -1 {
|
||||
return bs
|
||||
}
|
||||
|
||||
// We assume wsl.exe is trying to print an ASCII codepoint,
|
||||
// meaning the zero byte is in the upper 8 bits of the
|
||||
// codepoint. That means we can use the zero's byte offset to
|
||||
// work out if we're seeing little-endian or big-endian
|
||||
// UTF-16.
|
||||
var endian binary.ByteOrder = binary.LittleEndian
|
||||
if zeroOff%2 == 0 {
|
||||
endian = binary.BigEndian
|
||||
}
|
||||
|
||||
var u16 []uint16
|
||||
for i := 0; i < len(bs); i += 2 {
|
||||
u16 = append(u16, endian.Uint16(bs[i:]))
|
||||
}
|
||||
return []byte(string(utf16.Decode(u16)))
|
||||
}
|
||||
25
net/dns/utf_test.go
Normal file
25
net/dns/utf_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package dns
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestMaybeUnUTF16(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"abc", "abc"}, // UTF-8
|
||||
{"a\x00b\x00c\x00", "abc"}, // UTF-16-LE
|
||||
{"\x00a\x00b\x00c", "abc"}, // UTF-16-BE
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
got := string(maybeUnUTF16([]byte(test.in)))
|
||||
if got != test.want {
|
||||
t.Errorf("maybeUnUTF16(%q) = %q, want %q", test.in, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,13 @@ package dns
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unicode/utf16"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -26,29 +26,7 @@ func wslDistros() ([]string, error) {
|
||||
return nil, fmt.Errorf("%v: %q", err, string(b))
|
||||
}
|
||||
|
||||
// The first line of output is a WSL header. E.g.
|
||||
//
|
||||
// C:\tsdev>wsl.exe -l
|
||||
// Windows Subsystem for Linux Distributions:
|
||||
// Ubuntu-20.04 (Default)
|
||||
//
|
||||
// We can skip it by passing '-q', but here we put it to work.
|
||||
// It turns out wsl.exe -l is broken, and outputs UTF-16 names
|
||||
// that nothing can read. (Try `wsl.exe -l | more`.)
|
||||
// So we look at the header to see if it's UTF-16.
|
||||
// If so, we run the rest through a UTF-16 parser.
|
||||
//
|
||||
// https://github.com/microsoft/WSL/issues/4607
|
||||
var output string
|
||||
if bytes.HasPrefix(b, []byte("W\x00i\x00n\x00d\x00o\x00w\x00s\x00")) {
|
||||
output, err = decodeUTF16(b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode wsl.exe -l output %q: %v", b, err)
|
||||
}
|
||||
} else {
|
||||
output = string(b)
|
||||
}
|
||||
lines := strings.Split(output, "\n")
|
||||
lines := strings.Split(string(b), "\n")
|
||||
if len(lines) < 1 {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -66,19 +44,6 @@ func wslDistros() ([]string, error) {
|
||||
return distros, nil
|
||||
}
|
||||
|
||||
func decodeUTF16(b []byte) (string, error) {
|
||||
if len(b) == 0 {
|
||||
return "", nil
|
||||
} else if len(b)%2 != 0 {
|
||||
return "", fmt.Errorf("decodeUTF16: invalid length %d", len(b))
|
||||
}
|
||||
var u16 []uint16
|
||||
for i := 0; i < len(b); i += 2 {
|
||||
u16 = append(u16, uint16(b[i])+(uint16(b[i+1])<<8))
|
||||
}
|
||||
return string(utf16.Decode(u16)), nil
|
||||
}
|
||||
|
||||
// wslManager is a DNS manager for WSL2 linux distributions.
|
||||
// It configures /etc/wsl.conf and /etc/resolv.conf.
|
||||
type wslManager struct {
|
||||
@@ -193,7 +158,8 @@ func (fs wslFS) Truncate(name string) error { return fs.WriteFile(name, nil, 064
|
||||
|
||||
func (fs wslFS) ReadFile(name string) ([]byte, error) {
|
||||
b, err := wslCombinedOutput(fs.cmd("cat", "--", name))
|
||||
if ee, _ := err.(*exec.ExitError); ee != nil && ee.ExitCode() == 1 {
|
||||
var ee *exec.ExitError
|
||||
if errors.As(err, &ee) && ee.ExitCode() == 1 {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return b, err
|
||||
@@ -225,7 +191,10 @@ func wslCombinedOutput(cmd *exec.Cmd) ([]byte, error) {
|
||||
cmd.Stdout = buf
|
||||
cmd.Stderr = buf
|
||||
err := wslRun(cmd)
|
||||
return buf.Bytes(), err
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return maybeUnUTF16(buf.Bytes()), nil
|
||||
}
|
||||
|
||||
func wslRun(cmd *exec.Cmd) (err error) {
|
||||
|
||||
314
net/dnscache/messagecache.go
Normal file
314
net/dnscache/messagecache.go
Normal file
@@ -0,0 +1,314 @@
|
||||
// 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 dnscache
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/golang/groupcache/lru"
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
)
|
||||
|
||||
// MessageCache is a cache that works at the DNS message layer,
|
||||
// with its cache keyed on a DNS wire-level question, and capable
|
||||
// of replying to DNS messages.
|
||||
//
|
||||
// Its zero value is ready for use with a default cache size.
|
||||
// Use SetMaxCacheSize to specify the cache size.
|
||||
//
|
||||
// It's safe for concurrent use.
|
||||
type MessageCache struct {
|
||||
// Clock is a clock, for testing.
|
||||
// If nil, time.Now is used.
|
||||
Clock func() time.Time
|
||||
|
||||
mu sync.Mutex
|
||||
cacheSizeSet int // 0 means default
|
||||
cache lru.Cache // msgQ => *msgCacheValue
|
||||
}
|
||||
|
||||
func (c *MessageCache) now() time.Time {
|
||||
if c.Clock != nil {
|
||||
return c.Clock()
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// SetMaxCacheSize sets the maximum number of DNS cache entries that
|
||||
// can be stored.
|
||||
func (c *MessageCache) SetMaxCacheSize(n int) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.cacheSizeSet = n
|
||||
c.pruneLocked()
|
||||
}
|
||||
|
||||
// Flush clears the cache.
|
||||
func (c *MessageCache) Flush() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.cache.Clear()
|
||||
}
|
||||
|
||||
// pruneLocked prunes down the cache size to the configured (or
|
||||
// default) max size.
|
||||
func (c *MessageCache) pruneLocked() {
|
||||
max := c.cacheSizeSet
|
||||
if max == 0 {
|
||||
max = 500
|
||||
}
|
||||
for c.cache.Len() > max {
|
||||
c.cache.RemoveOldest()
|
||||
}
|
||||
}
|
||||
|
||||
// msgQ is the MessageCache cache key.
|
||||
//
|
||||
// It's basically a golang.org/x/net/dns/dnsmessage#Question but the
|
||||
// Class is omitted (we only cache ClassINET) and we store a Go string
|
||||
// instead of a 256 byte dnsmessage.Name array.
|
||||
type msgQ struct {
|
||||
Name string
|
||||
Type dnsmessage.Type // A, AAAA, MX, etc
|
||||
}
|
||||
|
||||
// A *msgCacheValue is the cached value for a msgQ (question) key.
|
||||
//
|
||||
// Despite using pointers for storage and methods, the value is
|
||||
// immutable once placed in the cache.
|
||||
type msgCacheValue struct {
|
||||
Expires time.Time
|
||||
|
||||
// Answers are the minimum data to reconstruct a DNS response
|
||||
// message. TTLs are added later when converting to a
|
||||
// dnsmessage.Resource.
|
||||
Answers []msgResource
|
||||
}
|
||||
|
||||
type msgResource struct {
|
||||
Name string
|
||||
Type dnsmessage.Type // dnsmessage.UnknownResource.Type
|
||||
Data []byte // dnsmessage.UnknownResource.Data
|
||||
}
|
||||
|
||||
// ErrCacheMiss is a sentinel error returned by MessageCache.ReplyFromCache
|
||||
// when the request can not be satisified from cache.
|
||||
var ErrCacheMiss = errors.New("cache miss")
|
||||
|
||||
var parserPool = &sync.Pool{
|
||||
New: func() interface{} { return new(dnsmessage.Parser) },
|
||||
}
|
||||
|
||||
// ReplyFromCache writes a DNS reply to w for the provided DNS query message,
|
||||
// which must begin with the two ID bytes of a DNS message.
|
||||
//
|
||||
// If there's a cache miss, the message is invalid or unexpected,
|
||||
// ErrCacheMiss is returned. On cache hit, either nil or an error from
|
||||
// a w.Write call is returned.
|
||||
func (c *MessageCache) ReplyFromCache(w io.Writer, dnsQueryMessage []byte) error {
|
||||
cacheKey, txID, ok := getDNSQueryCacheKey(dnsQueryMessage)
|
||||
if !ok {
|
||||
return ErrCacheMiss
|
||||
}
|
||||
now := c.now()
|
||||
|
||||
c.mu.Lock()
|
||||
cacheEntI, _ := c.cache.Get(cacheKey)
|
||||
v, ok := cacheEntI.(*msgCacheValue)
|
||||
if ok && now.After(v.Expires) {
|
||||
c.cache.Remove(cacheKey)
|
||||
ok = false
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
if !ok {
|
||||
return ErrCacheMiss
|
||||
}
|
||||
|
||||
ttl := uint32(v.Expires.Sub(now).Seconds())
|
||||
|
||||
packedRes, err := packDNSResponse(cacheKey, txID, ttl, v.Answers)
|
||||
if err != nil {
|
||||
return ErrCacheMiss
|
||||
}
|
||||
_, err = w.Write(packedRes)
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
errNotCacheable = errors.New("question not cacheable")
|
||||
)
|
||||
|
||||
// AddCacheEntry adds a cache entry to the cache.
|
||||
// It returns an error if the entry could not be cached.
|
||||
func (c *MessageCache) AddCacheEntry(qPacket, res []byte) error {
|
||||
cacheKey, qID, ok := getDNSQueryCacheKey(qPacket)
|
||||
if !ok {
|
||||
return errNotCacheable
|
||||
}
|
||||
now := c.now()
|
||||
v := &msgCacheValue{}
|
||||
|
||||
p := parserPool.Get().(*dnsmessage.Parser)
|
||||
defer parserPool.Put(p)
|
||||
|
||||
resh, err := p.Start(res)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading header in response: %w", err)
|
||||
}
|
||||
if resh.ID != qID {
|
||||
return fmt.Errorf("response ID doesn't match query ID")
|
||||
}
|
||||
q, err := p.Question()
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading 1st question in response: %w", err)
|
||||
}
|
||||
if _, err := p.Question(); err != dnsmessage.ErrSectionDone {
|
||||
if err == nil {
|
||||
return errors.New("unexpected 2nd question in response")
|
||||
}
|
||||
return fmt.Errorf("after reading 1st question in response: %w", err)
|
||||
}
|
||||
if resName := asciiLowerName(q.Name).String(); resName != cacheKey.Name {
|
||||
return fmt.Errorf("response question name %q != question name %q", resName, cacheKey.Name)
|
||||
}
|
||||
for {
|
||||
rh, err := p.AnswerHeader()
|
||||
if err == dnsmessage.ErrSectionDone {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading answer: %w", err)
|
||||
}
|
||||
res, err := p.UnknownResource()
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading resource: %w", err)
|
||||
}
|
||||
if rh.Class != dnsmessage.ClassINET {
|
||||
continue
|
||||
}
|
||||
|
||||
// Set the cache entry's expiration to the soonest
|
||||
// we've seen. (They should all be the same, though)
|
||||
expires := now.Add(time.Duration(rh.TTL) * time.Second)
|
||||
if v.Expires.IsZero() || expires.Before(v.Expires) {
|
||||
v.Expires = expires
|
||||
}
|
||||
v.Answers = append(v.Answers, msgResource{
|
||||
Name: rh.Name.String(),
|
||||
Type: rh.Type,
|
||||
Data: res.Data, // doesn't alias; a copy from dnsmessage.unpackUnknownResource
|
||||
})
|
||||
}
|
||||
c.addCacheValue(cacheKey, v)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *MessageCache) addCacheValue(cacheKey msgQ, v *msgCacheValue) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.cache.Add(cacheKey, v)
|
||||
c.pruneLocked()
|
||||
}
|
||||
|
||||
func getDNSQueryCacheKey(msg []byte) (cacheKey msgQ, txID uint16, ok bool) {
|
||||
p := parserPool.Get().(*dnsmessage.Parser)
|
||||
defer parserPool.Put(p)
|
||||
h, err := p.Start(msg)
|
||||
const dnsHeaderSize = 12
|
||||
if err != nil || h.OpCode != 0 || h.Response || h.Truncated ||
|
||||
len(msg) < dnsHeaderSize { // p.Start checks this anyway, but to be explicit for slicing below
|
||||
return cacheKey, 0, false
|
||||
}
|
||||
var (
|
||||
numQ = binary.BigEndian.Uint16(msg[4:6])
|
||||
numAns = binary.BigEndian.Uint16(msg[6:8])
|
||||
numAuth = binary.BigEndian.Uint16(msg[8:10])
|
||||
numAddn = binary.BigEndian.Uint16(msg[10:12])
|
||||
)
|
||||
_ = numAddn // ignore this for now; do client OSes send EDNS additional? assume so, ignore.
|
||||
if !(numQ == 1 && numAns == 0 && numAuth == 0) {
|
||||
// Something weird. We don't want to deal with it.
|
||||
return cacheKey, 0, false
|
||||
}
|
||||
q, err := p.Question()
|
||||
if err != nil {
|
||||
// Already verified numQ == 1 so shouldn't happen, but:
|
||||
return cacheKey, 0, false
|
||||
}
|
||||
if q.Class != dnsmessage.ClassINET {
|
||||
// We only cache the Internet class.
|
||||
return cacheKey, 0, false
|
||||
}
|
||||
return msgQ{Name: asciiLowerName(q.Name).String(), Type: q.Type}, h.ID, true
|
||||
}
|
||||
|
||||
func asciiLowerName(n dnsmessage.Name) dnsmessage.Name {
|
||||
nb := n.Data[:]
|
||||
if int(n.Length) < len(n.Data) {
|
||||
nb = nb[:n.Length]
|
||||
}
|
||||
for i, b := range nb {
|
||||
if 'A' <= b && b <= 'Z' {
|
||||
n.Data[i] += 0x20
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// packDNSResponse builds a DNS response for the given question and
|
||||
// transaction ID. The response resource records will have have the
|
||||
// same provided TTL.
|
||||
func packDNSResponse(q msgQ, txID uint16, ttl uint32, answers []msgResource) ([]byte, error) {
|
||||
var baseMem []byte // TODO: guess a max size based on looping over answers?
|
||||
b := dnsmessage.NewBuilder(baseMem, dnsmessage.Header{
|
||||
ID: txID,
|
||||
Response: true,
|
||||
OpCode: 0,
|
||||
Authoritative: false,
|
||||
Truncated: false,
|
||||
RCode: dnsmessage.RCodeSuccess,
|
||||
})
|
||||
name, err := dnsmessage.NewName(q.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := b.StartQuestions(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := b.Question(dnsmessage.Question{
|
||||
Name: name,
|
||||
Type: q.Type,
|
||||
Class: dnsmessage.ClassINET,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := b.StartAnswers(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, r := range answers {
|
||||
name, err := dnsmessage.NewName(r.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := b.UnknownResource(dnsmessage.ResourceHeader{
|
||||
Name: name,
|
||||
Type: r.Type,
|
||||
Class: dnsmessage.ClassINET,
|
||||
TTL: ttl,
|
||||
}, dnsmessage.UnknownResource{
|
||||
Type: r.Type,
|
||||
Data: r.Data,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return b.Finish()
|
||||
}
|
||||
292
net/dnscache/messagecache_test.go
Normal file
292
net/dnscache/messagecache_test.go
Normal file
@@ -0,0 +1,292 @@
|
||||
// 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 dnscache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
func TestMessageCache(t *testing.T) {
|
||||
clock := &tstest.Clock{
|
||||
Start: time.Date(1987, 11, 1, 0, 0, 0, 0, time.UTC),
|
||||
}
|
||||
mc := &MessageCache{Clock: clock.Now}
|
||||
mc.SetMaxCacheSize(2)
|
||||
clock.Advance(time.Second)
|
||||
|
||||
var out bytes.Buffer
|
||||
if err := mc.ReplyFromCache(&out, makeQ(1, "foo.com.")); err != ErrCacheMiss {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if err := mc.AddCacheEntry(
|
||||
makeQ(2, "foo.com."),
|
||||
makeRes(2, "FOO.COM.", ttlOpt(10),
|
||||
&dnsmessage.AResource{A: [4]byte{127, 0, 0, 1}},
|
||||
&dnsmessage.AResource{A: [4]byte{127, 0, 0, 2}})); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Expect cache hit, with 10 seconds remaining.
|
||||
out.Reset()
|
||||
if err := mc.ReplyFromCache(&out, makeQ(3, "foo.com.")); err != nil {
|
||||
t.Fatalf("expected cache hit; got: %v", err)
|
||||
}
|
||||
if p := mustParseResponse(t, out.Bytes()); p.TxID != 3 {
|
||||
t.Errorf("TxID = %v; want %v", p.TxID, 3)
|
||||
} else if p.TTL != 10 {
|
||||
t.Errorf("TTL = %v; want 10", p.TTL)
|
||||
}
|
||||
|
||||
// One second elapses, expect a cache hit, with 9 seconds
|
||||
// remaining.
|
||||
clock.Advance(time.Second)
|
||||
out.Reset()
|
||||
if err := mc.ReplyFromCache(&out, makeQ(4, "foo.com.")); err != nil {
|
||||
t.Fatalf("expected cache hit; got: %v", err)
|
||||
}
|
||||
if p := mustParseResponse(t, out.Bytes()); p.TxID != 4 {
|
||||
t.Errorf("TxID = %v; want %v", p.TxID, 4)
|
||||
} else if p.TTL != 9 {
|
||||
t.Errorf("TTL = %v; want 9", p.TTL)
|
||||
}
|
||||
|
||||
// Expect cache miss on MX record.
|
||||
if err := mc.ReplyFromCache(&out, makeQ(4, "foo.com.", dnsmessage.TypeMX)); err != ErrCacheMiss {
|
||||
t.Fatalf("expected cache miss on MX; got: %v", err)
|
||||
}
|
||||
// Expect cache miss on CHAOS class.
|
||||
if err := mc.ReplyFromCache(&out, makeQ(4, "foo.com.", dnsmessage.ClassCHAOS)); err != ErrCacheMiss {
|
||||
t.Fatalf("expected cache miss on CHAOS; got: %v", err)
|
||||
}
|
||||
|
||||
// Ten seconds elapses; expect a cache miss.
|
||||
clock.Advance(10 * time.Second)
|
||||
if err := mc.ReplyFromCache(&out, makeQ(5, "foo.com.")); err != ErrCacheMiss {
|
||||
t.Fatalf("expected cache miss, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
type parsedMeta struct {
|
||||
TxID uint16
|
||||
TTL uint32
|
||||
}
|
||||
|
||||
func mustParseResponse(t testing.TB, r []byte) (ret parsedMeta) {
|
||||
t.Helper()
|
||||
var p dnsmessage.Parser
|
||||
h, err := p.Start(r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ret.TxID = h.ID
|
||||
qq, err := p.AllQuestions()
|
||||
if err != nil {
|
||||
t.Fatalf("AllQuestions: %v", err)
|
||||
}
|
||||
if len(qq) != 1 {
|
||||
t.Fatalf("num questions = %v; want 1", len(qq))
|
||||
}
|
||||
aa, err := p.AllAnswers()
|
||||
if err != nil {
|
||||
t.Fatalf("AllAnswers: %v", err)
|
||||
}
|
||||
for _, r := range aa {
|
||||
if ret.TTL == 0 {
|
||||
ret.TTL = r.Header.TTL
|
||||
}
|
||||
if ret.TTL != r.Header.TTL {
|
||||
t.Fatal("mixed TTLs")
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
type responseOpt bool
|
||||
|
||||
type ttlOpt uint32
|
||||
|
||||
func makeQ(txID uint16, name string, opt ...interface{}) []byte {
|
||||
opt = append(opt, responseOpt(false))
|
||||
return makeDNSPkt(txID, name, opt...)
|
||||
}
|
||||
|
||||
func makeRes(txID uint16, name string, opt ...interface{}) []byte {
|
||||
opt = append(opt, responseOpt(true))
|
||||
return makeDNSPkt(txID, name, opt...)
|
||||
}
|
||||
|
||||
func makeDNSPkt(txID uint16, name string, opt ...interface{}) []byte {
|
||||
typ := dnsmessage.TypeA
|
||||
class := dnsmessage.ClassINET
|
||||
var response bool
|
||||
var answers []dnsmessage.ResourceBody
|
||||
var ttl uint32 = 1 // one second by default
|
||||
for _, o := range opt {
|
||||
switch o := o.(type) {
|
||||
case dnsmessage.Type:
|
||||
typ = o
|
||||
case dnsmessage.Class:
|
||||
class = o
|
||||
case responseOpt:
|
||||
response = bool(o)
|
||||
case dnsmessage.ResourceBody:
|
||||
answers = append(answers, o)
|
||||
case ttlOpt:
|
||||
ttl = uint32(o)
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown opt type %T", o))
|
||||
}
|
||||
}
|
||||
qname := dnsmessage.MustNewName(name)
|
||||
msg := dnsmessage.Message{
|
||||
Header: dnsmessage.Header{ID: txID, Response: response},
|
||||
Questions: []dnsmessage.Question{
|
||||
{
|
||||
Name: qname,
|
||||
Type: typ,
|
||||
Class: class,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, rb := range answers {
|
||||
msg.Answers = append(msg.Answers, dnsmessage.Resource{
|
||||
Header: dnsmessage.ResourceHeader{
|
||||
Name: qname,
|
||||
Type: typ,
|
||||
Class: class,
|
||||
TTL: ttl,
|
||||
},
|
||||
Body: rb,
|
||||
})
|
||||
}
|
||||
buf, err := msg.Pack()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return buf
|
||||
}
|
||||
|
||||
func TestASCIILowerName(t *testing.T) {
|
||||
n := asciiLowerName(dnsmessage.MustNewName("Foo.COM."))
|
||||
if got, want := n.String(), "foo.com."; got != want {
|
||||
t.Errorf("got = %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDNSQueryCacheKey(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pkt []byte
|
||||
want msgQ
|
||||
txID uint16
|
||||
anyTX bool
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
},
|
||||
{
|
||||
name: "a",
|
||||
pkt: makeQ(123, "foo.com."),
|
||||
want: msgQ{"foo.com.", dnsmessage.TypeA},
|
||||
txID: 123,
|
||||
},
|
||||
{
|
||||
name: "aaaa",
|
||||
pkt: makeQ(6, "foo.com.", dnsmessage.TypeAAAA),
|
||||
want: msgQ{"foo.com.", dnsmessage.TypeAAAA},
|
||||
txID: 6,
|
||||
},
|
||||
{
|
||||
name: "normalize_case",
|
||||
pkt: makeQ(123, "FoO.CoM."),
|
||||
want: msgQ{"foo.com.", dnsmessage.TypeA},
|
||||
txID: 123,
|
||||
},
|
||||
{
|
||||
name: "ignore_response",
|
||||
pkt: makeRes(123, "foo.com."),
|
||||
},
|
||||
{
|
||||
name: "ignore_question_with_answers",
|
||||
pkt: makeQ(2, "foo.com.", &dnsmessage.AResource{A: [4]byte{127, 0, 0, 1}}),
|
||||
},
|
||||
{
|
||||
name: "whatever_go_generates", // in case Go's net package grows functionality we don't handle
|
||||
pkt: getGoNetPacketDNSQuery("from-go.foo."),
|
||||
want: msgQ{"from-go.foo.", dnsmessage.TypeA},
|
||||
anyTX: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, gotTX, ok := getDNSQueryCacheKey(tt.pkt)
|
||||
if !ok {
|
||||
if tt.txID == 0 && got == (msgQ{}) {
|
||||
return
|
||||
}
|
||||
t.Fatal("failed")
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("got %+v, want %+v", got, tt.want)
|
||||
}
|
||||
if gotTX != tt.txID && !tt.anyTX {
|
||||
t.Errorf("got tx %v, want %v", gotTX, tt.txID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getGoNetPacketDNSQuery(name string) []byte {
|
||||
if runtime.GOOS == "windows" {
|
||||
// On Windows, Go's net.Resolver doesn't use the DNS client.
|
||||
// See https://github.com/golang/go/issues/33097 which
|
||||
// was approved but not yet implemented.
|
||||
// For now just pretend it's implemented to make this test
|
||||
// pass on Windows with complicated the caller.
|
||||
return makeQ(123, name)
|
||||
}
|
||||
res := make(chan []byte, 1)
|
||||
r := &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return goResolverConn(res), nil
|
||||
},
|
||||
}
|
||||
r.LookupIP(context.Background(), "ip4", name)
|
||||
return <-res
|
||||
}
|
||||
|
||||
type goResolverConn chan<- []byte
|
||||
|
||||
func (goResolverConn) Close() error { return nil }
|
||||
func (goResolverConn) LocalAddr() net.Addr { return todoAddr{} }
|
||||
func (goResolverConn) RemoteAddr() net.Addr { return todoAddr{} }
|
||||
func (goResolverConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (goResolverConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (goResolverConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
func (goResolverConn) Read([]byte) (int, error) { return 0, errors.New("boom") }
|
||||
func (c goResolverConn) Write(p []byte) (int, error) {
|
||||
select {
|
||||
case c <- p[2:]: // skip 2 byte length for TCP mode DNS query
|
||||
default:
|
||||
}
|
||||
return 0, errors.New("boom")
|
||||
}
|
||||
|
||||
type todoAddr struct{}
|
||||
|
||||
func (todoAddr) Network() string { return "unused" }
|
||||
func (todoAddr) String() string { return "unused-todoAddr" }
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ func (p *Pipe) Read(b []byte) (n int, err error) {
|
||||
if debugPipe {
|
||||
orig := b
|
||||
defer func() {
|
||||
log.Printf("Pipe(%q).Read( %q) n=%d, err=%v", p.name, string(orig[:n]), n, err)
|
||||
log.Printf("Pipe(%q).Read(%q) n=%d, err=%v", p.name, string(orig[:n]), n, err)
|
||||
}()
|
||||
}
|
||||
for n == 0 {
|
||||
|
||||
@@ -60,9 +60,6 @@ func TestPipeTimeout(t *testing.T) {
|
||||
t.Run("block-write", func(t *testing.T) {
|
||||
p := NewPipe("p1", 1<<16)
|
||||
p.SetWriteDeadline(time.Now().Add(10 * time.Millisecond))
|
||||
if _, err := p.Write([]byte{'h'}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := p.Block(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -75,9 +72,6 @@ func TestPipeTimeout(t *testing.T) {
|
||||
p.Write([]byte{'h', 'i'})
|
||||
p.SetReadDeadline(time.Now().Add(10 * time.Millisecond))
|
||||
b := make([]byte, 1)
|
||||
if _, err := p.Read(b); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := p.Block(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -39,6 +39,16 @@ type Header interface {
|
||||
Marshal(buf []byte) error
|
||||
}
|
||||
|
||||
// HeaderChecksummer is implemented by Header implementations that
|
||||
// need to do a checksum over their paylods.
|
||||
type HeaderChecksummer interface {
|
||||
Header
|
||||
|
||||
// WriteCheck writes the correct checksum into buf, which should
|
||||
// be be the already-marshalled header and payload.
|
||||
WriteChecksum(buf []byte)
|
||||
}
|
||||
|
||||
// Generate generates a new packet with the given Header and
|
||||
// payload. This function allocates memory, see Header.Marshal for an
|
||||
// allocation-free option.
|
||||
@@ -49,5 +59,9 @@ func Generate(h Header, payload []byte) []byte {
|
||||
copy(buf[hlen:], payload)
|
||||
h.Marshal(buf)
|
||||
|
||||
if hc, ok := h.(HeaderChecksummer); ok {
|
||||
hc.WriteChecksum(buf)
|
||||
}
|
||||
|
||||
return buf
|
||||
}
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
|
||||
package packet
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
|
||||
"tailscale.com/types/ipproto"
|
||||
)
|
||||
|
||||
// icmp6HeaderLength is the size of the ICMPv6 packet header, not
|
||||
// including the outer IP layer or the variable "response data"
|
||||
// trailer.
|
||||
@@ -42,3 +48,120 @@ type ICMP6Code uint8
|
||||
const (
|
||||
ICMP6NoCode ICMP6Code = 0
|
||||
)
|
||||
|
||||
// ICMP6Header is an IPv4+ICMPv4 header.
|
||||
type ICMP6Header struct {
|
||||
IP6Header
|
||||
Type ICMP6Type
|
||||
Code ICMP6Code
|
||||
}
|
||||
|
||||
// Len implements Header.
|
||||
func (h ICMP6Header) Len() int {
|
||||
return h.IP6Header.Len() + icmp6HeaderLength
|
||||
}
|
||||
|
||||
// Marshal implements Header.
|
||||
func (h ICMP6Header) Marshal(buf []byte) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
if len(buf) > maxPacketLength {
|
||||
return errLargePacket
|
||||
}
|
||||
// The caller does not need to set this.
|
||||
h.IPProto = ipproto.ICMPv6
|
||||
|
||||
h.IP6Header.Marshal(buf)
|
||||
|
||||
const o = ip6HeaderLength // start offset of ICMPv6 header
|
||||
buf[o+0] = uint8(h.Type)
|
||||
buf[o+1] = uint8(h.Code)
|
||||
buf[o+2] = 0 // checksum, to be filled in later
|
||||
buf[o+3] = 0 // checksum, to be filled in later
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToResponse implements Header. TODO: it doesn't implement it
|
||||
// correctly, instead it statically generates an ICMP Echo Reply
|
||||
// packet.
|
||||
func (h *ICMP6Header) ToResponse() {
|
||||
// TODO: this doesn't implement ToResponse correctly, as it
|
||||
// assumes the ICMP request type.
|
||||
h.Type = ICMP6EchoReply
|
||||
h.Code = ICMP6NoCode
|
||||
h.IP6Header.ToResponse()
|
||||
}
|
||||
|
||||
// WriteChecksum implements HeaderChecksummer, writing just the checksum bytes
|
||||
// into the otherwise fully marshaled ICMP6 packet p (which should include the
|
||||
// IPv6 header, ICMPv6 header, and payload).
|
||||
func (h ICMP6Header) WriteChecksum(p []byte) {
|
||||
const payOff = ip6HeaderLength + icmp6HeaderLength
|
||||
xsum := icmp6Checksum(p[ip6HeaderLength:payOff], h.Src.As16(), h.Dst.As16(), p[payOff:])
|
||||
binary.BigEndian.PutUint16(p[ip6HeaderLength+2:], xsum)
|
||||
}
|
||||
|
||||
// Adapted from gVisor:
|
||||
|
||||
// icmp6Checksum calculates the ICMP checksum over the provided ICMPv6
|
||||
// header (without the IPv6 header), IPv6 src/dst addresses and the
|
||||
// payload.
|
||||
//
|
||||
// The header's existing checksum must be zeroed.
|
||||
func icmp6Checksum(header []byte, src, dst [16]byte, payload []byte) uint16 {
|
||||
// Calculate the IPv6 pseudo-header upper-layer checksum.
|
||||
xsum := checksumBytes(src[:], 0)
|
||||
xsum = checksumBytes(dst[:], xsum)
|
||||
|
||||
var scratch [4]byte
|
||||
binary.BigEndian.PutUint32(scratch[:], uint32(len(header)+len(payload)))
|
||||
xsum = checksumBytes(scratch[:], xsum)
|
||||
xsum = checksumBytes(append(scratch[:0], 0, 0, 0, uint8(ipproto.ICMPv6)), xsum)
|
||||
xsum = checksumBytes(payload, xsum)
|
||||
|
||||
var hdrz [icmp6HeaderLength]byte
|
||||
copy(hdrz[:], header)
|
||||
// Zero out the header.
|
||||
hdrz[2] = 0
|
||||
hdrz[3] = 0
|
||||
xsum = ^checksumBytes(hdrz[:], xsum)
|
||||
return xsum
|
||||
}
|
||||
|
||||
// checksumCombine combines the two uint16 to form their
|
||||
// checksum. This is done by adding them and the carry.
|
||||
//
|
||||
// Note that checksum a must have been computed on an even number of
|
||||
// bytes.
|
||||
func checksumCombine(a, b uint16) uint16 {
|
||||
v := uint32(a) + uint32(b)
|
||||
return uint16(v + v>>16)
|
||||
}
|
||||
|
||||
// checksumBytes calculates the checksum (as defined in RFC 1071) of
|
||||
// the bytes in buf.
|
||||
//
|
||||
// The initial checksum must have been computed on an even number of bytes.
|
||||
func checksumBytes(buf []byte, initial uint16) uint16 {
|
||||
v := uint32(initial)
|
||||
|
||||
odd := len(buf)%2 == 1
|
||||
if odd {
|
||||
v += uint32(buf[0])
|
||||
buf = buf[1:]
|
||||
}
|
||||
|
||||
n := len(buf)
|
||||
odd = n&1 != 0
|
||||
if odd {
|
||||
n--
|
||||
v += uint32(buf[n]) << 8
|
||||
}
|
||||
|
||||
for i := 0; i < n; i += 2 {
|
||||
v += (uint32(buf[i]) << 8) + uint32(buf[i+1])
|
||||
}
|
||||
|
||||
return checksumCombine(uint16(v), uint16(v>>16))
|
||||
}
|
||||
|
||||
80
net/packet/icmp6_test.go
Normal file
80
net/packet/icmp6_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package packet
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/ipproto"
|
||||
)
|
||||
|
||||
func TestICMPv6PingResponse(t *testing.T) {
|
||||
pingHdr := ICMP6Header{
|
||||
IP6Header: IP6Header{
|
||||
Src: netaddr.MustParseIP("1::1"),
|
||||
Dst: netaddr.MustParseIP("2::2"),
|
||||
IPProto: ipproto.ICMPv6,
|
||||
},
|
||||
Type: ICMP6EchoRequest,
|
||||
Code: ICMP6NoCode,
|
||||
}
|
||||
|
||||
// echoReqLen is 2 bytes identifier + 2 bytes seq number.
|
||||
// https://datatracker.ietf.org/doc/html/rfc4443#section-4.1
|
||||
// Packet.IsEchoRequest verifies that these 4 bytes are present.
|
||||
const echoReqLen = 4
|
||||
buf := make([]byte, pingHdr.Len()+echoReqLen)
|
||||
if err := pingHdr.Marshal(buf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var p Parsed
|
||||
p.Decode(buf)
|
||||
if !p.IsEchoRequest() {
|
||||
t.Fatalf("not an echo request, got: %+v", p)
|
||||
}
|
||||
|
||||
pingHdr.ToResponse()
|
||||
buf = make([]byte, pingHdr.Len()+echoReqLen)
|
||||
if err := pingHdr.Marshal(buf); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p.Decode(buf)
|
||||
if p.IsEchoRequest() {
|
||||
t.Fatalf("unexpectedly still an echo request: %+v", p)
|
||||
}
|
||||
if !p.IsEchoResponse() {
|
||||
t.Fatalf("not an echo response: %+v", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestICMPv6Checksum(t *testing.T) {
|
||||
const req = "\x60\x0f\x07\x00\x00\x10\x3a\x40\xfd\x7a\x11\x5c\xa1\xe0\xab\x12" +
|
||||
"\x48\x43\xcd\x96\x62\x7b\x65\x28\x26\x07\xf8\xb0\x40\x0a\x08\x07" +
|
||||
"\x00\x00\x00\x00\x00\x00\x20\x0e\x80\x00\x4a\x9a\x2e\xea\x00\x02" +
|
||||
"\x61\xb1\x9e\xad\x00\x06\x45\xaa"
|
||||
// The packet that we'd originally generated incorrectly, but with the checksum
|
||||
// bytes fixed per WireShark's correct calculation:
|
||||
const wantRes = "\x60\x00\xf8\xff\x00\x10\x3a\x40\x26\x07\xf8\xb0\x40\x0a\x08\x07" +
|
||||
"\x00\x00\x00\x00\x00\x00\x20\x0e\xfd\x7a\x11\x5c\xa1\xe0\xab\x12" +
|
||||
"\x48\x43\xcd\x96\x62\x7b\x65\x28\x81\x00\x49\x9a\x2e\xea\x00\x02" +
|
||||
"\x61\xb1\x9e\xad\x00\x06\x45\xaa"
|
||||
|
||||
var p Parsed
|
||||
p.Decode([]byte(req))
|
||||
if !p.IsEchoRequest() {
|
||||
t.Fatalf("not an echo request, got: %+v", p)
|
||||
}
|
||||
|
||||
h := p.ICMP6Header()
|
||||
h.ToResponse()
|
||||
pong := Generate(&h, p.Payload())
|
||||
|
||||
if string(pong) != wantRes {
|
||||
t.Errorf("wrong packet\n\n got: %x\nwant: %x", pong, wantRes)
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func (h *IP6Header) ToResponse() {
|
||||
|
||||
// marshalPseudo serializes h into buf in the "pseudo-header" form
|
||||
// required when calculating UDP checksums.
|
||||
func (h IP6Header) marshalPseudo(buf []byte) error {
|
||||
func (h IP6Header) marshalPseudo(buf []byte, proto ipproto.Proto) error {
|
||||
if len(buf) < h.Len() {
|
||||
return errSmallBuffer
|
||||
}
|
||||
@@ -72,6 +72,6 @@ func (h IP6Header) marshalPseudo(buf []byte) error {
|
||||
buf[36] = 0
|
||||
buf[37] = 0
|
||||
buf[38] = 0
|
||||
buf[39] = 17 // NextProto
|
||||
buf[39] = byte(proto) // NextProto
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ func (p *Parsed) String() string {
|
||||
}
|
||||
|
||||
// Decode extracts data from the packet in b into q.
|
||||
// It performs extremely simple packet decoding for basic IPv4 packet types.
|
||||
// It performs extremely simple packet decoding for basic IPv4 and IPv6 packet types.
|
||||
// It extracts only the subprotocol id, IP addresses, and (if any) ports,
|
||||
// and shouldn't need any memory allocation.
|
||||
func (q *Parsed) Decode(b []byte) {
|
||||
@@ -339,9 +339,6 @@ func (q *Parsed) IP6Header() IP6Header {
|
||||
}
|
||||
|
||||
func (q *Parsed) ICMP4Header() ICMP4Header {
|
||||
if q.IPVersion != 4 {
|
||||
panic("IP4Header called on non-IPv4 Parsed")
|
||||
}
|
||||
return ICMP4Header{
|
||||
IP4Header: q.IP4Header(),
|
||||
Type: ICMP4Type(q.b[q.subofs+0]),
|
||||
@@ -349,10 +346,15 @@ func (q *Parsed) ICMP4Header() ICMP4Header {
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Parsed) UDP4Header() UDP4Header {
|
||||
if q.IPVersion != 4 {
|
||||
panic("IP4Header called on non-IPv4 Parsed")
|
||||
func (q *Parsed) ICMP6Header() ICMP6Header {
|
||||
return ICMP6Header{
|
||||
IP6Header: q.IP6Header(),
|
||||
Type: ICMP6Type(q.b[q.subofs+0]),
|
||||
Code: ICMP6Code(q.b[q.subofs+1]),
|
||||
}
|
||||
}
|
||||
|
||||
func (q *Parsed) UDP4Header() UDP4Header {
|
||||
return UDP4Header{
|
||||
IP4Header: q.IP4Header(),
|
||||
SrcPort: q.Src.Port(),
|
||||
@@ -410,7 +412,7 @@ func (q *Parsed) IsEchoRequest() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// IsEchoRequest reports whether q is an IPv4 ICMP Echo Response.
|
||||
// IsEchoResponse reports whether q is an IPv4 ICMP Echo Response.
|
||||
func (q *Parsed) IsEchoResponse() bool {
|
||||
switch q.IPProto {
|
||||
case ipproto.ICMPv4:
|
||||
|
||||
@@ -40,7 +40,7 @@ func (h UDP6Header) Marshal(buf []byte) error {
|
||||
binary.BigEndian.PutUint16(buf[46:48], 0) // blank checksum
|
||||
|
||||
// UDP checksum with IP pseudo header.
|
||||
h.IP6Header.marshalPseudo(buf)
|
||||
h.IP6Header.marshalPseudo(buf, ipproto.UDP)
|
||||
binary.BigEndian.PutUint16(buf[46:48], ip4Checksum(buf[:]))
|
||||
|
||||
h.IP6Header.Marshal(buf)
|
||||
|
||||
@@ -63,11 +63,18 @@ type igdCounters struct {
|
||||
|
||||
func NewTestIGD(logf logger.Logf, t TestIGDOptions) (*TestIGD, error) {
|
||||
d := &TestIGD{
|
||||
logf: logf,
|
||||
doPMP: t.PMP,
|
||||
doPCP: t.PCP,
|
||||
doUPnP: t.UPnP,
|
||||
}
|
||||
d.logf = func(msg string, args ...interface{}) {
|
||||
// Don't log after the device has closed;
|
||||
// stray trailing logging angers testing.T.Logf.
|
||||
if d.closed.Get() {
|
||||
return
|
||||
}
|
||||
logf(msg, args...)
|
||||
}
|
||||
var err error
|
||||
if d.upnpConn, err = testListenUDP(); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -19,6 +19,10 @@ import (
|
||||
// https://www.rfc-editor.org/rfc/pdfrfc/rfc6887.txt.pdf
|
||||
// https://tools.ietf.org/html/rfc6887
|
||||
|
||||
//go:generate go run tailscale.com/cmd/addlicense -year 2021 -file pcpresultcode_string.go go run golang.org/x/tools/cmd/stringer -type=pcpResultCode -trimprefix=pcpCode
|
||||
|
||||
type pcpResultCode uint8
|
||||
|
||||
// PCP constants
|
||||
const (
|
||||
pcpVersion = 2
|
||||
@@ -26,8 +30,14 @@ const (
|
||||
|
||||
pcpMapLifetimeSec = 7200 // TODO does the RFC recommend anything? This is taken from PMP.
|
||||
|
||||
pcpCodeOK = 0
|
||||
pcpCodeNotAuthorized = 2
|
||||
pcpCodeOK pcpResultCode = 0
|
||||
pcpCodeNotAuthorized pcpResultCode = 2
|
||||
// From RFC 6887:
|
||||
// ADDRESS_MISMATCH: The source IP address of the request packet does
|
||||
// not match the contents of the PCP Client's IP Address field, due
|
||||
// to an unexpected NAT on the path between the PCP client and the
|
||||
// PCP-controlled NAT or firewall.
|
||||
pcpCodeAddressMismatch pcpResultCode = 12
|
||||
|
||||
pcpOpReply = 0x80 // OR'd into request's op code on response
|
||||
pcpOpAnnounce = 0
|
||||
@@ -140,7 +150,7 @@ func pcpAnnounceRequest(myIP netaddr.IP) []byte {
|
||||
|
||||
type pcpResponse struct {
|
||||
OpCode uint8
|
||||
ResultCode uint8
|
||||
ResultCode pcpResultCode
|
||||
Lifetime uint32
|
||||
Epoch uint32
|
||||
}
|
||||
@@ -150,7 +160,7 @@ func parsePCPResponse(b []byte) (res pcpResponse, ok bool) {
|
||||
return
|
||||
}
|
||||
res.OpCode = b[1]
|
||||
res.ResultCode = b[3]
|
||||
res.ResultCode = pcpResultCode(b[3])
|
||||
res.Lifetime = binary.BigEndian.Uint32(b[4:])
|
||||
res.Epoch = binary.BigEndian.Uint32(b[8:])
|
||||
return res, true
|
||||
|
||||
37
net/portmapper/pcpresultcode_string.go
Normal file
37
net/portmapper/pcpresultcode_string.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// 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.
|
||||
|
||||
// Code generated by "stringer -type=pcpResultCode -trimprefix=pcpCode"; DO NOT EDIT.
|
||||
|
||||
package portmapper
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[pcpCodeOK-0]
|
||||
_ = x[pcpCodeNotAuthorized-2]
|
||||
_ = x[pcpCodeAddressMismatch-12]
|
||||
}
|
||||
|
||||
const (
|
||||
_pcpResultCode_name_0 = "OK"
|
||||
_pcpResultCode_name_1 = "NotAuthorized"
|
||||
_pcpResultCode_name_2 = "AddressMismatch"
|
||||
)
|
||||
|
||||
func (i pcpResultCode) String() string {
|
||||
switch {
|
||||
case i == 0:
|
||||
return _pcpResultCode_name_0
|
||||
case i == 2:
|
||||
return _pcpResultCode_name_1
|
||||
case i == 12:
|
||||
return _pcpResultCode_name_2
|
||||
default:
|
||||
return "pcpResultCode(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
}
|
||||
32
net/portmapper/pmpresultcode_string.go
Normal file
32
net/portmapper/pmpresultcode_string.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// 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.
|
||||
|
||||
// Code generated by "stringer -type=pmpResultCode -trimprefix=pmpCode"; DO NOT EDIT.
|
||||
|
||||
package portmapper
|
||||
|
||||
import "strconv"
|
||||
|
||||
func _() {
|
||||
// An "invalid array index" compiler error signifies that the constant values have changed.
|
||||
// Re-run the stringer command to generate them again.
|
||||
var x [1]struct{}
|
||||
_ = x[pmpCodeOK-0]
|
||||
_ = x[pmpCodeUnsupportedVersion-1]
|
||||
_ = x[pmpCodeNotAuthorized-2]
|
||||
_ = x[pmpCodeNetworkFailure-3]
|
||||
_ = x[pmpCodeOutOfResources-4]
|
||||
_ = x[pmpCodeUnsupportedOpcode-5]
|
||||
}
|
||||
|
||||
const _pmpResultCode_name = "OKUnsupportedVersionNotAuthorizedNetworkFailureOutOfResourcesUnsupportedOpcode"
|
||||
|
||||
var _pmpResultCode_index = [...]uint8{0, 2, 20, 33, 47, 61, 78}
|
||||
|
||||
func (i pmpResultCode) String() string {
|
||||
if i >= pmpResultCode(len(_pmpResultCode_index)-1) {
|
||||
return "pmpResultCode(" + strconv.FormatInt(int64(i), 10) + ")"
|
||||
}
|
||||
return _pmpResultCode_name[_pmpResultCode_index[i]:_pmpResultCode_index[i+1]]
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
)
|
||||
|
||||
// Debug knobs for "tailscaled debug --portmap".
|
||||
@@ -563,6 +564,8 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
}
|
||||
}
|
||||
|
||||
//go:generate go run tailscale.com/cmd/addlicense -year 2021 -file pmpresultcode_string.go go run golang.org/x/tools/cmd/stringer -type=pmpResultCode -trimprefix=pmpCode
|
||||
|
||||
type pmpResultCode uint16
|
||||
|
||||
// NAT-PMP constants.
|
||||
@@ -685,11 +688,13 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
if c.sawPMPRecently() {
|
||||
res.PMP = true
|
||||
} else if !DisablePMP {
|
||||
metricPMPSent.Add(1)
|
||||
uc.WriteTo(pmpReqExternalAddrPacket, pxpAddr)
|
||||
}
|
||||
if c.sawPCPRecently() {
|
||||
res.PCP = true
|
||||
} else if !DisablePCP {
|
||||
metricPCPSent.Add(1)
|
||||
uc.WriteTo(pcpAnnounceRequest(myIP), pxpAddr)
|
||||
}
|
||||
if c.sawUPnPRecently() {
|
||||
@@ -734,6 +739,7 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
// See https://github.com/tailscale/tailscale/issues/3197 for
|
||||
// an example of a device that strictly implements UPnP, and
|
||||
// only responds to multicast queries.
|
||||
metricUPnPSent.Add(1)
|
||||
uc.WriteTo(uPnPPacket, upnpAddr)
|
||||
uc.WriteTo(uPnPPacket, upnpMulticastAddr)
|
||||
}
|
||||
@@ -759,11 +765,15 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
port := uint16(addr.(*net.UDPAddr).Port)
|
||||
switch port {
|
||||
case c.upnpPort():
|
||||
metricUPnPResponse.Add(1)
|
||||
if ip == gw && mem.Contains(mem.B(buf[:n]), mem.S(":InternetGatewayDevice:")) {
|
||||
meta, err := parseUPnPDiscoResponse(buf[:n])
|
||||
if err != nil {
|
||||
c.logf("unrecognized UPnP discovery response; ignoring")
|
||||
metricUPnPParseErr.Add(1)
|
||||
c.logf("unrecognized UPnP discovery response; ignoring: %v", err)
|
||||
continue
|
||||
}
|
||||
metricUPnPOK.Add(1)
|
||||
c.logf("[v1] UPnP reply %+v, %q", meta, buf[:n])
|
||||
res.UPnP = true
|
||||
c.mu.Lock()
|
||||
@@ -771,10 +781,12 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
if c.uPnPMeta != meta {
|
||||
c.logf("UPnP meta changed: %+v", meta)
|
||||
c.uPnPMeta = meta
|
||||
metricUPnPUpdatedMeta.Add(1)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
case c.pxpPort(): // same value for PMP and PCP
|
||||
metricPXPResponse.Add(1)
|
||||
if pres, ok := parsePCPResponse(buf[:n]); ok {
|
||||
if pres.OpCode == pcpOpReply|pcpOpAnnounce {
|
||||
pcpHeard = true
|
||||
@@ -785,20 +797,35 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
case pcpCodeOK:
|
||||
c.logf("[v1] Got PCP response: epoch: %v", pres.Epoch)
|
||||
res.PCP = true
|
||||
metricPCPOK.Add(1)
|
||||
continue
|
||||
case pcpCodeNotAuthorized:
|
||||
// A PCP service is running, but refuses to
|
||||
// provide port mapping services.
|
||||
res.PCP = false
|
||||
metricPCPNotAuthorized.Add(1)
|
||||
continue
|
||||
case pcpCodeAddressMismatch:
|
||||
// A PCP service is running, but it is behind a NAT, so it can't help us.
|
||||
res.PCP = false
|
||||
metricPCPAddressMismatch.Add(1)
|
||||
continue
|
||||
default:
|
||||
// Fall through to unexpected log line.
|
||||
}
|
||||
}
|
||||
metricPCPUnhandledResponseCode.Add(1)
|
||||
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)
|
||||
metricPMPUnhandledOpcode.Add(1)
|
||||
continue
|
||||
}
|
||||
switch pres.ResultCode {
|
||||
case pmpCodeOK:
|
||||
metricPMPOK.Add(1)
|
||||
c.logf("[v1] Got PMP response; IP: %v, epoch: %v", pres.PublicAddr, pres.SecondsSinceEpoch)
|
||||
res.PMP = true
|
||||
c.mu.Lock()
|
||||
@@ -807,7 +834,20 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
c.pmpLastEpoch = pres.SecondsSinceEpoch
|
||||
c.mu.Unlock()
|
||||
continue
|
||||
case pmpCodeNotAuthorized:
|
||||
metricPMPNotAuthorized.Add(1)
|
||||
c.logf("PMP probe failed due result code: %+v", pres)
|
||||
continue
|
||||
case pmpCodeNetworkFailure:
|
||||
metricPMPNetworkFailure.Add(1)
|
||||
c.logf("PMP probe failed due result code: %+v", pres)
|
||||
continue
|
||||
case pmpCodeOutOfResources:
|
||||
metricPMPOutOfResources.Add(1)
|
||||
c.logf("PMP probe failed due result code: %+v", pres)
|
||||
continue
|
||||
}
|
||||
metricPMPUnhandledResponseCode.Add(1)
|
||||
c.logf("unexpected PMP probe response: %+v", pres)
|
||||
}
|
||||
}
|
||||
@@ -826,3 +866,74 @@ var uPnPPacket = []byte("M-SEARCH * HTTP/1.1\r\n" +
|
||||
"ST: ssdp:all\r\n" +
|
||||
"MAN: \"ssdp:discover\"\r\n" +
|
||||
"MX: 2\r\n\r\n")
|
||||
|
||||
// PCP/PMP metrics
|
||||
var (
|
||||
// metricPXPResponse counts the number of times we received a PMP/PCP response.
|
||||
metricPXPResponse = clientmetric.NewCounter("portmap_pxp_response")
|
||||
|
||||
// metricPCPSent counts the number of times we sent a PCP request.
|
||||
metricPCPSent = clientmetric.NewCounter("portmap_pcp_sent")
|
||||
|
||||
// metricPCPOK counts the number of times
|
||||
// we received a successful PCP response.
|
||||
metricPCPOK = clientmetric.NewCounter("portmap_pcp_ok")
|
||||
|
||||
// metricPCPAddressMismatch counts the number of times
|
||||
// we received a PCP address mismatch result code.
|
||||
metricPCPAddressMismatch = clientmetric.NewCounter("portmap_pcp_address_mismatch")
|
||||
|
||||
// metricPCPNotAuthorized counts the number of times
|
||||
// we received a PCP not authorized result code.
|
||||
metricPCPNotAuthorized = clientmetric.NewCounter("portmap_pcp_not_authorized")
|
||||
|
||||
// metricPCPUnhandledResponseCode counts the number of times
|
||||
// we received an (as yet) unhandled PCP result code.
|
||||
metricPCPUnhandledResponseCode = clientmetric.NewCounter("portmap_pcp_unhandled_response_code")
|
||||
|
||||
// metricPMPSent counts the number of times we sent a PMP request.
|
||||
metricPMPSent = clientmetric.NewCounter("portmap_pmp_sent")
|
||||
|
||||
// metricPMPOK counts the number of times
|
||||
// we received a succesful PMP response.
|
||||
metricPMPOK = clientmetric.NewCounter("portmap_pmp_ok")
|
||||
|
||||
// metricPMPUnhandledOpcode counts the number of times
|
||||
// we received an unhandled PMP opcode.
|
||||
metricPMPUnhandledOpcode = clientmetric.NewCounter("portmap_pmp_unhandled_opcode")
|
||||
|
||||
// metricPMPUnhandledResponseCode counts the number of times
|
||||
// we received an unhandled PMP result code.
|
||||
metricPMPUnhandledResponseCode = clientmetric.NewCounter("portmap_pmp_unhandled_response_code")
|
||||
|
||||
// metricPMPOutOfResources counts the number of times
|
||||
// we received a PCP out of resources result code.
|
||||
metricPMPOutOfResources = clientmetric.NewCounter("portmap_pmp_out_of_resources")
|
||||
|
||||
// metricPMPNetworkFailure counts the number of times
|
||||
// we received a PCP network failure result code.
|
||||
metricPMPNetworkFailure = clientmetric.NewCounter("portmap_pmp_network_failure")
|
||||
|
||||
// metricPMPNotAuthorized counts the number of times
|
||||
// we received a PCP not authorized result code.
|
||||
metricPMPNotAuthorized = clientmetric.NewCounter("portmap_pmp_not_authorized")
|
||||
)
|
||||
|
||||
// UPnP metrics
|
||||
var (
|
||||
// metricUPnPSent counts the number of times we sent a UPnP request.
|
||||
metricUPnPSent = clientmetric.NewCounter("portmap_upnp_sent")
|
||||
|
||||
// metricUPnPResponse counts the number of times we received a UPnP response.
|
||||
metricUPnPResponse = clientmetric.NewCounter("portmap_upnp_response")
|
||||
|
||||
// metricUPnPParseErr counts the number of times we failed to parse a UPnP response.
|
||||
metricUPnPParseErr = clientmetric.NewCounter("portmap_upnp_parse_err")
|
||||
|
||||
// metricUPnPOK counts the number of times we received a usable UPnP response.
|
||||
metricUPnPOK = clientmetric.NewCounter("portmap_upnp_ok")
|
||||
|
||||
// metricUPnPUpdatedMeta counts the number of times
|
||||
// we received a UPnP response with a new meta.
|
||||
metricUPnPUpdatedMeta = clientmetric.NewCounter("portmap_upnp_updated_meta")
|
||||
)
|
||||
|
||||
145
net/proxymux/mux.go
Normal file
145
net/proxymux/mux.go
Normal file
@@ -0,0 +1,145 @@
|
||||
// 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 proxymux splits a net.Listener in two, routing SOCKS5
|
||||
// connections to one and HTTP requests to the other.
|
||||
//
|
||||
// It allows for hosting both a SOCKS5 proxy and an HTTP proxy on the
|
||||
// same listener.
|
||||
package proxymux
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SplitSOCKSAndHTTP accepts connections on ln and passes connections
|
||||
// through to either socksListener or httpListener, depending the
|
||||
// first byte sent by the client.
|
||||
func SplitSOCKSAndHTTP(ln net.Listener) (socksListener, httpListener net.Listener) {
|
||||
sl := &listener{
|
||||
addr: ln.Addr(),
|
||||
c: make(chan net.Conn),
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
hl := &listener{
|
||||
addr: ln.Addr(),
|
||||
c: make(chan net.Conn),
|
||||
closed: make(chan struct{}),
|
||||
}
|
||||
|
||||
go splitSOCKSAndHTTPListener(ln, sl, hl)
|
||||
|
||||
return sl, hl
|
||||
}
|
||||
|
||||
func splitSOCKSAndHTTPListener(ln net.Listener, sl, hl *listener) {
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
sl.Close()
|
||||
hl.Close()
|
||||
return
|
||||
}
|
||||
go routeConn(conn, sl, hl)
|
||||
}
|
||||
}
|
||||
|
||||
func routeConn(c net.Conn, socksListener, httpListener *listener) {
|
||||
if err := c.SetReadDeadline(time.Now().Add(15 * time.Second)); err != nil {
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
|
||||
var b [1]byte
|
||||
if _, err := io.ReadFull(c, b[:]); err != nil {
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.SetReadDeadline(time.Time{}); err != nil {
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
|
||||
conn := &connWithOneByte{
|
||||
Conn: c,
|
||||
b: b[0],
|
||||
}
|
||||
|
||||
// First byte of a SOCKS5 session is a version byte set to 5.
|
||||
var ln *listener
|
||||
if b[0] == 5 {
|
||||
ln = socksListener
|
||||
} else {
|
||||
ln = httpListener
|
||||
}
|
||||
select {
|
||||
case ln.c <- conn:
|
||||
case <-ln.closed:
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
type listener struct {
|
||||
addr net.Addr
|
||||
c chan net.Conn
|
||||
mu sync.Mutex // serializes close() on closed. It's okay to receive on closed without locking.
|
||||
closed chan struct{}
|
||||
}
|
||||
|
||||
func (ln *listener) Accept() (net.Conn, error) {
|
||||
// Once closed, reliably stay closed, don't race with attempts at
|
||||
// further connections.
|
||||
select {
|
||||
case <-ln.closed:
|
||||
return nil, net.ErrClosed
|
||||
default:
|
||||
}
|
||||
select {
|
||||
case ret := <-ln.c:
|
||||
return ret, nil
|
||||
case <-ln.closed:
|
||||
return nil, net.ErrClosed
|
||||
}
|
||||
}
|
||||
|
||||
func (ln *listener) Close() error {
|
||||
ln.mu.Lock()
|
||||
defer ln.mu.Unlock()
|
||||
select {
|
||||
case <-ln.closed:
|
||||
// Already closed
|
||||
default:
|
||||
close(ln.closed)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ln *listener) Addr() net.Addr {
|
||||
return ln.addr
|
||||
}
|
||||
|
||||
// connWithOneByte is a net.Conn that returns b for the first read
|
||||
// request, then forwards everything else to Conn.
|
||||
type connWithOneByte struct {
|
||||
net.Conn
|
||||
|
||||
b byte
|
||||
bRead bool
|
||||
}
|
||||
|
||||
func (c *connWithOneByte) Read(bs []byte) (int, error) {
|
||||
if c.bRead {
|
||||
return c.Conn.Read(bs)
|
||||
}
|
||||
if len(bs) == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
c.bRead = true
|
||||
bs[0] = c.b
|
||||
return 1, nil
|
||||
}
|
||||
172
net/proxymux/mux_test.go
Normal file
172
net/proxymux/mux_test.go
Normal file
@@ -0,0 +1,172 @@
|
||||
// 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 proxymux
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/net/socks5"
|
||||
)
|
||||
|
||||
func TestSplitSOCKSAndHTTP(t *testing.T) {
|
||||
s := mkWorld(t)
|
||||
defer s.Close()
|
||||
|
||||
s.checkURL(s.httpClient, false)
|
||||
s.checkURL(s.socksClient, false)
|
||||
}
|
||||
|
||||
func TestSplitSOCKSAndHTTPCloseSocks(t *testing.T) {
|
||||
s := mkWorld(t)
|
||||
defer s.Close()
|
||||
|
||||
s.socksListener.Close()
|
||||
s.checkURL(s.httpClient, false)
|
||||
s.checkURL(s.socksClient, true)
|
||||
}
|
||||
|
||||
func TestSplitSOCKSAndHTTPCloseHTTP(t *testing.T) {
|
||||
s := mkWorld(t)
|
||||
defer s.Close()
|
||||
|
||||
s.httpListener.Close()
|
||||
s.checkURL(s.httpClient, true)
|
||||
s.checkURL(s.socksClient, false)
|
||||
}
|
||||
|
||||
func TestSplitSOCKSAndHTTPCloseBoth(t *testing.T) {
|
||||
s := mkWorld(t)
|
||||
defer s.Close()
|
||||
|
||||
s.httpListener.Close()
|
||||
s.socksListener.Close()
|
||||
s.checkURL(s.httpClient, true)
|
||||
s.checkURL(s.socksClient, true)
|
||||
}
|
||||
|
||||
type world struct {
|
||||
t *testing.T
|
||||
|
||||
// targetListener/target is the HTTP server the client wants to
|
||||
// reach. It unconditionally responds with HTTP 418 "I'm a
|
||||
// teapot".
|
||||
targetListener net.Listener
|
||||
target http.Server
|
||||
targetURL string
|
||||
|
||||
// httpListener/httpProxy is an HTTP proxy that can proxy to
|
||||
// target.
|
||||
httpListener net.Listener
|
||||
httpProxy http.Server
|
||||
|
||||
// socksListener/socksProxy is a SOCKS5 proxy that can dial
|
||||
// targetListener.
|
||||
socksListener net.Listener
|
||||
socksProxy *socks5.Server
|
||||
|
||||
// jointListener is the mux that serves both HTTP and SOCKS5
|
||||
// proxying.
|
||||
jointListener net.Listener
|
||||
|
||||
// httpClient and socksClient are HTTP clients configured to proxy
|
||||
// through httpProxy and socksProxy respectively.
|
||||
httpClient *http.Client
|
||||
socksClient *http.Client
|
||||
}
|
||||
|
||||
func (s *world) checkURL(c *http.Client, wantErr bool) {
|
||||
s.t.Helper()
|
||||
resp, err := c.Get(s.targetURL)
|
||||
if wantErr {
|
||||
if err == nil {
|
||||
s.t.Errorf("HTTP request succeeded unexpectedly: got HTTP code %d, wanted failure", resp.StatusCode)
|
||||
}
|
||||
} else if err != nil {
|
||||
s.t.Errorf("HTTP request failed: %v", err)
|
||||
} else if c := resp.StatusCode; c != http.StatusTeapot {
|
||||
s.t.Errorf("unexpected status code: got %d, want %d", c, http.StatusTeapot)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *world) Close() {
|
||||
s.jointListener.Close()
|
||||
s.socksListener.Close()
|
||||
s.httpProxy.Close()
|
||||
s.httpListener.Close()
|
||||
s.target.Close()
|
||||
s.targetListener.Close()
|
||||
}
|
||||
|
||||
func mkWorld(t *testing.T) (ret *world) {
|
||||
t.Helper()
|
||||
|
||||
ret = &world{
|
||||
t: t,
|
||||
}
|
||||
var err error
|
||||
|
||||
ret.targetListener, err = net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ret.target = http.Server{
|
||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusTeapot)
|
||||
}),
|
||||
}
|
||||
go ret.target.Serve(ret.targetListener)
|
||||
ret.targetURL = fmt.Sprintf("http://%s/", ret.targetListener.Addr().String())
|
||||
|
||||
ret.jointListener, err = net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ret.socksListener, ret.httpListener = SplitSOCKSAndHTTP(ret.jointListener)
|
||||
|
||||
httpProxy := http.Server{
|
||||
Handler: httputil.NewSingleHostReverseProxy(&url.URL{
|
||||
Scheme: "http",
|
||||
Host: ret.targetListener.Addr().String(),
|
||||
Path: "/",
|
||||
}),
|
||||
}
|
||||
go httpProxy.Serve(ret.httpListener)
|
||||
|
||||
socksProxy := socks5.Server{}
|
||||
go socksProxy.Serve(ret.socksListener)
|
||||
|
||||
ret.httpClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: func(*http.Request) (*url.URL, error) {
|
||||
return &url.URL{
|
||||
Scheme: "http",
|
||||
Host: ret.jointListener.Addr().String(),
|
||||
Path: "/",
|
||||
}, nil
|
||||
},
|
||||
DisableKeepAlives: true, // one connection per request
|
||||
},
|
||||
}
|
||||
|
||||
ret.socksClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: func(*http.Request) (*url.URL, error) {
|
||||
return &url.URL{
|
||||
Scheme: "socks5",
|
||||
Host: ret.jointListener.Addr().String(),
|
||||
Path: "/",
|
||||
}, nil
|
||||
},
|
||||
DisableKeepAlives: true, // one connection per request
|
||||
},
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package tssocks is the glue between Tailscale and the net/socks5 package.
|
||||
package tssocks
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/socks5"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
)
|
||||
|
||||
// NewServer returns a new SOCKS5 server configured to dial out to
|
||||
// Tailscale addresses.
|
||||
//
|
||||
// The returned server is not yet listening. The caller must call
|
||||
// Serve with a listener.
|
||||
//
|
||||
// If ns is non-nil, it is used for dialing when needed.
|
||||
func NewServer(logf logger.Logf, e wgengine.Engine, ns *netstack.Impl) *socks5.Server {
|
||||
d := &dialer{ns: ns}
|
||||
e.AddNetworkMapCallback(d.onNewNetmap)
|
||||
return &socks5.Server{
|
||||
Logf: logf,
|
||||
Dialer: d.DialContext,
|
||||
}
|
||||
}
|
||||
|
||||
// dialer is the Tailscale SOCKS5 dialer.
|
||||
type dialer struct {
|
||||
ns *netstack.Impl
|
||||
|
||||
mu sync.Mutex
|
||||
dns netstack.DNSMap
|
||||
}
|
||||
|
||||
func (d *dialer) onNewNetmap(nm *netmap.NetworkMap) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.dns = netstack.DNSMapFromNetworkMap(nm)
|
||||
}
|
||||
|
||||
func (d *dialer) resolve(ctx context.Context, addr string) (netaddr.IPPort, error) {
|
||||
d.mu.Lock()
|
||||
dns := d.dns
|
||||
d.mu.Unlock()
|
||||
return dns.Resolve(ctx, addr)
|
||||
}
|
||||
|
||||
func (d *dialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
ipp, err := d.resolve(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d.ns != nil && d.useNetstackForIP(ipp.IP()) {
|
||||
return d.ns.DialContextTCP(ctx, ipp.String())
|
||||
}
|
||||
var stdDialer net.Dialer
|
||||
return stdDialer.DialContext(ctx, network, ipp.String())
|
||||
}
|
||||
|
||||
func (d *dialer) useNetstackForIP(ip netaddr.IP) bool {
|
||||
if d.ns == nil {
|
||||
return false
|
||||
}
|
||||
// TODO(bradfitz): this isn't exactly right.
|
||||
// We should also support subnets when the
|
||||
// prefs are configured as such.
|
||||
return tsaddr.IsTailscaleIP(ip)
|
||||
}
|
||||
115
net/tsdial/dnsmap.go
Normal file
115
net/tsdial/dnsmap.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// 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 tsdial
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
// dnsMap maps MagicDNS names (both base + FQDN) to their first IP.
|
||||
// It must not be mutated once created.
|
||||
//
|
||||
// Example keys are "foo.domain.tld.beta.tailscale.net" and "foo",
|
||||
// both without trailing dots.
|
||||
type dnsMap map[string]netaddr.IP
|
||||
|
||||
func dnsMapFromNetworkMap(nm *netmap.NetworkMap) dnsMap {
|
||||
if nm == nil {
|
||||
return nil
|
||||
}
|
||||
ret := make(dnsMap)
|
||||
suffix := nm.MagicDNSSuffix()
|
||||
have4 := false
|
||||
if nm.Name != "" && len(nm.Addresses) > 0 {
|
||||
ip := nm.Addresses[0].IP()
|
||||
ret[strings.TrimRight(nm.Name, ".")] = ip
|
||||
if dnsname.HasSuffix(nm.Name, suffix) {
|
||||
ret[dnsname.TrimSuffix(nm.Name, suffix)] = ip
|
||||
}
|
||||
for _, a := range nm.Addresses {
|
||||
if a.IP().Is4() {
|
||||
have4 = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
if p.Name == "" {
|
||||
continue
|
||||
}
|
||||
for _, a := range p.Addresses {
|
||||
ip := a.IP()
|
||||
if ip.Is4() && !have4 {
|
||||
continue
|
||||
}
|
||||
ret[strings.TrimRight(p.Name, ".")] = ip
|
||||
if dnsname.HasSuffix(p.Name, suffix) {
|
||||
ret[dnsname.TrimSuffix(p.Name, suffix)] = ip
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, rec := range nm.DNS.ExtraRecords {
|
||||
if rec.Type != "" {
|
||||
continue
|
||||
}
|
||||
ip, err := netaddr.ParseIP(rec.Value)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
ret[strings.TrimRight(rec.Name, ".")] = ip
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// errUnresolved is a sentinel error returned by dnsMap.resolveMemory.
|
||||
var errUnresolved = errors.New("address well formed but not resolved")
|
||||
|
||||
func splitHostPort(addr string) (host string, port uint16, err error) {
|
||||
host, portStr, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
port16, err := strconv.ParseUint(portStr, 10, 16)
|
||||
if err != nil {
|
||||
return "", 0, fmt.Errorf("invalid port in address %q", addr)
|
||||
}
|
||||
return host, uint16(port16), nil
|
||||
}
|
||||
|
||||
// Resolve resolves addr into an IP:port using first the MagicDNS contents
|
||||
// of m, else using the system resolver.
|
||||
//
|
||||
// The error is [exactly] errUnresolved if the addr is a name that isn't known
|
||||
// in the map.
|
||||
func (m dnsMap) resolveMemory(ctx context.Context, network, addr string) (_ netaddr.IPPort, err error) {
|
||||
host, port, err := splitHostPort(addr)
|
||||
if err != nil {
|
||||
// addr malformed or invalid port.
|
||||
return netaddr.IPPort{}, err
|
||||
}
|
||||
if ip, err := netaddr.ParseIP(host); err == nil {
|
||||
// addr was literal ip:port.
|
||||
return netaddr.IPPortFrom(ip, port), nil
|
||||
}
|
||||
|
||||
// Host is not an IP, so assume it's a DNS name.
|
||||
|
||||
// Try MagicDNS first, otherwise a real DNS lookup.
|
||||
ip := m[host]
|
||||
if !ip.IsZero() {
|
||||
return netaddr.IPPortFrom(ip, port), nil
|
||||
}
|
||||
|
||||
return netaddr.IPPort{}, errUnresolved
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package netstack
|
||||
package tsdial
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
@@ -19,7 +19,7 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
nm *netmap.NetworkMap
|
||||
want DNSMap
|
||||
want dnsMap
|
||||
}{
|
||||
{
|
||||
name: "self",
|
||||
@@ -30,7 +30,7 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
|
||||
pfx("100::123/128"),
|
||||
},
|
||||
},
|
||||
want: DNSMap{
|
||||
want: dnsMap{
|
||||
"foo": ip("100.102.103.104"),
|
||||
"foo.tailnet": ip("100.102.103.104"),
|
||||
},
|
||||
@@ -59,7 +59,7 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
want: DNSMap{
|
||||
want: dnsMap{
|
||||
"foo": ip("100.102.103.104"),
|
||||
"foo.tailnet": ip("100.102.103.104"),
|
||||
"a": ip("100.0.0.201"),
|
||||
@@ -91,7 +91,7 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
want: DNSMap{
|
||||
want: dnsMap{
|
||||
"foo": ip("100::123"),
|
||||
"foo.tailnet": ip("100::123"),
|
||||
"a": ip("100::201"),
|
||||
@@ -103,7 +103,7 @@ func TestDNSMapFromNetworkMap(t *testing.T) {
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := DNSMapFromNetworkMap(tt.nm)
|
||||
got := dnsMapFromNetworkMap(tt.nm)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("mismatch:\n got %v\nwant %v\n", got, tt.want)
|
||||
}
|
||||
101
net/tsdial/dohclient.go
Normal file
101
net/tsdial/dohclient.go
Normal file
@@ -0,0 +1,101 @@
|
||||
// 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 tsdial
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/dnscache"
|
||||
)
|
||||
|
||||
// dohConn is a net.PacketConn suitable for returning from
|
||||
// net.Dialer.Dial to send DNS queries over PeerAPI to exit nodes'
|
||||
// ExitDNS DoH proxy service.
|
||||
type dohConn struct {
|
||||
ctx context.Context
|
||||
baseURL string
|
||||
hc *http.Client // if nil, default is used
|
||||
dnsCache *dnscache.MessageCache
|
||||
|
||||
rbuf bytes.Buffer
|
||||
}
|
||||
|
||||
var (
|
||||
_ net.Conn = (*dohConn)(nil)
|
||||
_ net.PacketConn = (*dohConn)(nil) // be a PacketConn to change net.Resolver semantics
|
||||
)
|
||||
|
||||
func (*dohConn) Close() error { return nil }
|
||||
func (*dohConn) LocalAddr() net.Addr { return todoAddr{} }
|
||||
func (*dohConn) RemoteAddr() net.Addr { return todoAddr{} }
|
||||
func (*dohConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (*dohConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (*dohConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
|
||||
func (c *dohConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
|
||||
return c.Write(p)
|
||||
}
|
||||
|
||||
func (c *dohConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
|
||||
n, err = c.Read(p)
|
||||
return n, todoAddr{}, err
|
||||
}
|
||||
|
||||
func (c *dohConn) Read(p []byte) (n int, err error) {
|
||||
return c.rbuf.Read(p)
|
||||
}
|
||||
|
||||
func (c *dohConn) Write(packet []byte) (n int, err error) {
|
||||
if c.dnsCache != nil {
|
||||
err := c.dnsCache.ReplyFromCache(&c.rbuf, packet)
|
||||
if err == nil {
|
||||
// Cache hit.
|
||||
// TODO(bradfitz): add clientmetric
|
||||
return len(packet), nil
|
||||
}
|
||||
c.rbuf.Reset()
|
||||
}
|
||||
req, err := http.NewRequestWithContext(c.ctx, "POST", c.baseURL, bytes.NewReader(packet))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
const dohType = "application/dns-message"
|
||||
req.Header.Set("Content-Type", dohType)
|
||||
hc := c.hc
|
||||
if hc == nil {
|
||||
hc = http.DefaultClient
|
||||
}
|
||||
hres, err := hc.Do(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer hres.Body.Close()
|
||||
if hres.StatusCode != 200 {
|
||||
return 0, errors.New(hres.Status)
|
||||
}
|
||||
if ct := hres.Header.Get("Content-Type"); ct != dohType {
|
||||
return 0, fmt.Errorf("unexpected response Content-Type %q", ct)
|
||||
}
|
||||
_, err = io.Copy(&c.rbuf, hres.Body)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if c.dnsCache != nil {
|
||||
c.dnsCache.AddCacheEntry(packet, c.rbuf.Bytes())
|
||||
}
|
||||
return len(packet), nil
|
||||
}
|
||||
|
||||
type todoAddr struct{}
|
||||
|
||||
func (todoAddr) Network() string { return "unused" }
|
||||
func (todoAddr) String() string { return "unused-todoAddr" }
|
||||
32
net/tsdial/dohclient_test.go
Normal file
32
net/tsdial/dohclient_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// 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 tsdial
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var dohBase = flag.String("doh-base", "", "DoH base URL for manual DoH tests; e.g. \"http://100.68.82.120:47830/dns-query\"")
|
||||
|
||||
func TestDoHResolve(t *testing.T) {
|
||||
if *dohBase == "" {
|
||||
t.Skip("skipping manual test without --doh-base= set")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
var r net.Resolver
|
||||
r.Dial = func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return &dohConn{ctx: ctx, baseURL: *dohBase}, nil
|
||||
}
|
||||
addrs, err := r.LookupIP(ctx, "ip4", "google.com.")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Got: %q", addrs)
|
||||
}
|
||||
43
net/tsdial/peerapi_macios_ext.go
Normal file
43
net/tsdial/peerapi_macios_ext.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file's built on iOS and on two of three macOS build variants:
|
||||
// the two GUI variants that both use Extensions (Network Extension
|
||||
// and System Extension). It's not used on tailscaled-on-macOS.
|
||||
|
||||
//go:build ts_macext && (darwin || ios)
|
||||
// +build ts_macext
|
||||
// +build darwin ios
|
||||
|
||||
package tsdial
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"syscall"
|
||||
|
||||
"tailscale.com/net/netns"
|
||||
)
|
||||
|
||||
func init() {
|
||||
peerDialControlFunc = peerDialControlFuncNetworkExtension
|
||||
}
|
||||
|
||||
func peerDialControlFuncNetworkExtension(d *Dialer) func(network, address string, c syscall.RawConn) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
index := -1
|
||||
if x, ok := d.interfaceIndexLocked(d.tunName); ok {
|
||||
index = x
|
||||
}
|
||||
var lc net.ListenConfig
|
||||
netns.SetListenConfigInterfaceIndex(&lc, index)
|
||||
return func(network, address string, c syscall.RawConn) error {
|
||||
if index == -1 {
|
||||
return errors.New("failed to find TUN interface to bind to")
|
||||
}
|
||||
return lc.Control(network, address, c)
|
||||
}
|
||||
}
|
||||
281
net/tsdial/tsdial.go
Normal file
281
net/tsdial/tsdial.go
Normal file
@@ -0,0 +1,281 @@
|
||||
// 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 tsdial provides a Dialer type that can dial out of tailscaled.
|
||||
package tsdial
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netknob"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
// Dialer dials out of tailscaled, while taking care of details while
|
||||
// handling the dozens of edge cases depending on the server mode
|
||||
// (TUN, netstack), the OS network sandboxing style (macOS/iOS
|
||||
// Extension, none), user-selected route acceptance prefs, etc.
|
||||
type Dialer struct {
|
||||
// UseNetstackForIP if non-nil is whether NetstackDialTCP (if
|
||||
// it's non-nil) should be used to dial the provided IP.
|
||||
UseNetstackForIP func(netaddr.IP) bool
|
||||
|
||||
// NetstackDialTCP dials the provided IPPort using netstack.
|
||||
// If nil, it's not used.
|
||||
NetstackDialTCP func(context.Context, netaddr.IPPort) (net.Conn, error)
|
||||
|
||||
peerDialControlFuncAtomic atomic.Value // of func() func(network, address string, c syscall.RawConn) error
|
||||
|
||||
peerClientOnce sync.Once
|
||||
peerClient *http.Client
|
||||
|
||||
peerDialerOnce sync.Once
|
||||
peerDialer *net.Dialer
|
||||
|
||||
mu sync.Mutex
|
||||
dns dnsMap
|
||||
tunName string // tun device name
|
||||
linkMon *monitor.Mon
|
||||
exitDNSDoHBase string // non-empty if DoH-proxying exit node in use; base URL+path (without '?')
|
||||
dnsCache *dnscache.MessageCache // nil until first first non-empty SetExitDNSDoH
|
||||
}
|
||||
|
||||
// SetTUNName sets the name of the tun device in use ("tailscale0", "utun6",
|
||||
// etc). This is needed on some platforms to set sockopts to bind
|
||||
// to the same interface index.
|
||||
func (d *Dialer) SetTUNName(name string) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.tunName = name
|
||||
}
|
||||
|
||||
// TUNName returns the name of the tun device in use, if any.
|
||||
// Example format ("tailscale0", "utun6").
|
||||
func (d *Dialer) TUNName() string {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
return d.tunName
|
||||
}
|
||||
|
||||
// SetExitDNSDoH sets (or clears) the exit node DNS DoH server base URL to use.
|
||||
// The doh URL should contain the scheme, authority, and path, but without
|
||||
// a '?' and/or query parameters.
|
||||
//
|
||||
// For example, "http://100.68.82.120:47830/dns-query".
|
||||
func (d *Dialer) SetExitDNSDoH(doh string) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
if d.exitDNSDoHBase == doh {
|
||||
return
|
||||
}
|
||||
d.exitDNSDoHBase = doh
|
||||
if doh != "" && d.dnsCache == nil {
|
||||
d.dnsCache = new(dnscache.MessageCache)
|
||||
}
|
||||
if d.dnsCache != nil {
|
||||
d.dnsCache.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Dialer) SetLinkMonitor(mon *monitor.Mon) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.linkMon = mon
|
||||
}
|
||||
|
||||
func (d *Dialer) interfaceIndexLocked(ifName string) (index int, ok bool) {
|
||||
if d.linkMon == nil {
|
||||
return 0, false
|
||||
}
|
||||
st := d.linkMon.InterfaceState()
|
||||
iface, ok := st.Interface[ifName]
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
return iface.Index, true
|
||||
}
|
||||
|
||||
// peerDialControlFunc is non-nil on platforms that require a way to
|
||||
// bind to dial out to other peers.
|
||||
var peerDialControlFunc func(*Dialer) func(network, address string, c syscall.RawConn) error
|
||||
|
||||
// PeerDialControlFunc returns a function
|
||||
// that can assigned to net.Dialer.Control to set sockopts or whatnot
|
||||
// to make a dial escape the current platform's network sandbox.
|
||||
//
|
||||
// On many platforms the returned func will be nil.
|
||||
//
|
||||
// Notably, this is non-nil on iOS and macOS when run as a Network or
|
||||
// System Extension (the GUI variants).
|
||||
func (d *Dialer) PeerDialControlFunc() func(network, address string, c syscall.RawConn) error {
|
||||
if peerDialControlFunc == nil {
|
||||
return nil
|
||||
}
|
||||
return peerDialControlFunc(d)
|
||||
}
|
||||
|
||||
// SetNetMap sets the current network map and notably, the DNS names
|
||||
// in its DNS configuration.
|
||||
func (d *Dialer) SetNetMap(nm *netmap.NetworkMap) {
|
||||
m := dnsMapFromNetworkMap(nm)
|
||||
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.dns = m
|
||||
}
|
||||
|
||||
func (d *Dialer) userDialResolve(ctx context.Context, network, addr string) (netaddr.IPPort, error) {
|
||||
d.mu.Lock()
|
||||
dns := d.dns
|
||||
exitDNSDoH := d.exitDNSDoHBase
|
||||
d.mu.Unlock()
|
||||
|
||||
// MagicDNS or otherwise baked in to the NetworkMap? Try that first.
|
||||
ipp, err := dns.resolveMemory(ctx, network, addr)
|
||||
if err != errUnresolved {
|
||||
return ipp, err
|
||||
}
|
||||
|
||||
// Otherwise, hit the network.
|
||||
|
||||
// TODO(bradfitz): wire up net/dnscache too.
|
||||
|
||||
host, port, err := splitHostPort(addr)
|
||||
if err != nil {
|
||||
// addr is malformed.
|
||||
return netaddr.IPPort{}, err
|
||||
}
|
||||
|
||||
var r net.Resolver
|
||||
if exitDNSDoH != "" && runtime.GOOS != "windows" { // Windows: https://github.com/golang/go/issues/33097
|
||||
r.PreferGo = true
|
||||
r.Dial = func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return &dohConn{
|
||||
ctx: ctx,
|
||||
baseURL: exitDNSDoH,
|
||||
hc: d.PeerAPIHTTPClient(),
|
||||
dnsCache: d.dnsCache,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
ips, err := r.LookupIP(ctx, ipNetOfNetwork(network), host)
|
||||
if err != nil {
|
||||
return netaddr.IPPort{}, err
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return netaddr.IPPort{}, fmt.Errorf("DNS lookup returned no results for %q", host)
|
||||
}
|
||||
ip, _ := netaddr.FromStdIP(ips[0])
|
||||
return netaddr.IPPortFrom(ip, port), nil
|
||||
}
|
||||
|
||||
// ipNetOfNetwork returns "ip", "ip4", or "ip6" corresponding
|
||||
// to the input value of "tcp", "tcp4", "udp6" etc network
|
||||
// names.
|
||||
func ipNetOfNetwork(n string) string {
|
||||
if strings.HasSuffix(n, "4") {
|
||||
return "ip4"
|
||||
}
|
||||
if strings.HasSuffix(n, "6") {
|
||||
return "ip6"
|
||||
}
|
||||
return "ip"
|
||||
}
|
||||
|
||||
// UserDial connects to the provided network address as if a user were initiating the dial.
|
||||
// (e.g. from a SOCKS or HTTP outbound proxy)
|
||||
func (d *Dialer) UserDial(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
ipp, err := d.userDialResolve(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if d.UseNetstackForIP != nil && d.UseNetstackForIP(ipp.IP()) {
|
||||
if d.NetstackDialTCP == nil {
|
||||
return nil, errors.New("Dialer not initialized correctly")
|
||||
}
|
||||
return d.NetstackDialTCP(ctx, ipp)
|
||||
}
|
||||
// TODO(bradfitz): netns, etc
|
||||
var stdDialer net.Dialer
|
||||
return stdDialer.DialContext(ctx, network, ipp.String())
|
||||
}
|
||||
|
||||
// dialPeerAPI connects to a Tailscale peer's peerapi over TCP.
|
||||
//
|
||||
// network must a "tcp" type, and addr must be an ip:port. Name resolution
|
||||
// is not supported.
|
||||
func (d *Dialer) dialPeerAPI(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
switch network {
|
||||
case "tcp", "tcp6", "tcp4":
|
||||
default:
|
||||
return nil, fmt.Errorf("peerAPI dial requires tcp; %q not supported", network)
|
||||
}
|
||||
ipp, err := netaddr.ParseIPPort(addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("peerAPI dial requires ip:port, not name resolution: %w", err)
|
||||
}
|
||||
if d.UseNetstackForIP != nil && d.UseNetstackForIP(ipp.IP()) {
|
||||
if d.NetstackDialTCP == nil {
|
||||
return nil, errors.New("Dialer not initialized correctly")
|
||||
}
|
||||
return d.NetstackDialTCP(ctx, ipp)
|
||||
}
|
||||
return d.getPeerDialer().DialContext(ctx, network, addr)
|
||||
}
|
||||
|
||||
// getPeerDialer returns the *net.Dialer to use to dial peers to use
|
||||
// peer API.
|
||||
//
|
||||
// This is not used in netstack mode.
|
||||
//
|
||||
// The primary function of this is to work on macOS & iOS's in the
|
||||
// Network/System Extension so it can mark the dialer as staying
|
||||
// withing the network namespace/sandbox.
|
||||
func (d *Dialer) getPeerDialer() *net.Dialer {
|
||||
d.peerDialerOnce.Do(func() {
|
||||
d.peerDialer = &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: netknob.PlatformTCPKeepAlive(),
|
||||
Control: d.PeerDialControlFunc(),
|
||||
}
|
||||
})
|
||||
return d.peerDialer
|
||||
}
|
||||
|
||||
// PeerAPIHTTPClient returns an HTTP Client to call peers' peerapi
|
||||
// endpoints. //
|
||||
// The returned Client must not be mutated; it's owned by the Dialer
|
||||
// and shared by callers.
|
||||
func (d *Dialer) PeerAPIHTTPClient() *http.Client {
|
||||
d.peerClientOnce.Do(func() {
|
||||
t := http.DefaultTransport.(*http.Transport).Clone()
|
||||
t.Dial = nil
|
||||
t.DialContext = d.dialPeerAPI
|
||||
d.peerClient = &http.Client{Transport: t}
|
||||
})
|
||||
return d.peerClient
|
||||
}
|
||||
|
||||
// PeerAPITransport returns a Transport to call peers' peerapi
|
||||
// endpoints.
|
||||
//
|
||||
// The returned value must not be mutated; it's owned by the Dialer
|
||||
// and shared by callers.
|
||||
func (d *Dialer) PeerAPITransport() *http.Transport {
|
||||
return d.PeerAPIHTTPClient().Transport.(*http.Transport)
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
@@ -119,13 +119,13 @@ func ensureStateDirPerms(dirPath string) error {
|
||||
// We configure the DACL such that any files or directories created within
|
||||
// dirPath will also inherit this DACL.
|
||||
explicitAccess := []windows.EXPLICIT_ACCESS{
|
||||
windows.EXPLICIT_ACCESS{
|
||||
{
|
||||
windows.GENERIC_ALL,
|
||||
windows.SET_ACCESS,
|
||||
windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
|
||||
userTrustee,
|
||||
},
|
||||
windows.EXPLICIT_ACCESS{
|
||||
{
|
||||
windows.GENERIC_ALL,
|
||||
windows.SET_ACCESS,
|
||||
windows.SUB_CONTAINERS_AND_OBJECTS_INHERIT,
|
||||
|
||||
@@ -15,13 +15,13 @@ func TestParsePort(t *testing.T) {
|
||||
expect int
|
||||
}
|
||||
tests := []InOut{
|
||||
InOut{"1.2.3.4:5678", 5678},
|
||||
InOut{"0.0.0.0.999", 999},
|
||||
InOut{"1.2.3.4:*", 0},
|
||||
InOut{"5.5.5.5:0", 0},
|
||||
InOut{"[1::2]:5", 5},
|
||||
InOut{"[1::2].5", 5},
|
||||
InOut{"gibberish", -1},
|
||||
{"1.2.3.4:5678", 5678},
|
||||
{"0.0.0.0.999", 999},
|
||||
{"1.2.3.4:*", 0},
|
||||
{"5.5.5.5:0", 0},
|
||||
{"[1::2]:5", 5},
|
||||
{"[1::2].5", 5},
|
||||
{"gibberish", -1},
|
||||
}
|
||||
|
||||
for _, io := range tests {
|
||||
|
||||
@@ -48,7 +48,9 @@ func TestBasics(t *testing.T) {
|
||||
}()
|
||||
|
||||
go func() {
|
||||
c, err := Connect(sock, port)
|
||||
s := DefaultConnectionStrategy(sock)
|
||||
s.UsePort(port)
|
||||
c, err := Connect(s)
|
||||
if err != nil {
|
||||
errs <- err
|
||||
return
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func connect(path string, port uint16) (net.Conn, error) {
|
||||
pipe, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", port))
|
||||
func connect(s *ConnectionStrategy) (net.Conn, error) {
|
||||
pipe, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", s.port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user