Compare commits
82 Commits
aaron/logl
...
v1.20.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e643357dc | ||
|
|
59a1b849f0 | ||
|
|
296d10a05d | ||
|
|
f37cdaefa7 | ||
|
|
1e6ca50e3b | ||
|
|
dbb6597381 | ||
|
|
2e80227276 | ||
|
|
96b76e4cc6 | ||
|
|
ec04759c41 | ||
|
|
88c4bde778 | ||
|
|
7052c6fe25 | ||
|
|
e510abc8d0 | ||
|
|
958917dce8 | ||
|
|
7c1a1aa5d9 | ||
|
|
90423bf3de | ||
|
|
0028a8d4d5 | ||
|
|
8519cab83d | ||
|
|
4cd062071c | ||
|
|
04a7f5066d | ||
|
|
a14d445fc7 | ||
|
|
1a4293c15c | ||
|
|
b8ad90c2bf | ||
|
|
b1b0fd119b | ||
|
|
1dc1c8b709 | ||
|
|
408522ddad | ||
|
|
1ffc21ad71 | ||
|
|
dee0833b27 | ||
|
|
b03170b901 | ||
|
|
c5243562d7 | ||
|
|
1a4e8da084 | ||
|
|
138662e248 | ||
|
|
1b426cc232 | ||
|
|
8d0ed1c9ba | ||
|
|
e68d87eb44 | ||
|
|
2cfc96aa90 | ||
|
|
addda5b96f | ||
|
|
64c2657448 | ||
|
|
3690bfecb0 | ||
|
|
28bf53f502 | ||
|
|
c8b63a409e | ||
|
|
a201b89e4a | ||
|
|
506c727e30 | ||
|
|
e2d9c99e5b | ||
|
|
01a9906bf8 | ||
|
|
2aeb93003f | ||
|
|
2513d2d728 | ||
|
|
dd45bba76b | ||
|
|
ebdd25920e | ||
|
|
431329e47c | ||
|
|
7d9b1de3aa | ||
|
|
2c94e3c4ad | ||
|
|
04c2c5bd80 | ||
|
|
96cab21383 | ||
|
|
63d9c7b9b3 | ||
|
|
b09000ad5d | ||
|
|
eb26c081b1 | ||
|
|
44937b59e7 | ||
|
|
535b925d1b | ||
|
|
434af15a04 | ||
|
|
bc537adb1a | ||
|
|
0aa4c6f147 | ||
|
|
ae319b4636 | ||
|
|
c7f5bc0f69 | ||
|
|
81bc812402 | ||
|
|
0848b36dd2 | ||
|
|
39f22a357d | ||
|
|
394c9de02b | ||
|
|
c7052154d5 | ||
|
|
3dedcd1640 | ||
|
|
5a9914a92f | ||
|
|
66164b9307 | ||
|
|
40e2b312b6 | ||
|
|
689426d6bc | ||
|
|
add6dc8ccc | ||
|
|
894693f352 | ||
|
|
4512e213d5 | ||
|
|
8f43ddf1a2 | ||
|
|
681d4897cc | ||
|
|
93ae11105d | ||
|
|
84a1106fa7 | ||
|
|
aac974a5e5 | ||
|
|
6590fc3a94 |
2
.github/workflows/cifuzz.yml
vendored
2
.github/workflows/cifuzz.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
dry-run: false
|
||||
language: go
|
||||
- name: Upload Crash
|
||||
uses: actions/upload-artifact@v2.2.4
|
||||
uses: actions/upload-artifact@v2.3.1
|
||||
if: failure() && steps.build.outcome == 'success'
|
||||
with:
|
||||
name: artifacts
|
||||
|
||||
2
.github/workflows/cross-darwin.yml
vendored
2
.github/workflows/cross-darwin.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
2
.github/workflows/cross-freebsd.yml
vendored
2
.github/workflows/cross-freebsd.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
2
.github/workflows/cross-openbsd.yml
vendored
2
.github/workflows/cross-openbsd.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
2
.github/workflows/cross-windows.yml
vendored
2
.github/workflows/cross-windows.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
2
.github/workflows/depaware.yml
vendored
2
.github/workflows/depaware.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
|
||||
|
||||
2
.github/workflows/go_generate.yml
vendored
2
.github/workflows/go_generate.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
|
||||
|
||||
2
.github/workflows/license.yml
vendored
2
.github/workflows/license.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
|
||||
|
||||
2
.github/workflows/linux-race.yml
vendored
2
.github/workflows/linux-race.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
2
.github/workflows/linux.yml
vendored
2
.github/workflows/linux.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
2
.github/workflows/linux32.yml
vendored
2
.github/workflows/linux32.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
2
.github/workflows/staticcheck.yml
vendored
2
.github/workflows/staticcheck.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
|
||||
|
||||
6
.github/workflows/vm.yml
vendored
6
.github/workflows/vm.yml
vendored
@@ -12,11 +12,13 @@ jobs:
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
- name: Set GOPATH
|
||||
run: echo "GOPATH=$HOME/go" >> $GITHUB_ENV
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v1
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17
|
||||
id: go
|
||||
|
||||
- name: Checkout Code
|
||||
uses: actions/checkout@v1
|
||||
|
||||
2
.github/workflows/windows-race.yml
vendored
2
.github/workflows/windows-race.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17.x
|
||||
|
||||
|
||||
2
.github/workflows/windows.yml
vendored
2
.github/workflows/windows.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
||||
steps:
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v2.1.4
|
||||
uses: actions/setup-go@v2.1.5
|
||||
with:
|
||||
go-version: 1.17.x
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ ARG VERSION_GIT_HASH=""
|
||||
ENV VERSION_GIT_HASH=$VERSION_GIT_HASH
|
||||
ARG TARGETARCH
|
||||
|
||||
RUN GOARCH=$TARGETARCH go install -tags=xversion -ldflags="\
|
||||
RUN GOARCH=$TARGETARCH go install -ldflags="\
|
||||
-X tailscale.com/version.Long=$VERSION_LONG \
|
||||
-X tailscale.com/version.Short=$VERSION_SHORT \
|
||||
-X tailscale.com/version.GitCommit=$VERSION_GIT_HASH" \
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# Use of this source code is governed by a BSD-style
|
||||
# license that can be found in the LICENSE file.
|
||||
|
||||
FROM alpine:3.14
|
||||
FROM alpine:3.15
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.19.0
|
||||
1.20.2
|
||||
|
||||
158
api.md
158
api.md
@@ -23,6 +23,11 @@ Currently based on {some authentication method}. Visit the [admin panel](https:/
|
||||
- [POST tailnet ACL validate](#tailnet-acl-validate-post): run validation tests against the tailnet's existing ACL
|
||||
- [Devices](#tailnet-devices)
|
||||
- [GET tailnet devices](#tailnet-devices-get)
|
||||
- [Keys](#tailnet-keys)
|
||||
- [GET tailnet keys](#tailnet-keys-get)
|
||||
- [POST tailnet key](#tailnet-keys-post)
|
||||
- [GET tailnet key](#tailnet-keys-key-get)
|
||||
- [DELETE tailnet key](#tailnet-keys-key-delete)
|
||||
- [DNS](#tailnet-dns)
|
||||
- [GET tailnet DNS nameservers](#tailnet-dns-nameservers-get)
|
||||
- [POST tailnet DNS nameservers](#tailnet-dns-nameservers-post)
|
||||
@@ -670,6 +675,159 @@ Response
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-keys></a>
|
||||
|
||||
### Keys
|
||||
|
||||
<a name=tailnet-keys-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/keys` - list the keys for a tailnet
|
||||
|
||||
Returns a list of active keys for a tailnet
|
||||
for the user who owns the API key used to perform this query.
|
||||
Supply the tailnet of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Returns
|
||||
|
||||
Returns a JSON object with the IDs of all active keys.
|
||||
This includes both API keys and also machine authentication keys.
|
||||
In the future, this may provide more information about each key than just the ID.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/keys' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{"keys": [
|
||||
{"id": "kYKVU14CNTRL"},
|
||||
{"id": "k68VdZ3CNTRL"},
|
||||
{"id": "kJ9nq43CNTRL"},
|
||||
{"id": "kkThgj1CNTRL"}
|
||||
]}
|
||||
```
|
||||
|
||||
<a name=tailnet-keys-post></a>
|
||||
|
||||
#### `POST /api/v2/tailnet/:tailnet/keys` - create a new key for a tailnet
|
||||
|
||||
Create a new key in a tailnet associated
|
||||
with the user who owns the API key used to perform this request.
|
||||
Supply the tailnet in the path.
|
||||
|
||||
##### Parameters
|
||||
|
||||
###### POST Body
|
||||
`capabilities` - A mapping of resources to permissible actions.
|
||||
```
|
||||
{
|
||||
"capabilities": {
|
||||
"devices": {
|
||||
"create": {
|
||||
"reusable": false,
|
||||
"ephemeral": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Returns
|
||||
|
||||
Returns a JSON object with the provided capabilities in addition to the
|
||||
generated key. The key should be recorded and kept safe and secure as it
|
||||
wields the capabilities specified in the request. The identity of the key
|
||||
is embedded in the key itself and can be used to perform operations on
|
||||
the key (e.g., revoking it or retrieving information about it).
|
||||
The full key can no longer be retrieved by the server.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
echo '{
|
||||
"capabilities": {
|
||||
"devices": {
|
||||
"create": {
|
||||
"reusable": false,
|
||||
"ephemeral": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}' | curl -X POST --data-binary @- https://api.tailscale.com/api/v2/tailnet/example.com/keys \
|
||||
-u "tskey-yourapikey123:" \
|
||||
-H "Content-Type: application/json" | jsonfmt
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"id": "k123456CNTRL",
|
||||
"key": "tskey-k123456CNTRL-abcdefghijklmnopqrstuvwxyz",
|
||||
"created": "2021-12-09T23:22:39Z",
|
||||
"expires": "2022-03-09T23:22:39Z",
|
||||
"capabilities": {"devices": {"create": {"reusable": false, "ephemeral": false}}}
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-keys-key-get></a>
|
||||
|
||||
#### `GET /api/v2/tailnet/:tailnet/keys/:keyid` - get information for a specific key
|
||||
|
||||
Returns a JSON object with information about specific key.
|
||||
Supply the tailnet and key ID of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Returns
|
||||
|
||||
Returns a JSON object with information about the key such as
|
||||
when it was created and when it expires.
|
||||
It also lists the capabilities associated with the key.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl 'https://api.tailscale.com/api/v2/tailnet/example.com/keys/k123456CNTRL' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
Response:
|
||||
```
|
||||
{
|
||||
"id": "k123456CNTRL",
|
||||
"created": "2021-12-09T22:13:53Z",
|
||||
"expires": "2022-03-09T22:13:53Z",
|
||||
"capabilities": {"devices": {"create": {"reusable": false, "ephemeral": false}}}
|
||||
}
|
||||
```
|
||||
|
||||
<a name=tailnet-keys-key-delete></a>
|
||||
|
||||
#### `DELETE /api/v2/tailnet/:tailnet/keys/:keyid` - delete a specific key
|
||||
|
||||
Deletes a specific key.
|
||||
Supply the tailnet and key ID of interest in the path.
|
||||
|
||||
##### Parameters
|
||||
No parameters.
|
||||
|
||||
##### Returns
|
||||
This reports status 200 upon success.
|
||||
|
||||
##### Example
|
||||
|
||||
```
|
||||
curl -X DELETE 'https://api.tailscale.com/api/v2/tailnet/example.com/keys/k123456CNTRL' \
|
||||
-u "tskey-yourapikey123:"
|
||||
```
|
||||
|
||||
<a name=tailnet-dns></a>
|
||||
|
||||
### DNS
|
||||
|
||||
@@ -45,4 +45,4 @@ EOF
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exec go build -ldflags "-X tailscale.com/version.Long=${LONG} -X tailscale.com/version.Short=${SHORT} -X tailscale.com/version.GitCommit=${GIT_HASH}" "$@"
|
||||
exec ./tool/go build -ldflags "-X tailscale.com/version.Long=${LONG} -X tailscale.com/version.Short=${SHORT} -X tailscale.com/version.GitCommit=${GIT_HASH}" "$@"
|
||||
|
||||
@@ -19,10 +19,20 @@
|
||||
|
||||
set -eu
|
||||
|
||||
# Use the "go" binary from the "tool" directory (which is github.com/tailscale/go)
|
||||
export PATH=$PWD/tool:$PATH
|
||||
|
||||
eval $(./build_dist.sh shellvars)
|
||||
DEFAULT_TAGS="v${VERSION_SHORT},v${VERSION_MINOR}"
|
||||
DEFAULT_REPOS="tailscale/tailscale,ghcr.io/tailscale/tailscale"
|
||||
DEFAULT_BASE="ghcr.io/tailscale/alpine-base:3.14"
|
||||
|
||||
PUSH="${PUSH:-false}"
|
||||
REPOS="${REPOS:-${DEFAULT_REPOS}}"
|
||||
TAGS="${TAGS:-${DEFAULT_TAGS}}"
|
||||
BASE="${BASE:-${DEFAULT_BASE}}"
|
||||
|
||||
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" \
|
||||
@@ -30,6 +40,7 @@ go run github.com/tailscale/mkctr@latest \
|
||||
-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
|
||||
--base="${BASE}" \
|
||||
--tags="${TAGS}" \
|
||||
--repos="${REPOS}" \
|
||||
--push="${PUSH}"
|
||||
|
||||
@@ -240,6 +240,16 @@ func BugReport(ctx context.Context, note string) (string, error) {
|
||||
return strings.TrimSpace(string(body)), nil
|
||||
}
|
||||
|
||||
// DebugAction invokes a debug action, such as "rebind" or "restun".
|
||||
// These are development tools and subject to change or removal over time.
|
||||
func DebugAction(ctx context.Context, action string) error {
|
||||
body, err := send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error %w: %s", err, body)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Status returns the Tailscale daemon's status.
|
||||
func Status(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return status(ctx, "")
|
||||
|
||||
@@ -67,8 +67,8 @@ func NewManualCertManager(certdir, hostname string) (certProvider, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can not load cert: %w", err)
|
||||
}
|
||||
if x509Cert.VerifyHostname(hostname) != nil {
|
||||
return nil, errors.New("refuse to load cert: hostname mismatch with key")
|
||||
if err := x509Cert.VerifyHostname(hostname); err != nil {
|
||||
return nil, fmt.Errorf("cert invalid for hostname %q: %w", hostname, err)
|
||||
}
|
||||
return &manualCertManager{cert: &cert, hostname: hostname}, nil
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// The hello binary runs hello.ipn.dev.
|
||||
// The hello binary runs hello.ts.net.
|
||||
package main // import "tailscale.com/cmd/hello"
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
@@ -16,6 +18,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
@@ -69,11 +72,31 @@ func main() {
|
||||
if *httpsAddr != "" {
|
||||
log.Printf("running HTTPS server on %s", *httpsAddr)
|
||||
go func() {
|
||||
errc <- http.ListenAndServeTLS(*httpsAddr,
|
||||
"/etc/hello/hello.ipn.dev.crt",
|
||||
"/etc/hello/hello.ipn.dev.key",
|
||||
nil,
|
||||
)
|
||||
hs := &http.Server{
|
||||
Addr: *httpsAddr,
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
switch hi.ServerName {
|
||||
case "hello.ts.net":
|
||||
return tailscale.GetCertificate(hi)
|
||||
case "hello.ipn.dev":
|
||||
c, err := tls.LoadX509KeyPair(
|
||||
"/etc/hello/hello.ipn.dev.crt",
|
||||
"/etc/hello/hello.ipn.dev.key",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
return nil, errors.New("invalid SNI name")
|
||||
},
|
||||
},
|
||||
IdleTimeout: 30 * time.Second,
|
||||
ReadHeaderTimeout: 20 * time.Second,
|
||||
MaxHeaderBytes: 10 << 10,
|
||||
}
|
||||
errc <- hs.ListenAndServeTLS("", "")
|
||||
}()
|
||||
}
|
||||
log.Fatal(<-errc)
|
||||
@@ -127,8 +150,9 @@ func tailscaleIP(who *apitype.WhoIsResponse) string {
|
||||
func root(w http.ResponseWriter, r *http.Request) {
|
||||
if r.TLS == nil && *httpsAddr != "" {
|
||||
host := r.Host
|
||||
if strings.Contains(r.Host, "100.101.102.103") {
|
||||
host = "hello.ipn.dev"
|
||||
if strings.Contains(r.Host, "100.101.102.103") ||
|
||||
strings.Contains(r.Host, "hello.ipn.dev") {
|
||||
host = "hello.ts.net"
|
||||
}
|
||||
http.Redirect(w, r, "https://"+host, http.StatusFound)
|
||||
return
|
||||
@@ -137,6 +161,10 @@ func root(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/", http.StatusFound)
|
||||
return
|
||||
}
|
||||
if r.TLS != nil && *httpsAddr != "" && strings.Contains(r.Host, "hello.ipn.dev") {
|
||||
http.Redirect(w, r, "https://hello.ts.net", http.StatusFound)
|
||||
return
|
||||
}
|
||||
tmpl, err := getTmpl()
|
||||
if err != nil {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
|
||||
46
cmd/printdep/printdep.go
Normal file
46
cmd/printdep/printdep.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// 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.
|
||||
|
||||
// The printdep command is a build system tool for printing out information
|
||||
// about dependencies.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
ts "tailscale.com"
|
||||
)
|
||||
|
||||
var (
|
||||
goToolchain = flag.Bool("go", false, "print the supported Go toolchain git hash (a github.com/tailscale/go commit)")
|
||||
goToolchainURL = flag.Bool("go-url", false, "print the URL to the tarball of the Tailscale Go toolchain")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *goToolchain {
|
||||
fmt.Println(strings.TrimSpace(ts.GoToolchainRev))
|
||||
}
|
||||
if *goToolchainURL {
|
||||
var suffix string
|
||||
switch runtime.GOARCH {
|
||||
case "amd64":
|
||||
// None
|
||||
case "arm64":
|
||||
suffix = "-" + runtime.GOARCH
|
||||
default:
|
||||
log.Fatalf("unsupported GOARCH %q", runtime.GOARCH)
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "linux", "darwin":
|
||||
default:
|
||||
log.Fatalf("unsupported GOOS %q", runtime.GOOS)
|
||||
}
|
||||
fmt.Printf("https://github.com/tailscale/go/releases/download/build-%s/%s%s.tar.gz\n", strings.TrimSpace(ts.GoToolchainRev), runtime.GOOS, suffix)
|
||||
}
|
||||
}
|
||||
@@ -108,7 +108,7 @@ func runCert(ctx context.Context, args []string) error {
|
||||
if version.IsMacSysExt() {
|
||||
dir = "io.tailscale.ipn.macsys"
|
||||
}
|
||||
printf("Warning: the macOS CLI runs in a sandbox; this binary's filesystem writes go to $HOME/Library/Containers/%s\n", dir)
|
||||
printf("Warning: the macOS CLI runs in a sandbox; this binary's filesystem writes go to $HOME/Library/Containers/%s/Data\n", dir)
|
||||
}
|
||||
if certArgs.certFile != "" {
|
||||
certChanged, err := writeIfChanged(certArgs.certFile, certPEM, 0644)
|
||||
|
||||
@@ -786,6 +786,16 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
wantSimpleUp: true,
|
||||
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
|
||||
},
|
||||
{
|
||||
name: "just_edit_reset",
|
||||
flags: []string{"--reset"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: ipn.DefaultControlURL,
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{WantRunningSet: true},
|
||||
},
|
||||
{
|
||||
name: "control_synonym",
|
||||
flags: []string{},
|
||||
|
||||
@@ -70,6 +70,16 @@ var debugCmd = &ffcli.Command{
|
||||
Exec: runLocalCreds,
|
||||
ShortHelp: "print how to access Tailscale local API",
|
||||
},
|
||||
{
|
||||
Name: "restun",
|
||||
Exec: localAPIAction("restun"),
|
||||
ShortHelp: "force a magicsock restun",
|
||||
},
|
||||
{
|
||||
Name: "rebind",
|
||||
Exec: localAPIAction("rebind"),
|
||||
ShortHelp: "force a magicsock rebind",
|
||||
},
|
||||
{
|
||||
Name: "prefs",
|
||||
Exec: runPrefs,
|
||||
@@ -244,6 +254,15 @@ func runDERPMap(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func localAPIAction(action string) func(context.Context, []string) error {
|
||||
return func(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return errors.New("unexpected arguments")
|
||||
}
|
||||
return tailscale.DebugAction(ctx, action)
|
||||
}
|
||||
}
|
||||
|
||||
func runEnv(ctx context.Context, args []string) error {
|
||||
for _, e := range os.Environ() {
|
||||
outln(e)
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -64,6 +65,16 @@ var pingArgs struct {
|
||||
}
|
||||
|
||||
func runPing(ctx context.Context, args []string) error {
|
||||
st, err := tailscale.Status(ctx)
|
||||
if err != nil {
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
description, ok := isRunningOrStarting(st)
|
||||
if !ok {
|
||||
printf("%s\n", description)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
c, bc, ctx, cancel := connect(ctx)
|
||||
defer cancel()
|
||||
|
||||
|
||||
@@ -121,24 +121,10 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
switch st.BackendState {
|
||||
default:
|
||||
fmt.Fprintf(Stderr, "unexpected state: %s\n", st.BackendState)
|
||||
description, ok := isRunningOrStarting(st)
|
||||
if !ok {
|
||||
outln(description)
|
||||
os.Exit(1)
|
||||
case ipn.Stopped.String():
|
||||
outln("Tailscale is stopped.")
|
||||
os.Exit(1)
|
||||
case ipn.NeedsLogin.String():
|
||||
outln("Logged out.")
|
||||
if st.AuthURL != "" {
|
||||
printf("\nLog in at: %s\n", st.AuthURL)
|
||||
}
|
||||
os.Exit(1)
|
||||
case ipn.NeedsMachineAuth.String():
|
||||
outln("Machine is not yet authorized by tailnet admin.")
|
||||
os.Exit(1)
|
||||
case ipn.Running.String(), ipn.Starting.String():
|
||||
// Run below.
|
||||
}
|
||||
|
||||
if len(st.Health) > 0 {
|
||||
@@ -222,6 +208,27 @@ func runStatus(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// isRunningOrStarting reports whether st is in state Running or Starting.
|
||||
// It also returns a description of the status suitable to display to a user.
|
||||
func isRunningOrStarting(st *ipnstate.Status) (description string, ok bool) {
|
||||
switch st.BackendState {
|
||||
default:
|
||||
return fmt.Sprintf("unexpected state: %s", st.BackendState), false
|
||||
case ipn.Stopped.String():
|
||||
return "Tailscale is stopped.", false
|
||||
case ipn.NeedsLogin.String():
|
||||
s := "Logged out."
|
||||
if st.AuthURL != "" {
|
||||
s += fmt.Sprintf("\nLog in at: %s", st.AuthURL)
|
||||
}
|
||||
return s, false
|
||||
case ipn.NeedsMachineAuth.String():
|
||||
return "Machine is not yet authorized by tailnet admin.", false
|
||||
case ipn.Running.String(), ipn.Starting.String():
|
||||
return st.BackendState, true
|
||||
}
|
||||
}
|
||||
|
||||
func dnsOrQuoteHostname(st *ipnstate.Status, ps *ipnstate.PeerStatus) string {
|
||||
baseName := dnsname.TrimSuffix(ps.DNSName, st.MagicDNSSuffix)
|
||||
if baseName != "" {
|
||||
|
||||
@@ -413,7 +413,6 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
|
||||
|
||||
justEdit := env.backendState == ipn.Running.String() &&
|
||||
!env.upArgs.forceReauth &&
|
||||
!env.upArgs.reset &&
|
||||
env.upArgs.authKeyOrFile == "" &&
|
||||
!controlURLChanged &&
|
||||
!tagsChanged
|
||||
|
||||
@@ -375,7 +375,7 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data.AdvertiseExitNode = true
|
||||
} else {
|
||||
if data.AdvertiseRoutes != "" {
|
||||
data.AdvertiseRoutes = ","
|
||||
data.AdvertiseRoutes += ","
|
||||
}
|
||||
data.AdvertiseRoutes += r.String()
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/net/neterror from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/filter
|
||||
|
||||
@@ -181,9 +181,9 @@ 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
|
||||
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/logpolicy from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/logtail from tailscale.com/logpolicy+
|
||||
tailscale.com/logtail/backoff from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/logtail/filch from tailscale.com/logpolicy
|
||||
@@ -195,6 +195,7 @@ 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/neterror from tailscale.com/net/netcheck+
|
||||
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
|
||||
|
||||
@@ -295,7 +295,6 @@ func run() error {
|
||||
var debugMux *http.ServeMux
|
||||
if args.debug != "" {
|
||||
debugMux = newDebugMux()
|
||||
go runDebugServer(debugMux, args.debug)
|
||||
}
|
||||
|
||||
linkMon, err := monitor.New(logf)
|
||||
@@ -314,6 +313,14 @@ func run() error {
|
||||
if _, ok := e.(wgengine.ResolvingEngine).GetResolver(); !ok {
|
||||
panic("internal error: exit node resolver not wired up")
|
||||
}
|
||||
if debugMux != nil {
|
||||
if ig, ok := e.(wgengine.InternalsGetter); ok {
|
||||
if _, mc, ok := ig.GetInternals(); ok {
|
||||
debugMux.HandleFunc("/debug/magicsock", mc.ServeHTTPDebug)
|
||||
}
|
||||
}
|
||||
go runDebugServer(debugMux, args.debug)
|
||||
}
|
||||
|
||||
ns, err := newNetstack(logf, dialer, e)
|
||||
if err != nil {
|
||||
|
||||
@@ -55,6 +55,11 @@ func isWindowsService() bool {
|
||||
return v
|
||||
}
|
||||
|
||||
// runWindowsService starts running Tailscale under the Windows
|
||||
// Service environment.
|
||||
//
|
||||
// At this point we're still the parent process that
|
||||
// Windows started.
|
||||
func runWindowsService(pol *logpolicy.Policy) error {
|
||||
return svc.Run(serviceName, &ipnService{Policy: pol})
|
||||
}
|
||||
@@ -93,6 +98,7 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
|
||||
select {
|
||||
case <-doneCh:
|
||||
case cmd := <-r:
|
||||
log.Printf("Got Windows Service event: %v", cmdName(cmd.Cmd))
|
||||
switch cmd.Cmd {
|
||||
case svc.Stop:
|
||||
cancel()
|
||||
@@ -109,6 +115,42 @@ func (service *ipnService) Execute(args []string, r <-chan svc.ChangeRequest, ch
|
||||
return false, windows.NO_ERROR
|
||||
}
|
||||
|
||||
func cmdName(c svc.Cmd) string {
|
||||
switch c {
|
||||
case svc.Stop:
|
||||
return "Stop"
|
||||
case svc.Pause:
|
||||
return "Pause"
|
||||
case svc.Continue:
|
||||
return "Continue"
|
||||
case svc.Interrogate:
|
||||
return "Interrogate"
|
||||
case svc.Shutdown:
|
||||
return "Shutdown"
|
||||
case svc.ParamChange:
|
||||
return "ParamChange"
|
||||
case svc.NetBindAdd:
|
||||
return "NetBindAdd"
|
||||
case svc.NetBindRemove:
|
||||
return "NetBindRemove"
|
||||
case svc.NetBindEnable:
|
||||
return "NetBindEnable"
|
||||
case svc.NetBindDisable:
|
||||
return "NetBindDisable"
|
||||
case svc.DeviceEvent:
|
||||
return "DeviceEvent"
|
||||
case svc.HardwareProfileChange:
|
||||
return "HardwareProfileChange"
|
||||
case svc.PowerEvent:
|
||||
return "PowerEvent"
|
||||
case svc.SessionChange:
|
||||
return "SessionChange"
|
||||
case svc.PreShutdown:
|
||||
return "PreShutdown"
|
||||
}
|
||||
return fmt.Sprintf("Unknown-Service-Cmd-%d", c)
|
||||
}
|
||||
|
||||
func beWindowsSubprocess() bool {
|
||||
if beFirewallKillswitch() {
|
||||
return true
|
||||
|
||||
@@ -12,10 +12,12 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"go4.org/mem"
|
||||
"golang.org/x/time/rate"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
@@ -37,8 +39,8 @@ type Client struct {
|
||||
rate *rate.Limiter // if non-nil, rate limiter to use
|
||||
|
||||
// Owned by Recv:
|
||||
peeked int // bytes to discard on next Recv
|
||||
readErr error // sticky read error
|
||||
peeked int // bytes to discard on next Recv
|
||||
readErr atomic.Value // of error; sticky (set by Recv)
|
||||
}
|
||||
|
||||
// ClientOpt is an option passed to NewClient.
|
||||
@@ -261,10 +263,18 @@ func (c *Client) ForwardPacket(srcKey, dstKey key.NodePublic, pkt []byte) (err e
|
||||
|
||||
func (c *Client) writeTimeoutFired() { c.nc.Close() }
|
||||
|
||||
func (c *Client) SendPing(data [8]byte) error {
|
||||
return c.sendPingOrPong(framePing, data)
|
||||
}
|
||||
|
||||
func (c *Client) SendPong(data [8]byte) error {
|
||||
return c.sendPingOrPong(framePong, data)
|
||||
}
|
||||
|
||||
func (c *Client) sendPingOrPong(typ frameType, data [8]byte) error {
|
||||
c.wmu.Lock()
|
||||
defer c.wmu.Unlock()
|
||||
if err := writeFrameHeader(c.bw, framePong, 8); err != nil {
|
||||
if err := writeFrameHeader(c.bw, typ, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := c.bw.Write(data[:]); err != nil {
|
||||
@@ -375,6 +385,12 @@ type PingMessage [8]byte
|
||||
|
||||
func (PingMessage) msg() {}
|
||||
|
||||
// PongMessage is a reply to a PingMessage from a client or server
|
||||
// with the payload sent previously in a PingMessage.
|
||||
type PongMessage [8]byte
|
||||
|
||||
func (PongMessage) msg() {}
|
||||
|
||||
// KeepAliveMessage is a one-way empty message from server to client, just to
|
||||
// keep the connection alive. It's like a PingMessage, but doesn't solicit
|
||||
// a reply from the client.
|
||||
@@ -427,13 +443,14 @@ func (c *Client) Recv() (m ReceivedMessage, err error) {
|
||||
}
|
||||
|
||||
func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err error) {
|
||||
if c.readErr != nil {
|
||||
return nil, c.readErr
|
||||
readErr, _ := c.readErr.Load().(error)
|
||||
if readErr != nil {
|
||||
return nil, readErr
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("derp.Recv: %w", err)
|
||||
c.readErr = err
|
||||
c.readErr.Store(err)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -536,6 +553,15 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
copy(pm[:], b[:])
|
||||
return pm, nil
|
||||
|
||||
case framePong:
|
||||
var pm PongMessage
|
||||
if n < 8 {
|
||||
c.logf("[unexpected] dropping short ping frame")
|
||||
continue
|
||||
}
|
||||
copy(pm[:], b[:])
|
||||
return pm, nil
|
||||
|
||||
case frameHealth:
|
||||
return HealthMessage{Problem: string(b[:])}, nil
|
||||
|
||||
@@ -564,3 +590,22 @@ func (c *Client) setSendRateLimiter(sm ServerInfoMessage) {
|
||||
sm.TokenBucketBytesBurst)
|
||||
}
|
||||
}
|
||||
|
||||
// LocalAddr returns the TCP connection's local address.
|
||||
//
|
||||
// If the client is broken in some previously detectable way, it
|
||||
// returns an error.
|
||||
func (c *Client) LocalAddr() (netaddr.IPPort, error) {
|
||||
readErr, _ := c.readErr.Load().(error)
|
||||
if readErr != nil {
|
||||
return netaddr.IPPort{}, readErr
|
||||
}
|
||||
if c.nc == nil {
|
||||
return netaddr.IPPort{}, errors.New("nil conn")
|
||||
}
|
||||
a := c.nc.LocalAddr()
|
||||
if a == nil {
|
||||
return netaddr.IPPort{}, errors.New("nil addr")
|
||||
}
|
||||
return netaddr.ParseIPPort(a.String())
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"math"
|
||||
"math/big"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -124,6 +125,8 @@ type Server struct {
|
||||
packetsForwardedOut expvar.Int
|
||||
packetsForwardedIn expvar.Int
|
||||
peerGoneFrames expvar.Int // number of peer gone frames sent
|
||||
gotPing expvar.Int // number of ping frames from client
|
||||
sentPong expvar.Int // number of pong frames enqueued to client
|
||||
accepts expvar.Int
|
||||
curClients expvar.Int
|
||||
curHomeClients expvar.Int // ones with preferred
|
||||
@@ -283,9 +286,8 @@ type PacketForwarder interface {
|
||||
// It is a defined type so that non-net connections can be used.
|
||||
type Conn interface {
|
||||
io.WriteCloser
|
||||
|
||||
LocalAddr() net.Addr
|
||||
// The *Deadline methods follow the semantics of net.Conn.
|
||||
|
||||
SetDeadline(time.Time) error
|
||||
SetReadDeadline(time.Time) error
|
||||
SetWriteDeadline(time.Time) error
|
||||
@@ -662,6 +664,7 @@ func (s *Server) accept(nc Conn, brw *bufio.ReadWriter, remoteAddr string, connN
|
||||
connectedAt: time.Now(),
|
||||
sendQueue: make(chan pkt, perClientSendQueueDepth),
|
||||
discoSendQueue: make(chan pkt, perClientSendQueueDepth),
|
||||
sendPongCh: make(chan [8]byte, 1),
|
||||
peerGone: make(chan key.NodePublic),
|
||||
canMesh: clientInfo.MeshKey != "" && clientInfo.MeshKey == s.meshKey,
|
||||
}
|
||||
@@ -729,6 +732,8 @@ func (c *sclient) run(ctx context.Context) error {
|
||||
err = c.handleFrameWatchConns(ft, fl)
|
||||
case frameClosePeer:
|
||||
err = c.handleFrameClosePeer(ft, fl)
|
||||
case framePing:
|
||||
err = c.handleFramePing(ft, fl)
|
||||
default:
|
||||
err = c.handleUnknownFrame(ft, fl)
|
||||
}
|
||||
@@ -766,6 +771,33 @@ func (c *sclient) handleFrameWatchConns(ft frameType, fl uint32) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *sclient) handleFramePing(ft frameType, fl uint32) error {
|
||||
c.s.gotPing.Add(1)
|
||||
var m PingMessage
|
||||
if fl < uint32(len(m)) {
|
||||
return fmt.Errorf("short ping: %v", fl)
|
||||
}
|
||||
if fl > 1000 {
|
||||
// unreasonably extra large. We leave some extra
|
||||
// space for future extensibility, but not too much.
|
||||
return fmt.Errorf("ping body too large: %v", fl)
|
||||
}
|
||||
_, err := io.ReadFull(c.br, m[:])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if extra := int64(fl) - int64(len(m)); extra > 0 {
|
||||
_, err = io.CopyN(ioutil.Discard, c.br, extra)
|
||||
}
|
||||
select {
|
||||
case c.sendPongCh <- [8]byte(m):
|
||||
default:
|
||||
// They're pinging too fast. Ignore.
|
||||
// TODO(bradfitz): add a rate limiter too.
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *sclient) handleFrameClosePeer(ft frameType, fl uint32) error {
|
||||
if fl != keyLen {
|
||||
return fmt.Errorf("handleFrameClosePeer wrong size")
|
||||
@@ -1202,6 +1234,7 @@ type sclient struct {
|
||||
remoteIPPort netaddr.IPPort // zero if remoteAddr is not ip:port.
|
||||
sendQueue chan pkt // packets queued to this client; never closed
|
||||
discoSendQueue chan pkt // important packets queued to this client; never closed
|
||||
sendPongCh chan [8]byte // pong replies to send to the client; never closed
|
||||
peerGone chan key.NodePublic // write request that a previous sender has disconnected (not used by mesh peers)
|
||||
meshUpdate chan struct{} // write request to write peerStateChange
|
||||
canMesh bool // clientInfo had correct mesh token for inter-region routing
|
||||
@@ -1342,6 +1375,9 @@ func (c *sclient) sendLoop(ctx context.Context) error {
|
||||
werr = c.sendPacket(msg.src, msg.bs)
|
||||
c.recordQueueTime(msg.enqueuedAt)
|
||||
continue
|
||||
case msg := <-c.sendPongCh:
|
||||
werr = c.sendPong(msg)
|
||||
continue
|
||||
case <-keepAliveTick.C:
|
||||
werr = c.sendKeepAlive()
|
||||
continue
|
||||
@@ -1368,6 +1404,9 @@ func (c *sclient) sendLoop(ctx context.Context) error {
|
||||
case msg := <-c.discoSendQueue:
|
||||
werr = c.sendPacket(msg.src, msg.bs)
|
||||
c.recordQueueTime(msg.enqueuedAt)
|
||||
case msg := <-c.sendPongCh:
|
||||
werr = c.sendPong(msg)
|
||||
continue
|
||||
case <-keepAliveTick.C:
|
||||
werr = c.sendKeepAlive()
|
||||
}
|
||||
@@ -1384,6 +1423,17 @@ func (c *sclient) sendKeepAlive() error {
|
||||
return writeFrameHeader(c.bw.bw(), frameKeepAlive, 0)
|
||||
}
|
||||
|
||||
// sendPong sends a pong reply, without flushing.
|
||||
func (c *sclient) sendPong(data [8]byte) error {
|
||||
c.s.sentPong.Add(1)
|
||||
c.setWriteDeadline()
|
||||
if err := writeFrameHeader(c.bw.bw(), framePong, uint32(len(data))); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := c.bw.Write(data[:])
|
||||
return err
|
||||
}
|
||||
|
||||
// sendPeerGone sends a peerGone frame, without flushing.
|
||||
func (c *sclient) sendPeerGone(peer key.NodePublic) error {
|
||||
c.s.peerGoneFrames.Add(1)
|
||||
@@ -1625,6 +1675,8 @@ func (s *Server) ExpVar() expvar.Var {
|
||||
m.Set("unknown_frames", &s.unknownFrames)
|
||||
m.Set("home_moves_in", &s.homeMovesIn)
|
||||
m.Set("home_moves_out", &s.homeMovesOut)
|
||||
m.Set("got_ping", &s.gotPing)
|
||||
m.Set("sent_pong", &s.sentPong)
|
||||
m.Set("peer_gone_frames", &s.peerGoneFrames)
|
||||
m.Set("packets_forwarded_out", &s.packetsForwardedOut)
|
||||
m.Set("packets_forwarded_in", &s.packetsForwardedIn)
|
||||
|
||||
@@ -812,6 +812,14 @@ func TestClientRecv(t *testing.T) {
|
||||
},
|
||||
want: PingMessage{1, 2, 3, 4, 5, 6, 7, 8},
|
||||
},
|
||||
{
|
||||
name: "pong",
|
||||
input: []byte{
|
||||
byte(framePong), 0, 0, 0, 8,
|
||||
1, 2, 3, 4, 5, 6, 7, 8,
|
||||
},
|
||||
want: PongMessage{1, 2, 3, 4, 5, 6, 7, 8},
|
||||
},
|
||||
{
|
||||
name: "health_bad",
|
||||
input: []byte{
|
||||
@@ -858,6 +866,23 @@ func TestClientRecv(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientSendPing(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
c := &Client{
|
||||
bw: bufio.NewWriter(&buf),
|
||||
}
|
||||
if err := c.SendPing([8]byte{1, 2, 3, 4, 5, 6, 7, 8}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []byte{
|
||||
byte(framePing), 0, 0, 0, 8,
|
||||
1, 2, 3, 4, 5, 6, 7, 8,
|
||||
}
|
||||
if !bytes.Equal(buf.Bytes(), want) {
|
||||
t.Errorf("unexpected output\nwrote: % 02x\n want: % 02x", buf.Bytes(), want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientSendPong(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
c := &Client{
|
||||
@@ -873,7 +898,6 @@ func TestClientSendPong(t *testing.T) {
|
||||
if !bytes.Equal(buf.Bytes(), want) {
|
||||
t.Errorf("unexpected output\nwrote: % 02x\n want: % 02x", buf.Bytes(), want)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestServerDupClients(t *testing.T) {
|
||||
@@ -1316,3 +1340,30 @@ func TestClientSendRateLimiting(t *testing.T) {
|
||||
t.Errorf("limited conn's bytes count = %v; want >=%v, <%v", bytesLimited, bytes1K*2, bytes1K)
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerRepliesToPing(t *testing.T) {
|
||||
ts := newTestServer(t)
|
||||
defer ts.close(t)
|
||||
|
||||
tc := newRegularClient(t, ts, "alice")
|
||||
|
||||
data := [8]byte{1, 2, 3, 4, 5, 6, 7, 42}
|
||||
|
||||
if err := tc.c.SendPing(data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for {
|
||||
m, err := tc.c.recvTimeout(time.Second)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
switch m := m.(type) {
|
||||
case PongMessage:
|
||||
if ([8]byte(m)) != data {
|
||||
t.Fatalf("got pong %2x; want %2x", [8]byte(m), data)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ package derphttp
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
@@ -72,6 +73,7 @@ type Client struct {
|
||||
client *derp.Client
|
||||
connGen int // incremented once per new connection; valid values are >0
|
||||
serverPubKey key.NodePublic
|
||||
pingOut map[derp.PingMessage]chan<- bool // chan to send to on pong
|
||||
}
|
||||
|
||||
// NewRegionClient returns a new DERP-over-HTTP client. It connects lazily.
|
||||
@@ -698,6 +700,95 @@ func (c *Client) Send(dstKey key.NodePublic, b []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *Client) registerPing(m derp.PingMessage, ch chan<- bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.pingOut == nil {
|
||||
c.pingOut = map[derp.PingMessage]chan<- bool{}
|
||||
}
|
||||
c.pingOut[m] = ch
|
||||
}
|
||||
|
||||
func (c *Client) unregisterPing(m derp.PingMessage) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
delete(c.pingOut, m)
|
||||
}
|
||||
|
||||
func (c *Client) handledPong(m derp.PongMessage) bool {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
k := derp.PingMessage(m)
|
||||
if ch, ok := c.pingOut[k]; ok {
|
||||
ch <- true
|
||||
delete(c.pingOut, k)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Ping sends a ping to the peer and waits for it either to be
|
||||
// acknowledged (in which case Ping returns nil) or waits for ctx to
|
||||
// be over and returns an error. It will wait at most 5 seconds
|
||||
// before returning an error.
|
||||
//
|
||||
// Another goroutine must be in a loop calling Recv or
|
||||
// RecvDetail or ping responses won't be handled.
|
||||
func (c *Client) Ping(ctx context.Context) error {
|
||||
maxDL := time.Now().Add(5 * time.Second)
|
||||
if dl, ok := ctx.Deadline(); !ok || dl.After(maxDL) {
|
||||
var cancel context.CancelFunc
|
||||
ctx, cancel = context.WithDeadline(ctx, maxDL)
|
||||
defer cancel()
|
||||
}
|
||||
var data derp.PingMessage
|
||||
rand.Read(data[:])
|
||||
gotPing := make(chan bool, 1)
|
||||
c.registerPing(data, gotPing)
|
||||
defer c.unregisterPing(data)
|
||||
if err := c.SendPing(data); err != nil {
|
||||
return err
|
||||
}
|
||||
select {
|
||||
case <-gotPing:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
// SendPing writes a ping message, without any implicit connect or
|
||||
// reconnect. This is a lower-level interface that writes a frame
|
||||
// without any implicit handling of the response pong, if any. For a
|
||||
// higher-level interface, use Ping.
|
||||
func (c *Client) SendPing(data [8]byte) error {
|
||||
c.mu.Lock()
|
||||
closed, client := c.closed, c.client
|
||||
c.mu.Unlock()
|
||||
if closed {
|
||||
return ErrClientClosed
|
||||
}
|
||||
if client == nil {
|
||||
return errors.New("client not connected")
|
||||
}
|
||||
return client.SendPing(data)
|
||||
}
|
||||
|
||||
// LocalAddr reports c's local TCP address, without any implicit
|
||||
// connect or reconnect.
|
||||
func (c *Client) LocalAddr() (netaddr.IPPort, error) {
|
||||
c.mu.Lock()
|
||||
closed, client := c.closed, c.client
|
||||
c.mu.Unlock()
|
||||
if closed {
|
||||
return netaddr.IPPort{}, ErrClientClosed
|
||||
}
|
||||
if client == nil {
|
||||
return netaddr.IPPort{}, errors.New("client not connected")
|
||||
}
|
||||
return client.LocalAddr()
|
||||
}
|
||||
|
||||
func (c *Client) ForwardPacket(from, to key.NodePublic, b []byte) error {
|
||||
client, _, err := c.connect(context.TODO(), "derphttp.Client.ForwardPacket")
|
||||
if err != nil {
|
||||
@@ -805,14 +896,22 @@ func (c *Client) RecvDetail() (m derp.ReceivedMessage, connGen int, err error) {
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
m, err = client.Recv()
|
||||
if err != nil {
|
||||
c.closeForReconnect(client)
|
||||
if c.isClosed() {
|
||||
err = ErrClientClosed
|
||||
for {
|
||||
m, err = client.Recv()
|
||||
switch m := m.(type) {
|
||||
case derp.PongMessage:
|
||||
if c.handledPong(m) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
c.closeForReconnect(client)
|
||||
if c.isClosed() {
|
||||
err = ErrClientClosed
|
||||
}
|
||||
}
|
||||
return m, connGen, err
|
||||
}
|
||||
return m, connGen, err
|
||||
}
|
||||
|
||||
func (c *Client) isClosed() bool {
|
||||
|
||||
@@ -154,3 +154,55 @@ func waitConnect(t testing.TB, c *Client) {
|
||||
t.Fatalf("client first Recv was unexpected type %T", v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPing(t *testing.T) {
|
||||
serverPrivateKey := key.NewNode()
|
||||
s := derp.NewServer(serverPrivateKey, t.Logf)
|
||||
defer s.Close()
|
||||
|
||||
httpsrv := &http.Server{
|
||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
||||
Handler: Handler(s),
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp4", "localhost:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
serverURL := "http://" + ln.Addr().String()
|
||||
t.Logf("server URL: %s", serverURL)
|
||||
|
||||
go func() {
|
||||
if err := httpsrv.Serve(ln); err != nil {
|
||||
if err == http.ErrServerClosed {
|
||||
return
|
||||
}
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
c, err := NewClient(key.NewNode(), serverURL, t.Logf)
|
||||
if err != nil {
|
||||
t.Fatalf("NewClient: %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
if err := c.Connect(context.Background()); err != nil {
|
||||
t.Fatalf("client Connect: %v", err)
|
||||
}
|
||||
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
for {
|
||||
m, err := c.Recv()
|
||||
if err != nil {
|
||||
errc <- err
|
||||
return
|
||||
}
|
||||
t.Logf("Recv: %T", m)
|
||||
}
|
||||
}()
|
||||
err = c.Ping(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("Ping: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
1
go.toolchain.branch
Normal file
1
go.toolchain.branch
Normal file
@@ -0,0 +1 @@
|
||||
tailscale.go1.17
|
||||
1
go.toolchain.rev
Normal file
1
go.toolchain.rev
Normal file
@@ -0,0 +1 @@
|
||||
e44d304e5486d10f5485250ed5ba58fffd438d29
|
||||
@@ -9,6 +9,7 @@ package health
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"sort"
|
||||
@@ -28,6 +29,8 @@ var (
|
||||
watchers = map[*watchHandle]func(Subsystem, error){} // opt func to run if error state changes
|
||||
timer *time.Timer
|
||||
|
||||
debugHandler = map[string]http.Handler{}
|
||||
|
||||
inMapPoll bool
|
||||
inMapPollSince time.Time
|
||||
lastMapPollEndedAt time.Time
|
||||
@@ -116,6 +119,18 @@ func SetNetworkCategoryHealth(err error) { set(SysNetworkCategory, err) }
|
||||
|
||||
func NetworkCategoryHealth() error { return get(SysNetworkCategory) }
|
||||
|
||||
func RegisterDebugHandler(typ string, h http.Handler) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
debugHandler[typ] = h
|
||||
}
|
||||
|
||||
func DebugHandler(typ string) http.Handler {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return debugHandler[typ]
|
||||
}
|
||||
|
||||
func get(key Subsystem) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
@@ -168,7 +183,8 @@ func GotStreamedMapResponse() {
|
||||
selfCheckLocked()
|
||||
}
|
||||
|
||||
// SetInPollNetMap records that we're in
|
||||
// SetInPollNetMap records whether the client has an open
|
||||
// HTTP long poll open to the control plane.
|
||||
func SetInPollNetMap(v bool) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
@@ -183,6 +199,14 @@ func SetInPollNetMap(v bool) {
|
||||
}
|
||||
}
|
||||
|
||||
// GetInPollNetMap reports whether the client has an open
|
||||
// HTTP long poll open to the control plane.
|
||||
func GetInPollNetMap() bool {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
return inMapPoll
|
||||
}
|
||||
|
||||
// SetMagicSockDERPHome notes what magicsock's view of its home DERP is.
|
||||
func SetMagicSockDERPHome(region int) {
|
||||
mu.Lock()
|
||||
|
||||
@@ -111,7 +111,8 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
},
|
||||
prefs: &ipn.Prefs{},
|
||||
want: &dns.Config{
|
||||
Routes: map[dnsname.FQDN][]dnstype.Resolver{},
|
||||
OnlyIPv6: true,
|
||||
Routes: map[dnsname.FQDN][]dnstype.Resolver{},
|
||||
Hosts: map[dnsname.FQDN][]netaddr.IP{
|
||||
"b.net.": ips("fe75::2"),
|
||||
"myname.net.": ips("fe75::1"),
|
||||
|
||||
@@ -55,6 +55,7 @@ import (
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
"tailscale.com/wgengine/magicsock"
|
||||
"tailscale.com/wgengine/router"
|
||||
"tailscale.com/wgengine/wgcfg"
|
||||
"tailscale.com/wgengine/wgcfg/nmcfg"
|
||||
@@ -380,6 +381,7 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
|
||||
}
|
||||
})
|
||||
sb.MutateSelfStatus(func(ss *ipnstate.PeerStatus) {
|
||||
ss.Online = health.GetInPollNetMap()
|
||||
if b.netMap != nil {
|
||||
ss.HostName = b.netMap.Hostinfo.Hostname
|
||||
ss.DNSName = b.netMap.Name
|
||||
@@ -536,6 +538,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
// Since st.NetMap==nil means "netmap is unchanged", there is
|
||||
// no other way to represent this change.
|
||||
b.setNetMapLocked(nil)
|
||||
b.e.SetNetworkMap(new(netmap.NetworkMap))
|
||||
}
|
||||
|
||||
prefs := b.prefs
|
||||
@@ -1018,7 +1021,12 @@ func (b *LocalBackend) updateFilter(netMap *netmap.NetworkMap, prefs *ipn.Prefs)
|
||||
// wifi": you get internet access, but to additionally
|
||||
// get LAN access the LAN(s) need to be offered
|
||||
// explicitly as well.
|
||||
s, err := shrinkDefaultRoute(r)
|
||||
localInterfaceRoutes, hostIPs, err := interfaceRoutes()
|
||||
if err != nil {
|
||||
b.logf("getting local interface routes: %v", err)
|
||||
continue
|
||||
}
|
||||
s, err := shrinkDefaultRoute(r, localInterfaceRoutes, hostIPs)
|
||||
if err != nil {
|
||||
b.logf("computing default route filter: %v", err)
|
||||
continue
|
||||
@@ -1162,17 +1170,14 @@ func interfaceRoutes() (ips *netaddr.IPSet, hostIPs []netaddr.IP, err error) {
|
||||
}
|
||||
|
||||
// shrinkDefaultRoute returns an IPSet representing the IPs in route,
|
||||
// minus those in removeFromDefaultRoute and local interface subnets.
|
||||
func shrinkDefaultRoute(route netaddr.IPPrefix) (*netaddr.IPSet, error) {
|
||||
interfaceRoutes, hostIPs, err := interfaceRoutes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// minus those in removeFromDefaultRoute and localInterfaceRoutes,
|
||||
// plus the IPs in hostIPs.
|
||||
func shrinkDefaultRoute(route netaddr.IPPrefix, localInterfaceRoutes *netaddr.IPSet, hostIPs []netaddr.IP) (*netaddr.IPSet, error) {
|
||||
var b netaddr.IPSetBuilder
|
||||
// Add the default route.
|
||||
b.AddPrefix(route)
|
||||
// Remove the local interface routes.
|
||||
b.RemoveSet(interfaceRoutes)
|
||||
b.RemoveSet(localInterfaceRoutes)
|
||||
|
||||
// Having removed all the LAN subnets, re-add the hosts's own
|
||||
// IPs. It's fine for clients to connect to an exit node's public
|
||||
@@ -1946,6 +1951,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
|
||||
// selfV6Only is whether we only have IPv6 addresses ourselves.
|
||||
selfV6Only := tsaddr.PrefixesContainsFunc(nm.Addresses, tsaddr.PrefixIs6) &&
|
||||
!tsaddr.PrefixesContainsFunc(nm.Addresses, tsaddr.PrefixIs4)
|
||||
dcfg.OnlyIPv6 = selfV6Only
|
||||
|
||||
// Populate MagicDNS records. We do this unconditionally so that
|
||||
// quad-100 can always respond to MagicDNS queries, even if the OS
|
||||
@@ -2115,7 +2121,7 @@ func (b *LocalBackend) TailscaleVarRoot() string {
|
||||
return b.varRoot
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "ios", "android":
|
||||
case "ios", "android", "darwin":
|
||||
dir, _ := paths.AppSharedDir.Load().(string)
|
||||
return dir
|
||||
}
|
||||
@@ -2377,7 +2383,9 @@ func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs) *router
|
||||
}
|
||||
}
|
||||
|
||||
rs.Routes = append(rs.Routes, netaddr.IPPrefixFrom(tsaddr.TailscaleServiceIP(), 32))
|
||||
if tsaddr.PrefixesContainsFunc(rs.LocalAddrs, tsaddr.PrefixIs4) {
|
||||
rs.Routes = append(rs.Routes, netaddr.IPPrefixFrom(tsaddr.TailscaleServiceIP(), 32))
|
||||
}
|
||||
|
||||
return rs
|
||||
}
|
||||
@@ -3134,3 +3142,33 @@ func exitNodeCanProxyDNS(nm *netmap.NetworkMap, exitNodeID tailcfg.StableNodeID)
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (b *LocalBackend) DebugRebind() error {
|
||||
mc, err := b.magicConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mc.Rebind()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) DebugReSTUN() error {
|
||||
mc, err := b.magicConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mc.ReSTUN("explicit-debug")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) magicConn() (*magicsock.Conn, error) {
|
||||
ig, ok := b.e.(wgengine.InternalsGetter)
|
||||
if !ok {
|
||||
return nil, errors.New("engine isn't InternalsGetter")
|
||||
}
|
||||
_, mc, ok := ig.GetInternals()
|
||||
if !ok {
|
||||
return nil, errors.New("failed to get internals")
|
||||
}
|
||||
return mc, nil
|
||||
}
|
||||
|
||||
@@ -178,9 +178,31 @@ func TestShrinkDefaultRoute(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
// Construct a fake local network environment to make this test hermetic.
|
||||
// localInterfaceRoutes and hostIPs would normally come from calling interfaceRoutes,
|
||||
// and localAddresses would normally come from calling interfaces.LocalAddresses.
|
||||
var b netaddr.IPSetBuilder
|
||||
for _, c := range []string{"127.0.0.0/8", "192.168.9.0/24", "fe80::/32"} {
|
||||
p := netaddr.MustParseIPPrefix(c)
|
||||
b.AddPrefix(p)
|
||||
}
|
||||
localInterfaceRoutes, err := b.IPSet()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
hostIPs := []netaddr.IP{
|
||||
netaddr.MustParseIP("127.0.0.1"),
|
||||
netaddr.MustParseIP("192.168.9.39"),
|
||||
netaddr.MustParseIP("fe80::1"),
|
||||
netaddr.MustParseIP("fe80::437d:feff:feca:49a7"),
|
||||
}
|
||||
localAddresses := []netaddr.IP{
|
||||
netaddr.MustParseIP("192.168.9.39"),
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
def := netaddr.MustParseIPPrefix(test.route)
|
||||
got, err := shrinkDefaultRoute(def)
|
||||
got, err := shrinkDefaultRoute(def, localInterfaceRoutes, hostIPs)
|
||||
if err != nil {
|
||||
t.Fatalf("shrinkDefaultRoute(%q): %v", test.route, err)
|
||||
}
|
||||
@@ -194,11 +216,7 @@ func TestShrinkDefaultRoute(t *testing.T) {
|
||||
t.Errorf("shrink(%q).Contains(%v) = true, want false", test.route, ip)
|
||||
}
|
||||
}
|
||||
ips, _, err := interfaces.LocalAddresses()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, ip := range ips {
|
||||
for _, ip := range localAddresses {
|
||||
want := test.localIPFn(ip)
|
||||
if gotContains := got.Contains(ip); gotContains != want {
|
||||
t.Errorf("shrink(%q).Contains(%v) = %v, want %v", test.route, ip, gotContains, want)
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/logtail/backoff"
|
||||
@@ -553,6 +554,12 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
case "/v0/metrics":
|
||||
h.handleServeMetrics(w, r)
|
||||
return
|
||||
case "/v0/magicsock":
|
||||
h.handleServeMagicsock(w, r)
|
||||
return
|
||||
case "/v0/dnsfwd":
|
||||
h.handleServeDNSFwd(w, r)
|
||||
return
|
||||
}
|
||||
who := h.peerUser.DisplayName
|
||||
fmt.Fprintf(w, `<html>
|
||||
@@ -781,6 +788,21 @@ func (h *peerAPIHandler) handleServeEnv(w http.ResponseWriter, r *http.Request)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeMagicsock(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.isSelf {
|
||||
http.Error(w, "not owner", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
eng := h.ps.b.e
|
||||
if ig, ok := eng.(wgengine.InternalsGetter); ok {
|
||||
if _, mc, ok := ig.GetInternals(); ok {
|
||||
mc.ServeHTTPDebug(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
http.Error(w, "miswired", 500)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.isSelf {
|
||||
http.Error(w, "not owner", http.StatusForbidden)
|
||||
@@ -790,6 +812,19 @@ func (h *peerAPIHandler) handleServeMetrics(w http.ResponseWriter, r *http.Reque
|
||||
clientmetric.WritePrometheusExpositionFormat(w)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeDNSFwd(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.isSelf {
|
||||
http.Error(w, "not owner", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
dh := health.DebugHandler("dnsfwd")
|
||||
if dh == nil {
|
||||
http.Error(w, "not wired up", 500)
|
||||
return
|
||||
}
|
||||
dh.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) replyToDNSQueries() bool {
|
||||
if h.isSelf {
|
||||
// If the peer is owned by the same user, just allow it
|
||||
|
||||
@@ -87,8 +87,9 @@ func (nt *notifyThrottler) drain(count int) []ipn.Notify {
|
||||
type mockControl struct {
|
||||
tb testing.TB
|
||||
opts controlclient.Options
|
||||
logf logger.Logf
|
||||
logfActual logger.Logf
|
||||
statusFunc func(controlclient.Status)
|
||||
preventLog syncs.AtomicBool
|
||||
|
||||
mu sync.Mutex
|
||||
calls []string
|
||||
@@ -104,6 +105,13 @@ func newMockControl(tb testing.TB) *mockControl {
|
||||
}
|
||||
}
|
||||
|
||||
func (cc *mockControl) logf(format string, args ...interface{}) {
|
||||
if cc.preventLog.Get() || cc.logfActual == nil {
|
||||
return
|
||||
}
|
||||
cc.logfActual(format, args...)
|
||||
}
|
||||
|
||||
func (cc *mockControl) SetStatusFunc(fn func(controlclient.Status)) {
|
||||
cc.statusFunc = fn
|
||||
}
|
||||
@@ -284,6 +292,7 @@ func TestStateMachine(t *testing.T) {
|
||||
t.Cleanup(e.Close)
|
||||
|
||||
cc := newMockControl(t)
|
||||
t.Cleanup(func() { cc.preventLog.Set(true) }) // hacky way to pacify issue 3020
|
||||
b, err := NewLocalBackend(logf, "logid", store, nil, e)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLocalBackend: %v", err)
|
||||
@@ -291,7 +300,7 @@ func TestStateMachine(t *testing.T) {
|
||||
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
|
||||
cc.mu.Lock()
|
||||
cc.opts = opts
|
||||
cc.logf = opts.Logf
|
||||
cc.logfActual = opts.Logf
|
||||
cc.authBlocked = true
|
||||
cc.persist = cc.opts.Persist
|
||||
cc.mu.Unlock()
|
||||
@@ -305,6 +314,9 @@ func TestStateMachine(t *testing.T) {
|
||||
notifies.expect(0)
|
||||
|
||||
b.SetNotifyCallback(func(n ipn.Notify) {
|
||||
if cc.preventLog.Get() {
|
||||
return
|
||||
}
|
||||
if n.State != nil ||
|
||||
n.Prefs != nil ||
|
||||
n.BrowseToURL != nil ||
|
||||
@@ -315,6 +327,7 @@ func TestStateMachine(t *testing.T) {
|
||||
logf("\n(ignored) %v\n\n", n)
|
||||
}
|
||||
})
|
||||
t.Cleanup(func() { b.SetNotifyCallback(nil) }) // hacky way to pacify issue 3020
|
||||
|
||||
// Check that it hasn't called us right away.
|
||||
// The state machine should be idle until we call Start().
|
||||
@@ -948,7 +961,7 @@ func TestWGEngineStatusRace(t *testing.T) {
|
||||
b.SetControlClientGetterForTesting(func(opts controlclient.Options) (controlclient.Client, error) {
|
||||
cc.mu.Lock()
|
||||
defer cc.mu.Unlock()
|
||||
cc.logf = opts.Logf
|
||||
cc.logfActual = opts.Logf
|
||||
return cc, nil
|
||||
})
|
||||
|
||||
|
||||
74
ipn/ipnserver/proxyconnect.go
Normal file
74
ipn/ipnserver/proxyconnect.go
Normal file
@@ -0,0 +1,74 @@
|
||||
// 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 ipnserver
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// handleProxyConnectConn handles a CONNECT request to
|
||||
// log.tailscale.io (or whatever the configured log server is). This
|
||||
// is intended for use by the Windows GUI client to log via when an
|
||||
// exit node is in use, so the logs don't go out via the exit node and
|
||||
// instead go directly, like tailscaled's. The dialer tried to do that
|
||||
// in the unprivileged GUI by binding to a specific interface, but the
|
||||
// "Internet Kill Switch" installed by tailscaled for exit nodes
|
||||
// precludes that from working and instead the GUI fails to dial out.
|
||||
// So, go through tailscaled (with a CONNECT request) instead.
|
||||
func (s *Server) handleProxyConnectConn(ctx context.Context, br *bufio.Reader, c net.Conn, logf logger.Logf) {
|
||||
defer c.Close()
|
||||
|
||||
c.SetReadDeadline(time.Now().Add(5 * time.Second)) // should be long enough to send the HTTP headers
|
||||
req, err := http.ReadRequest(br)
|
||||
if err != nil {
|
||||
logf("ReadRequest: %v", err)
|
||||
return
|
||||
}
|
||||
c.SetReadDeadline(time.Time{})
|
||||
|
||||
if req.Method != "CONNECT" {
|
||||
logf("ReadRequest: unexpected method %q, not CONNECT", req.Method)
|
||||
return
|
||||
}
|
||||
|
||||
hostPort := req.RequestURI
|
||||
logHost := logpolicy.LogHost()
|
||||
allowed := net.JoinHostPort(logHost, "443")
|
||||
if hostPort != allowed {
|
||||
logf("invalid CONNECT target %q; want %q", hostPort, allowed)
|
||||
io.WriteString(c, "HTTP/1.1 403 Forbidden\r\n\r\nBad CONNECT target.\n")
|
||||
return
|
||||
}
|
||||
|
||||
tr := logpolicy.NewLogtailTransport(logHost)
|
||||
back, err := tr.DialContext(ctx, "tcp", hostPort)
|
||||
if err != nil {
|
||||
logf("error CONNECT dialing %v: %v", hostPort, err)
|
||||
io.WriteString(c, "HTTP/1.1 502 Fail\r\n\r\nConnect failure.\n")
|
||||
return
|
||||
}
|
||||
defer back.Close()
|
||||
|
||||
io.WriteString(c, "HTTP/1.1 200 OK\r\n\r\n")
|
||||
|
||||
errc := make(chan error, 2)
|
||||
go func() {
|
||||
_, err := io.Copy(c, back)
|
||||
errc <- err
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(back, br)
|
||||
errc <- err
|
||||
}()
|
||||
<-errc
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -35,7 +36,6 @@ 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"
|
||||
@@ -239,12 +239,28 @@ func bufferHasHTTPRequest(br *bufio.Reader) bool {
|
||||
mem.Contains(mem.B(peek), mem.S(" HTTP/"))
|
||||
}
|
||||
|
||||
// bufferIsConnect reports whether br looks like it's likely an HTTP
|
||||
// CONNECT request.
|
||||
//
|
||||
// Invariant: br has already had at least 4 bytes Peek'ed.
|
||||
func bufferIsConnect(br *bufio.Reader) bool {
|
||||
peek, _ := br.Peek(br.Buffered())
|
||||
return mem.HasPrefix(mem.B(peek), mem.S("CONN"))
|
||||
}
|
||||
|
||||
func (s *Server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) {
|
||||
// First see if it's an HTTP request.
|
||||
br := bufio.NewReader(c)
|
||||
c.SetReadDeadline(time.Now().Add(time.Second))
|
||||
br.Peek(4)
|
||||
c.SetReadDeadline(time.Time{})
|
||||
|
||||
// Handle logtail CONNECT requests early. (See docs on handleProxyConnectConn)
|
||||
if bufferIsConnect(br) {
|
||||
s.handleProxyConnectConn(ctx, br, c, logf)
|
||||
return
|
||||
}
|
||||
|
||||
isHTTPReq := bufferHasHTTPRequest(br)
|
||||
|
||||
ci, err := s.addConn(c, isHTTPReq)
|
||||
@@ -869,14 +885,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
|
||||
@@ -908,6 +916,14 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
startTime := time.Now()
|
||||
log.Printf("exec: %#v %v", executable, args)
|
||||
cmd := exec.Command(executable, args...)
|
||||
if runtime.GOOS == "windows" {
|
||||
extraEnv, err := loadExtraEnv()
|
||||
if err != nil {
|
||||
logf("errors loading extra env file; ignoring: %v", err)
|
||||
} else {
|
||||
cmd.Env = append(os.Environ(), extraEnv...)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a pipe object to use as the subproc's stdin.
|
||||
// When the writer goes away, the reader gets EOF.
|
||||
@@ -1177,3 +1193,47 @@ func findTrueNASTaildropDir(name string) (dir string, err error) {
|
||||
}
|
||||
return "", fmt.Errorf("shared folder %q not found", name)
|
||||
}
|
||||
|
||||
func loadExtraEnv() (env []string, err error) {
|
||||
if runtime.GOOS != "windows" {
|
||||
return nil, nil
|
||||
}
|
||||
name := filepath.Join(os.Getenv("ProgramData"), "Tailscale", "tailscaled-env.txt")
|
||||
contents, err := os.ReadFile(name)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, line := range strings.Split(string(contents), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
k, v, ok := stringsCut(line, "=")
|
||||
if !ok || k == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(v, `"`) {
|
||||
var err error
|
||||
v, err = strconv.Unquote(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid value in line %q: %v", line, err)
|
||||
}
|
||||
env = append(env, k+"="+v)
|
||||
} else {
|
||||
env = append(env, line)
|
||||
}
|
||||
}
|
||||
return env, nil
|
||||
}
|
||||
|
||||
// stringsCut is Go 1.18's strings.Cut.
|
||||
// TODO(bradfitz): delete this when we depend on Go 1.18.
|
||||
func stringsCut(s, sep string) (before, after string, found bool) {
|
||||
if i := strings.Index(s, sep); i >= 0 {
|
||||
return s[:i], s[i+len(sep):], true
|
||||
}
|
||||
return s, "", false
|
||||
}
|
||||
|
||||
@@ -113,6 +113,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.serveDERPMap(w, r)
|
||||
case "/localapi/v0/metrics":
|
||||
h.serveMetrics(w, r)
|
||||
case "/localapi/v0/debug":
|
||||
h.serveDebug(w, r)
|
||||
case "/":
|
||||
io.WriteString(w, "tailscaled\n")
|
||||
default:
|
||||
@@ -195,6 +197,35 @@ func (h *Handler) serveMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
clientmetric.WritePrometheusExpositionFormat(w)
|
||||
}
|
||||
|
||||
func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
action := r.FormValue("action")
|
||||
var err error
|
||||
switch action {
|
||||
case "rebind":
|
||||
err = h.b.DebugRebind()
|
||||
case "restun":
|
||||
err = h.b.DebugReSTUN()
|
||||
case "":
|
||||
err = fmt.Errorf("missing parameter 'action'")
|
||||
default:
|
||||
err = fmt.Errorf("unknown action %q", action)
|
||||
}
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 400)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
io.WriteString(w, "done\n")
|
||||
}
|
||||
|
||||
// serveProfileFunc is the implementation of Handler.serveProfile, after auth,
|
||||
// for platforms where we want to link it in.
|
||||
var serveProfileFunc func(http.ResponseWriter, *http.Request)
|
||||
|
||||
41
ipn/prefs.go
41
ipn/prefs.go
@@ -426,6 +426,47 @@ func (p *Prefs) AdminPageURL() string {
|
||||
return url + "/admin/machines"
|
||||
}
|
||||
|
||||
// AdvertisesExitNode reports whether p is advertising both the v4 and
|
||||
// v6 /0 exit node routes.
|
||||
func (p *Prefs) AdvertisesExitNode() bool {
|
||||
if p == nil {
|
||||
return false
|
||||
}
|
||||
var v4, v6 bool
|
||||
for _, r := range p.AdvertiseRoutes {
|
||||
if r.Bits() != 0 {
|
||||
continue
|
||||
}
|
||||
if r.IP().Is4() {
|
||||
v4 = true
|
||||
} else if r.IP().Is6() {
|
||||
v6 = true
|
||||
}
|
||||
}
|
||||
return v4 && v6
|
||||
}
|
||||
|
||||
// SetAdvertiseExitNode mutates p (if non-nil) to add or remove the two
|
||||
// /0 exit node routes.
|
||||
func (p *Prefs) SetAdvertiseExitNode(runExit bool) {
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
all := p.AdvertiseRoutes
|
||||
p.AdvertiseRoutes = p.AdvertiseRoutes[:0]
|
||||
for _, r := range all {
|
||||
if r.Bits() != 0 {
|
||||
p.AdvertiseRoutes = append(p.AdvertiseRoutes, r)
|
||||
}
|
||||
}
|
||||
if !runExit {
|
||||
return
|
||||
}
|
||||
p.AdvertiseRoutes = append(p.AdvertiseRoutes,
|
||||
netaddr.IPPrefixFrom(netaddr.IPv4(0, 0, 0, 0), 0),
|
||||
netaddr.IPPrefixFrom(netaddr.IPv6Unspecified(), 0))
|
||||
}
|
||||
|
||||
// PrefsFromBytes deserializes Prefs from a JSON blob. If
|
||||
// enforceDefaults is true, Prefs.RouteAll and Prefs.AllowSingleHosts
|
||||
// are forced on.
|
||||
|
||||
@@ -646,3 +646,35 @@ func TestMaskedPrefsPretty(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefsExitNode(t *testing.T) {
|
||||
var p *Prefs
|
||||
if p.AdvertisesExitNode() {
|
||||
t.Errorf("nil shouldn't advertise exit node")
|
||||
}
|
||||
p = NewPrefs()
|
||||
if p.AdvertisesExitNode() {
|
||||
t.Errorf("default shouldn't advertise exit node")
|
||||
}
|
||||
p.AdvertiseRoutes = []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.0.0/16"),
|
||||
}
|
||||
p.SetAdvertiseExitNode(true)
|
||||
if got, want := len(p.AdvertiseRoutes), 3; got != want {
|
||||
t.Errorf("routes = %d; want %d", got, want)
|
||||
}
|
||||
p.SetAdvertiseExitNode(true)
|
||||
if got, want := len(p.AdvertiseRoutes), 3; got != want {
|
||||
t.Errorf("routes = %d; want %d", got, want)
|
||||
}
|
||||
if !p.AdvertisesExitNode() {
|
||||
t.Errorf("not advertising after enable")
|
||||
}
|
||||
p.SetAdvertiseExitNode(false)
|
||||
if p.AdvertisesExitNode() {
|
||||
t.Errorf("advertising after disable")
|
||||
}
|
||||
if got, want := len(p.AdvertiseRoutes), 1; got != want {
|
||||
t.Errorf("routes = %d; want %d", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,14 @@
|
||||
package logpolicy
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
@@ -29,6 +32,7 @@ import (
|
||||
|
||||
"golang.org/x/term"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/log/filelogger"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/logtail/filch"
|
||||
"tailscale.com/net/dnscache"
|
||||
@@ -38,6 +42,7 @@ import (
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
@@ -65,6 +70,15 @@ func getLogTarget() string {
|
||||
return getLogTargetOnce.v
|
||||
}
|
||||
|
||||
// LogHost returns the hostname only (without port) of the configured
|
||||
// logtail server, or the default.
|
||||
func LogHost() string {
|
||||
if v := getLogTarget(); v != "" {
|
||||
return v
|
||||
}
|
||||
return logtail.DefaultHost
|
||||
}
|
||||
|
||||
// Config represents an instance of logs in a collection.
|
||||
type Config struct {
|
||||
Collection string
|
||||
@@ -524,8 +538,20 @@ func New(collection string) *Policy {
|
||||
}
|
||||
}
|
||||
lw := logtail.NewLogger(c, log.Printf)
|
||||
|
||||
var logOutput io.Writer = lw
|
||||
|
||||
if runtime.GOOS == "windows" && c.Collection == logtail.CollectionNode {
|
||||
logID := newc.PublicID.String()
|
||||
exe, _ := os.Executable()
|
||||
if strings.EqualFold(filepath.Base(exe), "tailscaled.exe") {
|
||||
diskLogf := filelogger.New("tailscale-service", logID, lw.Logf)
|
||||
logOutput = logger.FuncWriter(diskLogf)
|
||||
}
|
||||
}
|
||||
|
||||
log.SetFlags(0) // other logflags are set on console, not here
|
||||
log.SetOutput(lw)
|
||||
log.SetOutput(logOutput)
|
||||
|
||||
log.Printf("Program starting: v%v, Go %v: %#v",
|
||||
version.Long,
|
||||
@@ -602,6 +628,24 @@ func NewLogtailTransport(host string) *http.Transport {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
if version.IsWindowsGUI() && strings.HasPrefix(netw, "tcp") {
|
||||
if c, err := safesocket.Connect(safesocket.DefaultConnectionStrategy("")); err == nil {
|
||||
fmt.Fprintf(c, "CONNECT %s HTTP/1.0\r\n\r\n", addr)
|
||||
br := bufio.NewReader(c)
|
||||
res, err := http.ReadResponse(br, nil)
|
||||
if err == nil && res.StatusCode != 200 {
|
||||
err = errors.New(res.Status)
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("logtail: CONNECT response from tailscaled: %v", err)
|
||||
c.Close()
|
||||
} else {
|
||||
log.Printf("logtail: connected via tailscaled")
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we failed to dial, try again with bootstrap DNS.
|
||||
log.Printf("logtail: dial %q failed: %v (in %v), trying bootstrap...", addr, err, d)
|
||||
dnsCache := &dnscache.Resolver{
|
||||
|
||||
@@ -523,6 +523,11 @@ func (l *Logger) encode(buf []byte) []byte {
|
||||
return b
|
||||
}
|
||||
|
||||
// Logf logs to l using the provided fmt-style format and optional arguments.
|
||||
func (l *Logger) Logf(format string, args ...interface{}) {
|
||||
fmt.Fprintf(l, format, args...)
|
||||
}
|
||||
|
||||
// Write logs an encoded JSON blob.
|
||||
//
|
||||
// If the []byte passed to Write is not an encoded JSON blob,
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/dns/resolver"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
@@ -40,6 +41,16 @@ type Config struct {
|
||||
// it to resolve, you also need to add appropriate routes to
|
||||
// Routes.
|
||||
Hosts map[dnsname.FQDN][]netaddr.IP
|
||||
// OnlyIPv6, if true, uses the IPv6 service IP (for MagicDNS)
|
||||
// instead of the IPv4 version (100.100.100.100).
|
||||
OnlyIPv6 bool
|
||||
}
|
||||
|
||||
func (c *Config) serviceIP() netaddr.IP {
|
||||
if c.OnlyIPv6 {
|
||||
return tsaddr.TailscaleServiceIPv6()
|
||||
}
|
||||
return tsaddr.TailscaleServiceIP()
|
||||
}
|
||||
|
||||
// WriteToBufioWriter write a debug version of c for logs to w, omitting
|
||||
|
||||
@@ -344,7 +344,14 @@ func (m *directManager) SetDNS(config OSConfig) (err error) {
|
||||
// cause a disruptive DNS outage each time we reset an empty
|
||||
// OS configuration.
|
||||
if changed && isResolvedRunning() && !runningAsGUIDesktopUser() {
|
||||
exec.Command("systemctl", "restart", "systemd-resolved.service").Run()
|
||||
t0 := time.Now()
|
||||
err := restartResolved()
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err != nil {
|
||||
m.logf("error restarting resolved after %v: %v", d, err)
|
||||
} else {
|
||||
m.logf("restarted resolved after %v", d)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"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"
|
||||
@@ -122,7 +121,7 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
|
||||
// through quad-100.
|
||||
rcfg.Routes = routes
|
||||
rcfg.Routes["."] = cfg.DefaultResolvers
|
||||
ocfg.Nameservers = []netaddr.IP{tsaddr.TailscaleServiceIP()}
|
||||
ocfg.Nameservers = []netaddr.IP{cfg.serviceIP()}
|
||||
return rcfg, ocfg, nil
|
||||
}
|
||||
|
||||
@@ -159,7 +158,7 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
|
||||
// or routes + MagicDNS, or just MagicDNS, or on an OS that cannot
|
||||
// split-DNS. Install a split config pointing at quad-100.
|
||||
rcfg.Routes = routes
|
||||
ocfg.Nameservers = []netaddr.IP{tsaddr.TailscaleServiceIP()}
|
||||
ocfg.Nameservers = []netaddr.IP{cfg.serviceIP()}
|
||||
|
||||
// If the OS can't do native split-dns, read out the underlying
|
||||
// resolver config and blend it into our config.
|
||||
|
||||
@@ -4,8 +4,71 @@
|
||||
|
||||
package dns
|
||||
|
||||
import "tailscale.com/types/logger"
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
func NewOSConfigurator(logf logger.Logf, _ string) (OSConfigurator, error) {
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
type kv struct {
|
||||
k, v string
|
||||
}
|
||||
|
||||
func (kv kv) String() string {
|
||||
return fmt.Sprintf("%s=%s", kv.k, kv.v)
|
||||
}
|
||||
|
||||
func NewOSConfigurator(logf logger.Logf, interfaceName string) (OSConfigurator, error) {
|
||||
return newOSConfigurator(logf, interfaceName,
|
||||
newOSConfigEnv{
|
||||
rcIsResolvd: rcIsResolvd,
|
||||
fs: directFS{},
|
||||
})
|
||||
}
|
||||
|
||||
// newOSConfigEnv are the funcs newOSConfigurator needs, pulled out for testing.
|
||||
type newOSConfigEnv struct {
|
||||
fs directFS
|
||||
rcIsResolvd func(resolvConfContents []byte) bool
|
||||
}
|
||||
|
||||
func newOSConfigurator(logf logger.Logf, interfaceName string, env newOSConfigEnv) (ret OSConfigurator, err error) {
|
||||
var debug []kv
|
||||
dbg := func(k, v string) {
|
||||
debug = append(debug, kv{k, v})
|
||||
}
|
||||
defer func() {
|
||||
if ret != nil {
|
||||
dbg("ret", fmt.Sprintf("%T", ret))
|
||||
}
|
||||
logf("dns: %v", debug)
|
||||
}()
|
||||
|
||||
bs, err := env.fs.ReadFile(resolvConf)
|
||||
if os.IsNotExist(err) {
|
||||
dbg("rc", "missing")
|
||||
return newDirectManager(logf), nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("reading /etc/resolv.conf: %w", err)
|
||||
}
|
||||
|
||||
if env.rcIsResolvd(bs) {
|
||||
dbg("resolvd", "yes")
|
||||
return newResolvdManager(logf, interfaceName)
|
||||
}
|
||||
|
||||
dbg("resolvd", "missing")
|
||||
return newDirectManager(logf), nil
|
||||
}
|
||||
|
||||
func rcIsResolvd(resolvConfContents []byte) bool {
|
||||
// If we have the string "# resolvd:" in resolv.conf resolvd(8) is
|
||||
// managing things.
|
||||
if bytes.Contains(resolvConfContents, []byte("# resolvd:")) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ package dns
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -34,6 +36,8 @@ const (
|
||||
versionKey = `SOFTWARE\Microsoft\Windows NT\CurrentVersion`
|
||||
)
|
||||
|
||||
var configureWSL, _ = strconv.ParseBool(os.Getenv("TS_DEBUG_CONFIGURE_WSL"))
|
||||
|
||||
type windowsManager struct {
|
||||
logf logger.Logf
|
||||
guid string
|
||||
@@ -307,13 +311,15 @@ func (m windowsManager) SetDNS(cfg OSConfig) error {
|
||||
|
||||
// On initial setup of WSL, the restart caused by --shutdown is slow,
|
||||
// so we do it out-of-line.
|
||||
go func() {
|
||||
if err := m.wslManager.SetDNS(cfg); err != nil {
|
||||
m.logf("WSL SetDNS: %v", err) // continue
|
||||
} else {
|
||||
m.logf("WSL SetDNS: success")
|
||||
}
|
||||
}()
|
||||
if configureWSL {
|
||||
go func() {
|
||||
if err := m.wslManager.SetDNS(cfg); err != nil {
|
||||
m.logf("WSL SetDNS: %v", err) // continue
|
||||
} else {
|
||||
m.logf("WSL SetDNS: success")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
161
net/dns/resolvd.go
Normal file
161
net/dns/resolvd.go
Normal file
@@ -0,0 +1,161 @@
|
||||
// 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 openbsd
|
||||
// +build openbsd
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
func newResolvdManager(logf logger.Logf, interfaceName string) (*resolvdManager, error) {
|
||||
return &resolvdManager{
|
||||
logf: logf,
|
||||
ifName: interfaceName,
|
||||
fs: directFS{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// resolvdManager is an OSConfigurator which uses route(1) to teach OpenBSD's
|
||||
// resolvd(8) about DNS servers.
|
||||
type resolvdManager struct {
|
||||
logf logger.Logf
|
||||
ifName string
|
||||
fs directFS
|
||||
}
|
||||
|
||||
func (m *resolvdManager) SetDNS(config OSConfig) error {
|
||||
args := []string{
|
||||
"nameserver",
|
||||
m.ifName,
|
||||
}
|
||||
|
||||
origResolv, err := m.readAndCopy(resolvConf, backupConf, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newResolvConf := removeSearchLines(origResolv)
|
||||
|
||||
for _, ns := range config.Nameservers {
|
||||
args = append(args, ns.String())
|
||||
}
|
||||
|
||||
var newSearch = []string{
|
||||
"search",
|
||||
}
|
||||
for _, s := range config.SearchDomains {
|
||||
newSearch = append(newSearch, s.WithoutTrailingDot())
|
||||
}
|
||||
|
||||
newResolvConf = append(newResolvConf, []byte(strings.Join(newSearch, " "))...)
|
||||
|
||||
err = m.fs.WriteFile(resolvConf, newResolvConf, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd := exec.Command("/sbin/route", args...)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (m *resolvdManager) SupportsSplitDNS() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *resolvdManager) GetBaseConfig() (OSConfig, error) {
|
||||
cfg, err := m.readResolvConf()
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (m *resolvdManager) Close() error {
|
||||
// resolvd handles teardown of nameservers so we only need to write back the original
|
||||
// config and be done.
|
||||
|
||||
_, err := m.readAndCopy(backupConf, resolvConf, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return m.fs.Remove(backupConf)
|
||||
}
|
||||
|
||||
func (m *resolvdManager) readAndCopy(a, b string, mode os.FileMode) ([]byte, error) {
|
||||
orig, err := m.fs.ReadFile(a)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = m.fs.WriteFile(b, orig, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return orig, nil
|
||||
}
|
||||
|
||||
func (m resolvdManager) readResolvConf() (config OSConfig, err error) {
|
||||
b, err := m.fs.ReadFile(resolvConf)
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(bytes.NewReader(b))
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
// resolvd manages "nameserver" lines, we only need to handle
|
||||
// "search".
|
||||
if strings.HasPrefix(line, "search") {
|
||||
domain := strings.TrimPrefix(line, "search")
|
||||
domain = strings.TrimSpace(domain)
|
||||
fqdn, err := dnsname.ToFQDN(domain)
|
||||
if err != nil {
|
||||
return OSConfig{}, fmt.Errorf("parsing search domains %q: %w", line, err)
|
||||
}
|
||||
config.SearchDomains = append(config.SearchDomains, fqdn)
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(line, "nameserver") {
|
||||
s := strings.TrimPrefix(line, "nameserver")
|
||||
parts := strings.Split(s, " # ")
|
||||
if len(parts) == 0 {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
nameserver := strings.TrimSpace(parts[0])
|
||||
ip, err := netaddr.ParseIP(nameserver)
|
||||
if err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
config.Nameservers = append(config.Nameservers, ip)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if err = scanner.Err(); err != nil {
|
||||
return OSConfig{}, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func removeSearchLines(orig []byte) []byte {
|
||||
re := regexp.MustCompile(`(?m)^search\s+.+$`)
|
||||
return re.ReplaceAll(orig, []byte(""))
|
||||
}
|
||||
78
net/dns/resolver/debug.go
Normal file
78
net/dns/resolver/debug.go
Normal file
@@ -0,0 +1,78 @@
|
||||
// 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 resolver
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/health"
|
||||
)
|
||||
|
||||
func init() {
|
||||
health.RegisterDebugHandler("dnsfwd", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
n, _ := strconv.Atoi(r.FormValue("n"))
|
||||
if n <= 0 {
|
||||
n = 100
|
||||
} else if n > 10000 {
|
||||
n = 10000
|
||||
}
|
||||
fl, ok := fwdLogAtomic.Load().(*fwdLog)
|
||||
if !ok || n != len(fl.ent) {
|
||||
fl = &fwdLog{ent: make([]fwdLogEntry, n)}
|
||||
fwdLogAtomic.Store(fl)
|
||||
}
|
||||
fl.ServeHTTP(w, r)
|
||||
}))
|
||||
}
|
||||
|
||||
var fwdLogAtomic atomic.Value // of *fwdLog
|
||||
|
||||
type fwdLog struct {
|
||||
mu sync.Mutex
|
||||
pos int // ent[pos] is next entry
|
||||
ent []fwdLogEntry
|
||||
}
|
||||
|
||||
type fwdLogEntry struct {
|
||||
Domain string
|
||||
Time time.Time
|
||||
}
|
||||
|
||||
func (fl *fwdLog) addName(name string) {
|
||||
if fl == nil {
|
||||
return
|
||||
}
|
||||
fl.mu.Lock()
|
||||
defer fl.mu.Unlock()
|
||||
if len(fl.ent) == 0 {
|
||||
return
|
||||
}
|
||||
fl.ent[fl.pos] = fwdLogEntry{Domain: name, Time: time.Now()}
|
||||
fl.pos++
|
||||
if fl.pos == len(fl.ent) {
|
||||
fl.pos = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (fl *fwdLog) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
fl.mu.Lock()
|
||||
defer fl.mu.Unlock()
|
||||
|
||||
fmt.Fprintf(w, "<html><h1>DNS forwards</h1>")
|
||||
now := time.Now()
|
||||
for i := 0; i < len(fl.ent); i++ {
|
||||
ent := fl.ent[(i+fl.pos)%len(fl.ent)]
|
||||
if ent.Domain == "" {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(w, "%v ago: %v<br>\n", now.Sub(ent.Time).Round(time.Second), html.EscapeString(ent.Domain))
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
dns "golang.org/x/net/dns/dnsmessage"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/net/neterror"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/types/dnstype"
|
||||
@@ -482,7 +483,7 @@ func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDe
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if packetWasTruncated(err) {
|
||||
if neterror.PacketWasTruncated(err) {
|
||||
err = nil
|
||||
} else {
|
||||
metricDNSFwdUDPErrorRead.Add(1)
|
||||
@@ -576,8 +577,8 @@ func (f *forwarder) forward(query packet) error {
|
||||
return f.forwardWithDestChan(ctx, query, f.responses)
|
||||
}
|
||||
|
||||
// forward forwards the query to all upstream nameservers and waits
|
||||
// for the first response.
|
||||
// forwardWithDestChan 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).
|
||||
@@ -598,7 +599,21 @@ func (f *forwarder) forwardWithDestChan(ctx context.Context, query packet, respo
|
||||
// out, playing on Sonos still works.
|
||||
if hasRDNSBonjourPrefix(domain) {
|
||||
metricDNSFwdDropBonjour.Add(1)
|
||||
return nil
|
||||
res, err := nxDomainResponse(query)
|
||||
if err != nil {
|
||||
f.logf("error parsing bonjour query: %v", err)
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case responseChan <- res:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if fl, ok := fwdLogAtomic.Load().(*fwdLog); ok {
|
||||
fl.addName(string(domain))
|
||||
}
|
||||
|
||||
clampEDNSSize(query.bs, maxResponseBytes)
|
||||
@@ -696,6 +711,28 @@ func nameFromQuery(bs []byte) (dnsname.FQDN, error) {
|
||||
return dnsname.ToFQDN(rawNameToLower(n))
|
||||
}
|
||||
|
||||
// nxDomainResponse returns an NXDomain DNS reply for the provided request.
|
||||
func nxDomainResponse(req packet) (res packet, err error) {
|
||||
p := dnsParserPool.Get().(*dnsParser)
|
||||
defer dnsParserPool.Put(p)
|
||||
|
||||
if err := p.parseQuery(req.bs); err != nil {
|
||||
return packet{}, err
|
||||
}
|
||||
|
||||
h := p.Header
|
||||
h.Response = true
|
||||
h.RecursionAvailable = h.RecursionDesired
|
||||
h.RCode = dns.RCodeNameError
|
||||
b := dns.NewBuilder(nil, h)
|
||||
// TODO(bradfitz): should we add an SOA record in the Authority
|
||||
// section too? (for the nxdomain negative caching TTL)
|
||||
// For which zone? Does iOS care?
|
||||
res.bs, err = b.Finish()
|
||||
res.addr = req.addr
|
||||
return res, err
|
||||
}
|
||||
|
||||
// closePool is a dynamic set of io.Closers to close as a group.
|
||||
// It's intended to be Closed at most once.
|
||||
//
|
||||
|
||||
@@ -168,3 +168,25 @@ func TestMaxDoHInFlight(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkNameFromQuery(b *testing.B) {
|
||||
builder := dns.NewBuilder(nil, dns.Header{})
|
||||
builder.StartQuestions()
|
||||
builder.Question(dns.Question{
|
||||
Name: dns.MustNewName("foo.example."),
|
||||
Type: dns.TypeA,
|
||||
Class: dns.ClassINET,
|
||||
})
|
||||
msg, err := builder.Finish()
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
b.ResetTimer()
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := nameFromQuery(msg)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
// 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 resolver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Avoid allocation when calling errors.Is below
|
||||
// by converting syscall.Errno to error here.
|
||||
var (
|
||||
networkDown error = syscall.ENETDOWN
|
||||
networkUnreachable error = syscall.ENETUNREACH
|
||||
)
|
||||
|
||||
func networkIsDown(err error) bool {
|
||||
return errors.Is(err, networkDown)
|
||||
}
|
||||
|
||||
func networkIsUnreachable(err error) bool {
|
||||
return errors.Is(err, networkUnreachable)
|
||||
}
|
||||
|
||||
// packetWasTruncated returns true if err indicates truncation but the RecvFrom
|
||||
// that generated err was otherwise successful. It always returns false on this
|
||||
// platform.
|
||||
func packetWasTruncated(err error) bool { return false }
|
||||
@@ -1,16 +0,0 @@
|
||||
// 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 !darwin && !windows
|
||||
// +build !darwin,!windows
|
||||
|
||||
package resolver
|
||||
|
||||
func networkIsDown(err error) bool { return false }
|
||||
func networkIsUnreachable(err error) bool { return false }
|
||||
|
||||
// packetWasTruncated returns true if err indicates truncation but the RecvFrom
|
||||
// that generated err was otherwise successful. It always returns false on this
|
||||
// platform.
|
||||
func packetWasTruncated(err error) bool { return false }
|
||||
@@ -1,43 +0,0 @@
|
||||
// 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 resolver
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func networkIsDown(err error) bool {
|
||||
if oe, ok := err.(*net.OpError); ok && oe.Op == "write" {
|
||||
if se, ok := oe.Err.(*os.SyscallError); ok {
|
||||
if se.Syscall == "wsasendto" && se.Err == windows.WSAENETUNREACH {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func networkIsUnreachable(err error) bool {
|
||||
// TODO(bradfitz,josharian): something here? what is the
|
||||
// difference between down and unreachable? Add comments.
|
||||
return false
|
||||
}
|
||||
|
||||
// packetWasTruncated returns true if err indicates truncation but the RecvFrom
|
||||
// that generated err was otherwise successful. On Windows, Go's UDP RecvFrom
|
||||
// calls WSARecvFrom which returns the WSAEMSGSIZE error code when the received
|
||||
// datagram is larger than the provided buffer. When that happens, both a valid
|
||||
// size and an error are returned (as per the partial fix for golang/go#14074).
|
||||
// If the WSAEMSGSIZE error is returned, then we ignore the error to get
|
||||
// semantics similar to the POSIX operating systems. One caveat is that it
|
||||
// appears that the source address is not returned when WSAEMSGSIZE occurs, but
|
||||
// we do not currently look at the source address.
|
||||
func packetWasTruncated(err error) bool {
|
||||
return errors.Is(err, windows.WSAEMSGSIZE)
|
||||
}
|
||||
@@ -357,10 +357,7 @@ func (r *Resolver) HandleExitNodeDNSQuery(ctx context.Context, q []byte, from ne
|
||||
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:
|
||||
case "windows", "android":
|
||||
return handleExitNodeDNSQueryWithNetPkg(ctx, nil, resp)
|
||||
case "darwin":
|
||||
// /etc/resolv.conf is a lie and only says one upstream DNS
|
||||
@@ -377,7 +374,7 @@ func (r *Resolver) HandleExitNodeDNSQuery(ctx context.Context, q []byte, from ne
|
||||
// TODO: more than 1 resolver from /etc/resolv.conf?
|
||||
|
||||
var resolvers []resolverAndDelay
|
||||
if nameserver == tsaddr.TailscaleServiceIP() {
|
||||
if nameserver == tsaddr.TailscaleServiceIP() || nameserver == tsaddr.TailscaleServiceIPv6() {
|
||||
// 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
|
||||
@@ -532,6 +529,10 @@ func stubResolverForOS() (ip netaddr.IP, err error) {
|
||||
if c, ok := resolvConfCacheValue.Load().(resolvConfCache); ok && c.mod == cur.mod && c.size == cur.size {
|
||||
return c.ip, nil
|
||||
}
|
||||
// TODO(bradfitz): unify this /etc/resolv.conf parsing code with readResolv
|
||||
// in net/dns, which we can't use due to circular dependency reasons.
|
||||
// Move it to a leaf, including the OSConfig type (perhaps in its own dnstype
|
||||
// package?)
|
||||
err = lineread.File("/etc/resolv.conf", func(line []byte) error {
|
||||
if !ip.IsZero() {
|
||||
return nil
|
||||
@@ -540,6 +541,12 @@ func stubResolverForOS() (ip netaddr.IP, err error) {
|
||||
if len(line) == 0 || line[0] == '#' {
|
||||
return nil
|
||||
}
|
||||
// Normalize tabs to spaces to simplify parsing code later.
|
||||
for i, b := range line {
|
||||
if b == '\t' {
|
||||
line[i] = ' '
|
||||
}
|
||||
}
|
||||
if mem.HasPrefix(mem.B(line), mem.S("nameserver ")) {
|
||||
s := strings.TrimSpace(strings.TrimPrefix(string(line), "nameserver "))
|
||||
ip, err = netaddr.ParseIP(s)
|
||||
@@ -1087,11 +1094,13 @@ func rdnsNameToIPv6(name dnsname.FQDN) (ip netaddr.IP, ok bool) {
|
||||
// It is assumed that resp.Question is populated by respond before this is called.
|
||||
func (r *Resolver) respondReverse(query []byte, name dnsname.FQDN, resp *response) ([]byte, error) {
|
||||
if hasRDNSBonjourPrefix(name) {
|
||||
metricDNSReverseMissBonjour.Add(1)
|
||||
return nil, errNotOurName
|
||||
}
|
||||
|
||||
resp.Name, resp.Header.RCode = r.resolveLocalReverse(name)
|
||||
if resp.Header.RCode == dns.RCodeRefused {
|
||||
metricDNSReverseMissOther.Add(1)
|
||||
return nil, errNotOurName
|
||||
}
|
||||
|
||||
@@ -1235,4 +1244,7 @@ var (
|
||||
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")
|
||||
|
||||
metricDNSReverseMissBonjour = clientmetric.NewCounter("dns_reverse_miss_bonjour")
|
||||
metricDNSReverseMissOther = clientmetric.NewCounter("dns_reverse_miss_other")
|
||||
)
|
||||
|
||||
@@ -457,9 +457,7 @@ func TLSDialer(fwd DialContextFunc, dnsCache *Resolver, tlsConfigBase *tls.Confi
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
err := tlsConn.Handshake()
|
||||
handshakeTimeoutCancel()
|
||||
errc <- err
|
||||
errc <- tlsConn.Handshake()
|
||||
}()
|
||||
if err := <-errc; err != nil {
|
||||
tcpConn.Close()
|
||||
|
||||
@@ -172,6 +172,7 @@ func sortIPs(s []netaddr.IP) {
|
||||
type Interface struct {
|
||||
*net.Interface
|
||||
AltAddrs []net.Addr // if non-nil, returned by Addrs
|
||||
Desc string // extra description (used on Windows)
|
||||
}
|
||||
|
||||
func (i Interface) IsLoopback() bool { return isLoopback(i.Interface) }
|
||||
@@ -278,13 +279,16 @@ type State struct {
|
||||
// instead of Wifi. This field is not populated by GetState.
|
||||
IsExpensive bool
|
||||
|
||||
// DefaultRouteInterface is the interface name for the machine's default route.
|
||||
// DefaultRouteInterface is the interface name for the
|
||||
// machine's default route.
|
||||
//
|
||||
// It is not yet populated on all OSes.
|
||||
// Its exact value should not be assumed to be a map key for
|
||||
// the Interface maps above; it's only used for debugging.
|
||||
//
|
||||
// When non-empty, its value is the map key into Interface and
|
||||
// InterfaceIPs.
|
||||
DefaultRouteInterface string
|
||||
|
||||
// HTTPProxy is the HTTP proxy to use.
|
||||
// HTTPProxy is the HTTP proxy to use, if any.
|
||||
HTTPProxy string
|
||||
|
||||
// PAC is the URL to the Proxy Autoconfig URL, if applicable.
|
||||
@@ -293,7 +297,13 @@ type State struct {
|
||||
|
||||
func (s *State) String() string {
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "interfaces.State{defaultRoute=%v ifs={", s.DefaultRouteInterface)
|
||||
fmt.Fprintf(&sb, "interfaces.State{defaultRoute=%v ", s.DefaultRouteInterface)
|
||||
if s.DefaultRouteInterface != "" {
|
||||
if iface, ok := s.Interface[s.DefaultRouteInterface]; ok && iface.Desc != "" {
|
||||
fmt.Fprintf(&sb, "(%s) ", iface.Desc)
|
||||
}
|
||||
}
|
||||
sb.WriteString("ifs={")
|
||||
ifs := make([]string, 0, len(s.Interface))
|
||||
for k := range s.Interface {
|
||||
if anyInterestingIP(s.InterfaceIPs[k]) {
|
||||
@@ -507,7 +517,16 @@ func GetState() (*State, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.DefaultRouteInterface, _ = DefaultRouteInterface()
|
||||
dr, _ := DefaultRoute()
|
||||
s.DefaultRouteInterface = dr.InterfaceName
|
||||
|
||||
// Populate description (for Windows, primarily) if present.
|
||||
if desc := dr.InterfaceDesc; desc != "" {
|
||||
if iface, ok := s.Interface[dr.InterfaceName]; ok {
|
||||
iface.Desc = desc
|
||||
s.Interface[dr.InterfaceName] = iface
|
||||
}
|
||||
}
|
||||
|
||||
if s.AnyInterfaceUp() {
|
||||
req, err := http.NewRequest("GET", LoginEndpointForProxyDetermination, nil)
|
||||
@@ -667,3 +686,36 @@ func netInterfaces() ([]Interface, error) {
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// DefaultRouteDetails are the
|
||||
type DefaultRouteDetails struct {
|
||||
// InterfaceName is the interface name. It must always be populated.
|
||||
// It's like "eth0" (Linux), "Ethernet 2" (Windows), "en0" (macOS).
|
||||
InterfaceName string
|
||||
|
||||
// InterfaceDesc is populated on Windows at least. It's a
|
||||
// longer description, like "Red Hat VirtIO Ethernet Adapter".
|
||||
InterfaceDesc string
|
||||
|
||||
// InterfaceIndex is like net.Interface.Index.
|
||||
// Zero means not populated.
|
||||
InterfaceIndex int
|
||||
|
||||
// TODO(bradfitz): break this out into v4-vs-v6 once that need arises.
|
||||
}
|
||||
|
||||
// DefaultRouteInterface is like DefaultRoute but only returns the
|
||||
// interface name.
|
||||
func DefaultRouteInterface() (string, error) {
|
||||
dr, err := DefaultRoute()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return dr.InterfaceName, nil
|
||||
}
|
||||
|
||||
// DefaultRoute returns details of the network interface that owns
|
||||
// the default route, not including any tailscale interfaces.
|
||||
func DefaultRoute() (DefaultRouteDetails, error) {
|
||||
return defaultRoute()
|
||||
}
|
||||
|
||||
@@ -16,16 +16,18 @@ import (
|
||||
"inet.af/netaddr"
|
||||
)
|
||||
|
||||
func DefaultRouteInterface() (string, error) {
|
||||
func defaultRoute() (d DefaultRouteDetails, err error) {
|
||||
idx, err := DefaultRouteInterfaceIndex()
|
||||
if err != nil {
|
||||
return "", err
|
||||
return d, err
|
||||
}
|
||||
iface, err := net.InterfaceByIndex(idx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return d, err
|
||||
}
|
||||
return iface.Name, nil
|
||||
d.InterfaceName = iface.Name
|
||||
d.InterfaceIndex = idx
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP2.
|
||||
|
||||
@@ -11,6 +11,6 @@ import "errors"
|
||||
|
||||
var errTODO = errors.New("TODO")
|
||||
|
||||
func DefaultRouteInterface() (string, error) {
|
||||
return "TODO", errTODO
|
||||
func defaultRoute() (DefaultRouteDetails, error) {
|
||||
return DefaultRouteDetails{}, errTODO
|
||||
}
|
||||
|
||||
@@ -122,17 +122,18 @@ func likelyHomeRouterIPAndroid() (ret netaddr.IP, ok bool) {
|
||||
return ret, !ret.IsZero()
|
||||
}
|
||||
|
||||
// DefaultRouteInterface returns the name of the network interface that owns
|
||||
// the default route, not including any tailscale interfaces.
|
||||
func DefaultRouteInterface() (string, error) {
|
||||
func defaultRoute() (d DefaultRouteDetails, err error) {
|
||||
v, err := defaultRouteInterfaceProcNet()
|
||||
if err == nil {
|
||||
return v, nil
|
||||
d.InterfaceName = v
|
||||
return d, nil
|
||||
}
|
||||
if runtime.GOOS == "android" {
|
||||
return defaultRouteInterfaceAndroidIPRoute()
|
||||
v, err = defaultRouteInterfaceAndroidIPRoute()
|
||||
d.InterfaceName = v
|
||||
return d, err
|
||||
}
|
||||
return v, err
|
||||
return d, err
|
||||
}
|
||||
|
||||
var zeroRouteBytes = []byte("00000000")
|
||||
|
||||
@@ -104,3 +104,55 @@ func TestStateEqualFilteredIPFilter(t *testing.T) {
|
||||
t.Errorf("%+v == %+v when restricting to interesting interfaces and IPs", s1, s2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
s *State
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "typical_linux",
|
||||
s: &State{
|
||||
DefaultRouteInterface: "eth0",
|
||||
Interface: map[string]Interface{
|
||||
"eth0": {
|
||||
Interface: &net.Interface{
|
||||
Flags: net.FlagUp,
|
||||
},
|
||||
},
|
||||
"wlan0": {
|
||||
Interface: &net.Interface{},
|
||||
},
|
||||
},
|
||||
InterfaceIPs: map[string][]netaddr.IPPrefix{
|
||||
"eth0": []netaddr.IPPrefix{
|
||||
netaddr.MustParseIPPrefix("10.0.0.2/8"),
|
||||
},
|
||||
},
|
||||
HaveV4: true,
|
||||
},
|
||||
want: `interfaces.State{defaultRoute=eth0 ifs={eth0:[10.0.0.2/8]} v4=true v6=false}`,
|
||||
},
|
||||
{
|
||||
name: "default_desc",
|
||||
s: &State{
|
||||
DefaultRouteInterface: "foo",
|
||||
Interface: map[string]Interface{
|
||||
"foo": {
|
||||
Desc: "a foo thing",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: `interfaces.State{defaultRoute=foo (a foo thing) ifs={} v4=false v6=false}`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := tt.s.String()
|
||||
if got != tt.want {
|
||||
t.Errorf("wrong\n got: %s\nwant: %s\n", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
@@ -217,18 +216,20 @@ func GetWindowsDefault(family winipcfg.AddressFamily) (*winipcfg.IPAdapterAddres
|
||||
return bestIface, nil
|
||||
}
|
||||
|
||||
func DefaultRouteInterface() (string, error) {
|
||||
func defaultRoute() (d DefaultRouteDetails, err error) {
|
||||
// We always return the IPv4 default route.
|
||||
// TODO(bradfitz): adjust API if/when anything cares. They could in theory differ, though,
|
||||
// in which case we might send traffic to the wrong interface.
|
||||
iface, err := GetWindowsDefault(windows.AF_INET)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return d, err
|
||||
}
|
||||
if iface == nil {
|
||||
return "(none)", nil
|
||||
if iface != nil {
|
||||
d.InterfaceName = iface.FriendlyName()
|
||||
d.InterfaceDesc = iface.Description()
|
||||
d.InterfaceIndex = int(iface.IfIndex)
|
||||
}
|
||||
return fmt.Sprintf("%s (%s)", iface.FriendlyName(), iface.Description()), nil
|
||||
return d, nil
|
||||
}
|
||||
|
||||
var (
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/neterror"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/net/stun"
|
||||
@@ -1234,7 +1235,7 @@ func (rs *reportState) runProbe(ctx context.Context, dm *tailcfg.DERPMap, probe
|
||||
case probeIPv4:
|
||||
metricSTUNSend4.Add(1)
|
||||
n, err := rs.pc4.WriteTo(req, addr)
|
||||
if n == len(req) && err == nil {
|
||||
if n == len(req) && err == nil || neterror.TreatAsLostUDP(err) {
|
||||
rs.mu.Lock()
|
||||
rs.report.IPv4CanSend = true
|
||||
rs.mu.Unlock()
|
||||
@@ -1242,7 +1243,7 @@ func (rs *reportState) runProbe(ctx context.Context, dm *tailcfg.DERPMap, probe
|
||||
case probeIPv6:
|
||||
metricSTUNSend6.Add(1)
|
||||
n, err := rs.pc6.WriteTo(req, addr)
|
||||
if n == len(req) && err == nil {
|
||||
if n == len(req) && err == nil || neterror.TreatAsLostUDP(err) {
|
||||
rs.mu.Lock()
|
||||
rs.report.IPv6CanSend = true
|
||||
rs.mu.Unlock()
|
||||
|
||||
60
net/neterror/neterror.go
Normal file
60
net/neterror/neterror.go
Normal file
@@ -0,0 +1,60 @@
|
||||
// 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 neterror classifies network errors.
|
||||
package neterror
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"runtime"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var errEPERM error = syscall.EPERM // box it into interface just once
|
||||
|
||||
// TreatAsLostUDP reports whether err is an error from a UDP send
|
||||
// operation that should be treated as a UDP packet that just got
|
||||
// lost.
|
||||
//
|
||||
// Notably, on Linux this reports true for EPERM errors (from outbound
|
||||
// firewall blocks) which aren't really send errors; they're just
|
||||
// sends that are never going to make it because the local OS blocked
|
||||
// it.
|
||||
func TreatAsLostUDP(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
// Linux, while not documented in the man page,
|
||||
// returns EPERM when there's an OUTPUT rule with -j
|
||||
// DROP or -j REJECT. We use this very specific
|
||||
// Linux+EPERM check rather than something super broad
|
||||
// like net.Error.Temporary which could be anything.
|
||||
//
|
||||
// For now we only do this on Linux, as such outgoing
|
||||
// firewall violations mapping to syscall errors
|
||||
// hasn't yet been observed on other OSes.
|
||||
return errors.Is(err, errEPERM)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var packetWasTruncated func(error) bool // non-nil on Windows at least
|
||||
|
||||
// PacketWasTruncated reports whether err indicates truncation but the RecvFrom
|
||||
// that generated err was otherwise successful. On Windows, Go's UDP RecvFrom
|
||||
// calls WSARecvFrom which returns the WSAEMSGSIZE error code when the received
|
||||
// datagram is larger than the provided buffer. When that happens, both a valid
|
||||
// size and an error are returned (as per the partial fix for golang/go#14074).
|
||||
// If the WSAEMSGSIZE error is returned, then we ignore the error to get
|
||||
// semantics similar to the POSIX operating systems. One caveat is that it
|
||||
// appears that the source address is not returned when WSAEMSGSIZE occurs, but
|
||||
// we do not currently look at the source address.
|
||||
func PacketWasTruncated(err error) bool {
|
||||
if packetWasTruncated == nil {
|
||||
return false
|
||||
}
|
||||
return packetWasTruncated(err)
|
||||
}
|
||||
55
net/neterror/neterror_linux_test.go
Normal file
55
net/neterror/neterror_linux_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// 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 neterror
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTreatAsLostUDP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
want bool
|
||||
}{
|
||||
{"nil", nil, false},
|
||||
{"non-nil", errors.New("foo"), false},
|
||||
{"eperm", syscall.EPERM, true},
|
||||
{
|
||||
name: "operror",
|
||||
err: &net.OpError{
|
||||
Op: "write",
|
||||
Err: &os.SyscallError{
|
||||
Syscall: "sendto",
|
||||
Err: syscall.EPERM,
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "host_unreach",
|
||||
err: &net.OpError{
|
||||
Op: "write",
|
||||
Err: &os.SyscallError{
|
||||
Syscall: "sendto",
|
||||
Err: syscall.EHOSTUNREACH,
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := TreatAsLostUDP(tt.err); got != tt.want {
|
||||
t.Errorf("got = %v; want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
17
net/neterror/neterror_windows.go
Normal file
17
net/neterror/neterror_windows.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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 neterror
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func init() {
|
||||
packetWasTruncated = func(err error) bool {
|
||||
return errors.Is(err, windows.WSAEMSGSIZE)
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
@@ -67,8 +66,7 @@ func socketMarkWorks() bool {
|
||||
// If it doesn't, we have to use SO_BINDTODEVICE on our sockets instead.
|
||||
func useSocketMark() bool {
|
||||
socketMarkWorksOnce.Do(func() {
|
||||
ipRuleWorks := exec.Command("ip", "rule").Run() == nil
|
||||
socketMarkWorksOnce.v = ipRuleWorks && socketMarkWorks()
|
||||
socketMarkWorksOnce.v = socketMarkWorks()
|
||||
})
|
||||
return socketMarkWorksOnce.v
|
||||
}
|
||||
|
||||
@@ -22,12 +22,16 @@ const minFrag = 60 + 20 // max IPv4 header + basic TCP header
|
||||
type TCPFlag uint8
|
||||
|
||||
const (
|
||||
TCPFin TCPFlag = 0x01
|
||||
TCPSyn TCPFlag = 0x02
|
||||
TCPRst TCPFlag = 0x04
|
||||
TCPPsh TCPFlag = 0x08
|
||||
TCPAck TCPFlag = 0x10
|
||||
TCPSynAck TCPFlag = TCPSyn | TCPAck
|
||||
TCPFin TCPFlag = 0x01
|
||||
TCPSyn TCPFlag = 0x02
|
||||
TCPRst TCPFlag = 0x04
|
||||
TCPPsh TCPFlag = 0x08
|
||||
TCPAck TCPFlag = 0x10
|
||||
TCPUrg TCPFlag = 0x20
|
||||
TCPECNEcho TCPFlag = 0x40
|
||||
TCPCWR TCPFlag = 0x80
|
||||
TCPSynAck TCPFlag = TCPSyn | TCPAck
|
||||
TCPECNBits TCPFlag = TCPECNEcho | TCPCWR
|
||||
)
|
||||
|
||||
// Parsed is a minimal decoding of a packet suitable for use in filters.
|
||||
@@ -52,7 +56,7 @@ type Parsed struct {
|
||||
Src netaddr.IPPort
|
||||
// DstIP4 is the destination address. Family matches IPVersion.
|
||||
Dst netaddr.IPPort
|
||||
// TCPFlags is the packet's TCP flag bigs. Valid iff IPProto == TCP.
|
||||
// TCPFlags is the packet's TCP flag bits. Valid iff IPProto == TCP.
|
||||
TCPFlags TCPFlag
|
||||
}
|
||||
|
||||
@@ -180,7 +184,7 @@ func (q *Parsed) decode4(b []byte) {
|
||||
}
|
||||
q.Src = q.Src.WithPort(binary.BigEndian.Uint16(sub[0:2]))
|
||||
q.Dst = q.Dst.WithPort(binary.BigEndian.Uint16(sub[2:4]))
|
||||
q.TCPFlags = TCPFlag(sub[13]) & 0x3F
|
||||
q.TCPFlags = TCPFlag(sub[13])
|
||||
headerLength := (sub[12] & 0xF0) >> 2
|
||||
q.dataofs = q.subofs + int(headerLength)
|
||||
return
|
||||
@@ -282,7 +286,7 @@ func (q *Parsed) decode6(b []byte) {
|
||||
}
|
||||
q.Src = q.Src.WithPort(binary.BigEndian.Uint16(sub[0:2]))
|
||||
q.Dst = q.Dst.WithPort(binary.BigEndian.Uint16(sub[2:4]))
|
||||
q.TCPFlags = TCPFlag(sub[13]) & 0x3F
|
||||
q.TCPFlags = TCPFlag(sub[13])
|
||||
headerLength := (sub[12] & 0xF0) >> 2
|
||||
q.dataofs = q.subofs + int(headerLength)
|
||||
return
|
||||
@@ -374,8 +378,14 @@ func (q *Parsed) Payload() []byte {
|
||||
return q.b[q.dataofs:q.length]
|
||||
}
|
||||
|
||||
// IsTCPSyn reports whether q is a TCP SYN packet
|
||||
// (i.e. the first packet in a new connection).
|
||||
// Transport returns the transport header and payload (IP subprotocol, such as TCP or UDP).
|
||||
// This is a read-only view; that is, p retains the ownership of the buffer.
|
||||
func (p *Parsed) Transport() []byte {
|
||||
return p.b[p.subofs:]
|
||||
}
|
||||
|
||||
// IsTCPSyn reports whether q is a TCP SYN packet,
|
||||
// without ACK set. (i.e. the first packet in a new connection)
|
||||
func (q *Parsed) IsTCPSyn() bool {
|
||||
return (q.TCPFlags & TCPSynAck) == TCPSyn
|
||||
}
|
||||
@@ -424,6 +434,40 @@ func (q *Parsed) IsEchoResponse() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveECNBits modifies p and its underlying memory buffer to remove
|
||||
// ECN bits, if any. It reports whether it did so.
|
||||
//
|
||||
// It currently only does the TCP flags.
|
||||
func (p *Parsed) RemoveECNBits() bool {
|
||||
if p.IPVersion == 0 {
|
||||
return false
|
||||
}
|
||||
if p.IPProto != ipproto.TCP {
|
||||
// TODO(bradfitz): handle non-TCP too? for now only trying to
|
||||
// fix the Issue 2642 problem.
|
||||
return false
|
||||
}
|
||||
if p.TCPFlags&TCPECNBits == 0 {
|
||||
// Nothing to do.
|
||||
return false
|
||||
}
|
||||
|
||||
// Clear flags.
|
||||
|
||||
// First in the parsed output.
|
||||
p.TCPFlags = p.TCPFlags & ^TCPECNBits
|
||||
|
||||
// Then in the underlying memory.
|
||||
tcp := p.Transport()
|
||||
old := binary.BigEndian.Uint16(tcp[12:14])
|
||||
tcp[13] = byte(p.TCPFlags)
|
||||
new := binary.BigEndian.Uint16(tcp[12:14])
|
||||
oldSum := binary.BigEndian.Uint16(tcp[16:18])
|
||||
newSum := ^checksumUpdate2ByteAlignedUint16(^oldSum, old, new)
|
||||
binary.BigEndian.PutUint16(tcp[16:18], newSum)
|
||||
return true
|
||||
}
|
||||
|
||||
func Hexdump(b []byte) string {
|
||||
out := new(strings.Builder)
|
||||
for i := 0; i < len(b); i += 16 {
|
||||
@@ -455,3 +499,26 @@ func Hexdump(b []byte) string {
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
// From gVisor's unexported API:
|
||||
|
||||
// checksumUpdate2ByteAlignedUint16 updates a uint16 value in a calculated
|
||||
// checksum.
|
||||
//
|
||||
// The value MUST begin at a 2-byte boundary in the original buffer.
|
||||
func checksumUpdate2ByteAlignedUint16(xsum, old, new uint16) uint16 {
|
||||
// As per RFC 1071 page 4,
|
||||
//(4) Incremental Update
|
||||
//
|
||||
// ...
|
||||
//
|
||||
// To update the checksum, simply add the differences of the
|
||||
// sixteen bit integers that have been changed. To see why this
|
||||
// works, observe that every 16-bit integer has an additive inverse
|
||||
// and that addition is associative. From this it follows that
|
||||
// given the original value m, the new value m', and the old
|
||||
// checksum C, the new checksum C' is:
|
||||
//
|
||||
// C' = C + (-m) + m' = C + (m' - m)
|
||||
return checksumCombine(xsum, checksumCombine(new, ^old))
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ package packet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
@@ -561,3 +563,57 @@ func BenchmarkString(b *testing.B) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveECNBits(t *testing.T) {
|
||||
// withECNHex is a TCP SYN packet with ECN bits set in the TCP
|
||||
// header as captured by Wireshark on macOS against the
|
||||
// Tailscale interface. In this packet (because it's a SYN
|
||||
// control packet), the ECN bits are not set in the IP header.
|
||||
const withECNHex = `45 00 00 40 00 00 40 00
|
||||
40 06 0c 66 64 7b 65 28 64 7f 00 30 f1 ab 00 16
|
||||
5a 7a 63 e8 00 00 00 00 b0 c2 ff ff 97 76 00 00
|
||||
02 04 04 d8 01 03 03 06 01 01 08 0a 03 e1 bd 49
|
||||
00 00 00 00 04 02 00 00`
|
||||
|
||||
// Generated by hand-editing a pcap file in hexl-mode to set
|
||||
// the TCP flags to just SYN (0x02), then loading that pcap
|
||||
// file in wireshark to get the expected checksum value, then
|
||||
// putting that checksum value (0x9836) in the file.
|
||||
const wantStrippedHex = `45 00 00 40 00 00 40 00
|
||||
40 06 0c 66 64 7b 65 28 64 7f 00 30 f1 ab 00 16
|
||||
5a 7a 63 e8 00 00 00 00 b0 02 ff ff 98 36 00 00
|
||||
02 04 04 d8 01 03 03 06 01 01 08 0a 03 e1 bd 49
|
||||
00 00 00 00 04 02 00 00`
|
||||
|
||||
var p Parsed
|
||||
pktBuf := bytesOfHex(withECNHex)
|
||||
p.Decode(pktBuf)
|
||||
if want := TCPCWR | TCPECNEcho | TCPSyn; p.TCPFlags != want {
|
||||
t.Fatalf("pre flags = %v; want %v", p.TCPFlags, want)
|
||||
}
|
||||
|
||||
if !p.RemoveECNBits() {
|
||||
t.Fatal("didn't remove bits")
|
||||
}
|
||||
if want := TCPSyn; p.TCPFlags != want {
|
||||
t.Fatalf("post flags = %v; want %v", p.TCPFlags, want)
|
||||
}
|
||||
wantPkt := bytesOfHex(wantStrippedHex)
|
||||
if !bytes.Equal(pktBuf, wantPkt) {
|
||||
t.Fatalf("wrong result.\n got: % 2x\nwant: % 2x\n", pktBuf, wantPkt)
|
||||
}
|
||||
|
||||
if p.RemoveECNBits() {
|
||||
t.Fatal("unexpected true return value on second call")
|
||||
}
|
||||
}
|
||||
|
||||
var nonHex = regexp.MustCompile(`[^0-9a-fA-F]+`)
|
||||
|
||||
func bytesOfHex(s string) []byte {
|
||||
b, err := hex.DecodeString(nonHex.ReplaceAllString(s, ""))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/neterror"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
@@ -478,18 +479,27 @@ func (c *Client) createOrGetMapping(ctx context.Context) (external netaddr.IPPor
|
||||
// Only do PCP mapping in the case when PMP did not appear to be available recently.
|
||||
pkt := buildPCPRequestMappingPacket(myIP, localPort, prevPort, pcpMapLifetimeSec, wildcardIP)
|
||||
if _, err := uc.WriteTo(pkt, pxpAddru); err != nil {
|
||||
if neterror.TreatAsLostUDP(err) {
|
||||
err = NoMappingError{ErrNoPortMappingServices}
|
||||
}
|
||||
return netaddr.IPPort{}, err
|
||||
}
|
||||
} else {
|
||||
// Ask for our external address if needed.
|
||||
if m.external.IP().IsZero() {
|
||||
if _, err := uc.WriteTo(pmpReqExternalAddrPacket, pxpAddru); err != nil {
|
||||
if neterror.TreatAsLostUDP(err) {
|
||||
err = NoMappingError{ErrNoPortMappingServices}
|
||||
}
|
||||
return netaddr.IPPort{}, err
|
||||
}
|
||||
}
|
||||
|
||||
pkt := buildPMPRequestMappingPacket(localPort, prevPort, pmpMapLifetimeSec)
|
||||
if _, err := uc.WriteTo(pkt, pxpAddru); err != nil {
|
||||
if neterror.TreatAsLostUDP(err) {
|
||||
err = NoMappingError{ErrNoPortMappingServices}
|
||||
}
|
||||
return netaddr.IPPort{}, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,14 +36,26 @@ var (
|
||||
tsUlaRange oncePrefix
|
||||
ula4To6Range oncePrefix
|
||||
ulaEph6Range oncePrefix
|
||||
serviceIPv6 oncePrefix
|
||||
)
|
||||
|
||||
// TailscaleServiceIP returns the listen address of services
|
||||
// TailscaleServiceIP returns the IPv4 listen address of services
|
||||
// provided by Tailscale itself such as the MagicDNS proxy.
|
||||
//
|
||||
// For IPv6, use TailscaleServiceIPv6.
|
||||
func TailscaleServiceIP() netaddr.IP {
|
||||
return netaddr.IPv4(100, 100, 100, 100) // "100.100.100.100" for those grepping
|
||||
}
|
||||
|
||||
// TailscaleServiceIPv6 returns the IPv6 listen address of the services
|
||||
// provided by Tailscale itself such as the MagicDNS proxy.
|
||||
//
|
||||
// For IPv4, use TailscaleServiceIP.
|
||||
func TailscaleServiceIPv6() netaddr.IP {
|
||||
serviceIPv6.Do(func() { mustPrefix(&serviceIPv6.v, "fd7a:115c:a1e0::53/128") })
|
||||
return serviceIPv6.v.IP()
|
||||
}
|
||||
|
||||
// IsTailscaleIP reports whether ip is an IP address in a range that
|
||||
// Tailscale assigns from.
|
||||
func IsTailscaleIP(ip netaddr.IP) bool {
|
||||
@@ -176,6 +188,16 @@ func PrefixesContainsFunc(ipp []netaddr.IPPrefix, f func(netaddr.IPPrefix) bool)
|
||||
return false
|
||||
}
|
||||
|
||||
// PrefixesContainsIP reports whether any prefix in ipp contains ip.
|
||||
func PrefixesContainsIP(ipp []netaddr.IPPrefix, ip netaddr.IP) bool {
|
||||
for _, r := range ipp {
|
||||
if r.Contains(ip) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IPsContainsFunc reports whether f is true for any IP in ips.
|
||||
func IPsContainsFunc(ips []netaddr.IP, f func(netaddr.IP) bool) bool {
|
||||
for _, v := range ips {
|
||||
|
||||
@@ -31,6 +31,14 @@ func TestInCrostiniRange(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTailscaleServiceIPv6(t *testing.T) {
|
||||
got := TailscaleServiceIPv6().String()
|
||||
want := "fd7a:115c:a1e0::53"
|
||||
if got != want {
|
||||
t.Errorf("got %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChromeOSVMRange(t *testing.T) {
|
||||
if got, want := ChromeOSVMRange().String(), "100.115.92.0/23"; got != want {
|
||||
t.Errorf("got %q; want %q", got, want)
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/disco"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tstime/mono"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/types/key"
|
||||
@@ -407,17 +408,30 @@ func (t *Wrapper) sendOutbound(r tunReadResult) {
|
||||
t.outbound <- r
|
||||
}
|
||||
|
||||
var magicDNSIPPort = netaddr.MustParseIPPort("100.100.100.100:0")
|
||||
var (
|
||||
magicDNSIPPort = netaddr.IPPortFrom(tsaddr.TailscaleServiceIP(), 0) // 100.100.100.100:0
|
||||
magicDNSIPPortv6 = netaddr.IPPortFrom(tsaddr.TailscaleServiceIPv6(), 0)
|
||||
)
|
||||
|
||||
func (t *Wrapper) filterOut(p *packet.Parsed) filter.Response {
|
||||
// Fake ICMP echo responses to MagicDNS (100.100.100.100).
|
||||
if p.IsEchoRequest() && p.Dst == magicDNSIPPort {
|
||||
header := p.ICMP4Header()
|
||||
header.ToResponse()
|
||||
outp := packet.Generate(&header, p.Payload())
|
||||
t.InjectInboundCopy(outp)
|
||||
return filter.DropSilently // don't pass on to OS; already handled
|
||||
if p.IsEchoRequest() {
|
||||
switch p.Dst {
|
||||
case magicDNSIPPort:
|
||||
header := p.ICMP4Header()
|
||||
header.ToResponse()
|
||||
outp := packet.Generate(&header, p.Payload())
|
||||
t.InjectInboundCopy(outp)
|
||||
return filter.DropSilently // don't pass on to OS; already handled
|
||||
case magicDNSIPPortv6:
|
||||
header := p.ICMP6Header()
|
||||
header.ToResponse()
|
||||
outp := packet.Generate(&header, p.Payload())
|
||||
t.InjectInboundCopy(outp)
|
||||
return filter.DropSilently // don't pass on to OS; already handled
|
||||
}
|
||||
}
|
||||
// TODO(bradfitz): support pinging TailscaleServiceIPv6 too.
|
||||
|
||||
// Issue 1526 workaround: if we sent disco packets over
|
||||
// Tailscale from ourselves, then drop them, as that shouldn't
|
||||
|
||||
16
pull-toolchain.sh
Executable file
16
pull-toolchain.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
# Retrieve the latest Go toolchain.
|
||||
#
|
||||
set -eu
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
read -r go_branch <go.toolchain.branch
|
||||
upstream=$(git ls-remote https://github.com/tailscale/go "$go_branch" | awk '{print $1}')
|
||||
current=$(cat go.toolchain.rev)
|
||||
if [ "$upstream" != "$current" ]; then
|
||||
echo "$upstream" >go.toolchain.rev
|
||||
fi
|
||||
|
||||
if [ -n "$(git diff-index --name-only HEAD -- go.toolchain.rev)" ]; then
|
||||
echo "pull-toolchain.sh: changes imported. Use git commit to make them permanent." >&2
|
||||
fi
|
||||
@@ -24,6 +24,7 @@ main() {
|
||||
VERSION=""
|
||||
PACKAGETYPE=""
|
||||
APT_KEY_TYPE="" # Only for apt-based distros
|
||||
APT_SYSTEMCTL_START=false # Only needs to be true for Kali
|
||||
|
||||
if [ -f /etc/os-release ]; then
|
||||
# /etc/os-release populates a number of shell variables. We care about the following:
|
||||
|
||||
@@ -48,7 +48,8 @@ import (
|
||||
// 23: 2021-08-25: DNSConfig.Routes values may be empty (for ExtraRecords support in 1.14.1+)
|
||||
// 24: 2021-09-18: MapResponse.Health from control to node; node shows in "tailscale status"
|
||||
// 25: 2021-11-01: MapResponse.Debug.Exit
|
||||
const CurrentMapRequestVersion = 25
|
||||
// 26: 2022-01-12: (nothing, just bumping for 1.20.0)
|
||||
const CurrentMapRequestVersion = 26
|
||||
|
||||
type StableID string
|
||||
|
||||
@@ -577,6 +578,68 @@ func (h *Hostinfo) Equal(h2 *Hostinfo) bool {
|
||||
return reflect.DeepEqual(h, h2)
|
||||
}
|
||||
|
||||
// HowUnequal returns a list of paths through Hostinfo where h and h2 differ.
|
||||
// If they differ in nil-ness, the path is "nil", otherwise the path is like
|
||||
// "ShieldsUp" or "NetInfo.nil" or "NetInfo.PCP".
|
||||
func (h *Hostinfo) HowUnequal(h2 *Hostinfo) (path []string) {
|
||||
return appendStructPtrDiff(nil, "", reflect.ValueOf(h), reflect.ValueOf(h2))
|
||||
}
|
||||
|
||||
func appendStructPtrDiff(base []string, pfx string, p1, p2 reflect.Value) (ret []string) {
|
||||
ret = base
|
||||
if p1.IsNil() && p2.IsNil() {
|
||||
return base
|
||||
}
|
||||
mkPath := func(b string) string {
|
||||
if pfx == "" {
|
||||
return b
|
||||
}
|
||||
return pfx + "." + b
|
||||
}
|
||||
if p1.IsNil() || p2.IsNil() {
|
||||
return append(base, mkPath("nil"))
|
||||
}
|
||||
v1, v2 := p1.Elem(), p2.Elem()
|
||||
t := v1.Type()
|
||||
for i, n := 0, t.NumField(); i < n; i++ {
|
||||
sf := t.Field(i)
|
||||
switch sf.Type.Kind() {
|
||||
case reflect.String:
|
||||
if v1.Field(i).String() != v2.Field(i).String() {
|
||||
ret = append(ret, mkPath(sf.Name))
|
||||
}
|
||||
continue
|
||||
case reflect.Bool:
|
||||
if v1.Field(i).Bool() != v2.Field(i).Bool() {
|
||||
ret = append(ret, mkPath(sf.Name))
|
||||
}
|
||||
continue
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
if v1.Field(i).Int() != v2.Field(i).Int() {
|
||||
ret = append(ret, mkPath(sf.Name))
|
||||
}
|
||||
continue
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
|
||||
if v1.Field(i).Uint() != v2.Field(i).Uint() {
|
||||
ret = append(ret, mkPath(sf.Name))
|
||||
}
|
||||
continue
|
||||
case reflect.Slice, reflect.Map:
|
||||
if !reflect.DeepEqual(v1.Field(i).Interface(), v2.Field(i).Interface()) {
|
||||
ret = append(ret, mkPath(sf.Name))
|
||||
}
|
||||
continue
|
||||
case reflect.Ptr:
|
||||
if sf.Type.Elem().Kind() == reflect.Struct {
|
||||
ret = appendStructPtrDiff(ret, sf.Name, v1.Field(i), v2.Field(i))
|
||||
continue
|
||||
}
|
||||
}
|
||||
panic(fmt.Sprintf("unsupported type at %s: %s", mkPath(sf.Name), sf.Type.String()))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// SignatureType specifies a scheme for signing RegisterRequest messages. It
|
||||
// specifies the crypto algorithms to use, the contents of what is signed, and
|
||||
// any other relevant details. Historically, requests were unsigned so the zero
|
||||
|
||||
@@ -190,6 +190,82 @@ func TestHostinfoEqual(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostinfoHowEqual(t *testing.T) {
|
||||
tests := []struct {
|
||||
a, b *Hostinfo
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
a: nil,
|
||||
b: nil,
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
a: new(Hostinfo),
|
||||
b: nil,
|
||||
want: []string{"nil"},
|
||||
},
|
||||
{
|
||||
a: nil,
|
||||
b: new(Hostinfo),
|
||||
want: []string{"nil"},
|
||||
},
|
||||
{
|
||||
a: new(Hostinfo),
|
||||
b: new(Hostinfo),
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
a: &Hostinfo{
|
||||
IPNVersion: "1",
|
||||
ShieldsUp: false,
|
||||
RoutableIPs: []netaddr.IPPrefix{netaddr.MustParseIPPrefix("1.2.3.0/24")},
|
||||
},
|
||||
b: &Hostinfo{
|
||||
IPNVersion: "2",
|
||||
ShieldsUp: true,
|
||||
RoutableIPs: []netaddr.IPPrefix{netaddr.MustParseIPPrefix("1.2.3.0/25")},
|
||||
},
|
||||
want: []string{"IPNVersion", "ShieldsUp", "RoutableIPs"},
|
||||
},
|
||||
{
|
||||
a: &Hostinfo{
|
||||
IPNVersion: "1",
|
||||
},
|
||||
b: &Hostinfo{
|
||||
IPNVersion: "2",
|
||||
NetInfo: new(NetInfo),
|
||||
},
|
||||
want: []string{"IPNVersion", "NetInfo.nil"},
|
||||
},
|
||||
{
|
||||
a: &Hostinfo{
|
||||
IPNVersion: "1",
|
||||
NetInfo: &NetInfo{
|
||||
WorkingIPv6: "true",
|
||||
HavePortMap: true,
|
||||
LinkType: "foo",
|
||||
PreferredDERP: 123,
|
||||
DERPLatency: map[string]float64{
|
||||
"foo": 1.0,
|
||||
},
|
||||
},
|
||||
},
|
||||
b: &Hostinfo{
|
||||
IPNVersion: "2",
|
||||
NetInfo: &NetInfo{},
|
||||
},
|
||||
want: []string{"IPNVersion", "NetInfo.WorkingIPv6", "NetInfo.HavePortMap", "NetInfo.PreferredDERP", "NetInfo.LinkType", "NetInfo.DERPLatency"},
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
got := tt.a.HowUnequal(tt.b)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("%d. got %q; want %q", i, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeEqual(t *testing.T) {
|
||||
nodeHandles := []string{
|
||||
"ID", "StableID", "Name", "User", "Sharer",
|
||||
|
||||
89
tool/go
Executable file
89
tool/go
Executable file
@@ -0,0 +1,89 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# This script acts like the "go" command, but uses Tailscale's
|
||||
# currently-desired version from https://github.com/tailscale/go,
|
||||
# downloading it first if necessary.
|
||||
|
||||
set -eu
|
||||
|
||||
log() {
|
||||
echo "$@" >&2
|
||||
}
|
||||
|
||||
DEFAULT_TOOLCHAIN_DIR="${HOME}/.cache/tailscale-go"
|
||||
TOOLCHAIN="${TOOLCHAIN-${DEFAULT_TOOLCHAIN_DIR}}"
|
||||
TOOLCHAIN_GO="${TOOLCHAIN}/bin/go"
|
||||
read -r REV < "$(dirname "$0")/../go.toolchain.rev"
|
||||
|
||||
# Fast, quiet path, when Tailscale is already current.
|
||||
if [ -e "${TOOLCHAIN_GO}" ]; then
|
||||
short_hash=$("${TOOLCHAIN_GO}" version | sed 's/.*-ts//; s/ .*//')
|
||||
case $REV in
|
||||
"$short_hash"*)
|
||||
unset GOROOT
|
||||
exec "${TOOLCHAIN_GO}" "$@"
|
||||
esac
|
||||
fi
|
||||
|
||||
# This works for linux and darwin, which is sufficient
|
||||
# (we do not build tailscale-go for other targets).
|
||||
GOOS=$(uname -s | tr A-Z a-z)
|
||||
ARCH="$(uname -m)"
|
||||
if [ "$ARCH" = "aarch64" ]; then
|
||||
# Go uses the name "arm64".
|
||||
ARCH="arm64"
|
||||
elif [ "$ARCH" = "x86_64" ]; then
|
||||
# Go uses the name "amd64".
|
||||
ARCH="amd64"
|
||||
fi
|
||||
|
||||
get_cached() {
|
||||
if [ ! -d "$TOOLCHAIN" ]; then
|
||||
mkdir -p "$TOOLCHAIN"
|
||||
fi
|
||||
|
||||
archive="$TOOLCHAIN-$REV.tar.gz"
|
||||
mark="$TOOLCHAIN.extracted"
|
||||
extracted=
|
||||
[ ! -e "$mark" ] || read -r extracted junk <$mark
|
||||
|
||||
if [ "$extracted" = "$REV" ] && [ -e "${TOOLCHAIN_GO}" ]; then
|
||||
# already ok
|
||||
log "Go toolchain '$REV' already extracted."
|
||||
return 0
|
||||
fi
|
||||
|
||||
rm -f "$archive.new" "$TOOLCHAIN.extracted"
|
||||
if [ ! -e "$archive" ]; then
|
||||
log "Need to download go '$REV'."
|
||||
if [ "$ARCH" = "amd64" ]; then
|
||||
# For historic reasons, the tailscale/go amd64 release artifacts don't
|
||||
# have the arch in their name.
|
||||
BUILD="$GOOS"
|
||||
else
|
||||
BUILD="$GOOS-$ARCH"
|
||||
fi
|
||||
curl -f -L -o "$archive.new" "https://github.com/tailscale/go/releases/download/build-${REV}/${BUILD}.tar.gz"
|
||||
rm -f "$archive"
|
||||
mv "$archive.new" "$archive"
|
||||
fi
|
||||
|
||||
log "Extracting tailscale/go rev '$REV'" >&2
|
||||
log " into '$TOOLCHAIN'." >&2
|
||||
rm -rf "$TOOLCHAIN"
|
||||
mkdir -p "$TOOLCHAIN"
|
||||
(cd "$TOOLCHAIN" && tar --strip-components=1 -xf "$archive")
|
||||
echo "$REV" >$mark
|
||||
}
|
||||
|
||||
if [ "${REV}" = "SKIP" ] ||
|
||||
[ "${GOOS}" != "darwin" -a "${GOOS}" != "linux" ] ||
|
||||
[ "${ARCH}" != "amd64" -a "${ARCH}" != "arm64" ]; then
|
||||
# Use whichever go is available
|
||||
exec go "$@"
|
||||
else
|
||||
get_cached
|
||||
fi
|
||||
|
||||
unset GOROOT
|
||||
exec "${TOOLCHAIN_GO}" "$@"
|
||||
@@ -63,6 +63,16 @@ type Server struct {
|
||||
|
||||
mu sync.Mutex
|
||||
listeners map[listenKey]*listener
|
||||
dialer *tsdial.Dialer
|
||||
}
|
||||
|
||||
// Dial connects to the address on the tailnet.
|
||||
// It will start the server if it has not been started yet.
|
||||
func (s *Server) Dial(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
if err := s.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.dialer.UserDial(ctx, network, address)
|
||||
}
|
||||
|
||||
func (s *Server) doInit() {
|
||||
@@ -71,6 +81,13 @@ func (s *Server) doInit() {
|
||||
}
|
||||
}
|
||||
|
||||
// Start connects the server to the tailnet.
|
||||
// Optional: any calls to Dial/Listen will also call Start.
|
||||
func (s *Server) Start() error {
|
||||
s.initOnce.Do(s.doInit)
|
||||
return s.initErr
|
||||
}
|
||||
|
||||
func (s *Server) start() error {
|
||||
if v, _ := strconv.ParseBool(os.Getenv("TAILSCALE_USE_WIP_CODE")); !v {
|
||||
return errors.New("code disabled without environment variable TAILSCALE_USE_WIP_CODE set true")
|
||||
@@ -117,11 +134,11 @@ func (s *Server) start() error {
|
||||
return err
|
||||
}
|
||||
|
||||
dialer := new(tsdial.Dialer) // mutated below (before used)
|
||||
s.dialer = new(tsdial.Dialer) // mutated below (before used)
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
ListenPort: 0,
|
||||
LinkMonitor: linkMon,
|
||||
Dialer: dialer,
|
||||
Dialer: s.dialer,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -132,7 +149,7 @@ func (s *Server) start() error {
|
||||
return fmt.Errorf("%T is not a wgengine.InternalsGetter", eng)
|
||||
}
|
||||
|
||||
ns, err := netstack.Create(logf, tunDev, eng, magicConn, dialer)
|
||||
ns, err := netstack.Create(logf, tunDev, eng, magicConn, s.dialer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("netstack.Create: %w", err)
|
||||
}
|
||||
@@ -141,11 +158,11 @@ func (s *Server) start() error {
|
||||
if err := ns.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start netstack: %w", err)
|
||||
}
|
||||
dialer.UseNetstackForIP = func(ip netaddr.IP) bool {
|
||||
s.dialer.UseNetstackForIP = func(ip netaddr.IP) bool {
|
||||
_, ok := eng.PeerForIP(ip)
|
||||
return ok
|
||||
}
|
||||
dialer.NetstackDialTCP = func(ctx context.Context, dst netaddr.IPPort) (net.Conn, error) {
|
||||
s.dialer.NetstackDialTCP = func(ctx context.Context, dst netaddr.IPPort) (net.Conn, error) {
|
||||
return ns.DialContextTCP(ctx, dst)
|
||||
}
|
||||
|
||||
@@ -156,7 +173,7 @@ func (s *Server) start() error {
|
||||
}
|
||||
logid := "tslib-TODO"
|
||||
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, eng)
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, s.dialer, eng)
|
||||
if err != nil {
|
||||
return fmt.Errorf("NewLocalBackend: %v", err)
|
||||
}
|
||||
@@ -217,15 +234,16 @@ func (s *Server) forwardTCP(c net.Conn, port uint16) {
|
||||
}
|
||||
}
|
||||
|
||||
// Listen announces only on the Tailscale network.
|
||||
// It will start the server if it has not been started yet.
|
||||
func (s *Server) Listen(network, addr string) (net.Listener, error) {
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("tsnet: %w", err)
|
||||
}
|
||||
|
||||
s.initOnce.Do(s.doInit)
|
||||
if s.initErr != nil {
|
||||
return nil, s.initErr
|
||||
if err := s.Start(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key := listenKey{network, host, port}
|
||||
|
||||
@@ -68,12 +68,12 @@ func TestOneNodeUpNoAuth(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
|
||||
d1 := n1.StartDaemon(t)
|
||||
n1.AwaitResponding(t)
|
||||
d1 := n1.StartDaemon()
|
||||
n1.AwaitResponding()
|
||||
n1.MustUp()
|
||||
|
||||
t.Logf("Got IP: %v", n1.AwaitIP(t))
|
||||
n1.AwaitRunning(t)
|
||||
t.Logf("Got IP: %v", n1.AwaitIP())
|
||||
n1.AwaitRunning()
|
||||
|
||||
d1.MustCleanShutdown(t)
|
||||
|
||||
@@ -85,10 +85,10 @@ func TestOneNodeExpiredKey(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
|
||||
d1 := n1.StartDaemon(t)
|
||||
n1.AwaitResponding(t)
|
||||
d1 := n1.StartDaemon()
|
||||
n1.AwaitResponding()
|
||||
n1.MustUp()
|
||||
n1.AwaitRunning(t)
|
||||
n1.AwaitRunning()
|
||||
|
||||
nodes := env.Control.AllNodes()
|
||||
if len(nodes) != 1 {
|
||||
@@ -103,7 +103,7 @@ func TestOneNodeExpiredKey(t *testing.T) {
|
||||
cancel()
|
||||
|
||||
env.Control.SetExpireAllNodes(true)
|
||||
n1.AwaitNeedsLogin(t)
|
||||
n1.AwaitNeedsLogin()
|
||||
ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
|
||||
if err := env.Control.AwaitNodeInMapRequest(ctx, nodeKey); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -111,7 +111,7 @@ func TestOneNodeExpiredKey(t *testing.T) {
|
||||
cancel()
|
||||
|
||||
env.Control.SetExpireAllNodes(false)
|
||||
n1.AwaitRunning(t)
|
||||
n1.AwaitRunning()
|
||||
|
||||
d1.MustCleanShutdown(t)
|
||||
}
|
||||
@@ -152,14 +152,14 @@ func TestStateSavedOnStart(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
|
||||
d1 := n1.StartDaemon(t)
|
||||
n1.AwaitResponding(t)
|
||||
d1 := n1.StartDaemon()
|
||||
n1.AwaitResponding()
|
||||
n1.MustUp()
|
||||
|
||||
t.Logf("Got IP: %v", n1.AwaitIP(t))
|
||||
n1.AwaitRunning(t)
|
||||
t.Logf("Got IP: %v", n1.AwaitIP())
|
||||
n1.AwaitRunning()
|
||||
|
||||
p1 := n1.diskPrefs(t)
|
||||
p1 := n1.diskPrefs()
|
||||
t.Logf("Prefs1: %v", p1.Pretty())
|
||||
|
||||
// Bring it down, to prevent an EditPrefs call in the
|
||||
@@ -172,7 +172,7 @@ func TestStateSavedOnStart(t *testing.T) {
|
||||
t.Fatalf("up: %v", err)
|
||||
}
|
||||
|
||||
p2 := n1.diskPrefs(t)
|
||||
p2 := n1.diskPrefs()
|
||||
if pretty := p1.Pretty(); pretty == p2.Pretty() {
|
||||
t.Errorf("Prefs didn't change on disk after 'up', still: %s", pretty)
|
||||
}
|
||||
@@ -190,11 +190,11 @@ func TestOneNodeUpAuth(t *testing.T) {
|
||||
}))
|
||||
|
||||
n1 := newTestNode(t, env)
|
||||
d1 := n1.StartDaemon(t)
|
||||
d1 := n1.StartDaemon()
|
||||
|
||||
n1.AwaitListening(t)
|
||||
n1.AwaitListening()
|
||||
|
||||
st := n1.MustStatus(t)
|
||||
st := n1.MustStatus()
|
||||
t.Logf("Status: %s", st.BackendState)
|
||||
|
||||
t.Logf("Running up --login-server=%s ...", env.ControlServer.URL)
|
||||
@@ -215,9 +215,9 @@ func TestOneNodeUpAuth(t *testing.T) {
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatalf("up: %v", err)
|
||||
}
|
||||
t.Logf("Got IP: %v", n1.AwaitIP(t))
|
||||
t.Logf("Got IP: %v", n1.AwaitIP())
|
||||
|
||||
n1.AwaitRunning(t)
|
||||
n1.AwaitRunning()
|
||||
|
||||
if n := atomic.LoadInt32(&authCountAtomic); n != 1 {
|
||||
t.Errorf("Auth URLs completed = %d; want 1", n)
|
||||
@@ -233,26 +233,26 @@ func TestTwoNodes(t *testing.T) {
|
||||
// Create two nodes:
|
||||
n1 := newTestNode(t, env)
|
||||
n1SocksAddrCh := n1.socks5AddrChan()
|
||||
d1 := n1.StartDaemon(t)
|
||||
d1 := n1.StartDaemon()
|
||||
|
||||
n2 := newTestNode(t, env)
|
||||
n2SocksAddrCh := n2.socks5AddrChan()
|
||||
d2 := n2.StartDaemon(t)
|
||||
d2 := n2.StartDaemon()
|
||||
|
||||
n1Socks := n1.AwaitSocksAddr(t, n1SocksAddrCh)
|
||||
n2Socks := n1.AwaitSocksAddr(t, n2SocksAddrCh)
|
||||
n1Socks := n1.AwaitSocksAddr(n1SocksAddrCh)
|
||||
n2Socks := n1.AwaitSocksAddr(n2SocksAddrCh)
|
||||
t.Logf("node1 SOCKS5 addr: %v", n1Socks)
|
||||
t.Logf("node2 SOCKS5 addr: %v", n2Socks)
|
||||
|
||||
n1.AwaitListening(t)
|
||||
n2.AwaitListening(t)
|
||||
n1.AwaitListening()
|
||||
n2.AwaitListening()
|
||||
n1.MustUp()
|
||||
n2.MustUp()
|
||||
n1.AwaitRunning(t)
|
||||
n2.AwaitRunning(t)
|
||||
n1.AwaitRunning()
|
||||
n2.AwaitRunning()
|
||||
|
||||
if err := tstest.WaitFor(2*time.Second, func() error {
|
||||
st := n1.MustStatus(t)
|
||||
st := n1.MustStatus()
|
||||
if len(st.Peer) == 0 {
|
||||
return errors.New("no peers")
|
||||
}
|
||||
@@ -276,11 +276,11 @@ func TestNodeAddressIPFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
d1 := n1.StartDaemon(t)
|
||||
d1 := n1.StartDaemon()
|
||||
|
||||
n1.AwaitListening(t)
|
||||
n1.AwaitListening()
|
||||
n1.MustUp()
|
||||
n1.AwaitRunning(t)
|
||||
n1.AwaitRunning()
|
||||
|
||||
testNodes := env.Control.AllNodes()
|
||||
|
||||
@@ -302,11 +302,11 @@ func TestAddPingRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
n1.StartDaemon(t)
|
||||
n1.StartDaemon()
|
||||
|
||||
n1.AwaitListening(t)
|
||||
n1.AwaitListening()
|
||||
n1.MustUp()
|
||||
n1.AwaitRunning(t)
|
||||
n1.AwaitRunning()
|
||||
|
||||
gotPing := make(chan bool, 1)
|
||||
waitPing := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -357,28 +357,28 @@ func TestNoControlConnWhenDown(t *testing.T) {
|
||||
env := newTestEnv(t)
|
||||
n1 := newTestNode(t, env)
|
||||
|
||||
d1 := n1.StartDaemon(t)
|
||||
n1.AwaitResponding(t)
|
||||
d1 := n1.StartDaemon()
|
||||
n1.AwaitResponding()
|
||||
|
||||
// Come up the first time.
|
||||
n1.MustUp()
|
||||
ip1 := n1.AwaitIP(t)
|
||||
n1.AwaitRunning(t)
|
||||
ip1 := n1.AwaitIP()
|
||||
n1.AwaitRunning()
|
||||
|
||||
// Then bring it down and stop the daemon.
|
||||
n1.MustDown()
|
||||
d1.MustCleanShutdown(t)
|
||||
|
||||
env.LogCatcher.Reset()
|
||||
d2 := n1.StartDaemon(t)
|
||||
n1.AwaitResponding(t)
|
||||
d2 := n1.StartDaemon()
|
||||
n1.AwaitResponding()
|
||||
|
||||
st := n1.MustStatus(t)
|
||||
st := n1.MustStatus()
|
||||
if got, want := st.BackendState, "Stopped"; got != want {
|
||||
t.Fatalf("after restart, state = %q; want %q", got, want)
|
||||
}
|
||||
|
||||
ip2 := n1.AwaitIP(t)
|
||||
ip2 := n1.AwaitIP()
|
||||
if ip1 != ip2 {
|
||||
t.Errorf("IPs different: %q vs %q", ip1, ip2)
|
||||
}
|
||||
@@ -399,16 +399,66 @@ func TestOneNodeUpWindowsStyle(t *testing.T) {
|
||||
n1 := newTestNode(t, env)
|
||||
n1.upFlagGOOS = "windows"
|
||||
|
||||
d1 := n1.StartDaemonAsIPNGOOS(t, "windows")
|
||||
n1.AwaitResponding(t)
|
||||
d1 := n1.StartDaemonAsIPNGOOS("windows")
|
||||
n1.AwaitResponding()
|
||||
n1.MustUp("--unattended")
|
||||
|
||||
t.Logf("Got IP: %v", n1.AwaitIP(t))
|
||||
n1.AwaitRunning(t)
|
||||
t.Logf("Got IP: %v", n1.AwaitIP())
|
||||
n1.AwaitRunning()
|
||||
|
||||
d1.MustCleanShutdown(t)
|
||||
}
|
||||
|
||||
func TestLogoutRemovesAllPeers(t *testing.T) {
|
||||
t.Parallel()
|
||||
env := newTestEnv(t)
|
||||
// Spin up some nodes.
|
||||
nodes := make([]*testNode, 2)
|
||||
for i := range nodes {
|
||||
nodes[i] = newTestNode(t, env)
|
||||
nodes[i].StartDaemon()
|
||||
nodes[i].AwaitResponding()
|
||||
nodes[i].MustUp()
|
||||
nodes[i].AwaitIP()
|
||||
nodes[i].AwaitRunning()
|
||||
}
|
||||
|
||||
// Make every node ping every other node.
|
||||
// This makes sure magicsock is fully populated.
|
||||
for i := range nodes {
|
||||
for j := range nodes {
|
||||
if i <= j {
|
||||
continue
|
||||
}
|
||||
if err := tstest.WaitFor(20*time.Second, func() error {
|
||||
return nodes[i].Ping(nodes[j])
|
||||
}); err != nil {
|
||||
t.Fatalf("ping %v -> %v: %v", nodes[i].AwaitIP(), nodes[j].AwaitIP(), err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// wantNode0PeerCount waits until node[0] status includes exactly want peers.
|
||||
wantNode0PeerCount := func(want int) {
|
||||
if err := tstest.WaitFor(20*time.Second, func() error {
|
||||
s := nodes[0].MustStatus()
|
||||
if peers := s.Peers(); len(peers) != want {
|
||||
return fmt.Errorf("want %d peer(s) in status, got %v", want, peers)
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
wantNode0PeerCount(len(nodes) - 1) // all other nodes are peers
|
||||
nodes[0].MustLogOut()
|
||||
wantNode0PeerCount(0) // node[0] is logged out, so it should not have any peers
|
||||
nodes[0].MustUp()
|
||||
nodes[0].AwaitIP()
|
||||
wantNode0PeerCount(len(nodes) - 1) // all other nodes are peers again
|
||||
}
|
||||
|
||||
// testEnv contains the test environment (set of servers) used by one
|
||||
// or more nodes.
|
||||
type testEnv struct {
|
||||
@@ -508,7 +558,8 @@ func newTestNode(t *testing.T, env *testEnv) *testNode {
|
||||
}
|
||||
}
|
||||
|
||||
func (n *testNode) diskPrefs(t testing.TB) *ipn.Prefs {
|
||||
func (n *testNode) diskPrefs() *ipn.Prefs {
|
||||
t := n.env.t
|
||||
t.Helper()
|
||||
if _, err := ioutil.ReadFile(n.stateFile); err != nil {
|
||||
t.Fatalf("reading prefs: %v", err)
|
||||
@@ -530,11 +581,12 @@ func (n *testNode) diskPrefs(t testing.TB) *ipn.Prefs {
|
||||
|
||||
// AwaitResponding waits for n's tailscaled to be up enough to be
|
||||
// responding, but doesn't wait for any particular state.
|
||||
func (n *testNode) AwaitResponding(t testing.TB) {
|
||||
func (n *testNode) AwaitResponding() {
|
||||
t := n.env.t
|
||||
t.Helper()
|
||||
n.AwaitListening(t)
|
||||
n.AwaitListening()
|
||||
|
||||
st := n.MustStatus(t)
|
||||
st := n.MustStatus()
|
||||
t.Logf("Status: %s", st.BackendState)
|
||||
|
||||
if err := tstest.WaitFor(20*time.Second, func() error {
|
||||
@@ -575,7 +627,8 @@ func (n *testNode) socks5AddrChan() <-chan string {
|
||||
return ch
|
||||
}
|
||||
|
||||
func (n *testNode) AwaitSocksAddr(t testing.TB, ch <-chan string) string {
|
||||
func (n *testNode) AwaitSocksAddr(ch <-chan string) string {
|
||||
t := n.env.t
|
||||
t.Helper()
|
||||
timer := time.NewTimer(10 * time.Second)
|
||||
defer timer.Stop()
|
||||
@@ -644,17 +697,21 @@ func (d *Daemon) MustCleanShutdown(t testing.TB) {
|
||||
|
||||
// StartDaemon starts the node's tailscaled, failing if it fails to start.
|
||||
// StartDaemon ensures that the process will exit when the test completes.
|
||||
func (n *testNode) StartDaemon(t testing.TB) *Daemon {
|
||||
return n.StartDaemonAsIPNGOOS(t, runtime.GOOS)
|
||||
func (n *testNode) StartDaemon() *Daemon {
|
||||
return n.StartDaemonAsIPNGOOS(runtime.GOOS)
|
||||
}
|
||||
|
||||
func (n *testNode) StartDaemonAsIPNGOOS(t testing.TB, ipnGOOS string) *Daemon {
|
||||
func (n *testNode) StartDaemonAsIPNGOOS(ipnGOOS string) *Daemon {
|
||||
t := n.env.t
|
||||
cmd := exec.Command(n.env.daemon,
|
||||
"--tun=userspace-networking",
|
||||
"--state="+n.stateFile,
|
||||
"--socket="+n.sockFile,
|
||||
"--socks5-server=localhost:0",
|
||||
)
|
||||
if *verboseTailscaled {
|
||||
cmd.Args = append(cmd.Args, "-verbose=2")
|
||||
}
|
||||
cmd.Env = append(os.Environ(),
|
||||
"TS_LOG_TARGET="+n.env.LogCatcherServer.URL,
|
||||
"HTTP_PROXY="+n.env.TrafficTrapServer.URL,
|
||||
@@ -700,9 +757,25 @@ func (n *testNode) MustDown() {
|
||||
}
|
||||
}
|
||||
|
||||
func (n *testNode) MustLogOut() {
|
||||
t := n.env.t
|
||||
t.Logf("Running logout ...")
|
||||
if err := n.Tailscale("logout").Run(); err != nil {
|
||||
t.Fatalf("logout: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *testNode) Ping(otherNode *testNode) error {
|
||||
t := n.env.t
|
||||
ip := otherNode.AwaitIP().String()
|
||||
t.Logf("Running ping %v (from %v)...", ip, n.AwaitIP())
|
||||
return n.Tailscale("ping", ip).Run()
|
||||
}
|
||||
|
||||
// AwaitListening waits for the tailscaled to be serving local clients
|
||||
// over its localhost IPC mechanism. (Unix socket, etc)
|
||||
func (n *testNode) AwaitListening(t testing.TB) {
|
||||
func (n *testNode) AwaitListening() {
|
||||
t := n.env.t
|
||||
s := safesocket.DefaultConnectionStrategy(n.sockFile)
|
||||
s.UseFallback(false) // connect only to the tailscaled that we started
|
||||
if err := tstest.WaitFor(20*time.Second, func() (err error) {
|
||||
@@ -717,7 +790,8 @@ func (n *testNode) AwaitListening(t testing.TB) {
|
||||
}
|
||||
}
|
||||
|
||||
func (n *testNode) AwaitIPs(t testing.TB) []netaddr.IP {
|
||||
func (n *testNode) AwaitIPs() []netaddr.IP {
|
||||
t := n.env.t
|
||||
t.Helper()
|
||||
var addrs []netaddr.IP
|
||||
if err := tstest.WaitFor(20*time.Second, func() error {
|
||||
@@ -750,14 +824,16 @@ func (n *testNode) AwaitIPs(t testing.TB) []netaddr.IP {
|
||||
}
|
||||
|
||||
// AwaitIP returns the IP address of n.
|
||||
func (n *testNode) AwaitIP(t testing.TB) netaddr.IP {
|
||||
func (n *testNode) AwaitIP() netaddr.IP {
|
||||
t := n.env.t
|
||||
t.Helper()
|
||||
ips := n.AwaitIPs(t)
|
||||
ips := n.AwaitIPs()
|
||||
return ips[0]
|
||||
}
|
||||
|
||||
// AwaitRunning waits for n to reach the IPN state "Running".
|
||||
func (n *testNode) AwaitRunning(t testing.TB) {
|
||||
func (n *testNode) AwaitRunning() {
|
||||
t := n.env.t
|
||||
t.Helper()
|
||||
if err := tstest.WaitFor(20*time.Second, func() error {
|
||||
st, err := n.Status()
|
||||
@@ -774,7 +850,8 @@ func (n *testNode) AwaitRunning(t testing.TB) {
|
||||
}
|
||||
|
||||
// AwaitNeedsLogin waits for n to reach the IPN state "NeedsLogin".
|
||||
func (n *testNode) AwaitNeedsLogin(t testing.TB) {
|
||||
func (n *testNode) AwaitNeedsLogin() {
|
||||
t := n.env.t
|
||||
t.Helper()
|
||||
if err := tstest.WaitFor(20*time.Second, func() error {
|
||||
st, err := n.Status()
|
||||
@@ -822,7 +899,8 @@ func (n *testNode) Status() (*ipnstate.Status, error) {
|
||||
return st, nil
|
||||
}
|
||||
|
||||
func (n *testNode) MustStatus(tb testing.TB) *ipnstate.Status {
|
||||
func (n *testNode) MustStatus() *ipnstate.Status {
|
||||
tb := n.env.t
|
||||
tb.Helper()
|
||||
st, err := n.Status()
|
||||
if err != nil {
|
||||
|
||||
@@ -9,3 +9,9 @@ import _ "embed"
|
||||
|
||||
//go:embed VERSION.txt
|
||||
var Version string
|
||||
|
||||
// GoToolchainRev is the git hash from github.com/tailscale/go that this release
|
||||
// should be built using. It may end in a newline.
|
||||
//
|
||||
//go:embed go.toolchain.rev
|
||||
var GoToolchainRev string
|
||||
|
||||
@@ -62,3 +62,13 @@ func IsMacSysExt() bool {
|
||||
isMacSysExt.Store(v)
|
||||
return v
|
||||
}
|
||||
|
||||
// IsWindowsGUI reports whether the current process is the Windows GUI.
|
||||
func IsWindowsGUI() bool {
|
||||
if runtime.GOOS != "windows" {
|
||||
return false
|
||||
}
|
||||
exe, _ := os.Executable()
|
||||
exe = filepath.Base(exe)
|
||||
return strings.EqualFold(exe, "tailscale-ipn.exe") || strings.EqualFold(exe, "tailscale-ipn")
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
// Long is a full version number for this build, of the form
|
||||
// "x.y.z-commithash", or "date.yyyymmdd" if no actual version was
|
||||
// provided.
|
||||
var Long = "date.20211101"
|
||||
var Long = "date.20220107"
|
||||
|
||||
// Short is a short version number for this build, of the form
|
||||
// "x.y.z", or "date.yyyymmdd" if no actual version was provided.
|
||||
|
||||
202
wgengine/magicsock/debughttp.go
Normal file
202
wgengine/magicsock/debughttp.go
Normal file
@@ -0,0 +1,202 @@
|
||||
// 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 magicsock
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime/mono"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
// ServeHTTPDebug serves an HTML representation of the innards of c for debugging.
|
||||
//
|
||||
// It's accessible either from tailscaled's debug port (at
|
||||
// /debug/magicsock) or via peerapi to a peer that's owned by the same
|
||||
// user (so they can e.g. inspect their phones).
|
||||
func (c *Conn) ServeHTTPDebug(w http.ResponseWriter, r *http.Request) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprintf(w, "<h1>magicsock</h1>")
|
||||
|
||||
fmt.Fprintf(w, "<h2 id=derp><a href=#derp>#</a> DERP</h2><ul>")
|
||||
if c.derpMap != nil {
|
||||
type D struct {
|
||||
regionID int
|
||||
lastWrite time.Time
|
||||
createTime time.Time
|
||||
}
|
||||
ent := make([]D, 0, len(c.activeDerp))
|
||||
for rid, ad := range c.activeDerp {
|
||||
ent = append(ent, D{
|
||||
regionID: rid,
|
||||
lastWrite: *ad.lastWrite,
|
||||
createTime: ad.createTime,
|
||||
})
|
||||
}
|
||||
sort.Slice(ent, func(i, j int) bool {
|
||||
return ent[i].regionID < ent[j].regionID
|
||||
})
|
||||
for _, e := range ent {
|
||||
r, ok := c.derpMap.Regions[e.regionID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
home := ""
|
||||
if e.regionID == c.myDerp {
|
||||
home = "🏠"
|
||||
}
|
||||
fmt.Fprintf(w, "<li>%s %d - %v: created %v ago, write %v ago</li>\n",
|
||||
home, e.regionID, html.EscapeString(r.RegionCode),
|
||||
now.Sub(e.createTime).Round(time.Second),
|
||||
now.Sub(e.lastWrite).Round(time.Second),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
fmt.Fprintf(w, "</ul>\n")
|
||||
|
||||
fmt.Fprintf(w, "<h2 id=ipport><a href=#ipport>#</a> ip:port to endpoint</h2><ul>")
|
||||
{
|
||||
type kv struct {
|
||||
ipp netaddr.IPPort
|
||||
pi *peerInfo
|
||||
}
|
||||
ent := make([]kv, 0, len(c.peerMap.byIPPort))
|
||||
for k, v := range c.peerMap.byIPPort {
|
||||
ent = append(ent, kv{k, v})
|
||||
}
|
||||
sort.Slice(ent, func(i, j int) bool { return ipPortLess(ent[i].ipp, ent[j].ipp) })
|
||||
for _, e := range ent {
|
||||
ep := e.pi.ep
|
||||
shortStr := ep.publicKey.ShortString()
|
||||
fmt.Fprintf(w, "<li>%v: <a href='#%v'>%v</a></li>\n", e.ipp, strings.Trim(shortStr, "[]"), shortStr)
|
||||
}
|
||||
|
||||
}
|
||||
fmt.Fprintf(w, "</ul>\n")
|
||||
|
||||
fmt.Fprintf(w, "<h2 id=bykey><a href=#bykey>#</a> endpoints by key</h2>")
|
||||
{
|
||||
type kv struct {
|
||||
pub key.NodePublic
|
||||
pi *peerInfo
|
||||
}
|
||||
ent := make([]kv, 0, len(c.peerMap.byNodeKey))
|
||||
for k, v := range c.peerMap.byNodeKey {
|
||||
ent = append(ent, kv{k, v})
|
||||
}
|
||||
sort.Slice(ent, func(i, j int) bool { return ent[i].pub.Less(ent[j].pub) })
|
||||
|
||||
peers := map[key.NodePublic]*tailcfg.Node{}
|
||||
if c.netMap != nil {
|
||||
for _, p := range c.netMap.Peers {
|
||||
peers[p.Key] = p
|
||||
}
|
||||
}
|
||||
|
||||
for _, e := range ent {
|
||||
ep := e.pi.ep
|
||||
shortStr := e.pub.ShortString()
|
||||
name := peerDebugName(peers[e.pub])
|
||||
fmt.Fprintf(w, "<h3 id=%v><a href='#%v'>%v</a> - %s</h3>\n",
|
||||
strings.Trim(shortStr, "[]"),
|
||||
strings.Trim(shortStr, "[]"),
|
||||
shortStr,
|
||||
html.EscapeString(name))
|
||||
printEndpointHTML(w, ep)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func printEndpointHTML(w io.Writer, ep *endpoint) {
|
||||
lastRecv := ep.lastRecv.LoadAtomic()
|
||||
|
||||
ep.mu.Lock()
|
||||
defer ep.mu.Unlock()
|
||||
if ep.lastSend == 0 && lastRecv == 0 {
|
||||
return // no activity ever
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
mnow := mono.Now()
|
||||
fmtMono := func(m mono.Time) string {
|
||||
if m == 0 {
|
||||
return "-"
|
||||
}
|
||||
return mnow.Sub(m).Round(time.Millisecond).String()
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "<p>Best: <b>%+v</b>, %v ago (for %v)</p>\n", ep.bestAddr, fmtMono(ep.bestAddrAt), ep.trustBestAddrUntil.Sub(mnow).Round(time.Millisecond))
|
||||
fmt.Fprintf(w, "<p>heartbeating: %v</p>\n", ep.heartBeatTimer != nil)
|
||||
fmt.Fprintf(w, "<p>lastSend: %v ago</p>\n", fmtMono(ep.lastSend))
|
||||
fmt.Fprintf(w, "<p>lastFullPing: %v ago</p>\n", fmtMono(ep.lastFullPing))
|
||||
|
||||
eps := make([]netaddr.IPPort, 0, len(ep.endpointState))
|
||||
for ipp := range ep.endpointState {
|
||||
eps = append(eps, ipp)
|
||||
}
|
||||
sort.Slice(eps, func(i, j int) bool { return ipPortLess(eps[i], eps[j]) })
|
||||
io.WriteString(w, "<p>Endpoints:</p><ul>")
|
||||
for _, ipp := range eps {
|
||||
s := ep.endpointState[ipp]
|
||||
if ipp == ep.bestAddr.IPPort {
|
||||
fmt.Fprintf(w, "<li><b>%s</b>: (best)<ul>", ipp)
|
||||
} else {
|
||||
fmt.Fprintf(w, "<li>%s: ...<ul>", ipp)
|
||||
}
|
||||
fmt.Fprintf(w, "<li>lastPing: %v ago</li>\n", fmtMono(s.lastPing))
|
||||
if s.lastGotPing.IsZero() {
|
||||
fmt.Fprintf(w, "<li>disco-learned-at: -</li>\n")
|
||||
} else {
|
||||
fmt.Fprintf(w, "<li>disco-learned-at: %v ago</li>\n", now.Sub(s.lastGotPing).Round(time.Second))
|
||||
}
|
||||
fmt.Fprintf(w, "<li>callMeMaybeTime: %v</li>\n", s.callMeMaybeTime)
|
||||
for i := range s.recentPongs {
|
||||
if i == 5 {
|
||||
break
|
||||
}
|
||||
pos := (int(s.recentPong) - i) % len(s.recentPongs)
|
||||
pr := s.recentPongs[pos]
|
||||
fmt.Fprintf(w, "<li>pong %v ago: in %v, from %v src %v</li>\n",
|
||||
fmtMono(pr.pongAt), pr.latency.Round(time.Millisecond/10),
|
||||
pr.from, pr.pongSrc)
|
||||
}
|
||||
fmt.Fprintf(w, "</ul></li>\n")
|
||||
}
|
||||
io.WriteString(w, "</ul>")
|
||||
|
||||
}
|
||||
|
||||
func peerDebugName(p *tailcfg.Node) string {
|
||||
if p == nil {
|
||||
return ""
|
||||
}
|
||||
n := p.Name
|
||||
if i := strings.Index(n, "."); i != -1 {
|
||||
return n[:i]
|
||||
}
|
||||
return p.Hostinfo.Hostname
|
||||
}
|
||||
|
||||
func ipPortLess(a, b netaddr.IPPort) bool {
|
||||
if v := a.IP().Compare(b.IP()); v != 0 {
|
||||
return v < 0
|
||||
}
|
||||
return a.Port() < b.Port()
|
||||
}
|
||||
@@ -39,9 +39,11 @@ import (
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netcheck"
|
||||
"tailscale.com/net/neterror"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstime"
|
||||
@@ -233,6 +235,7 @@ type Conn struct {
|
||||
idleFunc func() time.Duration // nil means unknown
|
||||
testOnlyPacketListener nettype.PacketListener
|
||||
noteRecvActivity func(key.NodePublic) // or nil, see Options.NoteRecvActivity
|
||||
linkMon *monitor.Mon // or nil
|
||||
|
||||
// ================================================================
|
||||
// No locking required to access these fields, either because
|
||||
@@ -262,6 +265,7 @@ type Conn struct {
|
||||
stunReceiveFunc atomic.Value // of func(p []byte, fromAddr *net.UDPAddr)
|
||||
|
||||
// derpRecvCh is used by receiveDERP to read DERP messages.
|
||||
// It must have buffer size > 0; see issue 3736.
|
||||
derpRecvCh chan derpReadResult
|
||||
|
||||
// bind is the wireguard-go conn.Bind for Conn.
|
||||
@@ -296,11 +300,18 @@ type Conn struct {
|
||||
havePrivateKey syncs.AtomicBool
|
||||
publicKeyAtomic atomic.Value // of key.NodePublic (or NodeKey zero value if !havePrivateKey)
|
||||
|
||||
// derpMapAtomic is the same as derpMap, but without requiring
|
||||
// sync.Mutex. For use with NewRegionClient's callback, to avoid
|
||||
// lock ordering deadlocks. See issue 3726 and mu field docs.
|
||||
derpMapAtomic atomic.Value // of *tailcfg.DERPMap
|
||||
|
||||
// port is the preferred port from opts.Port; 0 means auto.
|
||||
port syncs.AtomicUint32
|
||||
|
||||
// ============================================================
|
||||
// mu guards all following fields; see userspaceEngine lock ordering rules
|
||||
// mu guards all following fields; see userspaceEngine lock
|
||||
// ordering rules against the engine. For derphttp, mu must
|
||||
// be held before derphttp.Client.mu.
|
||||
mu sync.Mutex
|
||||
muCond *sync.Cond
|
||||
|
||||
@@ -519,7 +530,7 @@ func (o *Options) derpActiveFunc() func() {
|
||||
// of NewConn. Mostly for tests.
|
||||
func newConn() *Conn {
|
||||
c := &Conn{
|
||||
derpRecvCh: make(chan derpReadResult),
|
||||
derpRecvCh: make(chan derpReadResult, 1), // must be buffered, see issue 3736
|
||||
derpStarted: make(chan struct{}),
|
||||
peerLastDerp: make(map[key.NodePublic]int),
|
||||
peerMap: newPeerMap(),
|
||||
@@ -549,6 +560,7 @@ func NewConn(opts Options) (*Conn, error) {
|
||||
if opts.LinkMonitor != nil {
|
||||
c.portMapper.SetGatewayLookupFunc(opts.LinkMonitor.GatewayAndSelfIP)
|
||||
}
|
||||
c.linkMon = opts.LinkMonitor
|
||||
|
||||
if err := c.initialBind(); err != nil {
|
||||
return nil, err
|
||||
@@ -1209,7 +1221,7 @@ func (c *Conn) sendUDPStd(addr *net.UDPAddr, b []byte) (sent bool, err error) {
|
||||
switch {
|
||||
case addr.IP.To4() != nil:
|
||||
_, err = c.pconn4.WriteTo(b, addr)
|
||||
if err != nil && c.noV4.Get() {
|
||||
if err != nil && (c.noV4.Get() || neterror.TreatAsLostUDP(err)) {
|
||||
return false, nil
|
||||
}
|
||||
case len(addr.IP) == net.IPv6len:
|
||||
@@ -1218,7 +1230,7 @@ func (c *Conn) sendUDPStd(addr *net.UDPAddr, b []byte) (sent bool, err error) {
|
||||
return false, nil
|
||||
}
|
||||
_, err = c.pconn6.WriteTo(b, addr)
|
||||
if err != nil && c.noV6.Get() {
|
||||
if err != nil && (c.noV6.Get() || neterror.TreatAsLostUDP(err)) {
|
||||
return false, nil
|
||||
}
|
||||
default:
|
||||
@@ -1347,19 +1359,23 @@ func (c *Conn) derpWriteChanOfAddr(addr netaddr.IPPort, peer key.NodePublic) cha
|
||||
}
|
||||
|
||||
// Note that derphttp.NewRegionClient does not dial the server
|
||||
// so it is safe to do under the mu lock.
|
||||
// (it doesn't block) so it is safe to do under the c.mu lock.
|
||||
dc := derphttp.NewRegionClient(c.privateKey, c.logf, func() *tailcfg.DERPRegion {
|
||||
// Warning: it is not legal to acquire
|
||||
// magicsock.Conn.mu from this callback.
|
||||
// It's run from derphttp.Client.connect (via Send, etc)
|
||||
// and the lock ordering rules are that magicsock.Conn.mu
|
||||
// must be acquired before derphttp.Client.mu.
|
||||
// See https://github.com/tailscale/tailscale/issues/3726
|
||||
if c.connCtx.Err() != nil {
|
||||
// If we're closing, don't try to acquire the lock.
|
||||
// We might already be in Conn.Close and the Lock would deadlock.
|
||||
// We're closing anyway; return nil to stop dialing.
|
||||
return nil
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if c.derpMap == nil {
|
||||
derpMap, _ := c.derpMapAtomic.Load().(*tailcfg.DERPMap)
|
||||
if derpMap == nil {
|
||||
return nil
|
||||
}
|
||||
return c.derpMap.Regions[regionID]
|
||||
return derpMap.Regions[regionID]
|
||||
})
|
||||
|
||||
dc.SetCanAckPings(true)
|
||||
@@ -1801,6 +1817,14 @@ func (c *Conn) sendDiscoMessage(dst netaddr.IPPort, dstKey key.NodePublic, dstDi
|
||||
} else {
|
||||
metricSentDiscoUDP.Add(1)
|
||||
}
|
||||
switch m.(type) {
|
||||
case *disco.Ping:
|
||||
metricSentDiscoPing.Add(1)
|
||||
case *disco.Pong:
|
||||
metricSentDiscoPong.Add(1)
|
||||
case *disco.CallMeMaybe:
|
||||
metricSentDiscoCallMeMaybe.Add(1)
|
||||
}
|
||||
} else if err == nil {
|
||||
// Can't send. (e.g. no IPv6 locally)
|
||||
} else {
|
||||
@@ -2240,12 +2264,32 @@ func (c *Conn) SetDERPMap(dm *tailcfg.DERPMap) {
|
||||
return
|
||||
}
|
||||
|
||||
c.derpMapAtomic.Store(dm)
|
||||
old := c.derpMap
|
||||
c.derpMap = dm
|
||||
if dm == nil {
|
||||
c.closeAllDerpLocked("derp-disabled")
|
||||
return
|
||||
}
|
||||
|
||||
// Reconnect any DERP region that changed definitions.
|
||||
if old != nil {
|
||||
changes := false
|
||||
for rid, oldDef := range old.Regions {
|
||||
if reflect.DeepEqual(oldDef, dm.Regions[rid]) {
|
||||
continue
|
||||
}
|
||||
changes = true
|
||||
if rid == c.myDerp {
|
||||
c.myDerp = 0
|
||||
}
|
||||
c.closeDerpLocked(rid, "derp-region-redefined")
|
||||
}
|
||||
if changes {
|
||||
c.logActiveDerpLocked()
|
||||
}
|
||||
}
|
||||
|
||||
go c.ReSTUN("derp-map-update")
|
||||
}
|
||||
|
||||
@@ -2385,14 +2429,61 @@ func (c *Conn) closeAllDerpLocked(why string) {
|
||||
c.logActiveDerpLocked()
|
||||
}
|
||||
|
||||
// maybeCloseDERPsOnRebind, in response to a rebind, closes all
|
||||
// DERP connections that don't have a local address in okayLocalIPs
|
||||
// and pings all those that do.
|
||||
func (c *Conn) maybeCloseDERPsOnRebind(okayLocalIPs []netaddr.IPPrefix) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
for regionID, ad := range c.activeDerp {
|
||||
la, err := ad.c.LocalAddr()
|
||||
if err != nil {
|
||||
c.closeOrReconectDERPLocked(regionID, "rebind-no-localaddr")
|
||||
continue
|
||||
}
|
||||
if !tsaddr.PrefixesContainsIP(okayLocalIPs, la.IP()) {
|
||||
c.closeOrReconectDERPLocked(regionID, "rebind-default-route-change")
|
||||
continue
|
||||
}
|
||||
regionID := regionID
|
||||
dc := ad.c
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
if err := dc.Ping(ctx); err != nil {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.closeOrReconectDERPLocked(regionID, "rebind-ping-fail")
|
||||
return
|
||||
}
|
||||
c.logf("post-rebind ping of DERP region %d okay", regionID)
|
||||
}()
|
||||
}
|
||||
c.logActiveDerpLocked()
|
||||
}
|
||||
|
||||
// closeOrReconectDERPLocked closes the DERP connection to the
|
||||
// provided regionID and starts reconnecting it if it's our current
|
||||
// home DERP.
|
||||
//
|
||||
// why is a reason for logging.
|
||||
//
|
||||
// c.mu must be held.
|
||||
func (c *Conn) closeOrReconectDERPLocked(regionID int, why string) {
|
||||
c.closeDerpLocked(regionID, why)
|
||||
if !c.privateKey.IsZero() && c.myDerp == regionID {
|
||||
c.startDerpHomeConnectLocked()
|
||||
}
|
||||
}
|
||||
|
||||
// c.mu must be held.
|
||||
// It is the responsibility of the caller to call logActiveDerpLocked after any set of closes.
|
||||
func (c *Conn) closeDerpLocked(node int, why string) {
|
||||
if ad, ok := c.activeDerp[node]; ok {
|
||||
c.logf("magicsock: closing connection to derp-%v (%v), age %v", node, why, time.Since(ad.createTime).Round(time.Second))
|
||||
func (c *Conn) closeDerpLocked(regionID int, why string) {
|
||||
if ad, ok := c.activeDerp[regionID]; ok {
|
||||
c.logf("magicsock: closing connection to derp-%v (%v), age %v", regionID, why, time.Since(ad.createTime).Round(time.Second))
|
||||
go ad.c.Close()
|
||||
ad.cancel()
|
||||
delete(c.activeDerp, node)
|
||||
delete(c.activeDerp, regionID)
|
||||
metricNumDERPConns.Set(int64(len(c.activeDerp)))
|
||||
}
|
||||
}
|
||||
@@ -2551,6 +2642,7 @@ func (c *connBind) Close() error {
|
||||
}
|
||||
// Send an empty read result to unblock receiveDERP,
|
||||
// which will then check connBind.Closed.
|
||||
// connBind.Closed takes c.mu, but c.derpRecvCh is buffered.
|
||||
c.derpRecvCh <- derpReadResult{}
|
||||
return nil
|
||||
}
|
||||
@@ -2823,13 +2915,15 @@ func (c *Conn) Rebind() {
|
||||
return
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
c.closeAllDerpLocked("rebind")
|
||||
if !c.privateKey.IsZero() {
|
||||
c.startDerpHomeConnectLocked()
|
||||
var ifIPs []netaddr.IPPrefix
|
||||
if c.linkMon != nil {
|
||||
st := c.linkMon.InterfaceState()
|
||||
defIf := st.DefaultRouteInterface
|
||||
ifIPs = st.InterfaceIPs[defIf]
|
||||
c.logf("Rebind; defIf=%q, ips=%v", defIf, ifIPs)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
c.maybeCloseDERPsOnRebind(ifIPs)
|
||||
c.resetEndpointStates()
|
||||
}
|
||||
|
||||
@@ -3041,6 +3135,7 @@ func (c *blockForeverConn) Close() error {
|
||||
return net.ErrClosed
|
||||
}
|
||||
c.closed = true
|
||||
c.cond.Broadcast()
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -3194,7 +3289,7 @@ const (
|
||||
// try to keep an established endpoint peering alive.
|
||||
// It's also the idle time at which we stop doing STUN queries to
|
||||
// keep NAT mappings alive.
|
||||
sessionActiveTimeout = 2 * time.Minute
|
||||
sessionActiveTimeout = 45 * time.Second
|
||||
|
||||
// upgradeInterval is how often we try to upgrade to a better path
|
||||
// even if we have some non-DERP route that works.
|
||||
@@ -3202,11 +3297,11 @@ const (
|
||||
|
||||
// heartbeatInterval is how often pings to the best UDP address
|
||||
// are sent.
|
||||
heartbeatInterval = 2 * time.Second
|
||||
heartbeatInterval = 3 * time.Second
|
||||
|
||||
// trustUDPAddrDuration is how long we trust a UDP address as the exclusive
|
||||
// path (without using DERP) without having heard a Pong reply.
|
||||
trustUDPAddrDuration = 5 * time.Second
|
||||
trustUDPAddrDuration = 6500 * time.Millisecond
|
||||
|
||||
// goodEnoughLatency is the latency at or under which we don't
|
||||
// try to upgrade to a better path.
|
||||
@@ -4047,13 +4142,16 @@ var (
|
||||
metricRecvDataIPv6 = clientmetric.NewCounter("magicsock_recv_data_ipv6")
|
||||
|
||||
// Disco packets
|
||||
metricSendDiscoUDP = clientmetric.NewCounter("magicsock_disco_send_udp")
|
||||
metricSendDiscoDERP = clientmetric.NewCounter("magicsock_disco_send_derp")
|
||||
metricSentDiscoUDP = clientmetric.NewCounter("magicsock_disco_sent_udp")
|
||||
metricSentDiscoDERP = clientmetric.NewCounter("magicsock_disco_sent_derp")
|
||||
metricRecvDiscoBadPeer = clientmetric.NewCounter("magicsock_disco_recv_bad_peer")
|
||||
metricRecvDiscoBadKey = clientmetric.NewCounter("magicsock_disco_recv_bad_key")
|
||||
metricRecvDiscoBadParse = clientmetric.NewCounter("magicsock_disco_recv_bad_parse")
|
||||
metricSendDiscoUDP = clientmetric.NewCounter("magicsock_disco_send_udp")
|
||||
metricSendDiscoDERP = clientmetric.NewCounter("magicsock_disco_send_derp")
|
||||
metricSentDiscoUDP = clientmetric.NewCounter("magicsock_disco_sent_udp")
|
||||
metricSentDiscoDERP = clientmetric.NewCounter("magicsock_disco_sent_derp")
|
||||
metricSentDiscoPing = clientmetric.NewCounter("magicsock_disco_sent_ping")
|
||||
metricSentDiscoPong = clientmetric.NewCounter("magicsock_disco_sent_pong")
|
||||
metricSentDiscoCallMeMaybe = clientmetric.NewCounter("magicsock_disco_sent_callmemaybe")
|
||||
metricRecvDiscoBadPeer = clientmetric.NewCounter("magicsock_disco_recv_bad_peer")
|
||||
metricRecvDiscoBadKey = clientmetric.NewCounter("magicsock_disco_recv_bad_key")
|
||||
metricRecvDiscoBadParse = clientmetric.NewCounter("magicsock_disco_recv_bad_parse")
|
||||
|
||||
metricRecvDiscoUDP = clientmetric.NewCounter("magicsock_disco_recv_udp")
|
||||
metricRecvDiscoDERP = clientmetric.NewCounter("magicsock_disco_recv_derp")
|
||||
|
||||
@@ -1763,3 +1763,27 @@ func (m *peerMap) validate() error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestBlockForeverConnUnblocks(t *testing.T) {
|
||||
c := newBlockForeverConn()
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
defer close(done)
|
||||
_, _, err := c.ReadFrom(make([]byte, 1))
|
||||
done <- err
|
||||
}()
|
||||
time.Sleep(50 * time.Millisecond) // give ReadFrom time to get blocked
|
||||
if err := c.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
timer := time.NewTimer(5 * time.Second)
|
||||
defer timer.Stop()
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != net.ErrClosed {
|
||||
t.Errorf("got %v; want net.ErrClosed", err)
|
||||
}
|
||||
case <-timer.C:
|
||||
t.Fatal("timeout")
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user