Compare commits

..

1 Commits

Author SHA1 Message Date
James Tucker
e09e8d0701 wgengine: do not terminate the process when in a debugger breakpoint
The watchdog attempts to check if an operation has locked up, but this
happens as a natural course of running under a debugger. The output is
noisy, and the final operation is fatal. Replace that with a short
output instead when $TS_DEBUGGER is set.

Fixes #8301
Signed-off-by: James Tucker <james@tailscale.com>
2023-06-07 15:50:31 -07:00
87 changed files with 475 additions and 2981 deletions

View File

@@ -1,15 +0,0 @@
name: "Dockerfile build"
on:
push:
branches:
- main
pull_request:
branches:
- "*"
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: "Build Docker image"
run: docker build .

View File

@@ -32,7 +32,7 @@ jobs:
- name: golangci-lint
# Note: this is the 'v3' tag as of 2023-04-17
uses: golangci/golangci-lint-action@639cd343e1d3b897ff35927a75193d57cfcba299
uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5
with:
version: v1.52.2

View File

@@ -47,7 +47,8 @@ RUN go install \
golang.org/x/crypto/ssh \
golang.org/x/crypto/acme \
nhooyr.io/websocket \
github.com/mdlayher/netlink
github.com/mdlayher/netlink \
golang.zx2c4.com/wireguard/device
COPY . .
@@ -72,4 +73,4 @@ RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
COPY --from=build-env /go/bin/* /usr/local/bin/
# For compat with the previous run.sh, although ideally you should be
# using build_docker.sh which sets an entrypoint for the image.
RUN mkdir /tailscale && ln -s /usr/local/bin/containerboot /tailscale/run.sh
RUN ln -s /usr/local/bin/containerboot /tailscale/run.sh

View File

@@ -2,4 +2,4 @@
# SPDX-License-Identifier: BSD-3-Clause
FROM alpine:3.16
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables iputils
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables

View File

@@ -1 +1 @@
1.45.0
1.43.0

17
api.md
View File

@@ -1222,11 +1222,6 @@ The remaining three methods operate on auth keys and API access tokens.
// expirySeconds (int) is the duration in seconds a new key is valid.
"expirySeconds": 86400
// description (string) is an optional short phrase that describes what
// this key is used for. It can be a maximum of 50 alphanumeric characters.
// Hyphens and underscores are also allowed.
"description": "short description of key purpose"
}
```
@@ -1313,9 +1308,6 @@ Note the following about required vs. optional values:
Specifies the duration in seconds until the key should expire.
Defaults to 90 days if not supplied.
- **`description`:** Optional in `POST` body.
A short string specifying the purpose of the key. Can be a maximum of 50 alphanumeric characters. Hyphens and spaces are also allowed.
### Request example
``` jsonc
@@ -1333,8 +1325,7 @@ curl "https://api.tailscale.com/api/v2/tailnet/example.com/keys" \
}
}
},
"expirySeconds": 86400,
"description": "dev access"
"expirySeconds": 86400
}'
```
@@ -1360,8 +1351,7 @@ It holds the capabilities specified in the request and can no longer be retrieve
"tags": [ "tag:example" ]
}
}
},
"description": "dev access"
}
}
```
@@ -1413,8 +1403,7 @@ The response is a JSON object with information about the key supplied.
]
}
}
},
"description": "dev access"
}
}
```

View File

@@ -125,7 +125,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
W tailscale.com/util/clientmetric from tailscale.com/net/tshttpproxy
tailscale.com/util/cloudenv from tailscale.com/hostinfo+
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
tailscale.com/util/cmpx from tailscale.com/cmd/derper+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/hostinfo+
tailscale.com/util/httpm from tailscale.com/client/tailscale
@@ -180,7 +179,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
bytes from bufio+
compress/flate from compress/gzip+
compress/gzip from internal/profile+
L compress/zlib from debug/elf
container/list from crypto/tls+
context from crypto/tls+
crypto from crypto/ecdsa+
@@ -204,8 +202,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
crypto/tls from golang.org/x/crypto/acme+
crypto/x509 from crypto/tls+
crypto/x509/pkix from crypto/x509+
L debug/dwarf from debug/elf
L debug/elf from golang.org/x/sys/unix
embed from crypto/internal/nistec+
encoding from encoding/json+
encoding/asn1 from crypto/x509+
@@ -221,7 +217,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
fmt from compress/flate+
go/token from google.golang.org/protobuf/internal/strs
hash from crypto+
L hash/adler32 from compress/zlib
hash/crc32 from compress/gzip+
hash/fnv from google.golang.org/protobuf/internal/detrand
hash/maphash from go4.org/mem
@@ -230,7 +225,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
io/fs from crypto/x509+
io/ioutil from github.com/mitchellh/go-ps+
log from expvar+
log/internal from log
math from compress/flate+
math/big from crypto/dsa+
math/bits from compress/flate+

View File

@@ -33,7 +33,6 @@ import (
"tailscale.com/net/stun"
"tailscale.com/tsweb"
"tailscale.com/types/key"
"tailscale.com/util/cmpx"
)
var (
@@ -437,7 +436,11 @@ func defaultMeshPSKFile() string {
}
func rateLimitedListenAndServeTLS(srv *http.Server) error {
ln, err := net.Listen("tcp", cmpx.Or(srv.Addr, ":https"))
addr := srv.Addr
if addr == "" {
addr = ":https"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}

View File

@@ -16,7 +16,6 @@ import (
"golang.org/x/oauth2/clientcredentials"
"tailscale.com/client/tailscale"
"tailscale.com/util/cmpx"
)
func main() {
@@ -40,7 +39,10 @@ func main() {
log.Fatal("at least one tag must be specified")
}
baseURL := cmpx.Or(os.Getenv("TS_BASE_URL"), "https://api.tailscale.com")
baseURL := os.Getenv("TS_BASE_URL")
if baseURL == "" {
baseURL = "https://api.tailscale.com"
}
credentials := clientcredentials.Config{
ClientID: clientID,

View File

@@ -22,7 +22,6 @@ import (
"tailscale.com/tstest"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
"tailscale.com/util/cmpx"
"tailscale.com/version/distro"
)
@@ -720,7 +719,10 @@ func TestPrefsFromUpArgs(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var warnBuf tstest.MemLogger
goos := cmpx.Or(tt.goos, "linux")
goos := tt.goos
if goos == "" {
goos = "linux"
}
st := tt.st
if st == nil {
st = new(ipnstate.Status)

View File

@@ -30,10 +30,10 @@ func newFunnelCommand(e *serveEnv) *ffcli.Command {
return &ffcli.Command{
Name: "funnel",
ShortHelp: "Turn on/off Funnel service",
ShortUsage: strings.Join([]string{
"funnel <serve-port> {on|off}",
"funnel status [--json]",
}, "\n "),
ShortUsage: strings.TrimSpace(`
funnel <serve-port> {on|off}
funnel status [--json]
`),
LongHelp: strings.Join([]string{
"Funnel allows you to publish a 'tailscale serve'",
"server publicly, open to the entire internet.",

View File

@@ -51,7 +51,7 @@ relay node.
fs.BoolVar(&pingArgs.tsmp, "tsmp", false, "do a TSMP-level ping (through WireGuard, but not either host OS stack)")
fs.BoolVar(&pingArgs.icmp, "icmp", false, "do a ICMP-level ping (through WireGuard, but not the local host OS stack)")
fs.BoolVar(&pingArgs.peerAPI, "peerapi", false, "try hitting the peer's peerapi HTTP server")
fs.IntVar(&pingArgs.num, "c", 10, "max number of pings to send. 0 for infinity.")
fs.IntVar(&pingArgs.num, "c", 10, "max number of pings to send")
fs.DurationVar(&pingArgs.timeout, "timeout", 5*time.Second, "timeout before giving up on a ping")
return fs
})(),

View File

@@ -35,14 +35,13 @@ func newServeCommand(e *serveEnv) *ffcli.Command {
return &ffcli.Command{
Name: "serve",
ShortHelp: "Serve content and local servers",
ShortUsage: strings.Join([]string{
"serve http:<port> <mount-point> <source> [off]",
"serve https:<port> <mount-point> <source> [off]",
"serve tcp:<port> tcp://localhost:<local-port> [off]",
"serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]",
"serve status [--json]",
"serve reset",
}, "\n "),
ShortUsage: strings.TrimSpace(`
serve https:<port> <mount-point> <source> [off]
serve tcp:<port> tcp://localhost:<local-port> [off]
serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]
serve status [--json]
serve reset
`),
LongHelp: strings.TrimSpace(`
*** BETA; all of this is subject to change ***
@@ -59,8 +58,8 @@ EXAMPLES
- To proxy requests to a web server at 127.0.0.1:3000:
$ tailscale serve https:443 / http://127.0.0.1:3000
Or, using the default port (443):
$ tailscale serve https / http://127.0.0.1:3000
Or, using the default port:
$ tailscale serve https / http://127.0.0.1:3000
- To serve a single file or a directory of files:
$ tailscale serve https / /home/alice/blog/index.html
@@ -69,12 +68,6 @@ EXAMPLES
- To serve simple static text:
$ tailscale serve https:8080 / text:"Hello, world!"
- To serve over HTTP (tailnet only):
$ tailscale serve http:80 / http://127.0.0.1:3000
Or, using the default port (80):
$ tailscale serve http / http://127.0.0.1:3000
- To forward incoming TCP connections on port 2222 to a local TCP server on
port 22 (e.g. to run OpenSSH in parallel with Tailscale SSH):
$ tailscale serve tcp:2222 tcp://localhost:22
@@ -182,7 +175,6 @@ func (e *serveEnv) getLocalClientStatus(ctx context.Context) (*ipnstate.Status,
// serve config types like proxy, path, and text.
//
// Examples:
// - tailscale serve http / http://localhost:3000
// - tailscale serve https / http://localhost:3000
// - tailscale serve https /images/ /var/www/images/
// - tailscale serve https:10000 /motd.txt text:"Hello, world!"
@@ -207,14 +199,19 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
return e.lc.SetServeConfig(ctx, sc)
}
parsePort := func(portStr string) (uint16, error) {
port64, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
return 0, err
}
return uint16(port64), nil
}
srcType, srcPortStr, found := strings.Cut(args[0], ":")
if !found {
if srcType == "https" && srcPortStr == "" {
// Default https port to 443.
srcPortStr = "443"
} else if srcType == "http" && srcPortStr == "" {
// Default http port to 80.
srcPortStr = "80"
} else {
return flag.ErrHelp
}
@@ -222,18 +219,18 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
turnOff := "off" == args[len(args)-1]
if len(args) < 2 || ((srcType == "https" || srcType == "http") && !turnOff && len(args) < 3) {
if len(args) < 2 || (srcType == "https" && !turnOff && len(args) < 3) {
fmt.Fprintf(os.Stderr, "error: invalid number of arguments\n\n")
return flag.ErrHelp
}
srcPort, err := parseServePort(srcPortStr)
srcPort, err := parsePort(srcPortStr)
if err != nil {
return fmt.Errorf("invalid port %q: %w", srcPortStr, err)
return err
}
switch srcType {
case "https", "http":
case "https":
mount, err := cleanMountPoint(args[1])
if err != nil {
return err
@@ -241,8 +238,7 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
if turnOff {
return e.handleWebServeRemove(ctx, srcPort, mount)
}
useTLS := srcType == "https"
return e.handleWebServe(ctx, srcPort, useTLS, mount, args[2])
return e.handleWebServe(ctx, srcPort, mount, args[2])
case "tcp", "tls-terminated-tcp":
if turnOff {
return e.handleTCPServeRemove(ctx, srcPort)
@@ -250,20 +246,20 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
return e.handleTCPServe(ctx, srcType, srcPort, args[1])
default:
fmt.Fprintf(os.Stderr, "error: invalid serve type %q\n", srcType)
fmt.Fprint(os.Stderr, "must be one of: http:<port>, https:<port>, tcp:<port> or tls-terminated-tcp:<port>\n\n", srcType)
fmt.Fprint(os.Stderr, "must be one of: https:<port>, tcp:<port> or tls-terminated-tcp:<port>\n\n", srcType)
return flag.ErrHelp
}
}
// handleWebServe handles the "tailscale serve (http/https):..." subcommand. It
// configures the serve config to forward HTTPS connections to the given source.
// handleWebServe handles the "tailscale serve https:..." subcommand.
// It configures the serve config to forward HTTPS connections to the
// given source.
//
// Examples:
// - tailscale serve http / http://localhost:3000
// - tailscale serve https / http://localhost:3000
// - tailscale serve https:8443 /files/ /home/alice/shared-files/
// - tailscale serve https:10000 /motd.txt text:"Hello, world!"
func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, useTLS bool, mount, source string) error {
func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, mount, source string) error {
h := new(ipn.HTTPHandler)
ts, _, _ := strings.Cut(source, ":")
@@ -322,7 +318,7 @@ func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, useTLS bo
return flag.ErrHelp
}
mak.Set(&sc.TCP, srvPort, &ipn.TCPPortHandler{HTTPS: useTLS, HTTP: !useTLS})
mak.Set(&sc.TCP, srvPort, &ipn.TCPPortHandler{HTTPS: true})
if _, ok := sc.Web[hp]; !ok {
mak.Set(&sc.Web, hp, new(ipn.WebServerConfig))
@@ -630,10 +626,7 @@ func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
printf("\n")
}
for hp := range sc.Web {
err := e.printWebStatusTree(sc, hp)
if err != nil {
return err
}
printWebStatusTree(sc, hp)
printf("\n")
}
printFunnelWarning(sc)
@@ -672,37 +665,20 @@ func printTCPStatusTree(ctx context.Context, sc *ipn.ServeConfig, st *ipnstate.S
return nil
}
func (e *serveEnv) printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) error {
// No-op if no serve config
func printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) {
if sc == nil {
return nil
return
}
fStatus := "tailnet only"
if sc.AllowFunnel[hp] {
fStatus = "Funnel on"
}
host, portStr, _ := net.SplitHostPort(string(hp))
port, err := parseServePort(portStr)
if err != nil {
return fmt.Errorf("invalid port %q: %w", portStr, err)
if portStr == "443" {
printf("https://%s (%s)\n", host, fStatus)
} else {
printf("https://%s:%s (%s)\n", host, portStr, fStatus)
}
scheme := "https"
if sc.IsServingHTTP(port) {
scheme = "http"
}
portPart := ":" + portStr
if scheme == "http" && portStr == "80" ||
scheme == "https" && portStr == "443" {
portPart = ""
}
if scheme == "http" {
hostname, _, _ := strings.Cut("host", ".")
printf("%s://%s%s (%s)\n", scheme, hostname, portPart, fStatus)
}
printf("%s://%s%s (%s)\n", scheme, host, portPart, fStatus)
srvTypeAndDesc := func(h *ipn.HTTPHandler) (string, string) {
switch {
case h.Path != "":
@@ -729,8 +705,6 @@ func (e *serveEnv) printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) erro
t, d := srvTypeAndDesc(h)
printf("%s %s%s %-5s %s\n", "|--", m, strings.Repeat(" ", maxLen-len(m)), t, d)
}
return nil
}
func elipticallyTruncate(s string, max int) string {
@@ -751,16 +725,3 @@ func (e *serveEnv) runServeReset(ctx context.Context, args []string) error {
sc := new(ipn.ServeConfig)
return e.lc.SetServeConfig(ctx, sc)
}
// parseServePort parses a port number from a string and returns it as a
// uint16. It returns an error if the port number is invalid or zero.
func parseServePort(s string) (uint16, error) {
p, err := strconv.ParseUint(s, 10, 16)
if err != nil {
return 0, err
}
if p == 0 {
return 0, errors.New("port number must be non-zero")
}
return uint16(p), nil
}

View File

@@ -89,59 +89,6 @@ func TestServeConfigMutations(t *testing.T) {
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
})
// https
add(step{reset: true})
add(step{ // allow omitting port (default to 80)
command: cmd("http / http://localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
},
},
})
add(step{ // support non Funnel port
command: cmd("http:9999 /abc http://localhost:3001"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}, 9999: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
"foo.test.ts.net:9999": {Handlers: map[string]*ipn.HTTPHandler{
"/abc": {Proxy: "http://127.0.0.1:3001"},
}},
},
},
})
add(step{
command: cmd("http:9999 /abc off"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
},
},
})
add(step{
command: cmd("http:8080 /abc http://127.0.0.1:3001"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}, 8080: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
"foo.test.ts.net:8080": {Handlers: map[string]*ipn.HTTPHandler{
"/abc": {Proxy: "http://127.0.0.1:3001"},
}},
},
},
})
// https
add(step{reset: true})
add(step{

View File

@@ -29,7 +29,6 @@ import (
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/util/cmpx"
"tailscale.com/util/groupmember"
"tailscale.com/version/distro"
)
@@ -156,7 +155,10 @@ func runWeb(ctx context.Context, args []string) error {
// urlOfListenAddr parses a given listen address into a formatted URL
func urlOfListenAddr(addr string) string {
host, port, _ := net.SplitHostPort(addr)
return fmt.Sprintf("http://%s", net.JoinHostPort(cmpx.Or(host, "127.0.0.1"), port))
if host == "" {
host = "127.0.0.1"
}
return fmt.Sprintf("http://%s", net.JoinHostPort(host, port))
}
// authorize returns the name of the user accessing the web UI after verifying

View File

@@ -114,7 +114,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/util/clientmetric from tailscale.com/net/netcheck+
tailscale.com/util/cloudenv from tailscale.com/net/dnscache+
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
tailscale.com/util/cmpx from tailscale.com/cmd/tailscale/cli+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
@@ -177,7 +176,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
bytes from bufio+
compress/flate from compress/gzip+
compress/gzip from net/http
compress/zlib from image/png+
compress/zlib from image/png
container/list from crypto/tls+
context from crypto/tls+
crypto from crypto/ecdsa+
@@ -202,8 +201,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
crypto/x509 from crypto/tls+
crypto/x509/pkix from crypto/x509+
database/sql/driver from github.com/google/uuid
L debug/dwarf from debug/elf
L debug/elf from golang.org/x/sys/unix
embed from tailscale.com/cmd/tailscale/cli+
encoding from encoding/json+
encoding/asn1 from crypto/x509+
@@ -231,7 +228,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
io/fs from crypto/x509+
io/ioutil from golang.org/x/sys/cpu+
log from expvar+
log/internal from log
math from compress/flate+
math/big from crypto/dsa+
math/bits from compress/flate+

View File

@@ -308,7 +308,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/util/clientmetric from tailscale.com/control/controlclient+
tailscale.com/util/cloudenv from tailscale.com/net/dns/resolver+
LW tailscale.com/util/cmpver from tailscale.com/net/dns+
tailscale.com/util/cmpx from tailscale.com/derp/derphttp+
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics+
tailscale.com/util/dnsname from tailscale.com/hostinfo+
@@ -398,7 +397,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
bytes from bufio+
compress/flate from compress/gzip+
compress/gzip from golang.org/x/net/http2+
L compress/zlib from debug/elf
container/heap from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
container/list from crypto/tls+
context from crypto/tls+
@@ -423,8 +421,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
crypto/tls from github.com/tcnksm/go-httpstat+
crypto/x509 from crypto/tls+
crypto/x509/pkix from crypto/x509+
L debug/dwarf from debug/elf
L debug/elf from golang.org/x/sys/unix
embed from tailscale.com+
encoding from encoding/json+
encoding/asn1 from crypto/x509+
@@ -440,7 +436,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
flag from net/http/httptest+
fmt from compress/flate+
hash from crypto+
hash/adler32 from tailscale.com/ipn/ipnlocal+
hash/adler32 from tailscale.com/ipn/ipnlocal
hash/crc32 from compress/gzip+
hash/fnv from tailscale.com/wgengine/magicsock+
hash/maphash from go4.org/mem
@@ -449,7 +445,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
io/fs from crypto/x509+
io/ioutil from github.com/godbus/dbus/v5+
log from expvar+
log/internal from log
LD log/syslog from tailscale.com/ssh/tailssh
math from compress/flate+
math/big from crypto/dsa+

View File

@@ -20,7 +20,7 @@ func dumpGoroutinesToURL(c *http.Client, targetURL string) {
zbuf := new(bytes.Buffer)
zw := gzip.NewWriter(zbuf)
zw.Write(goroutines.ScrubbedGoroutineDump(true))
zw.Write(goroutines.ScrubbedGoroutineDump())
zw.Close()
req, err := http.NewRequestWithContext(ctx, "PUT", targetURL, zbuf)

View File

@@ -40,7 +40,6 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/util/cmpx"
)
// Client is a DERP-over-HTTP client.
@@ -655,7 +654,10 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
// Start v4 dial
}
}
dst := cmpx.Or(dstPrimary, n.HostName)
dst := dstPrimary
if dst == "" {
dst = n.HostName
}
port := "443"
if n.DERPPort != 0 {
port = fmt.Sprint(n.DERPPort)

View File

@@ -115,4 +115,4 @@
in
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
}
# nix-direnv cache busting line: sha256-fgCrmtJs1svFz0Xn7iwLNrbBNlcO6V0yqGPMY0+V1VQ=
# nix-direnv cache busting line: sha256-l2uIma2oEdSN0zVo9BOFJF2gC3S60vXwTLVadv8yQPo=

4
go.mod
View File

@@ -48,7 +48,7 @@ require (
github.com/mdlayher/genetlink v1.3.2
github.com/mdlayher/netlink v1.7.2
github.com/mdlayher/sdnotify v1.0.0
github.com/miekg/dns v1.1.55
github.com/miekg/dns v1.1.54
github.com/mitchellh/go-ps v1.0.0
github.com/peterbourgon/ff/v3 v3.3.0
github.com/pkg/errors v0.9.1
@@ -79,7 +79,7 @@ require (
golang.org/x/net v0.10.0
golang.org/x/oauth2 v0.7.0
golang.org/x/sync v0.2.0
golang.org/x/sys v0.8.1-0.20230609144347-5059a07aa46a
golang.org/x/sys v0.8.0
golang.org/x/term v0.8.0
golang.org/x/time v0.3.0
golang.org/x/tools v0.9.1

View File

@@ -1 +1 @@
sha256-fgCrmtJs1svFz0Xn7iwLNrbBNlcO6V0yqGPMY0+V1VQ=
sha256-l2uIma2oEdSN0zVo9BOFJF2gC3S60vXwTLVadv8yQPo=

8
go.sum
View File

@@ -767,8 +767,8 @@ github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8Ku
github.com/mgechev/revive v1.3.1 h1:OlQkcH40IB2cGuprTPcjB0iIUddgVZgGmDX3IAMR8D4=
github.com/mgechev/revive v1.3.1/go.mod h1:YlD6TTWl2B8A103R9KWJSPVI9DrEf+oqr15q21Ld+5I=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/miekg/dns v1.1.54 h1:5jon9mWcb0sFJGpnI99tOMhCPyJ+RPVz5b63MQG0VWI=
github.com/miekg/dns v1.1.54/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
@@ -1432,8 +1432,8 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.1-0.20230609144347-5059a07aa46a h1:qMsju+PNttu/NMbq8bQ9waDdxgJMu9QNoUDuhnBaYt0=
golang.org/x/sys v0.8.1-0.20230609144347-5059a07aa46a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=

View File

@@ -1 +1 @@
tailscale.go1.21
tailscale.go1.20

View File

@@ -1 +1 @@
492f6d9d792fa6e4caa388e4d7bab46b48d07ad5
480a0c381923c53e70ed5e72f9a9f79ce1884859

View File

@@ -7,10 +7,8 @@ package hostinfo
import (
"bufio"
"bytes"
"io"
"os"
"os/exec"
"runtime"
"runtime/debug"
"strings"
@@ -436,12 +434,3 @@ func etcAptSourceFileIsDisabled(r io.Reader) bool {
}
return disabled
}
// IsSELinuxEnforcing reports whether SELinux is in "Enforcing" mode.
func IsSELinuxEnforcing() bool {
if runtime.GOOS != "linux" {
return false
}
out, _ := exec.Command("getenforce").Output()
return string(bytes.TrimSpace(out)) == "Enforcing"
}

View File

@@ -103,7 +103,6 @@ func (src *TCPPortHandler) Clone() *TCPPortHandler {
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _TCPPortHandlerCloneNeedsRegeneration = TCPPortHandler(struct {
HTTPS bool
HTTP bool
TCPForward string
TerminateTLS string
}{})

View File

@@ -228,14 +228,12 @@ func (v *TCPPortHandlerView) UnmarshalJSON(b []byte) error {
}
func (v TCPPortHandlerView) HTTPS() bool { return v.ж.HTTPS }
func (v TCPPortHandlerView) HTTP() bool { return v.ж.HTTP }
func (v TCPPortHandlerView) TCPForward() string { return v.ж.TCPForward }
func (v TCPPortHandlerView) TerminateTLS() string { return v.ж.TerminateTLS }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _TCPPortHandlerViewNeedsRegeneration = TCPPortHandler(struct {
HTTPS bool
HTTP bool
TCPForward string
TerminateTLS string
}{})

View File

@@ -49,7 +49,7 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
}
case "/debug/goroutines":
w.Header().Set("Content-Type", "text/plain")
w.Write(goroutines.ScrubbedGoroutineDump(true))
w.Write(goroutines.ScrubbedGoroutineDump())
case "/debug/prefs":
writeJSON(b.Prefs())
case "/debug/metrics":

View File

@@ -16,7 +16,6 @@ import (
"tailscale.com/types/dnstype"
"tailscale.com/types/netmap"
"tailscale.com/util/cloudenv"
"tailscale.com/util/cmpx"
"tailscale.com/util/dnsname"
)
@@ -309,7 +308,10 @@ func TestDNSConfigForNetmap(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
verOS := cmpx.Or(tt.os, "linux")
verOS := tt.os
if verOS == "" {
verOS = "linux"
}
var log tstest.MemLogger
got := dnsConfigForNetmap(tt.nm, tt.prefs.View(), log.Logf, verOS)
if !reflect.DeepEqual(got, tt.want) {

View File

@@ -4,6 +4,7 @@
package ipnlocal
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
@@ -17,6 +18,7 @@ import (
"net/netip"
"net/url"
"os"
"os/exec"
"os/user"
"path/filepath"
"runtime"
@@ -30,7 +32,6 @@ import (
"go4.org/mem"
"go4.org/netipx"
"golang.org/x/exp/slices"
"gvisor.dev/gvisor/pkg/tcpip"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/control/controlclient"
"tailscale.com/doctor"
@@ -70,7 +71,6 @@ import (
"tailscale.com/types/preftype"
"tailscale.com/types/ptr"
"tailscale.com/types/views"
"tailscale.com/util/cmpx"
"tailscale.com/util/deephash"
"tailscale.com/util/dnsname"
"tailscale.com/util/mak"
@@ -2581,7 +2581,7 @@ func (b *LocalBackend) checkSSHPrefsLocked(p *ipn.Prefs) error {
if distro.Get() == distro.QNAP && !envknob.UseWIPCode() {
return errors.New("The Tailscale SSH server does not run on QNAP.")
}
b.updateSELinuxHealthWarning()
checkSELinux()
// otherwise okay
case "darwin":
// okay only in tailscaled mode for now.
@@ -2827,14 +2827,14 @@ func (b *LocalBackend) GetPeerAPIPort(ip netip.Addr) (port uint16, ok bool) {
return 0, false
}
// handlePeerAPIConn serves an already-accepted connection c.
// ServePeerAPIConnection serves an already-accepted connection c.
//
// The remote parameter is the remote address.
// The local parameter is the local address (either a Tailscale IPv4
// or IPv6 IP and the peerapi port for that address).
//
// The connection will be closed by handlePeerAPIConn.
func (b *LocalBackend) handlePeerAPIConn(remote, local netip.AddrPort, c net.Conn) {
// The connection will be closed by ServePeerAPIConnection.
func (b *LocalBackend) ServePeerAPIConnection(remote, local netip.AddrPort, c net.Conn) {
b.mu.Lock()
defer b.mu.Unlock()
for _, pln := range b.peerAPIListeners {
@@ -2848,48 +2848,6 @@ func (b *LocalBackend) handlePeerAPIConn(remote, local netip.AddrPort, c net.Con
return
}
func (b *LocalBackend) isLocalIP(ip netip.Addr) bool {
nm := b.NetMap()
return nm != nil && slices.Contains(nm.Addresses, netip.PrefixFrom(ip, ip.BitLen()))
}
var (
magicDNSIP = tsaddr.TailscaleServiceIP()
magicDNSIPv6 = tsaddr.TailscaleServiceIPv6()
)
// TCPHandlerForDst returns a TCP handler for connections to dst, or nil if
// no handler is needed. It also returns a list of TCP socket options to
// apply to the socket before calling the handler.
func (b *LocalBackend) TCPHandlerForDst(src, dst netip.AddrPort) (handler func(c net.Conn) error, opts []tcpip.SettableSocketOption) {
if dst.Port() == 80 && (dst.Addr() == magicDNSIP || dst.Addr() == magicDNSIPv6) {
return b.HandleQuad100Port80Conn, opts
}
if !b.isLocalIP(dst.Addr()) {
return nil, nil
}
if dst.Port() == 22 && b.ShouldRunSSH() {
// Use a higher keepalive idle time for SSH connections, as they are
// typically long lived and idle connections are more likely to be
// intentional. Ideally we would turn this off entirely, but we can't
// tell the difference between a long lived connection that is idle
// vs a connection that is dead because the peer has gone away.
// We pick 72h as that is typically sufficient for a long weekend.
opts = append(opts, ptr.To(tcpip.KeepaliveIdleOption(72*time.Hour)))
return b.handleSSHConn, opts
}
if port, ok := b.GetPeerAPIPort(dst.Addr()); ok && dst.Port() == port {
return func(c net.Conn) error {
b.handlePeerAPIConn(src, dst, c)
return nil
}, opts
}
if handler := b.tcpHandlerForServe(dst.Port(), src); handler != nil {
return handler, opts
}
return nil, nil
}
func (b *LocalBackend) peerAPIServicesLocked() (ret []tailcfg.Service) {
for _, pln := range b.peerAPIListeners {
proto := tailcfg.PeerAPI4
@@ -3974,7 +3932,10 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
b.dialer.SetNetMap(nm)
var login string
if nm != nil {
login = cmpx.Or(nm.UserProfiles[nm.User].LoginName, "<missing-profile>")
login = nm.UserProfiles[nm.User].LoginName
if login == "" {
login = "<missing-profile>"
}
}
b.netMap = nm
if login != b.activeLogin {
@@ -4129,10 +4090,6 @@ func (b *LocalBackend) setServeProxyHandlersLocked() {
b.serveConfig.Web().Range(func(_ ipn.HostPort, conf ipn.WebServerConfigView) (cont bool) {
conf.Handlers().Range(func(_ string, h ipn.HTTPHandlerView) (cont bool) {
backend := h.Proxy()
if backend == "" {
// Only create proxy handlers for servers with a proxy backend.
return true
}
mak.Set(&backends, backend, true)
if _, ok := b.serveProxyHandlers.Load(backend); ok {
return true
@@ -4707,29 +4664,33 @@ func (b *LocalBackend) sshServerOrInit() (_ SSHServer, err error) {
var warnSSHSELinux = health.NewWarnable()
func (b *LocalBackend) updateSELinuxHealthWarning() {
if hostinfo.IsSELinuxEnforcing() {
func checkSELinux() {
if runtime.GOOS != "linux" {
return
}
out, _ := exec.Command("getenforce").Output()
if string(bytes.TrimSpace(out)) == "Enforcing" {
warnSSHSELinux.Set(errors.New("SELinux is enabled; Tailscale SSH may not work. See https://tailscale.com/s/ssh-selinux"))
} else {
warnSSHSELinux.Set(nil)
}
}
func (b *LocalBackend) handleSSHConn(c net.Conn) (err error) {
func (b *LocalBackend) HandleSSHConn(c net.Conn) (err error) {
s, err := b.sshServerOrInit()
if err != nil {
return err
}
b.updateSELinuxHealthWarning()
checkSELinux()
return s.HandleSSHConn(c)
}
// HandleQuad100Port80Conn serves http://100.100.100.100/ on port 80 (and
// the equivalent tsaddr.TailscaleServiceIPv6 address).
func (b *LocalBackend) HandleQuad100Port80Conn(c net.Conn) error {
func (b *LocalBackend) HandleQuad100Port80Conn(c net.Conn) {
var s http.Server
s.Handler = http.HandlerFunc(b.handleQuad100Port80Conn)
return s.Serve(netutil.NewOneConnListener(c, nil))
s.Serve(netutil.NewOneConnListener(c, nil))
}
func validQuad100Host(h string) bool {

View File

@@ -158,9 +158,7 @@ func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap, prefs ipn.PrefsVie
return nil
}
if b.tka != nil || nm.TKAEnabled {
b.logf("tkaSyncIfNeeded: enabled=%v, head=%v", nm.TKAEnabled, nm.TKAHead)
}
b.logf("tkaSyncIfNeeded: enabled=%v, head=%v", nm.TKAEnabled, nm.TKAHead)
ourNodeKey := prefs.Persist().PublicNodeKey()
@@ -199,7 +197,7 @@ func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap, prefs ipn.PrefsVie
health.SetTKAHealth(nil)
}
} else {
return fmt.Errorf("[bug] unreachable invariant of wantEnabled w/ isEnabled")
return fmt.Errorf("[bug] unreachable invariant of wantEnabled /w isEnabled")
}
}
@@ -889,18 +887,6 @@ func (b *LocalBackend) NetworkLockWrapPreauthKey(preauthKey string, tkaKey key.N
return fmt.Sprintf("%s--TL%s-%s", preauthKey, tkaSuffixEncoder.EncodeToString(sig.Serialize()), tkaSuffixEncoder.EncodeToString(priv)), nil
}
// NetworkLockVerifySigningDeeplink asks the authority to verify the given deeplink
// URL. See the comment for ValidateDeeplink for details.
func (b *LocalBackend) NetworkLockVerifySigningDeeplink(url string) tka.DeeplinkValidationResult {
b.mu.Lock()
defer b.mu.Unlock()
if b.tka == nil {
return tka.DeeplinkValidationResult{IsValid: false, Error: errNetworkLockNotActive.Error()}
}
return b.tka.authority.ValidateDeeplink(url)
}
func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeKeySignature, error) {
p, err := nodeInfo.NodePublic.MarshalBinary()
if err != nil {

View File

@@ -780,7 +780,7 @@ func (h *peerAPIHandler) handleServeIngress(w http.ResponseWriter, r *http.Reque
return
}
getConnOrReset := func() (net.Conn, bool) {
getConn := func() (net.Conn, bool) {
conn, _, err := w.(http.Hijacker).Hijack()
if err != nil {
h.logf("ingress: failed hijacking conn")
@@ -798,7 +798,7 @@ func (h *peerAPIHandler) handleServeIngress(w http.ResponseWriter, r *http.Reque
http.Error(w, "denied", http.StatusForbidden)
}
h.ps.b.HandleIngressTCPConn(h.peerNode, target, srcAddr, getConnOrReset, sendRST)
h.ps.b.HandleIngressTCPConn(h.peerNode, target, srcAddr, getConn, sendRST)
}
func (h *peerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Request) {

View File

@@ -162,13 +162,12 @@ func (s *serveListener) handleServeListenersAccept(ln net.Listener) error {
return err
}
srcAddr := conn.RemoteAddr().(*net.TCPAddr).AddrPort()
handler := s.b.tcpHandlerForServe(s.ap.Port(), srcAddr)
if handler == nil {
getConn := func() (net.Conn, bool) { return conn, true }
sendRST := func() {
s.b.logf("serve RST for %v", srcAddr)
conn.Close()
continue
}
go handler(conn)
go s.b.HandleInterceptedTCPConn(s.ap.Port(), srcAddr, getConn, sendRST)
}
}
@@ -257,7 +256,7 @@ func (b *LocalBackend) ServeConfig() ipn.ServeConfigView {
return b.serveConfig
}
func (b *LocalBackend) HandleIngressTCPConn(ingressPeer *tailcfg.Node, target ipn.HostPort, srcAddr netip.AddrPort, getConnOrReset func() (net.Conn, bool), sendRST func()) {
func (b *LocalBackend) HandleIngressTCPConn(ingressPeer *tailcfg.Node, target ipn.HostPort, srcAddr netip.AddrPort, getConn func() (net.Conn, bool), sendRST func()) {
b.mu.Lock()
sc := b.serveConfig
b.mu.Unlock()
@@ -290,7 +289,7 @@ func (b *LocalBackend) HandleIngressTCPConn(ingressPeer *tailcfg.Node, target ip
if b.getTCPHandlerForFunnelFlow != nil {
handler := b.getTCPHandlerForFunnelFlow(srcAddr, dport)
if handler != nil {
c, ok := getConnOrReset()
c, ok := getConn()
if !ok {
b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport)
return
@@ -299,41 +298,39 @@ func (b *LocalBackend) HandleIngressTCPConn(ingressPeer *tailcfg.Node, target ip
return
}
}
// TODO(bradfitz): pass ingressPeer etc in context to tcpHandlerForServe,
// TODO(bradfitz): pass ingressPeer etc in context to HandleInterceptedTCPConn,
// extend serveHTTPContext or similar.
handler := b.tcpHandlerForServe(dport, srcAddr)
if handler == nil {
sendRST()
return
}
c, ok := getConnOrReset()
if !ok {
b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport)
return
}
handler(c)
b.HandleInterceptedTCPConn(dport, srcAddr, getConn, sendRST)
}
// tcpHandlerForServe returns a handler for a TCP connection to be served via
// the ipn.ServeConfig.
func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort) (handler func(net.Conn) error) {
func (b *LocalBackend) HandleInterceptedTCPConn(dport uint16, srcAddr netip.AddrPort, getConn func() (net.Conn, bool), sendRST func()) {
b.mu.Lock()
sc := b.serveConfig
b.mu.Unlock()
if !sc.Valid() {
b.logf("[unexpected] localbackend: got TCP conn w/o serveConfig; from %v to port %v", srcAddr, dport)
return nil
sendRST()
return
}
tcph, ok := sc.TCP().GetOk(dport)
if !ok {
b.logf("[unexpected] localbackend: got TCP conn without TCP config for port %v; from %v", dport, srcAddr)
return nil
sendRST()
return
}
if tcph.HTTPS() || tcph.HTTP() {
if tcph.HTTPS() {
conn, ok := getConn()
if !ok {
b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport)
return
}
hs := &http.Server{
TLSConfig: &tls.Config{
GetCertificate: b.getTLSServeCertForPort(dport),
},
Handler: http.HandlerFunc(b.serveWebHandler),
BaseContext: func(_ net.Listener) context.Context {
return context.WithValue(context.Background(), serveHTTPContextKey{}, &serveHTTPContext{
@@ -342,92 +339,79 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort)
})
},
}
if tcph.HTTPS() {
hs.TLSConfig = &tls.Config{
GetCertificate: b.getTLSServeCertForPort(dport),
}
return func(c net.Conn) error {
return hs.ServeTLS(netutil.NewOneConnListener(c, nil), "", "")
}
}
return func(c net.Conn) error {
return hs.Serve(netutil.NewOneConnListener(c, nil))
}
hs.ServeTLS(netutil.NewOneConnListener(conn, nil), "", "")
return
}
if backDst := tcph.TCPForward(); backDst != "" {
return func(conn net.Conn) error {
defer conn.Close()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
backConn, err := b.dialer.SystemDial(ctx, "tcp", backDst)
cancel()
if err != nil {
b.logf("localbackend: failed to TCP proxy port %v (from %v) to %s: %v", dport, srcAddr, backDst, err)
return nil
}
defer backConn.Close()
if sni := tcph.TerminateTLS(); sni != "" {
conn = tls.Server(conn, &tls.Config{
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
pair, err := b.GetCertPEM(ctx, sni)
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(pair.CertPEM, pair.KeyPEM)
if err != nil {
return nil, err
}
return &cert, nil
},
})
}
// TODO(bradfitz): do the RegisterIPPortIdentity and
// UnregisterIPPortIdentity stuff that netstack does
errc := make(chan error, 1)
go func() {
_, err := io.Copy(backConn, conn)
errc <- err
}()
go func() {
_, err := io.Copy(conn, backConn)
errc <- err
}()
return <-errc
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
backConn, err := b.dialer.SystemDial(ctx, "tcp", backDst)
cancel()
if err != nil {
b.logf("localbackend: failed to TCP proxy port %v (from %v) to %s: %v", dport, srcAddr, backDst, err)
sendRST()
return
}
conn, ok := getConn()
if !ok {
b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport)
backConn.Close()
return
}
defer conn.Close()
defer backConn.Close()
if sni := tcph.TerminateTLS(); sni != "" {
conn = tls.Server(conn, &tls.Config{
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
pair, err := b.GetCertPEM(ctx, sni)
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(pair.CertPEM, pair.KeyPEM)
if err != nil {
return nil, err
}
return &cert, nil
},
})
}
// TODO(bradfitz): do the RegisterIPPortIdentity and
// UnregisterIPPortIdentity stuff that netstack does
errc := make(chan error, 1)
go func() {
_, err := io.Copy(backConn, conn)
errc <- err
}()
go func() {
_, err := io.Copy(conn, backConn)
errc <- err
}()
<-errc
return
}
b.logf("closing TCP conn to port %v (from %v) with actionless TCPPortHandler", dport, srcAddr)
return nil
}
func getServeHTTPContext(r *http.Request) (c *serveHTTPContext, ok bool) {
c, ok = r.Context().Value(serveHTTPContextKey{}).(*serveHTTPContext)
return c, ok
sendRST()
}
func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView, at string, ok bool) {
var z ipn.HTTPHandlerView // zero value
hostname := r.Host
if r.TLS == nil {
tcd := "." + b.Status().CurrentTailnet.MagicDNSSuffix
if !strings.HasSuffix(hostname, tcd) {
hostname += tcd
}
} else {
hostname = r.TLS.ServerName
return z, "", false
}
sctx, ok := getServeHTTPContext(r)
sctx, ok := r.Context().Value(serveHTTPContextKey{}).(*serveHTTPContext)
if !ok {
b.logf("[unexpected] localbackend: no serveHTTPContext in request")
return z, "", false
}
wsc, ok := b.webServerConfig(hostname, sctx.DestPort)
wsc, ok := b.webServerConfig(r.TLS.ServerName, sctx.DestPort)
if !ok {
return z, "", false
}
@@ -463,8 +447,11 @@ func (b *LocalBackend) proxyHandlerForBackend(backend string) (*httputil.Reverse
Rewrite: func(r *httputil.ProxyRequest) {
r.SetURL(u)
r.Out.Host = r.In.Host
addProxyForwardedHeaders(r)
b.addTailscaleIdentityHeaders(r)
r.Out.Header.Set("X-Forwarded-Host", r.In.Host)
r.Out.Header.Set("X-Forwarded-Proto", "https")
if c, ok := r.Out.Context().Value(serveHTTPContextKey{}).(*serveHTTPContext); ok {
r.Out.Header.Set("X-Forwarded-For", c.SrcAddr.Addr().String())
}
},
Transport: &http.Transport{
DialContext: b.dialer.SystemDial,
@@ -482,40 +469,6 @@ func (b *LocalBackend) proxyHandlerForBackend(backend string) (*httputil.Reverse
return rp, nil
}
func addProxyForwardedHeaders(r *httputil.ProxyRequest) {
r.Out.Header.Set("X-Forwarded-Host", r.In.Host)
if r.In.TLS != nil {
r.Out.Header.Set("X-Forwarded-Proto", "https")
}
if c, ok := getServeHTTPContext(r.Out); ok {
r.Out.Header.Set("X-Forwarded-For", c.SrcAddr.Addr().String())
}
}
func (b *LocalBackend) addTailscaleIdentityHeaders(r *httputil.ProxyRequest) {
// Clear any incoming values squatting in the headers.
r.Out.Header.Del("Tailscale-User-Login")
r.Out.Header.Del("Tailscale-User-Name")
r.Out.Header.Del("Tailscale-Headers-Info")
c, ok := getServeHTTPContext(r.Out)
if !ok {
return
}
node, user, ok := b.WhoIs(c.SrcAddr)
if !ok {
return // traffic from outside of Tailnet (funneled)
}
if node.IsTagged() {
// 2023-06-14: Not setting identity headers for tagged nodes.
// Only currently set for nodes with user identities.
return
}
r.Out.Header.Set("Tailscale-User-Login", user.LoginName)
r.Out.Header.Set("Tailscale-User-Name", user.DisplayName)
r.Out.Header.Set("Tailscale-Headers-Info", "https://tailscale.com/s/serve-headers")
}
func (b *LocalBackend) serveWebHandler(w http.ResponseWriter, r *http.Request) {
h, mountPoint, ok := b.getServeHandler(r)
if !ok {
@@ -648,8 +601,8 @@ func allNumeric(s string) bool {
return s != ""
}
func (b *LocalBackend) webServerConfig(hostname string, port uint16) (c ipn.WebServerConfigView, ok bool) {
key := ipn.HostPort(fmt.Sprintf("%s:%v", hostname, port))
func (b *LocalBackend) webServerConfig(sniName string, port uint16) (c ipn.WebServerConfigView, ok bool) {
key := ipn.HostPort(fmt.Sprintf("%s:%v", sniName, port))
b.mu.Lock()
defer b.mu.Unlock()

View File

@@ -10,22 +10,12 @@ import (
"fmt"
"net/http"
"net/http/httptest"
"net/netip"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"tailscale.com/ipn"
"tailscale.com/ipn/store/mem"
"tailscale.com/tailcfg"
"tailscale.com/tsd"
"tailscale.com/types/logid"
"tailscale.com/types/netmap"
"tailscale.com/util/cmpx"
"tailscale.com/util/must"
"tailscale.com/wgengine"
)
func TestExpandProxyArg(t *testing.T) {
@@ -150,7 +140,10 @@ func TestGetServeHandler(t *testing.T) {
},
TLS: &tls.ConnectionState{ServerName: serverName},
}
port := cmpx.Or(tt.port, 443)
port := tt.port
if port == 0 {
port = 443
}
req = req.WithContext(context.WithValue(req.Context(), serveHTTPContextKey{}, &serveHTTPContext{
DestPort: port,
}))
@@ -169,142 +162,6 @@ func TestGetServeHandler(t *testing.T) {
}
}
func TestServeHTTPProxy(t *testing.T) {
sys := &tsd.System{}
e, err := wgengine.NewUserspaceEngine(t.Logf, wgengine.Config{SetSubsystem: sys.Set})
if err != nil {
t.Fatal(err)
}
sys.Set(e)
sys.Set(new(mem.Store))
b, err := NewLocalBackend(t.Logf, logid.PublicID{}, sys, 0)
if err != nil {
t.Fatal(err)
}
defer b.Shutdown()
dir := t.TempDir()
b.SetVarRoot(dir)
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
pm.currentProfile = &ipn.LoginProfile{ID: "id0"}
b.pm = pm
b.netMap = &netmap.NetworkMap{
SelfNode: &tailcfg.Node{
Name: "example.ts.net",
},
UserProfiles: map[tailcfg.UserID]tailcfg.UserProfile{
tailcfg.UserID(1): {
LoginName: "someone@example.com",
DisplayName: "Some One",
},
},
}
b.nodeByAddr = map[netip.Addr]*tailcfg.Node{
netip.MustParseAddr("100.150.151.152"): {
ComputedName: "some-peer",
User: tailcfg.UserID(1),
},
netip.MustParseAddr("100.150.151.153"): {
ComputedName: "some-tagged-peer",
Tags: []string{"tag:server", "tag:test"},
User: tailcfg.UserID(1),
},
}
// Start test serve endpoint.
testServ := httptest.NewServer(http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
// Piping all the headers through the response writer
// so we can check their values in tests below.
for key, val := range r.Header {
w.Header().Add(key, strings.Join(val, ","))
}
},
))
defer testServ.Close()
conf := &ipn.ServeConfig{
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"example.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: testServ.URL},
}},
},
}
if err := b.SetServeConfig(conf); err != nil {
t.Fatal(err)
}
type headerCheck struct {
header string
want string
}
tests := []struct {
name string
srcIP string
wantHeaders []headerCheck
}{
{
name: "request-from-user-within-tailnet",
srcIP: "100.150.151.152",
wantHeaders: []headerCheck{
{"X-Forwarded-Proto", "https"},
{"X-Forwarded-For", "100.150.151.152"},
{"Tailscale-User-Login", "someone@example.com"},
{"Tailscale-User-Name", "Some One"},
{"Tailscale-Headers-Info", "https://tailscale.com/s/serve-headers"},
},
},
{
name: "request-from-tagged-node-within-tailnet",
srcIP: "100.150.151.153",
wantHeaders: []headerCheck{
{"X-Forwarded-Proto", "https"},
{"X-Forwarded-For", "100.150.151.153"},
{"Tailscale-User-Login", ""},
{"Tailscale-User-Name", ""},
{"Tailscale-Headers-Info", ""},
},
},
{
name: "request-from-outside-tailnet",
srcIP: "100.160.161.162",
wantHeaders: []headerCheck{
{"X-Forwarded-Proto", "https"},
{"X-Forwarded-For", "100.160.161.162"},
{"Tailscale-User-Login", ""},
{"Tailscale-User-Name", ""},
{"Tailscale-Headers-Info", ""},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := &http.Request{
URL: &url.URL{Path: "/"},
TLS: &tls.ConnectionState{ServerName: "example.ts.net"},
}
req = req.WithContext(context.WithValue(req.Context(), serveHTTPContextKey{}, &serveHTTPContext{
DestPort: 443,
SrcAddr: netip.MustParseAddrPort(tt.srcIP + ":1234"), // random src port for tests
}))
w := httptest.NewRecorder()
b.serveWebHandler(w, req)
// Verify the headers.
h := w.Result().Header
for _, c := range tt.wantHeaders {
if got := h.Get(c.header); got != c.want {
t.Errorf("invalid %q header; want=%q, got=%q", c.header, c.want, got)
}
}
})
}
}
func TestServeFileOrDirectory(t *testing.T) {
td := t.TempDir()
writeFile := func(suffix, contents string) {

View File

@@ -104,7 +104,6 @@ var handler = map[string]localAPIHandler{
"tka/force-local-disable": (*Handler).serveTKALocalDisable,
"tka/affected-sigs": (*Handler).serveTKAAffectedSigs,
"tka/wrap-preauth-key": (*Handler).serveTKAWrapPreauthKey,
"tka/verify-deeplink": (*Handler).serveTKAVerifySigningDeeplink,
"upload-client-metrics": (*Handler).serveUploadClientMetrics,
"watch-ipn-bus": (*Handler).serveWatchIPNBus,
"whois": (*Handler).serveWhoIs,
@@ -1611,35 +1610,6 @@ func (h *Handler) serveTKAWrapPreauthKey(w http.ResponseWriter, r *http.Request)
w.Write([]byte(wrappedKey))
}
func (h *Handler) serveTKAVerifySigningDeeplink(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead {
http.Error(w, "signing deeplink verification access denied", http.StatusForbidden)
return
}
if r.Method != httpm.POST {
http.Error(w, "use POST", http.StatusMethodNotAllowed)
return
}
type verifyRequest struct {
URL string
}
var req verifyRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid JSON for verifyRequest body", 400)
return
}
res := h.b.NetworkLockVerifySigningDeeplink(req.URL)
j, err := json.MarshalIndent(res, "", "\t")
if err != nil {
http.Error(w, "JSON encoding error", 500)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(j)
}
func (h *Handler) serveTKADisable(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "network-lock modify access denied", http.StatusForbidden)

View File

@@ -76,12 +76,6 @@ type TCPPortHandler struct {
// It is mutually exclusive with TCPForward.
HTTPS bool `json:",omitempty"`
// HTTP, if true, means that tailscaled should handle this connection as an
// HTTP request as configured by ServeConfig.Web.
//
// It is mutually exclusive with TCPForward.
HTTP bool `json:",omitempty"`
// TCPForward is the IP:port to forward TCP connections to.
// Whether or not TLS is terminated by tailscaled depends on
// TerminateTLS.
@@ -109,7 +103,7 @@ type HTTPHandler struct {
// temporary ones? Error codes? Redirects?
}
// WebHandlerExists reports whether if the ServeConfig Web handler exists for
// WebHandlerExists checks if the ServeConfig Web handler exists for
// the given host:port and mount point.
func (sc *ServeConfig) WebHandlerExists(hp HostPort, mount string) bool {
h := sc.GetWebHandler(hp, mount)
@@ -134,8 +128,9 @@ func (sc *ServeConfig) GetTCPPortHandler(port uint16) *TCPPortHandler {
return sc.TCP[port]
}
// IsTCPForwardingAny reports whether ServeConfig is currently forwarding in
// TCPForward mode on any port. This is exclusive of Web/HTTPS serving.
// IsTCPForwardingAny checks if ServeConfig is currently forwarding
// in TCPForward mode on any port.
// This is exclusive of Web/HTTPS serving.
func (sc *ServeConfig) IsTCPForwardingAny() bool {
if sc == nil || len(sc.TCP) == 0 {
return false
@@ -148,47 +143,34 @@ func (sc *ServeConfig) IsTCPForwardingAny() bool {
return false
}
// IsTCPForwardingOnPort reports whether if ServeConfig is currently forwarding
// in TCPForward mode on the given port. This is exclusive of Web/HTTPS serving.
// IsTCPForwardingOnPort checks if ServeConfig is currently forwarding
// in TCPForward mode on the given port.
// This is exclusive of Web/HTTPS serving.
func (sc *ServeConfig) IsTCPForwardingOnPort(port uint16) bool {
if sc == nil || sc.TCP[port] == nil {
return false
}
return !sc.IsServingWeb(port)
return !sc.TCP[port].HTTPS
}
// IsServingWeb reports whether if ServeConfig is currently serving Web
// (HTTP/HTTPS) on the given port. This is exclusive of TCPForwarding.
// IsServingWeb checks if ServeConfig is currently serving
// Web/HTTPS on the given port.
// This is exclusive of TCPForwarding.
func (sc *ServeConfig) IsServingWeb(port uint16) bool {
return sc.IsServingHTTP(port) || sc.IsServingHTTPS(port)
}
// IsServingHTTPS reports whether if ServeConfig is currently serving HTTPS on
// the given port. This is exclusive of HTTP and TCPForwarding.
func (sc *ServeConfig) IsServingHTTPS(port uint16) bool {
if sc == nil || sc.TCP[port] == nil {
return false
}
return sc.TCP[port].HTTPS
}
// IsServingHTTP reports whether if ServeConfig is currently serving HTTP on the
// given port. This is exclusive of HTTPS and TCPForwarding.
func (sc *ServeConfig) IsServingHTTP(port uint16) bool {
if sc == nil || sc.TCP[port] == nil {
return false
}
return sc.TCP[port].HTTP
}
// IsFunnelOn reports whether if ServeConfig is currently allowing funnel
// traffic for any host:port.
// IsFunnelOn checks if ServeConfig is currently allowing
// funnel traffic for any host:port.
//
// View version of ServeConfig.IsFunnelOn.
func (v ServeConfigView) IsFunnelOn() bool { return v.ж.IsFunnelOn() }
// IsFunnelOn reports whether if ServeConfig is currently allowing funnel
// traffic for any host:port.
// IsFunnelOn checks if ServeConfig is currently allowing
// funnel traffic for any host:port.
func (sc *ServeConfig) IsFunnelOn() bool {
if sc == nil {
return false

View File

@@ -73,10 +73,10 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47ecfdc1:LICENSE))
- [golang.org/x/exp/shiny](https://pkg.go.dev/golang.org/x/exp/shiny) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/334a2380:shiny/LICENSE))
- [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.7.0:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.10.0:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.9.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/5059a07a:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.8.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.8.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.7.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.9.0:LICENSE))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE))
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/7b0a1988a28f/LICENSE))

View File

@@ -58,13 +58,13 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/f1b76eb4bb35/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.10.0:LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.8.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47ecfdc1:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://github.com/tailscale/golang-x-net/blob/dd4570e13977/LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.10.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.9.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.9.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.10.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.8.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.8.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.9.0:LICENSE))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE))
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/7b0a1988a28f/LICENSE))
- [inet.af/peercred](https://pkg.go.dev/inet.af/peercred) ([BSD-3-Clause](https://github.com/inetaf/peercred/blob/0893ea02156a/LICENSE))

View File

@@ -84,7 +84,7 @@ Some packages may only be included on certain architectures or operating systems
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.10.0:LICENSE))
- [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](https://cs.opensource.google/go/x/oauth2/+/v0.7.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/5059a07a:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.8.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.8.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.9.0:LICENSE))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE))

View File

@@ -38,15 +38,15 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/f1b76eb4bb35/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.10.0:LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.8.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47ecfdc1:LICENSE))
- [golang.org/x/image/bmp](https://pkg.go.dev/golang.org/x/image/bmp) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.7.0:LICENSE))
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.10.0:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://github.com/tailscale/golang-x-net/blob/dd4570e13977/LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.10.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.9.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.9.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.10.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.8.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.8.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.9.0:LICENSE))
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))
- [gopkg.in/Knetic/govaluate.v3](https://pkg.go.dev/gopkg.in/Knetic/govaluate.v3) ([MIT](https://github.com/Knetic/govaluate/blob/v3.0.0/LICENSE))

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !windows && !wasm
//go:build !windows && !js
package filch

View File

@@ -1,636 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package recursive implements a simple recursive DNS resolver.
package recursive
import (
"context"
"errors"
"fmt"
"net"
"net/netip"
"strings"
"time"
"github.com/miekg/dns"
"golang.org/x/exp/constraints"
"golang.org/x/exp/slices"
"tailscale.com/envknob"
"tailscale.com/net/netns"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
"tailscale.com/util/mak"
"tailscale.com/util/multierr"
"tailscale.com/util/slicesx"
)
const (
// maxDepth is how deep from the root nameservers we'll recurse when
// resolving; passing this limit will instead return an error.
//
// maxDepth must be at least 20 to resolve "console.aws.amazon.com",
// which is a domain with a moderately complicated DNS setup. The
// current value of 30 was chosen semi-arbitrarily to ensure that we
// have about 50% headroom.
maxDepth = 30
// numStartingServers is the number of root nameservers that we use as
// initial candidates for our recursion.
numStartingServers = 3
// udpQueryTimeout is the amount of time we wait for a UDP response
// from a nameserver before falling back to a TCP connection.
udpQueryTimeout = 5 * time.Second
// These constants aren't typed in the DNS package, so we create typed
// versions here to avoid having to do repeated type casts.
qtypeA dns.Type = dns.Type(dns.TypeA)
qtypeAAAA dns.Type = dns.Type(dns.TypeAAAA)
)
var (
// ErrMaxDepth is returned when recursive resolving exceeds the maximum
// depth limit for this package.
ErrMaxDepth = fmt.Errorf("exceeded max depth %d when resolving", maxDepth)
// ErrAuthoritativeNoResponses is the error returned when an
// authoritative nameserver indicates that there are no responses to
// the given query.
ErrAuthoritativeNoResponses = errors.New("authoritative server returned no responses")
// ErrNoResponses is returned when our resolution process completes
// with no valid responses from any nameserver, but no authoritative
// server explicitly returned NXDOMAIN.
ErrNoResponses = errors.New("no responses to query")
)
var rootServersV4 = []netip.Addr{
netip.MustParseAddr("198.41.0.4"), // a.root-servers.net
netip.MustParseAddr("199.9.14.201"), // b.root-servers.net
netip.MustParseAddr("192.33.4.12"), // c.root-servers.net
netip.MustParseAddr("199.7.91.13"), // d.root-servers.net
netip.MustParseAddr("192.203.230.10"), // e.root-servers.net
netip.MustParseAddr("192.5.5.241"), // f.root-servers.net
netip.MustParseAddr("192.112.36.4"), // g.root-servers.net
netip.MustParseAddr("198.97.190.53"), // h.root-servers.net
netip.MustParseAddr("192.36.148.17"), // i.root-servers.net
netip.MustParseAddr("192.58.128.30"), // j.root-servers.net
netip.MustParseAddr("193.0.14.129"), // k.root-servers.net
netip.MustParseAddr("199.7.83.42"), // l.root-servers.net
netip.MustParseAddr("202.12.27.33"), // m.root-servers.net
}
var rootServersV6 = []netip.Addr{
netip.MustParseAddr("2001:503:ba3e::2:30"), // a.root-servers.net
netip.MustParseAddr("2001:500:200::b"), // b.root-servers.net
netip.MustParseAddr("2001:500:2::c"), // c.root-servers.net
netip.MustParseAddr("2001:500:2d::d"), // d.root-servers.net
netip.MustParseAddr("2001:500:a8::e"), // e.root-servers.net
netip.MustParseAddr("2001:500:2f::f"), // f.root-servers.net
netip.MustParseAddr("2001:500:12::d0d"), // g.root-servers.net
netip.MustParseAddr("2001:500:1::53"), // h.root-servers.net
netip.MustParseAddr("2001:7fe::53"), // i.root-servers.net
netip.MustParseAddr("2001:503:c27::2:30"), // j.root-servers.net
netip.MustParseAddr("2001:7fd::1"), // k.root-servers.net
netip.MustParseAddr("2001:500:9f::42"), // l.root-servers.net
netip.MustParseAddr("2001:dc3::35"), // m.root-servers.net
}
var debug = envknob.RegisterBool("TS_DEBUG_RECURSIVE_DNS")
// Resolver is a recursive DNS resolver that is designed for looking up A and AAAA records.
type Resolver struct {
// Dialer is used to create outbound connections. If nil, a zero
// net.Dialer will be used instead.
Dialer netns.Dialer
// Logf is the logging function to use; if none is specified, then logs
// will be dropped.
Logf logger.Logf
// NoIPv6, if set, will prevent this package from querying for AAAA
// records and will avoid contacting nameservers over IPv6.
NoIPv6 bool
// Test mocks
testQueryHook func(name dnsname.FQDN, nameserver netip.Addr, protocol string, qtype dns.Type) (*dns.Msg, error)
testExchangeHook func(nameserver netip.Addr, network string, msg *dns.Msg) (*dns.Msg, error)
rootServers []netip.Addr
timeNow func() time.Time
// Caching
// NOTE(andrew): if we make resolution parallel, this needs a mutex
queryCache map[dnsQuery]dnsMsgWithExpiry
// Possible future additions:
// - Additional nameservers? From the system maybe?
// - NoIPv4 for IPv4
// - DNS-over-HTTPS or DNS-over-TLS support
}
// queryState stores all state during the course of a single query
type queryState struct {
// rootServers are the root nameservers to start from
rootServers []netip.Addr
// TODO: metrics?
}
type dnsQuery struct {
nameserver netip.Addr
name dnsname.FQDN
qtype dns.Type
}
func (q dnsQuery) String() string {
return fmt.Sprintf("dnsQuery{nameserver:%q,name:%q,qtype:%v}", q.nameserver.String(), q.name, q.qtype)
}
type dnsMsgWithExpiry struct {
*dns.Msg
expiresAt time.Time
}
func (r *Resolver) now() time.Time {
if r.timeNow != nil {
return r.timeNow()
}
return time.Now()
}
func (r *Resolver) logf(format string, args ...any) {
if r.Logf == nil {
return
}
r.Logf(format, args...)
}
func (r *Resolver) dlogf(format string, args ...any) {
if r.Logf == nil || !debug() {
return
}
r.Logf(format, args...)
}
func (r *Resolver) depthlogf(depth int, format string, args ...any) {
if r.Logf == nil || !debug() {
return
}
prefix := fmt.Sprintf("[%d] %s", depth, strings.Repeat(" ", depth))
r.Logf(prefix+format, args...)
}
var defaultDialer net.Dialer
func (r *Resolver) dialer() netns.Dialer {
if r.Dialer != nil {
return r.Dialer
}
return &defaultDialer
}
func (r *Resolver) newState() *queryState {
var rootServers []netip.Addr
if len(r.rootServers) > 0 {
rootServers = r.rootServers
} else {
// Select a random subset of root nameservers to start from, since if
// we don't get responses from those, something else has probably gone
// horribly wrong.
roots4 := slices.Clone(rootServersV4)
slicesx.Shuffle(roots4)
roots4 = roots4[:numStartingServers]
var roots6 []netip.Addr
if !r.NoIPv6 {
roots6 = slices.Clone(rootServersV6)
slicesx.Shuffle(roots6)
roots6 = roots6[:numStartingServers]
}
// Interleave the root servers so that we try to contact them over
// IPv4, then IPv6, IPv4, IPv6, etc.
rootServers = slicesx.Interleave(roots4, roots6)
}
return &queryState{
rootServers: rootServers,
}
}
// Resolve will perform a recursive DNS resolution for the provided name,
// starting at a randomly-chosen root DNS server, and return the A and AAAA
// responses as a slice of netip.Addrs along with the minimum TTL for the
// returned records.
func (r *Resolver) Resolve(ctx context.Context, name string) (addrs []netip.Addr, minTTL time.Duration, err error) {
dnsName, err := dnsname.ToFQDN(name)
if err != nil {
return nil, 0, err
}
qstate := r.newState()
r.logf("querying IPv4 addresses for: %q", name)
addrs4, minTTL4, err4 := r.resolveRecursiveFromRoot(ctx, qstate, 0, dnsName, qtypeA)
var (
addrs6 []netip.Addr
minTTL6 time.Duration
err6 error
)
if !r.NoIPv6 {
r.logf("querying IPv6 addresses for: %q", name)
addrs6, minTTL6, err6 = r.resolveRecursiveFromRoot(ctx, qstate, 0, dnsName, qtypeAAAA)
}
if err4 != nil && err6 != nil {
if err4 == err6 {
return nil, 0, err4
}
return nil, 0, multierr.New(err4, err6)
}
if err4 != nil {
return addrs6, minTTL6, nil
} else if err6 != nil {
return addrs4, minTTL4, nil
}
minTTL = minTTL4
if minTTL6 < minTTL {
minTTL = minTTL6
}
addrs = append(addrs4, addrs6...)
if len(addrs) == 0 {
return nil, 0, ErrNoResponses
}
slicesx.Shuffle(addrs)
return addrs, minTTL, nil
}
func (r *Resolver) resolveRecursiveFromRoot(
ctx context.Context,
qstate *queryState,
depth int,
name dnsname.FQDN, // what we're querying
qtype dns.Type,
) ([]netip.Addr, time.Duration, error) {
r.depthlogf(depth, "resolving %q from root (type: %v)", name, qtype)
var depthError bool
for _, server := range qstate.rootServers {
addrs, minTTL, err := r.resolveRecursive(ctx, qstate, depth, name, server, qtype)
if err == nil {
return addrs, minTTL, err
} else if errors.Is(err, ErrAuthoritativeNoResponses) {
return nil, 0, ErrAuthoritativeNoResponses
} else if errors.Is(err, ErrMaxDepth) {
depthError = true
}
}
if depthError {
return nil, 0, ErrMaxDepth
}
return nil, 0, ErrNoResponses
}
func (r *Resolver) resolveRecursive(
ctx context.Context,
qstate *queryState,
depth int,
name dnsname.FQDN, // what we're querying
nameserver netip.Addr,
qtype dns.Type,
) ([]netip.Addr, time.Duration, error) {
if depth == maxDepth {
r.depthlogf(depth, "not recursing past maximum depth")
return nil, 0, ErrMaxDepth
}
// Ask this nameserver for an answer.
resp, err := r.queryNameserver(ctx, depth, name, nameserver, qtype)
if err != nil {
return nil, 0, err
}
// If we get an actual answer from the nameserver, then return it.
var (
answers []netip.Addr
cnames []dnsname.FQDN
minTTL = 24 * 60 * 60 // 24 hours in seconds
)
for _, answer := range resp.Answer {
if crec, ok := answer.(*dns.CNAME); ok {
cnameFQDN, err := dnsname.ToFQDN(crec.Target)
if err != nil {
r.logf("bad CNAME %q returned: %v", crec.Target, err)
continue
}
cnames = append(cnames, cnameFQDN)
continue
}
addr := addrFromRecord(answer)
if !addr.IsValid() {
r.logf("[unexpected] invalid record in %T answer", answer)
} else if addr.Is4() && qtype != qtypeA {
r.logf("[unexpected] got IPv4 answer but qtype=%v", qtype)
} else if addr.Is6() && qtype != qtypeAAAA {
r.logf("[unexpected] got IPv6 answer but qtype=%v", qtype)
} else {
answers = append(answers, addr)
minTTL = min(minTTL, int(answer.Header().Ttl))
}
}
if len(answers) > 0 {
r.depthlogf(depth, "got answers for %q: %v", name, answers)
return answers, time.Duration(minTTL) * time.Second, nil
}
r.depthlogf(depth, "no answers for %q", name)
// If we have a non-zero number of CNAMEs, then try resolving those
// (from the root again) and return the first one that succeeds.
//
// TODO: return the union of all responses?
// TODO: parallelism?
if len(cnames) > 0 {
r.depthlogf(depth, "got CNAME responses for %q: %v", name, cnames)
}
var cnameDepthError bool
for _, cname := range cnames {
answers, minTTL, err := r.resolveRecursiveFromRoot(ctx, qstate, depth+1, cname, qtype)
if err == nil {
return answers, minTTL, nil
} else if errors.Is(err, ErrAuthoritativeNoResponses) {
return nil, 0, ErrAuthoritativeNoResponses
} else if errors.Is(err, ErrMaxDepth) {
cnameDepthError = true
}
}
// If this is an authoritative response, then we know that continuing
// to look further is not going to result in any answers and we should
// bail out.
if resp.MsgHdr.Authoritative {
// If we failed to recurse into a CNAME due to a depth limit,
// propagate that here.
if cnameDepthError {
return nil, 0, ErrMaxDepth
}
r.depthlogf(depth, "got authoritative response with no answers; stopping")
return nil, 0, ErrAuthoritativeNoResponses
}
r.depthlogf(depth, "got %d NS responses and %d ADDITIONAL responses for %q", len(resp.Ns), len(resp.Extra), name)
// No CNAMEs and no answers; see if we got any AUTHORITY responses,
// which indicate which nameservers to query next.
var authorities []dnsname.FQDN
for _, rr := range resp.Ns {
ns, ok := rr.(*dns.NS)
if !ok {
continue
}
nsName, err := dnsname.ToFQDN(ns.Ns)
if err != nil {
r.logf("unexpected bad NS name %q: %v", ns.Ns, err)
continue
}
authorities = append(authorities, nsName)
}
// Also check for "glue" records, which are IP addresses provided by
// the DNS server for authority responses; these are required when the
// authority server is a subdomain of what's being resolved.
glueRecords := make(map[dnsname.FQDN][]netip.Addr)
for _, rr := range resp.Extra {
name, err := dnsname.ToFQDN(rr.Header().Name)
if err != nil {
r.logf("unexpected bad Name %q in Extra addr: %v", rr.Header().Name, err)
continue
}
if addr := addrFromRecord(rr); addr.IsValid() {
glueRecords[name] = append(glueRecords[name], addr)
} else {
r.logf("unexpected bad Extra %T addr", rr)
}
}
// Try authorities with glue records first, to minimize the number of
// additional DNS queries that we need to make.
authoritiesGlue, authoritiesNoGlue := slicesx.Partition(authorities, func(aa dnsname.FQDN) bool {
return len(glueRecords[aa]) > 0
})
authorityDepthError := false
r.depthlogf(depth, "authorities with glue records for recursion: %v", authoritiesGlue)
for _, authority := range authoritiesGlue {
for _, nameserver := range glueRecords[authority] {
answers, minTTL, err := r.resolveRecursive(ctx, qstate, depth+1, name, nameserver, qtype)
if err == nil {
return answers, minTTL, nil
} else if errors.Is(err, ErrAuthoritativeNoResponses) {
return nil, 0, ErrAuthoritativeNoResponses
} else if errors.Is(err, ErrMaxDepth) {
authorityDepthError = true
}
}
}
r.depthlogf(depth, "authorities with no glue records for recursion: %v", authoritiesNoGlue)
for _, authority := range authoritiesNoGlue {
// First, resolve the IP for the authority server from the
// root, querying for both IPv4 and IPv6 addresses regardless
// of what the current question type is.
//
// TODO: check for infinite recursion; it'll get caught by our
// recursion depth, but we want to bail early.
for _, authorityQtype := range []dns.Type{qtypeAAAA, qtypeA} {
answers, _, err := r.resolveRecursiveFromRoot(ctx, qstate, depth+1, authority, authorityQtype)
if err != nil {
r.depthlogf(depth, "error querying authority %q: %v", authority, err)
continue
}
r.depthlogf(depth, "resolved authority %q (type %v) to: %v", authority, authorityQtype, answers)
// Now, query this authority for the final address.
for _, nameserver := range answers {
answers, minTTL, err := r.resolveRecursive(ctx, qstate, depth+1, name, nameserver, qtype)
if err == nil {
return answers, minTTL, nil
} else if errors.Is(err, ErrAuthoritativeNoResponses) {
return nil, 0, ErrAuthoritativeNoResponses
} else if errors.Is(err, ErrMaxDepth) {
authorityDepthError = true
}
}
}
}
if authorityDepthError {
return nil, 0, ErrMaxDepth
}
return nil, 0, ErrNoResponses
}
func min[T constraints.Ordered](a, b T) T {
if a < b {
return a
}
return b
}
// queryNameserver sends a query for "name" to the nameserver "nameserver" for
// records of type "qtype", trying both UDP and TCP connections as
// appropriate.
func (r *Resolver) queryNameserver(
ctx context.Context,
depth int,
name dnsname.FQDN, // what we're querying
nameserver netip.Addr, // destination of query
qtype dns.Type,
) (*dns.Msg, error) {
// TODO(andrew): we should QNAME minimisation here to avoid sending the
// full name to intermediate/root nameservers. See:
// https://www.rfc-editor.org/rfc/rfc7816
// Handle the case where UDP is blocked by adding an explicit timeout
// for the UDP portion of this query.
udpCtx, udpCtxCancel := context.WithTimeout(ctx, udpQueryTimeout)
defer udpCtxCancel()
msg, err := r.queryNameserverProto(udpCtx, depth, name, nameserver, "udp", qtype)
if err == nil {
return msg, nil
}
msg, err2 := r.queryNameserverProto(ctx, depth, name, nameserver, "tcp", qtype)
if err2 == nil {
return msg, nil
}
return nil, multierr.New(err, err2)
}
// queryNameserverProto sends a query for "name" to the nameserver "nameserver"
// for records of type "qtype" over the provided protocol (either "udp"
// or "tcp"), and returns the DNS response or an error.
func (r *Resolver) queryNameserverProto(
ctx context.Context,
depth int,
name dnsname.FQDN, // what we're querying
nameserver netip.Addr, // destination of query
protocol string,
qtype dns.Type,
) (resp *dns.Msg, err error) {
if r.testQueryHook != nil {
return r.testQueryHook(name, nameserver, protocol, qtype)
}
now := r.now()
nameserverStr := nameserver.String()
cacheKey := dnsQuery{
nameserver: nameserver,
name: name,
qtype: qtype,
}
cacheEntry, ok := r.queryCache[cacheKey]
if ok && cacheEntry.expiresAt.Before(now) {
r.depthlogf(depth, "using cached response from %s about %q (type: %v)", nameserverStr, name, qtype)
return cacheEntry.Msg, nil
}
var network string
if nameserver.Is4() {
network = protocol + "4"
} else {
network = protocol + "6"
}
// Prepare a message asking for an appropriately-typed record
// for the name we're querying.
m := new(dns.Msg)
m.SetQuestion(name.WithTrailingDot(), uint16(qtype))
// Allow mocking out the network components with our exchange hook.
if r.testExchangeHook != nil {
resp, err = r.testExchangeHook(nameserver, network, m)
} else {
// Dial the current nameserver using our dialer.
var nconn net.Conn
nconn, err = r.dialer().DialContext(ctx, network, net.JoinHostPort(nameserverStr, "53"))
if err != nil {
return nil, err
}
var c dns.Client // TODO: share?
conn := &dns.Conn{
Conn: nconn,
UDPSize: c.UDPSize,
}
// Send the DNS request to the current nameserver.
r.depthlogf(depth, "asking %s over %s about %q (type: %v)", nameserverStr, protocol, name, qtype)
resp, _, err = c.ExchangeWithConnContext(ctx, m, conn)
}
if err != nil {
return nil, err
}
// If the message was truncated and we're using UDP, re-run with TCP.
if resp.MsgHdr.Truncated && protocol == "udp" {
r.depthlogf(depth, "response message truncated; re-running query with TCP")
resp, err = r.queryNameserverProto(ctx, depth, name, nameserver, "tcp", qtype)
if err != nil {
return nil, err
}
}
// Find minimum expiry for all records in this message.
var minTTL int
for _, rr := range resp.Answer {
minTTL = min(minTTL, int(rr.Header().Ttl))
}
for _, rr := range resp.Ns {
minTTL = min(minTTL, int(rr.Header().Ttl))
}
for _, rr := range resp.Extra {
minTTL = min(minTTL, int(rr.Header().Ttl))
}
mak.Set(&r.queryCache, cacheKey, dnsMsgWithExpiry{
Msg: resp,
expiresAt: now.Add(time.Duration(minTTL) * time.Second),
})
return resp, nil
}
func addrFromRecord(rr dns.RR) netip.Addr {
switch v := rr.(type) {
case *dns.A:
ip, ok := netip.AddrFromSlice(v.A)
if !ok || !ip.Is4() {
return netip.Addr{}
}
return ip
case *dns.AAAA:
ip, ok := netip.AddrFromSlice(v.AAAA)
if !ok || !ip.Is6() {
return netip.Addr{}
}
return ip
}
return netip.Addr{}
}

View File

@@ -1,741 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package recursive
import (
"context"
"errors"
"flag"
"fmt"
"net"
"net/netip"
"reflect"
"strings"
"testing"
"time"
"github.com/miekg/dns"
"golang.org/x/exp/slices"
"tailscale.com/envknob"
"tailscale.com/tstest"
)
const testDomain = "tailscale.com"
// Recursively resolving the AWS console requires being able to handle CNAMEs,
// glue records, falling back from UDP to TCP for oversize queries, and more;
// it's a great integration test for DNS resolution and they can handle the
// traffic :)
const complicatedTestDomain = "console.aws.amazon.com"
var flagNetworkAccess = flag.Bool("enable-network-access", false, "run tests that need external network access")
func init() {
envknob.Setenv("TS_DEBUG_RECURSIVE_DNS", "true")
}
func newResolver(tb testing.TB) *Resolver {
clock := &tstest.Clock{
Step: 50 * time.Millisecond,
}
return &Resolver{
Logf: tb.Logf,
timeNow: clock.Now,
}
}
func TestResolve(t *testing.T) {
if !*flagNetworkAccess {
t.SkipNow()
}
ctx := context.Background()
r := newResolver(t)
addrs, minTTL, err := r.Resolve(ctx, testDomain)
if err != nil {
t.Fatal(err)
}
t.Logf("addrs: %+v", addrs)
t.Logf("minTTL: %v", minTTL)
if len(addrs) < 1 {
t.Fatalf("expected at least one address")
}
if minTTL <= 10*time.Second || minTTL >= 24*time.Hour {
t.Errorf("invalid minimum TTL: %v", minTTL)
}
var has4, has6 bool
for _, addr := range addrs {
has4 = has4 || addr.Is4()
has6 = has6 || addr.Is6()
}
if !has4 {
t.Errorf("expected at least one IPv4 address")
}
if !has6 {
t.Errorf("expected at least one IPv6 address")
}
}
func TestResolveComplicated(t *testing.T) {
if !*flagNetworkAccess {
t.SkipNow()
}
ctx := context.Background()
r := newResolver(t)
addrs, minTTL, err := r.Resolve(ctx, complicatedTestDomain)
if err != nil {
t.Fatal(err)
}
t.Logf("addrs: %+v", addrs)
t.Logf("minTTL: %v", minTTL)
if len(addrs) < 1 {
t.Fatalf("expected at least one address")
}
if minTTL <= 10*time.Second || minTTL >= 24*time.Hour {
t.Errorf("invalid minimum TTL: %v", minTTL)
}
}
func TestResolveNoIPv6(t *testing.T) {
if !*flagNetworkAccess {
t.SkipNow()
}
r := newResolver(t)
r.NoIPv6 = true
addrs, _, err := r.Resolve(context.Background(), testDomain)
if err != nil {
t.Fatal(err)
}
t.Logf("addrs: %+v", addrs)
if len(addrs) < 1 {
t.Fatalf("expected at least one address")
}
for _, addr := range addrs {
if addr.Is6() {
t.Errorf("got unexpected IPv6 address: %v", addr)
}
}
}
func TestResolveFallbackToTCP(t *testing.T) {
var udpCalls, tcpCalls int
hook := func(nameserver netip.Addr, network string, req *dns.Msg) (*dns.Msg, error) {
if strings.HasPrefix(network, "udp") {
t.Logf("got %q query; returning truncated result", network)
udpCalls++
resp := &dns.Msg{}
resp.SetReply(req)
resp.Truncated = true
return resp, nil
}
t.Logf("got %q query; returning real result", network)
tcpCalls++
resp := &dns.Msg{}
resp.SetReply(req)
resp.Answer = append(resp.Answer, &dns.A{
Hdr: dns.RR_Header{
Name: req.Question[0].Name,
Rrtype: req.Question[0].Qtype,
Class: dns.ClassINET,
Ttl: 300,
},
A: net.IPv4(1, 2, 3, 4),
})
return resp, nil
}
r := newResolver(t)
r.testExchangeHook = hook
ctx := context.Background()
resp, err := r.queryNameserverProto(ctx, 0, "tailscale.com", netip.MustParseAddr("9.9.9.9"), "udp", dns.Type(dns.TypeA))
if err != nil {
t.Fatal(err)
}
if len(resp.Answer) < 1 {
t.Fatalf("no answers in response: %v", resp)
}
rrA, ok := resp.Answer[0].(*dns.A)
if !ok {
t.Fatalf("invalid RR type: %T", resp.Answer[0])
}
if !rrA.A.Equal(net.IPv4(1, 2, 3, 4)) {
t.Errorf("wanted A response 1.2.3.4, got: %v", rrA.A)
}
if tcpCalls != 1 {
t.Errorf("got %d, want 1 TCP calls", tcpCalls)
}
if udpCalls != 1 {
t.Errorf("got %d, want 1 UDP calls", udpCalls)
}
// Verify that we're cached and re-run to fetch from the cache.
if len(r.queryCache) < 1 {
t.Errorf("wanted entries in the query cache")
}
resp2, err := r.queryNameserverProto(ctx, 0, "tailscale.com", netip.MustParseAddr("9.9.9.9"), "udp", dns.Type(dns.TypeA))
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(resp, resp2) {
t.Errorf("expected equal responses; old=%+v new=%+v", resp, resp2)
}
// We didn't make any more network requests since we loaded from the cache.
if tcpCalls != 1 {
t.Errorf("got %d, want 1 TCP calls", tcpCalls)
}
if udpCalls != 1 {
t.Errorf("got %d, want 1 UDP calls", udpCalls)
}
}
func dnsIPRR(name string, addr netip.Addr) dns.RR {
if addr.Is4() {
return &dns.A{
Hdr: dns.RR_Header{
Name: name,
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: 300,
},
A: net.IP(addr.AsSlice()),
}
}
return &dns.AAAA{
Hdr: dns.RR_Header{
Name: name,
Rrtype: dns.TypeAAAA,
Class: dns.ClassINET,
Ttl: 300,
},
AAAA: net.IP(addr.AsSlice()),
}
}
func cnameRR(name, target string) dns.RR {
return &dns.CNAME{
Hdr: dns.RR_Header{
Name: name,
Rrtype: dns.TypeCNAME,
Class: dns.ClassINET,
Ttl: 300,
},
Target: target,
}
}
func nsRR(name, target string) dns.RR {
return &dns.NS{
Hdr: dns.RR_Header{
Name: name,
Rrtype: dns.TypeNS,
Class: dns.ClassINET,
Ttl: 300,
},
Ns: target,
}
}
type mockReply struct {
name string
qtype dns.Type
resp *dns.Msg
}
type replyMock struct {
tb testing.TB
replies map[netip.Addr][]mockReply
}
func (r *replyMock) exchangeHook(nameserver netip.Addr, network string, req *dns.Msg) (*dns.Msg, error) {
if len(req.Question) != 1 {
r.tb.Fatalf("unsupported multiple or empty question: %v", req.Question)
}
question := req.Question[0]
replies := r.replies[nameserver]
if len(replies) == 0 {
r.tb.Fatalf("no configured replies for nameserver: %v", nameserver)
}
for _, reply := range replies {
if reply.name == question.Name && reply.qtype == dns.Type(question.Qtype) {
return reply.resp.Copy(), nil
}
}
r.tb.Fatalf("no replies found for query %q of type %v to %v", question.Name, question.Qtype, nameserver)
panic("unreachable")
}
// responses for mocking, shared between the following tests
var (
rootServerAddr = netip.MustParseAddr("198.41.0.4") // a.root-servers.net.
comNSAddr = netip.MustParseAddr("192.5.6.30") // a.gtld-servers.net.
// DNS response from the root nameservers for a .com nameserver
comRecord = &dns.Msg{
Ns: []dns.RR{nsRR("com.", "a.gtld-servers.net.")},
Extra: []dns.RR{dnsIPRR("a.gtld-servers.net.", comNSAddr)},
}
// Random Amazon nameservers that we use in glue records
amazonNS = netip.MustParseAddr("205.251.192.197")
amazonNSv6 = netip.MustParseAddr("2600:9000:5306:1600::1")
// Nameservers for the tailscale.com domain
tailscaleNameservers = &dns.Msg{
Ns: []dns.RR{
nsRR("tailscale.com.", "ns-197.awsdns-24.com."),
nsRR("tailscale.com.", "ns-557.awsdns-05.net."),
nsRR("tailscale.com.", "ns-1558.awsdns-02.co.uk."),
nsRR("tailscale.com.", "ns-1359.awsdns-41.org."),
},
Extra: []dns.RR{
dnsIPRR("ns-197.awsdns-24.com.", amazonNS),
},
}
)
func TestBasicRecursion(t *testing.T) {
mock := &replyMock{
tb: t,
replies: map[netip.Addr][]mockReply{
// Query to the root server returns the .com server + a glue record
rootServerAddr: {
{name: "tailscale.com.", qtype: dns.Type(dns.TypeA), resp: comRecord},
{name: "tailscale.com.", qtype: dns.Type(dns.TypeAAAA), resp: comRecord},
},
// Query to the ".com" server return the nameservers for tailscale.com
comNSAddr: {
{name: "tailscale.com.", qtype: dns.Type(dns.TypeA), resp: tailscaleNameservers},
{name: "tailscale.com.", qtype: dns.Type(dns.TypeAAAA), resp: tailscaleNameservers},
},
// Query to the actual nameserver works.
amazonNS: {
{name: "tailscale.com.", qtype: dns.Type(dns.TypeA), resp: &dns.Msg{
MsgHdr: dns.MsgHdr{Authoritative: true},
Answer: []dns.RR{
dnsIPRR("tailscale.com.", netip.MustParseAddr("13.248.141.131")),
dnsIPRR("tailscale.com.", netip.MustParseAddr("76.223.15.28")),
},
}},
{name: "tailscale.com.", qtype: dns.Type(dns.TypeAAAA), resp: &dns.Msg{
MsgHdr: dns.MsgHdr{Authoritative: true},
Answer: []dns.RR{
dnsIPRR("tailscale.com.", netip.MustParseAddr("2600:9000:a602:b1e6:86d:8165:5e8c:295b")),
dnsIPRR("tailscale.com.", netip.MustParseAddr("2600:9000:a51d:27c1:1530:b9ef:2a6:b9e5")),
},
}},
},
},
}
r := newResolver(t)
r.testExchangeHook = mock.exchangeHook
r.rootServers = []netip.Addr{rootServerAddr}
// Query for tailscale.com, verify we get the right responses
ctx := context.Background()
addrs, minTTL, err := r.Resolve(ctx, "tailscale.com")
if err != nil {
t.Fatal(err)
}
wantAddrs := []netip.Addr{
netip.MustParseAddr("13.248.141.131"),
netip.MustParseAddr("76.223.15.28"),
netip.MustParseAddr("2600:9000:a602:b1e6:86d:8165:5e8c:295b"),
netip.MustParseAddr("2600:9000:a51d:27c1:1530:b9ef:2a6:b9e5"),
}
slices.SortFunc(addrs, func(x, y netip.Addr) bool { return x.String() < y.String() })
slices.SortFunc(wantAddrs, func(x, y netip.Addr) bool { return x.String() < y.String() })
if !reflect.DeepEqual(addrs, wantAddrs) {
t.Errorf("got addrs=%+v; want %+v", addrs, wantAddrs)
}
const wantMinTTL = 5 * time.Minute
if minTTL != wantMinTTL {
t.Errorf("got minTTL=%+v; want %+v", minTTL, wantMinTTL)
}
}
func TestNoAnswers(t *testing.T) {
mock := &replyMock{
tb: t,
replies: map[netip.Addr][]mockReply{
// Query to the root server returns the .com server + a glue record
rootServerAddr: {
{name: "tailscale.com.", qtype: dns.Type(dns.TypeA), resp: comRecord},
{name: "tailscale.com.", qtype: dns.Type(dns.TypeAAAA), resp: comRecord},
},
// Query to the ".com" server return the nameservers for tailscale.com
comNSAddr: {
{name: "tailscale.com.", qtype: dns.Type(dns.TypeA), resp: tailscaleNameservers},
{name: "tailscale.com.", qtype: dns.Type(dns.TypeAAAA), resp: tailscaleNameservers},
},
// Query to the actual nameserver returns no responses, authoritatively.
amazonNS: {
{name: "tailscale.com.", qtype: dns.Type(dns.TypeA), resp: &dns.Msg{
MsgHdr: dns.MsgHdr{Authoritative: true},
Answer: []dns.RR{},
}},
{name: "tailscale.com.", qtype: dns.Type(dns.TypeAAAA), resp: &dns.Msg{
MsgHdr: dns.MsgHdr{Authoritative: true},
Answer: []dns.RR{},
}},
},
},
}
r := &Resolver{
Logf: t.Logf,
testExchangeHook: mock.exchangeHook,
rootServers: []netip.Addr{rootServerAddr},
}
// Query for tailscale.com, verify we get the right responses
_, _, err := r.Resolve(context.Background(), "tailscale.com")
if err == nil {
t.Fatalf("got no error, want error")
}
if !errors.Is(err, ErrAuthoritativeNoResponses) {
t.Fatalf("got err=%v, want %v", err, ErrAuthoritativeNoResponses)
}
}
func TestRecursionCNAME(t *testing.T) {
mock := &replyMock{
tb: t,
replies: map[netip.Addr][]mockReply{
// Query to the root server returns the .com server + a glue record
rootServerAddr: {
{name: "subdomain.otherdomain.com.", qtype: dns.Type(dns.TypeA), resp: comRecord},
{name: "subdomain.otherdomain.com.", qtype: dns.Type(dns.TypeAAAA), resp: comRecord},
{name: "subdomain.tailscale.com.", qtype: dns.Type(dns.TypeA), resp: comRecord},
{name: "subdomain.tailscale.com.", qtype: dns.Type(dns.TypeAAAA), resp: comRecord},
},
// Query to the ".com" server return the nameservers for tailscale.com
comNSAddr: {
{name: "subdomain.otherdomain.com.", qtype: dns.Type(dns.TypeA), resp: tailscaleNameservers},
{name: "subdomain.otherdomain.com.", qtype: dns.Type(dns.TypeAAAA), resp: tailscaleNameservers},
{name: "subdomain.tailscale.com.", qtype: dns.Type(dns.TypeA), resp: tailscaleNameservers},
{name: "subdomain.tailscale.com.", qtype: dns.Type(dns.TypeAAAA), resp: tailscaleNameservers},
},
// Query to the actual nameserver works.
amazonNS: {
{name: "subdomain.otherdomain.com.", qtype: dns.Type(dns.TypeA), resp: &dns.Msg{
MsgHdr: dns.MsgHdr{Authoritative: true},
Answer: []dns.RR{cnameRR("subdomain.otherdomain.com.", "subdomain.tailscale.com.")},
}},
{name: "subdomain.otherdomain.com.", qtype: dns.Type(dns.TypeAAAA), resp: &dns.Msg{
MsgHdr: dns.MsgHdr{Authoritative: true},
Answer: []dns.RR{cnameRR("subdomain.otherdomain.com.", "subdomain.tailscale.com.")},
}},
{name: "subdomain.tailscale.com.", qtype: dns.Type(dns.TypeA), resp: &dns.Msg{
MsgHdr: dns.MsgHdr{Authoritative: true},
Answer: []dns.RR{dnsIPRR("tailscale.com.", netip.MustParseAddr("13.248.141.131"))},
}},
{name: "subdomain.tailscale.com.", qtype: dns.Type(dns.TypeAAAA), resp: &dns.Msg{
MsgHdr: dns.MsgHdr{Authoritative: true},
Answer: []dns.RR{dnsIPRR("tailscale.com.", netip.MustParseAddr("2600:9000:a602:b1e6:86d:8165:5e8c:295b"))},
}},
},
},
}
r := &Resolver{
Logf: t.Logf,
testExchangeHook: mock.exchangeHook,
rootServers: []netip.Addr{rootServerAddr},
}
// Query for tailscale.com, verify we get the right responses
addrs, minTTL, err := r.Resolve(context.Background(), "subdomain.otherdomain.com")
if err != nil {
t.Fatal(err)
}
wantAddrs := []netip.Addr{
netip.MustParseAddr("13.248.141.131"),
netip.MustParseAddr("2600:9000:a602:b1e6:86d:8165:5e8c:295b"),
}
slices.SortFunc(addrs, func(x, y netip.Addr) bool { return x.String() < y.String() })
slices.SortFunc(wantAddrs, func(x, y netip.Addr) bool { return x.String() < y.String() })
if !reflect.DeepEqual(addrs, wantAddrs) {
t.Errorf("got addrs=%+v; want %+v", addrs, wantAddrs)
}
const wantMinTTL = 5 * time.Minute
if minTTL != wantMinTTL {
t.Errorf("got minTTL=%+v; want %+v", minTTL, wantMinTTL)
}
}
func TestRecursionNoGlue(t *testing.T) {
coukNS := netip.MustParseAddr("213.248.216.1")
coukRecord := &dns.Msg{
Ns: []dns.RR{nsRR("com.", "dns1.nic.uk.")},
Extra: []dns.RR{dnsIPRR("dns1.nic.uk.", coukNS)},
}
intermediateNS := netip.MustParseAddr("205.251.193.66") // g-ns-322.awsdns-02.co.uk.
intermediateRecord := &dns.Msg{
Ns: []dns.RR{nsRR("awsdns-02.co.uk.", "g-ns-322.awsdns-02.co.uk.")},
Extra: []dns.RR{dnsIPRR("g-ns-322.awsdns-02.co.uk.", intermediateNS)},
}
const amazonNameserver = "ns-1558.awsdns-02.co.uk."
tailscaleNameservers := &dns.Msg{
Ns: []dns.RR{
nsRR("tailscale.com.", amazonNameserver),
},
}
tailscaleResponses := []mockReply{
{name: "tailscale.com.", qtype: dns.Type(dns.TypeA), resp: &dns.Msg{
MsgHdr: dns.MsgHdr{Authoritative: true},
Answer: []dns.RR{dnsIPRR("tailscale.com.", netip.MustParseAddr("13.248.141.131"))},
}},
{name: "tailscale.com.", qtype: dns.Type(dns.TypeAAAA), resp: &dns.Msg{
MsgHdr: dns.MsgHdr{Authoritative: true},
Answer: []dns.RR{dnsIPRR("tailscale.com.", netip.MustParseAddr("2600:9000:a602:b1e6:86d:8165:5e8c:295b"))},
}},
}
mock := &replyMock{
tb: t,
replies: map[netip.Addr][]mockReply{
rootServerAddr: {
// Query to the root server returns the .com server + a glue record
{name: "tailscale.com.", qtype: dns.Type(dns.TypeA), resp: comRecord},
{name: "tailscale.com.", qtype: dns.Type(dns.TypeAAAA), resp: comRecord},
// Querying the .co.uk nameserver returns the .co.uk nameserver + a glue record.
{name: amazonNameserver, qtype: dns.Type(dns.TypeA), resp: coukRecord},
{name: amazonNameserver, qtype: dns.Type(dns.TypeAAAA), resp: coukRecord},
},
// Queries to the ".com" server return the nameservers
// for tailscale.com, which don't contain a glue
// record.
comNSAddr: {
{name: "tailscale.com.", qtype: dns.Type(dns.TypeA), resp: tailscaleNameservers},
{name: "tailscale.com.", qtype: dns.Type(dns.TypeAAAA), resp: tailscaleNameservers},
},
// Queries to the ".co.uk" nameserver returns the
// address of the intermediate Amazon nameserver.
coukNS: {
{name: amazonNameserver, qtype: dns.Type(dns.TypeA), resp: intermediateRecord},
{name: amazonNameserver, qtype: dns.Type(dns.TypeAAAA), resp: intermediateRecord},
},
// Queries to the intermediate nameserver returns an
// answer for the final Amazon nameserver.
intermediateNS: {
{name: amazonNameserver, qtype: dns.Type(dns.TypeA), resp: &dns.Msg{
MsgHdr: dns.MsgHdr{Authoritative: true},
Answer: []dns.RR{dnsIPRR(amazonNameserver, amazonNS)},
}},
{name: amazonNameserver, qtype: dns.Type(dns.TypeAAAA), resp: &dns.Msg{
MsgHdr: dns.MsgHdr{Authoritative: true},
Answer: []dns.RR{dnsIPRR(amazonNameserver, amazonNSv6)},
}},
},
// Queries to the actual nameserver work and return
// responses to the query.
amazonNS: tailscaleResponses,
amazonNSv6: tailscaleResponses,
},
}
r := newResolver(t)
r.testExchangeHook = mock.exchangeHook
r.rootServers = []netip.Addr{rootServerAddr}
// Query for tailscale.com, verify we get the right responses
addrs, minTTL, err := r.Resolve(context.Background(), "tailscale.com")
if err != nil {
t.Fatal(err)
}
wantAddrs := []netip.Addr{
netip.MustParseAddr("13.248.141.131"),
netip.MustParseAddr("2600:9000:a602:b1e6:86d:8165:5e8c:295b"),
}
slices.SortFunc(addrs, func(x, y netip.Addr) bool { return x.String() < y.String() })
slices.SortFunc(wantAddrs, func(x, y netip.Addr) bool { return x.String() < y.String() })
if !reflect.DeepEqual(addrs, wantAddrs) {
t.Errorf("got addrs=%+v; want %+v", addrs, wantAddrs)
}
const wantMinTTL = 5 * time.Minute
if minTTL != wantMinTTL {
t.Errorf("got minTTL=%+v; want %+v", minTTL, wantMinTTL)
}
}
func TestRecursionLimit(t *testing.T) {
mock := &replyMock{
tb: t,
replies: map[netip.Addr][]mockReply{},
}
// Fill out a CNAME chain equal to our recursion limit; we won't get
// this far since each CNAME is more than 1 level "deep", but this
// ensures that we have more than the limit.
for i := 0; i < maxDepth+1; i++ {
curr := fmt.Sprintf("%d-tailscale.com.", i)
tailscaleNameservers := &dns.Msg{
Ns: []dns.RR{nsRR(curr, "ns-197.awsdns-24.com.")},
Extra: []dns.RR{dnsIPRR("ns-197.awsdns-24.com.", amazonNS)},
}
// Query to the root server returns the .com server + a glue record
mock.replies[rootServerAddr] = append(mock.replies[rootServerAddr],
mockReply{name: curr, qtype: dns.Type(dns.TypeA), resp: comRecord},
mockReply{name: curr, qtype: dns.Type(dns.TypeAAAA), resp: comRecord},
)
// Query to the ".com" server return the nameservers for NN-tailscale.com
mock.replies[comNSAddr] = append(mock.replies[comNSAddr],
mockReply{name: curr, qtype: dns.Type(dns.TypeA), resp: tailscaleNameservers},
mockReply{name: curr, qtype: dns.Type(dns.TypeAAAA), resp: tailscaleNameservers},
)
// Queries to the nameserver return a CNAME for the n+1th server.
next := fmt.Sprintf("%d-tailscale.com.", i+1)
mock.replies[amazonNS] = append(mock.replies[amazonNS],
mockReply{
name: curr,
qtype: dns.Type(dns.TypeA),
resp: &dns.Msg{
MsgHdr: dns.MsgHdr{Authoritative: true},
Answer: []dns.RR{cnameRR(curr, next)},
},
},
mockReply{
name: curr,
qtype: dns.Type(dns.TypeAAAA),
resp: &dns.Msg{
MsgHdr: dns.MsgHdr{Authoritative: true},
Answer: []dns.RR{cnameRR(curr, next)},
},
},
)
}
r := newResolver(t)
r.testExchangeHook = mock.exchangeHook
r.rootServers = []netip.Addr{rootServerAddr}
// Query for the first node in the chain, 0-tailscale.com, and verify
// we get a max-depth error.
ctx := context.Background()
_, _, err := r.Resolve(ctx, "0-tailscale.com")
if err == nil {
t.Fatal("expected error, got nil")
} else if !errors.Is(err, ErrMaxDepth) {
t.Fatalf("got err=%v, want ErrMaxDepth", err)
}
}
func TestInvalidResponses(t *testing.T) {
mock := &replyMock{
tb: t,
replies: map[netip.Addr][]mockReply{
// Query to the root server returns the .com server + a glue record
rootServerAddr: {
{name: "tailscale.com.", qtype: dns.Type(dns.TypeA), resp: comRecord},
{name: "tailscale.com.", qtype: dns.Type(dns.TypeAAAA), resp: comRecord},
},
// Query to the ".com" server return the nameservers for tailscale.com
comNSAddr: {
{name: "tailscale.com.", qtype: dns.Type(dns.TypeA), resp: tailscaleNameservers},
{name: "tailscale.com.", qtype: dns.Type(dns.TypeAAAA), resp: tailscaleNameservers},
},
// Query to the actual nameserver returns an invalid IP address
amazonNS: {
{name: "tailscale.com.", qtype: dns.Type(dns.TypeA), resp: &dns.Msg{
MsgHdr: dns.MsgHdr{Authoritative: true},
Answer: []dns.RR{&dns.A{
Hdr: dns.RR_Header{
Name: "tailscale.com.",
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: 300,
},
// Note: this is an IPv6 addr in an IPv4 response
A: net.IP(netip.MustParseAddr("2600:9000:a51d:27c1:1530:b9ef:2a6:b9e5").AsSlice()),
}},
}},
{name: "tailscale.com.", qtype: dns.Type(dns.TypeAAAA), resp: &dns.Msg{
MsgHdr: dns.MsgHdr{Authoritative: true},
// This an IPv4 response to an IPv6 query
Answer: []dns.RR{&dns.A{
Hdr: dns.RR_Header{
Name: "tailscale.com.",
Rrtype: dns.TypeA,
Class: dns.ClassINET,
Ttl: 300,
},
A: net.IP(netip.MustParseAddr("13.248.141.131").AsSlice()),
}},
}},
},
},
}
r := &Resolver{
Logf: t.Logf,
testExchangeHook: mock.exchangeHook,
rootServers: []netip.Addr{rootServerAddr},
}
// Query for tailscale.com, verify we get no responses since the
// addresses are invalid.
_, _, err := r.Resolve(context.Background(), "tailscale.com")
if err == nil {
t.Fatalf("got no error, want error")
}
if !errors.Is(err, ErrAuthoritativeNoResponses) {
t.Fatalf("got err=%v, want %v", err, ErrAuthoritativeNoResponses)
}
}
// TODO(andrew): test for more edge cases that aren't currently covered:
// * Nameservers that cross between IPv4 and IPv6
// * Authoritative no replies after following CNAME
// * Authoritative no replies after following non-glue NS record
// * Error querying non-glue NS record followed by success

View File

@@ -13,7 +13,6 @@ import (
"github.com/golang/groupcache/lru"
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/util/cmpx"
)
// MessageCache is a cache that works at the DNS message layer,
@@ -60,7 +59,10 @@ func (c *MessageCache) Flush() {
// pruneLocked prunes down the cache size to the configured (or
// default) max size.
func (c *MessageCache) pruneLocked() {
max := cmpx.Or(c.cacheSizeSet, 500)
max := c.cacheSizeSet
if max == 0 {
max = 500
}
for c.cache.Len() > max {
c.cache.RemoveOldest()
}

View File

@@ -22,10 +22,6 @@ type Listener struct {
ch chan Conn
closeOnce sync.Once
closed chan struct{}
// NewConn, if non-nil, is called to create a new pair of connections
// when dialing. If nil, NewConn is used.
NewConn func(network, addr string, maxBuf int) (Conn, Conn)
}
// Listen returns a new Listener for the provided address.
@@ -74,14 +70,7 @@ func (l *Listener) Dial(ctx context.Context, network, addr string) (_ net.Conn,
Addr: addr,
}
}
newConn := l.NewConn
if newConn == nil {
newConn = func(network, addr string, maxBuf int) (Conn, Conn) {
return NewConn(addr, maxBuf)
}
}
c, s := newConn(network, addr, bufferSize)
c, s := NewConn(addr, bufferSize)
defer func() {
if err != nil {
c.Close()

View File

@@ -42,7 +42,6 @@ import (
"tailscale.com/types/opt"
"tailscale.com/types/ptr"
"tailscale.com/util/clientmetric"
"tailscale.com/util/cmpx"
"tailscale.com/util/mak"
)
@@ -451,9 +450,10 @@ func makeProbePlan(dm *tailcfg.DERPMap, ifState *interfaces.State, last *Report)
do6 = false
}
n := reg.Nodes[try%len(reg.Nodes)]
prevLatency := cmpx.Or(
last.RegionLatency[reg.RegionID]*120/100,
defaultActiveRetransmitTime)
prevLatency := last.RegionLatency[reg.RegionID] * 120 / 100
if prevLatency == 0 {
prevLatency = defaultActiveRetransmitTime
}
delay := time.Duration(try) * prevLatency
if try > 1 {
delay += time.Duration(try) * 50 * time.Millisecond
@@ -1589,7 +1589,10 @@ func (rs *reportState) runProbe(ctx context.Context, dm *tailcfg.DERPMap, probe
// proto is 4 or 6
// If it returns nil, the node is skipped.
func (c *Client) nodeAddr(ctx context.Context, n *tailcfg.DERPNode, proto probeProto) (ap netip.AddrPort) {
port := cmpx.Or(n.STUNPort, 3478)
port := n.STUNPort
if port == 0 {
port = 3478
}
if port < 0 || port > 1<<16-1 {
return
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !wasm
//go:build !js
// Package tun creates a tuntap device, working around OS-specific
// quirks if necessary.

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !windows && !js && !wasip1
//go:build !windows && !js
package paths

View File

@@ -188,9 +188,6 @@ func (d *derpProber) updateMap(ctx context.Context) error {
if existing, ok := d.nodes[n.HostName]; ok {
return fmt.Errorf("derpmap has duplicate nodes: %+v and %+v", existing, n)
}
// Allow the prober to monitor nodes marked as
// STUN only in the default map
n.STUNOnly = false
d.nodes[n.HostName] = n
}
}

View File

@@ -2,6 +2,7 @@
".url": {
"SYNO.SDS.Tailscale": {
"type": "url",
"version": "1.8.3",
"title": "Tailscale",
"icon": "PACKAGE_ICON_256.PNG",
"url": "webman/3rdparty/Tailscale/",

View File

@@ -176,7 +176,7 @@ func (m *synologyBuilds) buildInnerPackage(b *dist.Build, dsmVersion int, goenv
static(fmt.Sprintf("logrotate-dsm%d", dsmVersion), "conf/logrotate.conf", 0644),
dir("ui"),
static("PACKAGE_ICON_256.PNG", "ui/PACKAGE_ICON_256.PNG", 0644),
static("config", "ui/config", 0644),
static("config", "ui/config", 0644), // TODO: this has "1.8.3" hard-coded in it; why? what is it? bug?
static("index.cgi", "ui/index.cgi", 0755))
if err != nil {
return nil, err

View File

@@ -16,4 +16,4 @@
) {
src = ./.;
}).shellNix
# nix-direnv cache busting line: sha256-fgCrmtJs1svFz0Xn7iwLNrbBNlcO6V0yqGPMY0+V1VQ=
# nix-direnv cache busting line: sha256-l2uIma2oEdSN0zVo9BOFJF2gC3S60vXwTLVadv8yQPo=

View File

@@ -34,7 +34,6 @@ import (
"golang.org/x/exp/slices"
"golang.org/x/sys/unix"
"tailscale.com/cmd/tailscaled/childproc"
"tailscale.com/hostinfo"
"tailscale.com/tempfork/gliderlabs/ssh"
"tailscale.com/types/logger"
"tailscale.com/version/distro"
@@ -121,18 +120,10 @@ func (ss *sshSession) newIncubatorCommand() (cmd *exec.Cmd) {
if isShell {
incubatorArgs = append(incubatorArgs, "--shell")
}
// Only the macOS version of the login command supports executing a
// command, all other versions only support launching a shell
// without taking any arguments.
shouldUseLoginCmd := isShell || runtime.GOOS == "darwin"
if hostinfo.IsSELinuxEnforcing() {
// If we're running on a SELinux-enabled system, the login
// command will be unable to set the correct context for the
// shell. Fall back to using the incubator to launch the shell.
// See http://github.com/tailscale/tailscale/issues/4908.
shouldUseLoginCmd = false
}
if shouldUseLoginCmd {
if isShell || runtime.GOOS == "darwin" {
// Only the macOS version of the login command supports executing a
// command, all other versions only support launching a shell
// without taking any arguments.
if lp, err := exec.LookPath("login"); err == nil {
incubatorArgs = append(incubatorArgs, "--login-cmd="+lp)
}
@@ -476,10 +467,10 @@ func (ss *sshSession) launchProcess() error {
}
go resizeWindow(ptyDup /* arbitrary fd */, winCh)
ss.wrStdin = pty
ss.rdStdout = os.NewFile(uintptr(ptyDup), pty.Name())
ss.rdStderr = nil // not available for pty
ss.childPipes = []io.Closer{tty}
ss.tty = tty
ss.stdin = pty
ss.stdout = os.NewFile(uintptr(ptyDup), pty.Name())
ss.stderr = nil // not available for pty
return nil
}
@@ -658,29 +649,40 @@ func (ss *sshSession) startWithPTY() (ptyFile, tty *os.File, err error) {
// startWithStdPipes starts cmd with os.Pipe for Stdin, Stdout and Stderr.
func (ss *sshSession) startWithStdPipes() (err error) {
var rdStdin, wrStdout, wrStderr io.ReadWriteCloser
var stdin io.WriteCloser
var stdout, stderr io.ReadCloser
defer func() {
if err != nil {
closeAll(rdStdin, ss.wrStdin, ss.rdStdout, wrStdout, ss.rdStderr, wrStderr)
for _, c := range []io.Closer{stdin, stdout, stderr} {
if c != nil {
c.Close()
}
}
}
}()
if ss.cmd == nil {
cmd := ss.cmd
if cmd == nil {
return errors.New("nil cmd")
}
if rdStdin, ss.wrStdin, err = os.Pipe(); err != nil {
stdin, err = cmd.StdinPipe()
if err != nil {
return err
}
if ss.rdStdout, wrStdout, err = os.Pipe(); err != nil {
stdout, err = cmd.StdoutPipe()
if err != nil {
return err
}
if ss.rdStderr, wrStderr, err = os.Pipe(); err != nil {
stderr, err = cmd.StderrPipe()
if err != nil {
return err
}
ss.cmd.Stdin = rdStdin
ss.cmd.Stdout = wrStdout
ss.cmd.Stderr = wrStderr
ss.childPipes = []io.Closer{rdStdin, wrStdout, wrStderr}
return ss.cmd.Start()
if err := cmd.Start(); err != nil {
return err
}
ss.stdin = stdin
ss.stdout = stdout
ss.stderr = stderr
return nil
}
func envForUser(u *userMeta) []string {

View File

@@ -422,7 +422,6 @@ func (srv *server) newConn() (*conn, error) {
c := &conn{srv: srv}
now := srv.now()
c.connID = fmt.Sprintf("ssh-conn-%s-%02x", now.UTC().Format("20060102T150405"), randBytes(5))
fwdHandler := &ssh.ForwardedTCPHandler{}
c.Server = &ssh.Server{
Version: "Tailscale",
ServerConfigCallback: c.ServerConfig,
@@ -431,9 +430,8 @@ func (srv *server) newConn() (*conn, error) {
PublicKeyHandler: c.PublicKeyHandler,
PasswordHandler: c.fakePasswordHandler,
Handler: c.handleSessionPostSSHAuth,
LocalPortForwardingCallback: c.mayForwardLocalPortTo,
ReversePortForwardingCallback: c.mayReversePortForwardTo,
Handler: c.handleSessionPostSSHAuth,
LocalPortForwardingCallback: c.mayForwardLocalPortTo,
SubsystemHandlers: map[string]ssh.SubsystemHandler{
"sftp": c.handleSessionPostSSHAuth,
},
@@ -443,10 +441,7 @@ func (srv *server) newConn() (*conn, error) {
ChannelHandlers: map[string]ssh.ChannelHandler{
"direct-tcpip": ssh.DirectTCPIPHandler,
},
RequestHandlers: map[string]ssh.RequestHandler{
"tcpip-forward": fwdHandler.HandleSSHRequest,
"cancel-tcpip-forward": fwdHandler.HandleSSHRequest,
},
RequestHandlers: map[string]ssh.RequestHandler{},
}
ss := c.Server
for k, v := range ssh.DefaultRequestHandlers {
@@ -468,17 +463,6 @@ func (srv *server) newConn() (*conn, error) {
return c, nil
}
// mayReversePortPortForwardTo reports whether the ctx should be allowed to port forward
// to the specified host and port.
// TODO(bradfitz/maisem): should we have more checks on host/port?
func (c *conn) mayReversePortForwardTo(ctx ssh.Context, destinationHost string, destinationPort uint32) bool {
if c.finalAction != nil && c.finalAction.AllowRemotePortForwarding {
metricRemotePortForward.Add(1)
return true
}
return false
}
// mayForwardLocalPortTo reports whether the ctx should be allowed to port forward
// to the specified host and port.
// TODO(bradfitz/maisem): should we have more checks on host/port?
@@ -823,16 +807,12 @@ type sshSession struct {
agentListener net.Listener // non-nil if agent-forwarding requested+allowed
// initialized by launchProcess:
cmd *exec.Cmd
wrStdin io.WriteCloser
rdStdout io.ReadCloser
rdStderr io.ReadCloser // rdStderr is nil for pty sessions
ptyReq *ssh.Pty // non-nil for pty sessions
// childPipes is a list of pipes that need to be closed when the process exits.
// For pty sessions, this is the tty fd.
// For non-pty sessions, this is the stdin, stdout, stderr fds.
childPipes []io.Closer
cmd *exec.Cmd
stdin io.WriteCloser
stdout io.ReadCloser
stderr io.Reader // nil for pty sessions
ptyReq *ssh.Pty // non-nil for pty sessions
tty *os.File // non-nil for pty sessions, must be closed after process exits
// We use this sync.Once to ensure that we only terminate the process once,
// either it exits itself or is terminated
@@ -1111,22 +1091,21 @@ func (ss *sshSession) run() {
var processDone atomic.Bool
go func() {
defer ss.wrStdin.Close()
if _, err := io.Copy(rec.writer("i", ss.wrStdin), ss); err != nil {
defer ss.stdin.Close()
if _, err := io.Copy(rec.writer("i", ss.stdin), ss); err != nil {
logf("stdin copy: %v", err)
ss.cancelCtx(err)
}
}()
outputDone := make(chan struct{})
var openOutputStreams atomic.Int32
if ss.rdStderr != nil {
if ss.stderr != nil {
openOutputStreams.Store(2)
} else {
openOutputStreams.Store(1)
}
go func() {
defer ss.rdStdout.Close()
_, err := io.Copy(rec.writer("o", ss), ss.rdStdout)
defer ss.stdout.Close()
_, err := io.Copy(rec.writer("o", ss), ss.stdout)
if err != nil && !errors.Is(err, io.EOF) {
isErrBecauseProcessExited := processDone.Load() && errors.Is(err, syscall.EIO)
if !isErrBecauseProcessExited {
@@ -1136,41 +1115,32 @@ func (ss *sshSession) run() {
}
if openOutputStreams.Add(-1) == 0 {
ss.CloseWrite()
close(outputDone)
}
}()
// rdStderr is nil for ptys.
if ss.rdStderr != nil {
// stderr is nil for ptys.
if ss.stderr != nil {
go func() {
defer ss.rdStderr.Close()
_, err := io.Copy(ss.Stderr(), ss.rdStderr)
_, err := io.Copy(ss.Stderr(), ss.stderr)
if err != nil {
logf("stderr copy: %v", err)
}
if openOutputStreams.Add(-1) == 0 {
ss.CloseWrite()
close(outputDone)
}
}()
}
if ss.tty != nil {
// If running a tty session, close the tty when the session is done.
defer ss.tty.Close()
}
err = ss.cmd.Wait()
processDone.Store(true)
// This will either make the SSH Termination goroutine be a no-op,
// or itself will be a no-op because the process was killed by the
// aforementioned goroutine.
ss.exitOnce.Do(func() {})
// Close the process-side of all pipes to signal the asynchronous
// io.Copy routines reading/writing from the pipes to terminate.
// Block for the io.Copy to finish before calling ss.Exit below.
closeAll(ss.childPipes...)
select {
case <-outputDone:
case <-ss.ctx.Done():
}
if err == nil {
ss.logf("Session complete")
ss.Exit(0)
@@ -1890,7 +1860,6 @@ var (
metricPolicyChangeKick = clientmetric.NewCounter("ssh_policy_change_kick")
metricSFTP = clientmetric.NewCounter("ssh_sftp_requests")
metricLocalPortForward = clientmetric.NewCounter("ssh_local_port_forward_requests")
metricRemotePortForward = clientmetric.NewCounter("ssh_remote_port_forward_requests")
)
// userVisibleError is a wrapper around an error that implements
@@ -1908,11 +1877,3 @@ type SSHTerminationError interface {
error
SSHTerminationMessage() string
}
func closeAll(cs ...io.Closer) {
for _, c := range cs {
if c != nil {
c.Close()
}
}
}

View File

@@ -25,7 +25,6 @@ import (
"os/user"
"reflect"
"runtime"
"strconv"
"strings"
"sync"
"sync/atomic"
@@ -948,19 +947,6 @@ func TestSSH(t *testing.T) {
// "foo\n" and "bar\n", not "\n" and "bar\n".
})
t.Run("large_file", func(t *testing.T) {
const wantSize = 1e6
var outBuf bytes.Buffer
cmd := execSSH("head", "-c", strconv.Itoa(wantSize), "/dev/zero")
cmd.Stdout = &outBuf
if err := cmd.Run(); err != nil {
t.Fatal(err)
}
if gotSize := outBuf.Len(); gotSize != wantSize {
t.Fatalf("got %d, want %d", gotSize, int(wantSize))
}
})
t.Run("stdin", func(t *testing.T) {
if cibuild.On() {
t.Skip("Skipping for now; see https://github.com/tailscale/tailscale/issues/4051")

View File

@@ -1,111 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package syncs
import (
"sync"
"golang.org/x/sys/cpu"
)
// ShardedMap is a synchronized map[K]V, internally sharded by a user-defined
// K-sharding function.
//
// The zero value is not safe for use; use NewShardedMap.
type ShardedMap[K comparable, V any] struct {
shardFunc func(K) int
shards []mapShard[K, V]
}
type mapShard[K comparable, V any] struct {
mu sync.Mutex
m map[K]V
_ cpu.CacheLinePad // avoid false sharing of neighboring shards' mutexes
}
// NewShardedMap returns a new ShardedMap with the given number of shards and
// sharding function.
//
// The shard func must return a integer in the range [0, shards) purely
// deterministically based on the provided K.
func NewShardedMap[K comparable, V any](shards int, shard func(K) int) *ShardedMap[K, V] {
m := &ShardedMap[K, V]{
shardFunc: shard,
shards: make([]mapShard[K, V], shards),
}
for i := range m.shards {
m.shards[i].m = make(map[K]V)
}
return m
}
func (m *ShardedMap[K, V]) shard(key K) *mapShard[K, V] {
return &m.shards[m.shardFunc(key)]
}
// GetOk returns m[key] and whether it was present.
func (m *ShardedMap[K, V]) GetOk(key K) (value V, ok bool) {
shard := m.shard(key)
shard.mu.Lock()
defer shard.mu.Unlock()
value, ok = shard.m[key]
return
}
// Get returns m[key] or the zero value of V if key is not present.
func (m *ShardedMap[K, V]) Get(key K) (value V) {
value, _ = m.GetOk(key)
return
}
// Set sets m[key] = value.
//
// It reports whether the map grew in size (that is, whether key was not already
// present in m).
func (m *ShardedMap[K, V]) Set(key K, value V) (grew bool) {
shard := m.shard(key)
shard.mu.Lock()
defer shard.mu.Unlock()
s0 := len(shard.m)
shard.m[key] = value
return len(shard.m) > s0
}
// Delete removes key from m.
//
// It reports whether the map size shrunk (that is, whether key was present in
// the map).
func (m *ShardedMap[K, V]) Delete(key K) (shrunk bool) {
shard := m.shard(key)
shard.mu.Lock()
defer shard.mu.Unlock()
s0 := len(shard.m)
delete(shard.m, key)
return len(shard.m) < s0
}
// Contains reports whether m contains key.
func (m *ShardedMap[K, V]) Contains(key K) bool {
shard := m.shard(key)
shard.mu.Lock()
defer shard.mu.Unlock()
_, ok := shard.m[key]
return ok
}
// Len returns the number of elements in m.
//
// It does so by locking shards one at a time, so it's not particularly cheap,
// nor does it give a consistent snapshot of the map. It's mostly intended for
// metrics or testing.
func (m *ShardedMap[K, V]) Len() int {
n := 0
for i := range m.shards {
shard := &m.shards[i]
shard.mu.Lock()
n += len(shard.m)
shard.mu.Unlock()
}
return n
}

View File

@@ -1,44 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package syncs
import "testing"
func TestShardedMap(t *testing.T) {
m := NewShardedMap[int, string](16, func(i int) int { return i % 16 })
if m.Contains(1) {
t.Errorf("got contains; want !contains")
}
if !m.Set(1, "one") {
t.Errorf("got !set; want set")
}
if m.Set(1, "one") {
t.Errorf("got set; want !set")
}
if !m.Contains(1) {
t.Errorf("got !contains; want contains")
}
if g, w := m.Get(1), "one"; g != w {
t.Errorf("got %q; want %q", g, w)
}
if _, ok := m.GetOk(1); !ok {
t.Errorf("got ok; want !ok")
}
if _, ok := m.GetOk(2); ok {
t.Errorf("got ok; want !ok")
}
if g, w := m.Len(), 1; g != w {
t.Errorf("got Len %v; want %v", g, w)
}
if m.Delete(2) {
t.Errorf("got deleted; want !deleted")
}
if !m.Delete(1) {
t.Errorf("got !deleted; want deleted")
}
if g, w := m.Len(), 0; g != w {
t.Errorf("got Len %v; want %v", g, w)
}
}

View File

@@ -3,7 +3,7 @@
package tailcfg
//go:generate go run tailscale.com/cmd/viewer --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location --clonefunc
//go:generate go run tailscale.com/cmd/viewer --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan --clonefunc
import (
"bytes"
@@ -99,8 +99,7 @@ type CapabilityVersion int
// - 60: 2023-04-06: Client understands IsWireGuardOnly
// - 61: 2023-04-18: Client understand SSHAction.SSHRecorderFailureAction
// - 62: 2023-05-05: Client can notify control over noise for SSHEventNotificationRequest recording failure events
// - 63: 2023-06-08: Client understands SSHAction.AllowRemotePortForwarding.
const CurrentCapabilityVersion CapabilityVersion = 63
const CurrentCapabilityVersion CapabilityVersion = 62
type StableID string
@@ -531,24 +530,6 @@ type Service struct {
// TODO(apenwarr): add "tags" here for each service?
}
// Location represents geographical location data about a
// Tailscale host. Location is optional and only set if
// explicitly declared by a node.
type Location struct {
Country string `json:",omitempty"` // User friendly country name, with proper capitalization ("Canada")
CountryCode string `json:",omitempty"` // ISO 3166-1 alpha-2 in upper case ("CA")
City string `json:",omitempty"` // User friendly city name, with proper capitalization ("Squamish")
CityCode string `json:",omitempty"` // TODO(charlotte): document
// Priority determines the priority an exit node is given when the
// location data between two or more nodes is tied.
// A higher value indicates that the exit node is more preferable
// for use.
// A value of 0 means the exit node does not have a priority
// preference. A negative int is not allowed.
Priority int `json:",omitempty"`
}
// Hostinfo contains a summary of a Tailscale host.
//
// Because it contains pointers (slices), this type should not be used
@@ -603,11 +584,6 @@ type Hostinfo struct {
Userspace opt.Bool `json:",omitempty"` // if the client is running in userspace (netstack) mode
UserspaceRouter opt.Bool `json:",omitempty"` // if the client's subnet router is running in userspace (netstack) mode
// Location represents geographical location data about a
// Tailscale host. Location is optional and only set if
// explicitly declared by a node.
Location *Location `json:",omitempty"`
// NOTE: any new fields containing pointers in this type
// require changes to Hostinfo.Equal.
}
@@ -2072,10 +2048,6 @@ type SSHAction struct {
// to use local port forwarding if requested.
AllowLocalPortForwarding bool `json:"allowLocalPortForwarding,omitempty"`
// AllowRemotePortForwarding, if true, allows accepted connections
// to use remote port forwarding if requested.
AllowRemotePortForwarding bool `json:"allowRemotePortForwarding,omitempty"`
// Recorders defines the destinations of the SSH session recorders.
// The recording will be uploaded to http://addr:port/record.
Recorders []netip.AddrPort `json:"recorders,omitempty"`

View File

@@ -119,10 +119,6 @@ func (src *Hostinfo) Clone() *Hostinfo {
dst.Services = append(src.Services[:0:0], src.Services...)
dst.NetInfo = src.NetInfo.Clone()
dst.SSH_HostKeys = append(src.SSH_HostKeys[:0:0], src.SSH_HostKeys...)
if dst.Location != nil {
dst.Location = new(Location)
*dst.Location = *src.Location
}
return dst
}
@@ -161,7 +157,6 @@ var _HostinfoCloneNeedsRegeneration = Hostinfo(struct {
Cloud string
Userspace opt.Bool
UserspaceRouter opt.Bool
Location *Location
}{})
// Clone makes a deep copy of NetInfo.
@@ -413,16 +408,15 @@ func (src *SSHAction) Clone() *SSHAction {
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _SSHActionCloneNeedsRegeneration = SSHAction(struct {
Message string
Reject bool
Accept bool
SessionDuration time.Duration
AllowAgentForwarding bool
HoldAndDelegate string
AllowLocalPortForwarding bool
AllowRemotePortForwarding bool
Recorders []netip.AddrPort
OnRecordingFailure *SSHRecorderFailureAction
Message string
Reject bool
Accept bool
SessionDuration time.Duration
AllowAgentForwarding bool
HoldAndDelegate string
AllowLocalPortForwarding bool
Recorders []netip.AddrPort
OnRecordingFailure *SSHRecorderFailureAction
}{})
// Clone makes a deep copy of SSHPrincipal.
@@ -463,29 +457,9 @@ var _ControlDialPlanCloneNeedsRegeneration = ControlDialPlan(struct {
Candidates []ControlIPCandidate
}{})
// Clone makes a deep copy of Location.
// The result aliases no memory with the original.
func (src *Location) Clone() *Location {
if src == nil {
return nil
}
dst := new(Location)
*dst = *src
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _LocationCloneNeedsRegeneration = Location(struct {
Country string
CountryCode string
City string
CityCode string
Priority int
}{})
// Clone duplicates src into dst and reports whether it succeeded.
// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,
// where T is one of User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location.
// where T is one of User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan.
func Clone(dst, src any) bool {
switch src := src.(type) {
case *User:
@@ -614,15 +588,6 @@ func Clone(dst, src any) bool {
*dst = src.Clone()
return true
}
case *Location:
switch dst := dst.(type) {
case *Location:
*dst = *src.Clone()
return true
case **Location:
*dst = src.Clone()
return true
}
}
return false
}

View File

@@ -65,7 +65,6 @@ func TestHostinfoEqual(t *testing.T) {
"Cloud",
"Userspace",
"UserspaceRouter",
"Location",
}
if have := fieldsOf(reflect.TypeOf(Hostinfo{})); !reflect.DeepEqual(have, hiHandles) {
t.Errorf("Hostinfo.Equal check might be out of sync\nfields: %q\nhandled: %q\n",

View File

@@ -20,7 +20,7 @@ import (
"tailscale.com/types/views"
)
//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location
//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan
// View returns a readonly view of User.
func (p *User) View() UserView {
@@ -303,15 +303,7 @@ func (v HostinfoView) SSH_HostKeys() views.Slice[string] { return views.SliceOf(
func (v HostinfoView) Cloud() string { return v.ж.Cloud }
func (v HostinfoView) Userspace() opt.Bool { return v.ж.Userspace }
func (v HostinfoView) UserspaceRouter() opt.Bool { return v.ж.UserspaceRouter }
func (v HostinfoView) Location() *Location {
if v.ж.Location == nil {
return nil
}
x := *v.ж.Location
return &x
}
func (v HostinfoView) Equal(v2 HostinfoView) bool { return v.ж.Equal(v2.ж) }
func (v HostinfoView) Equal(v2 HostinfoView) bool { return v.ж.Equal(v2.ж) }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _HostinfoViewNeedsRegeneration = Hostinfo(struct {
@@ -348,7 +340,6 @@ var _HostinfoViewNeedsRegeneration = Hostinfo(struct {
Cloud string
Userspace opt.Bool
UserspaceRouter opt.Bool
Location *Location
}{})
// View returns a readonly view of NetInfo.
@@ -949,7 +940,6 @@ func (v SSHActionView) SessionDuration() time.Duration { return v.ж.Ses
func (v SSHActionView) AllowAgentForwarding() bool { return v.ж.AllowAgentForwarding }
func (v SSHActionView) HoldAndDelegate() string { return v.ж.HoldAndDelegate }
func (v SSHActionView) AllowLocalPortForwarding() bool { return v.ж.AllowLocalPortForwarding }
func (v SSHActionView) AllowRemotePortForwarding() bool { return v.ж.AllowRemotePortForwarding }
func (v SSHActionView) Recorders() views.Slice[netip.AddrPort] { return views.SliceOf(v.ж.Recorders) }
func (v SSHActionView) OnRecordingFailure() *SSHRecorderFailureAction {
if v.ж.OnRecordingFailure == nil {
@@ -961,16 +951,15 @@ func (v SSHActionView) OnRecordingFailure() *SSHRecorderFailureAction {
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _SSHActionViewNeedsRegeneration = SSHAction(struct {
Message string
Reject bool
Accept bool
SessionDuration time.Duration
AllowAgentForwarding bool
HoldAndDelegate string
AllowLocalPortForwarding bool
AllowRemotePortForwarding bool
Recorders []netip.AddrPort
OnRecordingFailure *SSHRecorderFailureAction
Message string
Reject bool
Accept bool
SessionDuration time.Duration
AllowAgentForwarding bool
HoldAndDelegate string
AllowLocalPortForwarding bool
Recorders []netip.AddrPort
OnRecordingFailure *SSHRecorderFailureAction
}{})
// View returns a readonly view of SSHPrincipal.
@@ -1086,63 +1075,3 @@ func (v ControlDialPlanView) Candidates() views.Slice[ControlIPCandidate] {
var _ControlDialPlanViewNeedsRegeneration = ControlDialPlan(struct {
Candidates []ControlIPCandidate
}{})
// View returns a readonly view of Location.
func (p *Location) View() LocationView {
return LocationView{ж: p}
}
// LocationView provides a read-only view over Location.
//
// Its methods should only be called if `Valid()` returns true.
type LocationView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *Location
}
// Valid reports whether underlying value is non-nil.
func (v LocationView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v LocationView) AsStruct() *Location {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v LocationView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *LocationView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x Location
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v LocationView) Country() string { return v.ж.Country }
func (v LocationView) CountryCode() string { return v.ж.CountryCode }
func (v LocationView) City() string { return v.ж.City }
func (v LocationView) CityCode() string { return v.ж.CityCode }
func (v LocationView) Priority() int { return v.ж.Priority }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _LocationViewNeedsRegeneration = Location(struct {
Country string
CountryCode string
City string
CityCode string
Priority int
}{})

View File

@@ -1,221 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tka
import (
"crypto/hmac"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"net/url"
"strings"
)
const (
DeeplinkTailscaleURLScheme = "tailscale"
DeeplinkCommandSign = "sign-device"
)
// generateHMAC computes a SHA-256 HMAC for the concatenation of components,
// using the Authority stateID as secret.
func (a *Authority) generateHMAC(params NewDeeplinkParams) []byte {
stateID, _ := a.StateIDs()
key := make([]byte, 8)
binary.LittleEndian.PutUint64(key, stateID)
mac := hmac.New(sha256.New, key)
mac.Write([]byte(params.NodeKey))
mac.Write([]byte(params.TLPub))
mac.Write([]byte(params.DeviceName))
mac.Write([]byte(params.OSName))
mac.Write([]byte(params.LoginName))
return mac.Sum(nil)
}
type NewDeeplinkParams struct {
NodeKey string
TLPub string
DeviceName string
OSName string
LoginName string
}
// NewDeeplink creates a signed deeplink using the authority's stateID as a
// secret. This deeplink can then be validated by ValidateDeeplink.
func (a *Authority) NewDeeplink(params NewDeeplinkParams) (string, error) {
if params.NodeKey == "" || !strings.HasPrefix(params.NodeKey, "nodekey:") {
return "", fmt.Errorf("invalid node key %q", params.NodeKey)
}
if params.TLPub == "" || !strings.HasPrefix(params.TLPub, "tlpub:") {
return "", fmt.Errorf("invalid tlpub %q", params.TLPub)
}
if params.DeviceName == "" {
return "", fmt.Errorf("invalid device name %q", params.DeviceName)
}
if params.OSName == "" {
return "", fmt.Errorf("invalid os name %q", params.OSName)
}
if params.LoginName == "" {
return "", fmt.Errorf("invalid login name %q", params.LoginName)
}
u := url.URL{
Scheme: DeeplinkTailscaleURLScheme,
Host: DeeplinkCommandSign,
Path: "/v1/",
}
v := url.Values{}
v.Set("nk", params.NodeKey)
v.Set("tp", params.TLPub)
v.Set("dn", params.DeviceName)
v.Set("os", params.OSName)
v.Set("em", params.LoginName)
hmac := a.generateHMAC(params)
v.Set("hm", hex.EncodeToString(hmac))
u.RawQuery = v.Encode()
return u.String(), nil
}
type DeeplinkValidationResult struct {
IsValid bool
Error string
Version uint8
NodeKey string
TLPub string
DeviceName string
OSName string
EmailAddress string
}
// ValidateDeeplink validates a device signing deeplink using the authority's stateID.
// The input urlString follows this structure:
//
// tailscale://sign-device/v1/?nk=xxx&tp=xxx&dn=xxx&os=xxx&em=xxx&hm=xxx
//
// where:
// - "nk" is the nodekey of the node being signed
// - "tp" is the tailnet lock public key
// - "dn" is the name of the node
// - "os" is the operating system of the node
// - "em" is the email address associated with the node
// - "hm" is a SHA-256 HMAC computed over the concatenation of the above fields, encoded as a hex string
func (a *Authority) ValidateDeeplink(urlString string) DeeplinkValidationResult {
parsedUrl, err := url.Parse(urlString)
if err != nil {
return DeeplinkValidationResult{
IsValid: false,
Error: err.Error(),
}
}
if parsedUrl.Scheme != DeeplinkTailscaleURLScheme {
return DeeplinkValidationResult{
IsValid: false,
Error: fmt.Sprintf("unhandled scheme %s, expected %s", parsedUrl.Scheme, DeeplinkTailscaleURLScheme),
}
}
if parsedUrl.Host != DeeplinkCommandSign {
return DeeplinkValidationResult{
IsValid: false,
Error: fmt.Sprintf("unhandled host %s, expected %s", parsedUrl.Host, DeeplinkCommandSign),
}
}
path := parsedUrl.EscapedPath()
pathComponents := strings.Split(path, "/")
if len(pathComponents) != 3 {
return DeeplinkValidationResult{
IsValid: false,
Error: "invalid path components number found",
}
}
if pathComponents[1] != "v1" {
return DeeplinkValidationResult{
IsValid: false,
Error: fmt.Sprintf("expected v1 deeplink version, found something else: %s", pathComponents[1]),
}
}
nodeKey := parsedUrl.Query().Get("nk")
if len(nodeKey) == 0 {
return DeeplinkValidationResult{
IsValid: false,
Error: "missing nk (NodeKey) query parameter",
}
}
tlPub := parsedUrl.Query().Get("tp")
if len(tlPub) == 0 {
return DeeplinkValidationResult{
IsValid: false,
Error: "missing tp (TLPub) query parameter",
}
}
deviceName := parsedUrl.Query().Get("dn")
if len(deviceName) == 0 {
return DeeplinkValidationResult{
IsValid: false,
Error: "missing dn (DeviceName) query parameter",
}
}
osName := parsedUrl.Query().Get("os")
if len(deviceName) == 0 {
return DeeplinkValidationResult{
IsValid: false,
Error: "missing os (OSName) query parameter",
}
}
emailAddress := parsedUrl.Query().Get("em")
if len(emailAddress) == 0 {
return DeeplinkValidationResult{
IsValid: false,
Error: "missing em (EmailAddress) query parameter",
}
}
hmacString := parsedUrl.Query().Get("hm")
if len(hmacString) == 0 {
return DeeplinkValidationResult{
IsValid: false,
Error: "missing hm (HMAC) query parameter",
}
}
computedHMAC := a.generateHMAC(NewDeeplinkParams{
NodeKey: nodeKey,
TLPub: tlPub,
DeviceName: deviceName,
OSName: osName,
LoginName: emailAddress,
})
hmacHexBytes, err := hex.DecodeString(hmacString)
if err != nil {
return DeeplinkValidationResult{IsValid: false, Error: "could not hex-decode hmac"}
}
if !hmac.Equal(computedHMAC, hmacHexBytes) {
return DeeplinkValidationResult{
IsValid: false,
Error: "hmac authentication failed",
}
}
return DeeplinkValidationResult{
IsValid: true,
NodeKey: nodeKey,
TLPub: tlPub,
DeviceName: deviceName,
OSName: osName,
EmailAddress: emailAddress,
}
}

View File

@@ -1,52 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tka
import (
"testing"
)
func TestGenerateDeeplink(t *testing.T) {
pub, _ := testingKey25519(t, 1)
key := Key{Kind: Key25519, Public: pub, Votes: 2}
c := newTestchain(t, `
G1 -> L1
G1.template = genesis
`,
optTemplate("genesis", AUM{MessageKind: AUMCheckpoint, State: &State{
Keys: []Key{key},
DisablementSecrets: [][]byte{DisablementKDF([]byte{1, 2, 3})},
}}),
)
a, _ := Open(c.Chonk())
nodeKey := "nodekey:1234567890"
tlPub := "tlpub:1234567890"
deviceName := "Example Device"
osName := "iOS"
loginName := "insecure@example.com"
deeplink, err := a.NewDeeplink(NewDeeplinkParams{
NodeKey: nodeKey,
TLPub: tlPub,
DeviceName: deviceName,
OSName: osName,
LoginName: loginName,
})
if err != nil {
t.Errorf("deeplink generation failed: %v", err)
}
res := a.ValidateDeeplink(deeplink)
if !res.IsValid {
t.Errorf("deeplink validation failed: %s", res.Error)
}
if res.NodeKey != nodeKey {
t.Errorf("node key mismatch: %s != %s", res.NodeKey, nodeKey)
}
if res.TLPub != tlPub {
t.Errorf("tlpub mismatch: %s != %s", res.TLPub, tlPub)
}
}

View File

@@ -111,11 +111,7 @@ func autoflagsForTest(argv []string, env *Environment, goroot, nativeGOOS, nativ
// e.g. -mmacosx-version-min=11.3, -miphoneos-version-min=15.0
switch {
case env.IsSet("IPHONEOS_DEPLOYMENT_TARGET"):
if env.Get("TARGET_DEVICE_PLATFORM_NAME", "") == "iphonesimulator" {
xcodeFlags = append(xcodeFlags, "-miphonesimulator-version-min="+env.Get("IPHONEOS_DEPLOYMENT_TARGET", ""))
} else {
xcodeFlags = append(xcodeFlags, "-miphoneos-version-min="+env.Get("IPHONEOS_DEPLOYMENT_TARGET", ""))
}
xcodeFlags = append(xcodeFlags, "-miphoneos-version-min="+env.Get("IPHONEOS_DEPLOYMENT_TARGET", ""))
case env.IsSet("MACOSX_DEPLOYMENT_TARGET"):
xcodeFlags = append(xcodeFlags, "-mmacosx-version-min="+env.Get("MACOSX_DEPLOYMENT_TARGET", ""))
case env.IsSet("TVOS_DEPLOYMENT_TARGET"):

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env bash
#!/usr/bin/env sh
# Copyright (c) Tailscale Inc & AUTHORS
# SPDX-License-Identifier: BSD-3-Clause
#
@@ -6,9 +6,9 @@
# transparently builds gocross using a "bootstrap" Go toolchain, and
# then invokes gocross.
set -euo pipefail
set -eu
if [[ "${CI:-}" == "true" ]]; then
if [ "${CI:-}" = "true" ]; then
set -x
fi
@@ -17,7 +17,7 @@ fi
# accidentally mutate the input environment that will get passed to gocross at
# the bottom of this script.
(
repo_root="${BASH_SOURCE%/*}/../.."
repo_root="$(dirname $0)/../.."
# Figuring out if gocross needs a rebuild, as well as the rebuild itself, need
# to happen with CWD inside this repo. Since we're in a subshell entirely
@@ -28,21 +28,16 @@ cd "$repo_root"
toolchain="$HOME/.cache/tailscale-go"
if [[ -d "$toolchain" ]]; then
if [ -d "$toolchain" ]; then
# A toolchain exists, but is it recent enough to compile gocross? If not,
# wipe it out so that the next if block fetches a usable one.
want_go_minor=$(grep -E '^go ' "go.mod" | cut -f2 -d'.')
have_go_minor=$(head -1 "$toolchain/VERSION" | cut -f2 -d'.')
# Shortly before stable releases, we run release candidate
# toolchains, which have a non-numeric suffix on the version
# number. Remove the rc qualifier, we just care about the minor
# version.
have_go_minor="${have_go_minor%rc*}"
if [[ -z "$have_go_minor" || "$have_go_minor" -lt "$want_go_minor" ]]; then
have_go_minor=$(cut -f2 -d'.' <$toolchain/VERSION)
if [ -z "$have_go_minor" -o "$have_go_minor" -lt "$want_go_minor" ]; then
rm -rf "$toolchain" "$toolchain.extracted"
fi
fi
if [[ ! -d "$toolchain" ]]; then
if [ ! -d "$toolchain" ]; then
mkdir -p "$HOME/.cache"
# We need any Go toolchain to build gocross, but the toolchain also has to
@@ -61,10 +56,10 @@ if [[ ! -d "$toolchain" ]]; then
# (we do not build tailscale-go for other targets).
HOST_OS=$(uname -s | tr A-Z a-z)
HOST_ARCH="$(uname -m)"
if [[ "$HOST_ARCH" == "aarch64" ]]; then
if [ "$HOST_ARCH" = "aarch64" ]; then
# Go uses the name "arm64".
HOST_ARCH="arm64"
elif [[ "$HOST_ARCH" == "x86_64" ]]; then
elif [ "$HOST_ARCH" = "x86_64" ]; then
# Go uses the name "amd64".
HOST_ARCH="amd64"
fi
@@ -88,13 +83,13 @@ fi
gocross_path="gocross"
gocross_ok=0
wantver="$(git rev-parse HEAD)"
if [[ -x "$gocross_path" ]]; then
if [ -x "$gocross_path" ]; then
gotver="$($gocross_path gocross-version 2>/dev/null || echo '')"
if [[ "$gotver" == "$wantver" ]]; then
if [ "$gotver" = "$wantver" ]; then
gocross_ok=1
fi
fi
if [[ "$gocross_ok" == "0" ]]; then
if [ "$gocross_ok" = "0" ]; then
unset GOOS
unset GOARCH
unset GO111MODULE
@@ -104,4 +99,4 @@ if [[ "$gocross_ok" == "0" ]]; then
fi
) # End of the subshell execution.
exec "${BASH_SOURCE%/*}/../../gocross" "$@"
exec "$(dirname $0)/../../gocross" "$@"

View File

@@ -22,7 +22,6 @@ import (
"net/netip"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
@@ -80,7 +79,7 @@ type Server struct {
// If nil, a new FileStore is initialized at `Dir/tailscaled.state`.
// See tailscale.com/ipn/store for supported stores.
//
// Logs will automatically be uploaded to log.tailscale.io,
// Logs will automatically be uploaded to uploaded to log.tailscale.io,
// where the configuration file for logging will be saved at
// `Dir/tailscaled.log.conf`.
Store ipn.StateStore
@@ -442,16 +441,7 @@ func (s *Server) start() (reterr error) {
exe, err := os.Executable()
if err != nil {
switch runtime.GOOS {
case "js", "wasip1":
// These platforms don't implement os.Executable (at least as of Go
// 1.21), but we don't really care much: it's only used as a default
// directory and hostname when they're not supplied. But we can fall
// back to "tsnet" as well.
exe = "tsnet"
default:
return err
}
return err
}
prog := strings.TrimSuffix(strings.ToLower(filepath.Base(exe)), ".exe")

View File

@@ -27,7 +27,6 @@ import (
"tailscale.com/net/tsaddr"
"tailscale.com/tsweb/varz"
"tailscale.com/types/logger"
"tailscale.com/util/cmpx"
"tailscale.com/util/vizerror"
)
@@ -145,7 +144,10 @@ func (h Port80Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Redirect authorized user to the debug handler.
path = "/debug/"
}
host := cmpx.Or(h.FQDN, r.Host)
host := h.FQDN
if host == "" {
host = r.Host
}
target := "https://" + host + path
http.Redirect(w, r, target, http.StatusFound)
}

View File

@@ -18,7 +18,6 @@ import (
"time"
"tailscale.com/metrics"
"tailscale.com/util/cmpx"
"tailscale.com/version"
)
@@ -97,10 +96,16 @@ func writePromExpVar(w io.Writer, prefix string, kv expvar.KeyValue) {
switch v := kv.Value.(type) {
case *expvar.Int:
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", name, cmpx.Or(typ, "counter"), name, v.Value())
if typ == "" {
typ = "counter"
}
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", name, typ, name, v.Value())
return
case *expvar.Float:
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", name, cmpx.Or(typ, "gauge"), name, v.Value())
if typ == "" {
typ = "gauge"
}
fmt.Fprintf(w, "# TYPE %s %s\n%s %v\n", name, typ, name, v.Value())
return
case *metrics.Set:
v.Do(func(kv expvar.KeyValue) {

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build windows || js || wasip1
//go:build windows || js
package logger

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !windows && !js && !wasip1
//go:build !windows && !js
package logger

View File

@@ -75,15 +75,6 @@ func (v SliceView[T, V]) Len() int { return len(v.ж) }
// At returns a View of the element at index `i` of the slice.
func (v SliceView[T, V]) At(i int) V { return v.ж[i].View() }
// SliceFrom returns v[i:].
func (v SliceView[T, V]) SliceFrom(i int) SliceView[T, V] { return SliceView[T, V]{v.ж[i:]} }
// SliceTo returns v[:i]
func (v SliceView[T, V]) SliceTo(i int) SliceView[T, V] { return SliceView[T, V]{v.ж[:i]} }
// Slice returns v[i:j]
func (v SliceView[T, V]) Slice(i, j int) SliceView[T, V] { return SliceView[T, V]{v.ж[i:j]} }
// AppendTo appends the underlying slice values to dst.
func (v SliceView[T, V]) AppendTo(dst []V) []V {
for _, x := range v.ж {
@@ -131,15 +122,6 @@ func (v Slice[T]) Len() int { return len(v.ж) }
// At returns the element at index `i` of the slice.
func (v Slice[T]) At(i int) T { return v.ж[i] }
// SliceFrom returns v[i:].
func (v Slice[T]) SliceFrom(i int) Slice[T] { return Slice[T]{v.ж[i:]} }
// SliceTo returns v[:i]
func (v Slice[T]) SliceTo(i int) Slice[T] { return Slice[T]{v.ж[:i]} }
// Slice returns v[i:j]
func (v Slice[T]) Slice(i, j int) Slice[T] { return Slice[T]{v.ж[i:j]} }
// AppendTo appends the underlying slice values to dst.
func (v Slice[T]) AppendTo(dst []T) []T {
return append(dst, v.ж...)

View File

@@ -128,13 +128,4 @@ func TestViewUtils(t *testing.T) {
c.Check(SliceEqualAnyOrder(v, SliceOf([]string{"bar", "foo"})), qt.Equals, true)
c.Check(SliceEqualAnyOrder(v, SliceOf([]string{"foo"})), qt.Equals, false)
c.Check(SliceEqualAnyOrder(SliceOf([]string{"a", "a", "b"}), SliceOf([]string{"a", "b", "b"})), qt.Equals, false)
c.Check(SliceEqualAnyOrder(
SliceOf([]string{"a", "b", "c"}).SliceFrom(1),
SliceOf([]string{"b", "c"})),
qt.Equals, true)
c.Check(SliceEqualAnyOrder(
SliceOf([]string{"a", "b", "c"}).Slice(1, 2),
SliceOf([]string{"b", "c"}).SliceTo(1)),
qt.Equals, true)
}

View File

@@ -11,16 +11,15 @@ import (
"strconv"
)
// ScrubbedGoroutineDump returns either the current goroutine's stack or all
// goroutines' stacks, but with the actual values of arguments scrubbed out,
// lest it contain some private key material.
func ScrubbedGoroutineDump(all bool) []byte {
// ScrubbedGoroutineDump returns the list of all current goroutines, but with the actual
// values of arguments scrubbed out, lest it contain some private key material.
func ScrubbedGoroutineDump() []byte {
var buf []byte
// Grab stacks multiple times into increasingly larger buffer sizes
// to minimize the risk that we blow past our iOS memory limit.
for size := 1 << 10; size <= 1<<20; size += 1 << 10 {
buf = make([]byte, size)
buf = buf[:runtime.Stack(buf, all)]
buf = buf[:runtime.Stack(buf, true)]
if len(buf) < size {
// It fit.
break

View File

@@ -6,7 +6,7 @@ package goroutines
import "testing"
func TestScrubbedGoroutineDump(t *testing.T) {
t.Logf("Got:\n%s\n", ScrubbedGoroutineDump(true))
t.Logf("Got:\n%s\n", ScrubbedGoroutineDump())
}
func TestScrubHex(t *testing.T) {

View File

@@ -16,7 +16,6 @@ import (
"github.com/josharian/native"
"golang.org/x/sys/unix"
"tailscale.com/types/logger"
"tailscale.com/util/cmpx"
)
// DebugNetfilter prints debug information about netfilter rules to the
@@ -53,18 +52,27 @@ func DebugNetfilter(logf logger.Logf) error {
for _, ex := range rule.Exprs {
switch v := ex.(type) {
case *expr.Meta:
key := cmpx.Or(metaKeyNames[v.Key], "UNKNOWN")
key := metaKeyNames[v.Key]
if key == "" {
key = "UNKNOWN"
}
logf("netfilter: Meta: key=%s source_register=%v register=%d", key, v.SourceRegister, v.Register)
case *expr.Cmp:
op := cmpx.Or(cmpOpNames[v.Op], "UNKNOWN")
op := cmpOpNames[v.Op]
if op == "" {
op = "UNKNOWN"
}
logf("netfilter: Cmp: op=%s register=%d data=%s", op, v.Register, formatMaybePrintable(v.Data))
case *expr.Counter:
// don't print
case *expr.Verdict:
kind := cmpx.Or(verdictNames[v.Kind], "UNKNOWN")
kind := verdictNames[v.Kind]
if kind == "" {
kind = "UNKNOWN"
}
logf("netfilter: Verdict: kind=%s data=%s", kind, v.Chain)
case *expr.Target:

View File

@@ -42,18 +42,3 @@ func Shuffle[S ~[]T, T any](s S) {
s[i], s[j] = s[j], s[i]
}
}
// Partition returns two slices, the first containing the elements of the input
// slice for which the callback evaluates to true, the second containing the rest.
//
// This function does not mutate s.
func Partition[S ~[]T, T any](s S, cb func(T) bool) (trues, falses S) {
for _, elem := range s {
if cb(elem) {
trues = append(trues, elem)
} else {
falses = append(falses, elem)
}
}
return
}

View File

@@ -44,7 +44,6 @@ func BenchmarkInterleave(b *testing.B) {
)
}
}
func TestShuffle(t *testing.T) {
var sl []int
for i := 0; i < 100; i++ {
@@ -65,23 +64,3 @@ func TestShuffle(t *testing.T) {
t.Errorf("expected shuffle after 10 tries")
}
}
func TestPartition(t *testing.T) {
var sl []int
for i := 1; i <= 10; i++ {
sl = append(sl, i)
}
evens, odds := Partition(sl, func(elem int) bool {
return elem%2 == 0
})
wantEvens := []int{2, 4, 6, 8, 10}
wantOdds := []int{1, 3, 5, 7, 9}
if !reflect.DeepEqual(evens, wantEvens) {
t.Errorf("evens: got %v, want %v", evens, wantEvens)
}
if !reflect.DeepEqual(odds, wantOdds) {
t.Errorf("odds: got %v, want %v", odds, wantOdds)
}
}

View File

@@ -5090,58 +5090,40 @@ func betterAddr(a, b addrLatency) bool {
if !a.IsValid() {
return false
}
// Each address starts with a set of points (from 0 to 100) that
// represents how much faster they are than the highest-latency
// endpoint. For example, if a has latency 200ms and b has latency
// 190ms, then a starts with 0 points and b starts with 5 points since
// it's 5% faster.
var aPoints, bPoints int
if a.latency > b.latency && a.latency > 0 {
bPoints = int(100 - ((b.latency * 100) / a.latency))
} else if b.latency > 0 {
aPoints = int(100 - ((a.latency * 100) / b.latency))
if a.Addr().Is6() && b.Addr().Is4() {
// Prefer IPv6 for being a bit more robust, as long as
// the latencies are roughly equivalent.
if a.latency/10*9 < b.latency {
return true
}
} else if a.Addr().Is4() && b.Addr().Is6() {
if betterAddr(b, a) {
return false
}
}
// Prefer private IPs over public IPs as long as the latencies are
// roughly equivalent, since it's less likely that a user will have to
// pay for the bandwidth in a cloud environment.
// If we get here, then both addresses are the same IP type (i.e. both
// IPv4 or both IPv6). All decisions below are made solely on latency.
//
// Additionally, prefer any loopback address strongly over non-loopback
// addresses.
if a.Addr().IsLoopback() {
aPoints += 50
} else if a.Addr().IsPrivate() {
aPoints += 20
}
if b.Addr().IsLoopback() {
bPoints += 50
} else if b.Addr().IsPrivate() {
bPoints += 20
}
// Prefer IPv6 for being a bit more robust, as long as
// the latencies are roughly equivalent.
if a.Addr().Is6() {
aPoints += 10
}
if b.Addr().Is6() {
bPoints += 10
// Determine how much the latencies differ; we ensure the larger
// latency is the denominator, so this fraction will always be <= 1.0.
var latencyFraction float64
if a.latency >= b.latency {
latencyFraction = float64(b.latency) / float64(a.latency)
} else {
latencyFraction = float64(a.latency) / float64(b.latency)
}
// Don't change anything if the latency improvement is less than 1%; we
// want a bit of "stickiness" (a.k.a. hysteresis) to avoid flapping if
// there's two roughly-equivalent endpoints.
//
// Points are essentially the percentage improvement of latency vs. the
// slower endpoint; absent any boosts from private IPs, IPv6, etc., a
// will be a better address than b by a fraction of 1% or less if
// aPoints <= 1 and bPoints == 0.
if aPoints <= 1 && bPoints == 0 {
if latencyFraction >= 0.99 {
return false
}
return aPoints > bPoints
// The total difference is >1%, so a is better than b if it's
// lower-latency.
return a.latency < b.latency
}
// endpoint.mu must be held.

View File

@@ -303,11 +303,11 @@ func trySetSocketBuffer(pconn nettype.PacketConn, logf logger.Logf) {
rc.Control(func(fd uintptr) {
errRcv = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_RCVBUFFORCE, socketBufferSize)
if errRcv != nil {
logf("magicsock: [warning] failed to force-set UDP read buffer size to %d: %v; using kernel default values (impacts throughput only)", socketBufferSize, errRcv)
logf("magicsock: failed to force-set UDP read buffer size to %d: %v", socketBufferSize, errRcv)
}
errSnd = syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_SNDBUFFORCE, socketBufferSize)
if errSnd != nil {
logf("magicsock: [warning] failed to force-set UDP write buffer size to %d: %v; using kernel default values (impacts throughput only)", socketBufferSize, errSnd)
logf("magicsock: failed to force-set UDP write buffer size to %d: %v", socketBufferSize, errSnd)
}
})
}

View File

@@ -1631,101 +1631,52 @@ func TestBetterAddr(t *testing.T) {
return addrLatency{netip.MustParseAddrPort(ipps), d}
}
zero := addrLatency{}
const (
publicV4 = "1.2.3.4:555"
publicV4_2 = "5.6.7.8:999"
publicV6 = "[2001::5]:123"
privateV4 = "10.0.0.2:123"
)
tests := []struct {
a, b addrLatency
want bool // whether a is better than b
want bool
}{
{a: zero, b: zero, want: false},
{a: al(publicV4, 5*ms), b: zero, want: true},
{a: zero, b: al(publicV4, 5*ms), want: false},
{a: al(publicV4, 5*ms), b: al(publicV4_2, 10*ms), want: true},
{a: al(publicV4, 5*ms), b: al(publicV4, 10*ms), want: false}, // same IPPort
{a: al("10.0.0.2:123", 5*ms), b: zero, want: true},
{a: zero, b: al("10.0.0.2:123", 5*ms), want: false},
{a: al("10.0.0.2:123", 5*ms), b: al("1.2.3.4:555", 10*ms), want: true},
{a: al("10.0.0.2:123", 5*ms), b: al("10.0.0.2:123", 10*ms), want: false}, // same IPPort
// Don't prefer b to a if it's not substantially better.
{a: al(publicV4, 100*ms), b: al(publicV4_2, 100*ms), want: false},
{a: al(publicV4, 100*ms), b: al(publicV4_2, 101*ms), want: false},
{a: al(publicV4, 100*ms), b: al(publicV4_2, 103*ms), want: true},
// Latencies of zero don't result in a divide-by-zero
{a: al(publicV4, 0), b: al(publicV4_2, 0), want: false},
// Prefer private IPs to public IPs if roughly equivalent...
{
a: al(privateV4, 100*ms),
b: al(publicV4, 91*ms),
want: true,
},
{
a: al(publicV4, 91*ms),
b: al(privateV4, 100*ms),
want: false,
},
// ... but not if the private IP is slower.
{
a: al(privateV4, 100*ms),
b: al(publicV4, 30*ms),
want: false,
},
{
a: al(publicV4, 30*ms),
b: al(privateV4, 100*ms),
want: true,
},
{a: al("10.0.0.2:123", 100*ms), b: al("1.2.3.4:555", 101*ms), want: false},
{a: al("10.0.0.2:123", 100*ms), b: al("1.2.3.4:555", 103*ms), want: true},
// Prefer IPv6 if roughly equivalent:
{
a: al(publicV6, 100*ms),
b: al(publicV4, 91*ms),
a: al("[2001::5]:123", 100*ms),
b: al("1.2.3.4:555", 91*ms),
want: true,
},
{
a: al(publicV4, 91*ms),
b: al(publicV6, 100*ms),
a: al("1.2.3.4:555", 91*ms),
b: al("[2001::5]:123", 100*ms),
want: false,
},
// But not if IPv4 is much faster:
{
a: al(publicV6, 100*ms),
b: al(publicV4, 30*ms),
a: al("[2001::5]:123", 100*ms),
b: al("1.2.3.4:555", 30*ms),
want: false,
},
{
a: al(publicV4, 30*ms),
b: al(publicV6, 100*ms),
a: al("1.2.3.4:555", 30*ms),
b: al("[2001::5]:123", 100*ms),
want: true,
},
// Private IPs are preferred over public IPs even if the public
// IP is IPv6.
{
a: al("192.168.0.1:555", 100*ms),
b: al("[2001::5]:123", 101*ms),
want: true,
},
{
a: al("[2001::5]:123", 101*ms),
b: al("192.168.0.1:555", 100*ms),
want: false,
},
}
for i, tt := range tests {
for _, tt := range tests {
got := betterAddr(tt.a, tt.b)
if got != tt.want {
t.Errorf("[%d] betterAddr(%+v, %+v) = %v; want %v", i, tt.a, tt.b, got, tt.want)
t.Errorf("betterAddr(%+v, %+v) = %v; want %v", tt.a, tt.b, got, tt.want)
continue
}
gotBack := betterAddr(tt.b, tt.a)
if got && gotBack {
t.Errorf("[%d] betterAddr(%+v, %+v) and betterAddr(%+v, %+v) both unexpectedly true", i, tt.a, tt.b, tt.b, tt.a)
t.Errorf("betterAddr(%+v, %+v) and betterAddr(%+v, %+v) both unexpectedly true", tt.a, tt.b, tt.b, tt.a)
}
}

View File

@@ -256,7 +256,7 @@ func (ns *Impl) Start(lb *ipnlocal.LocalBackend) error {
ns.e.AddNetworkMapCallback(ns.updateIPs)
// size = 0 means use default buffer size
const tcpReceiveBufferSize = 0
const maxInFlightConnectionAttempts = 1024
const maxInFlightConnectionAttempts = 16
tcpFwd := tcp.NewForwarder(ns.ipstack, tcpReceiveBufferSize, maxInFlightConnectionAttempts, ns.acceptTCP)
udpFwd := udp.NewForwarder(ns.ipstack, ns.acceptUDP)
ns.ipstack.SetTransportProtocolHandler(tcp.ProtocolNumber, ns.wrapProtoHandler(tcpFwd.HandlePacket))
@@ -537,6 +537,10 @@ func (ns *Impl) isLocalIP(ip netip.Addr) bool {
return ns.atomicIsLocalIPFunc.Load()(ip)
}
func (ns *Impl) processSSH() bool {
return ns.lb != nil && ns.lb.ShouldRunSSH()
}
func (ns *Impl) peerAPIPortAtomic(ip netip.Addr) *atomic.Uint32 {
if ip.Is4() {
return &ns.peerapiPort4Atomic
@@ -836,7 +840,7 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
// request until we're sure that the connection can be handled by this
// endpoint. This function sets up the TCP connection and should be
// called immediately before a connection is handled.
getConnOrReset := func(opts ...tcpip.SettableSocketOption) *gonet.TCPConn {
createConn := func(opts ...tcpip.SettableSocketOption) *gonet.TCPConn {
ep, err := r.CreateEndpoint(&wq)
if err != nil {
ns.logf("CreateEndpoint error for %s: %v", stringifyTEI(reqDetails), err)
@@ -875,7 +879,7 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
// DNS
if reqDetails.LocalPort == 53 && (dialIP == magicDNSIP || dialIP == magicDNSIPv6) {
c := getConnOrReset()
c := createConn()
if c == nil {
return
}
@@ -884,13 +888,53 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
}
if ns.lb != nil {
handler, opts := ns.lb.TCPHandlerForDst(clientRemoteAddrPort, dstAddrPort)
if handler != nil {
c := getConnOrReset(opts...) // will send a RST if it fails
if reqDetails.LocalPort == 22 && ns.processSSH() && ns.isLocalIP(dialIP) {
// Use a higher keepalive idle time for SSH connections, as they are
// typically long lived and idle connections are more likely to be
// intentional. Ideally we would turn this off entirely, but we can't
// tell the difference between a long lived connection that is idle
// vs a connection that is dead because the peer has gone away.
// We pick 72h as that is typically sufficient for a long weekend.
idle := tcpip.KeepaliveIdleOption(72 * time.Hour)
c := createConn(&idle)
if c == nil {
return
}
handler(c)
if err := ns.lb.HandleSSHConn(c); err != nil {
ns.logf("ssh error: %v", err)
}
return
}
if port, ok := ns.lb.GetPeerAPIPort(dialIP); ok {
if reqDetails.LocalPort == port && ns.isLocalIP(dialIP) {
c := createConn()
if c == nil {
return
}
src := netip.AddrPortFrom(clientRemoteIP, reqDetails.RemotePort)
dst := netip.AddrPortFrom(dialIP, port)
ns.lb.ServePeerAPIConnection(src, dst, c)
return
}
}
if reqDetails.LocalPort == 80 && (dialIP == magicDNSIP || dialIP == magicDNSIPv6) {
c := createConn()
if c == nil {
return
}
ns.lb.HandleQuad100Port80Conn(c)
return
}
if ns.lb.ShouldInterceptTCPPort(reqDetails.LocalPort) && ns.isLocalIP(dialIP) {
getTCPConn := func() (_ net.Conn, ok bool) {
c := createConn()
return c, c != nil
}
sendRST := func() {
r.Complete(true)
}
ns.lb.HandleInterceptedTCPConn(reqDetails.LocalPort, clientRemoteAddrPort, getTCPConn, sendRST)
return
}
}
@@ -902,7 +946,7 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
r.Complete(true)
return
}
c := getConnOrReset() // will send a RST if it fails
c := createConn() // will send a RST if it fails
if c == nil {
return
}
@@ -915,7 +959,7 @@ func (ns *Impl) acceptTCP(r *tcp.ForwarderRequest) {
}
dialAddr := netip.AddrPortFrom(dialIP, uint16(reqDetails.LocalPort))
if !ns.forwardTCP(getConnOrReset, clientRemoteIP, &wq, dialAddr) {
if !ns.forwardTCP(createConn, clientRemoteIP, &wq, dialAddr) {
r.Complete(true) // sends a RST
}
}

View File

@@ -60,6 +60,11 @@ type watchdogEngine struct {
inFlightCtr uint64
}
// inDebugger returns true if the process has $TS_DEBUGGER set. This should be
// used to avoid terminating the process when operations take an unusual amount
// of time, as would be caused by a debugger breakpoint.
var inDebugger = envknob.RegisterBool("TS_DEBUGGER")
func (e *watchdogEngine) watchdogErr(name string, fn func() error) error {
// Track all in-flight operations so we can print more useful error
// messages on watchdog failure
@@ -88,6 +93,11 @@ func (e *watchdogEngine) watchdogErr(name string, fn func() error) error {
t.Stop()
return err
case <-t.C:
if inDebugger() {
e.logf("wgengine: watchdog timeout on %s (in debugger, maybe process was frozen?)", name)
return nil
}
buf := new(strings.Builder)
pprof.Lookup("goroutine").WriteTo(buf, 1)
e.logf("wgengine watchdog stacks:\n%s", buf.String())

View File

@@ -27,7 +27,6 @@ alligator
butterfly
iguana
pineapplefish
pinecone
anaconda
puffin
cardassian

View File

@@ -542,5 +542,4 @@ coelacanth
llama
shrimp
prawn
lobster
chipmunk
lobster