Compare commits

..

1 Commits

Author SHA1 Message Date
Tom DNetto 4df98808da cmd/derper: add default-off debug endpoint for profiling
Updates https://github.com/tailscale/corp/issues/11492
Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-06-07 13:06:12 -07:00
146 changed files with 1512 additions and 9038 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 - name: golangci-lint
# Note: this is the 'v3' tag as of 2023-04-17 # Note: this is the 'v3' tag as of 2023-04-17
uses: golangci/golangci-lint-action@639cd343e1d3b897ff35927a75193d57cfcba299 uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5
with: with:
version: v1.52.2 version: v1.52.2

View File

@ -90,11 +90,11 @@ jobs:
- name: build test wrapper - name: build test wrapper
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
- name: test all - name: test all
run: PATH=$PWD/tool:$PATH /tmp/testwrapper ./... ${{matrix.buildflags}} run: ./tool/go test ${{matrix.buildflags}} -exec=/tmp/testwrapper
env: env:
GOARCH: ${{ matrix.goarch }} GOARCH: ${{ matrix.goarch }}
- name: bench all - name: bench all
run: PATH=$PWD/tool:$PATH /tmp/testwrapper ./... ${{matrix.buildflags}} -bench=. -benchtime=1x -run=^$ run: ./tool/go test ${{matrix.buildflags}} -exec=/tmp/testwrapper -test.bench=. -test.benchtime=1x -test.run=^$
env: env:
GOARCH: ${{ matrix.goarch }} GOARCH: ${{ matrix.goarch }}
- name: check that no tracked files changed - name: check that no tracked files changed

View File

@ -47,7 +47,8 @@ RUN go install \
golang.org/x/crypto/ssh \ golang.org/x/crypto/ssh \
golang.org/x/crypto/acme \ golang.org/x/crypto/acme \
nhooyr.io/websocket \ nhooyr.io/websocket \
github.com/mdlayher/netlink github.com/mdlayher/netlink \
golang.zx2c4.com/wireguard/device
COPY . . COPY . .
@ -72,4 +73,4 @@ RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
COPY --from=build-env /go/bin/* /usr/local/bin/ COPY --from=build-env /go/bin/* /usr/local/bin/
# For compat with the previous run.sh, although ideally you should be # For compat with the previous run.sh, although ideally you should be
# using build_docker.sh which sets an entrypoint for the image. # 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 # SPDX-License-Identifier: BSD-3-Clause
FROM alpine:3.16 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

21
api.md
View File

@ -101,8 +101,8 @@ You can also [list all devices in the tailnet](#list-tailnet-devices) to get the
``` jsonc ``` jsonc
{ {
// addresses (array of strings) is a list of Tailscale IP // addresses (array of strings) is a list of Tailscale IP
// addresses for the device, including both IPv4 (formatted as 100.x.y.z) // addresses for the device, including both ipv4 (formatted as 100.x.y.z)
// and IPv6 (formatted as fd7a:115c:a1e0:a:b:c:d:e) addresses. // and ipv6 (formatted as fd7a:115c:a1e0:a:b:c:d:e) addresses.
"addresses": [ "addresses": [
"100.87.74.78", "100.87.74.78",
"fd7a:115c:a1e0:ac82:4843:ca90:697d:c36e" "fd7a:115c:a1e0:ac82:4843:ca90:697d:c36e"
@ -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 (int) is the duration in seconds a new key is valid.
"expirySeconds": 86400 "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. Specifies the duration in seconds until the key should expire.
Defaults to 90 days if not supplied. 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 ### Request example
``` jsonc ``` jsonc
@ -1333,8 +1325,7 @@ curl "https://api.tailscale.com/api/v2/tailnet/example.com/keys" \
} }
} }
}, },
"expirySeconds": 86400, "expirySeconds": 86400
"description": "dev access"
}' }'
``` ```
@ -1360,8 +1351,7 @@ It holds the capabilities specified in the request and can no longer be retrieve
"tags": [ "tag:example" ] "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

@ -946,21 +946,6 @@ func (lc *LocalClient) NetworkLockForceLocalDisable(ctx context.Context) error {
return nil return nil
} }
// NetworkLockVerifySigningDeeplink verifies the network lock deeplink contained
// in url and returns information extracted from it.
func (lc *LocalClient) NetworkLockVerifySigningDeeplink(ctx context.Context, url string) (*tka.DeeplinkValidationResult, error) {
vr := struct {
URL string
}{url}
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/verify-deeplink", 200, jsonBody(vr))
if err != nil {
return nil, fmt.Errorf("sending verify-deeplink: %w", err)
}
return decodeJSON[*tka.DeeplinkValidationResult](body)
}
// SetServeConfig sets or replaces the serving settings. // SetServeConfig sets or replaces the serving settings.
// If config is nil, settings are cleared and serving is disabled. // If config is nil, settings are cleared and serving is disabled.
func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error { func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {

View File

@ -72,7 +72,7 @@ func NewManualCertManager(certdir, hostname string) (certProvider, error) {
return nil, fmt.Errorf("can not load cert: %w", err) return nil, fmt.Errorf("can not load cert: %w", err)
} }
if err := x509Cert.VerifyHostname(hostname); err != nil { if err := x509Cert.VerifyHostname(hostname); err != nil {
// return nil, fmt.Errorf("cert invalid for hostname %q: %w", hostname, err) return nil, fmt.Errorf("cert invalid for hostname %q: %w", hostname, err)
} }
return &manualCertManager{cert: &cert, hostname: hostname}, nil return &manualCertManager{cert: &cert, hostname: hostname}, nil
} }
@ -89,7 +89,7 @@ func (m *manualCertManager) TLSConfig() *tls.Config {
func (m *manualCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) { func (m *manualCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
if hi.ServerName != m.hostname { if hi.ServerName != m.hostname {
//return nil, fmt.Errorf("cert mismatch with hostname: %q", hi.ServerName) return nil, fmt.Errorf("cert mismatch with hostname: %q", hi.ServerName)
} }
// Return a shallow copy of the cert so the caller can append to its // Return a shallow copy of the cert so the caller can append to its

View File

@ -12,16 +12,9 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus 💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
github.com/fxamacker/cbor/v2 from tailscale.com/tka github.com/fxamacker/cbor/v2 from tailscale.com/tka
github.com/golang/groupcache/lru from tailscale.com/net/dnscache github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/golang/protobuf/proto from github.com/matttproud/golang_protobuf_extensions/pbutil+ github.com/golang/protobuf/proto from github.com/matttproud/golang_protobuf_extensions/pbutil+
L github.com/google/nftables from tailscale.com/util/linuxfw
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
L github.com/google/nftables/expr from github.com/google/nftables+
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
L github.com/google/nftables/xt from github.com/google/nftables/expr+
github.com/hdevalence/ed25519consensus from tailscale.com/tka github.com/hdevalence/ed25519consensus from tailscale.com/tka
L github.com/josharian/native from github.com/mdlayher/netlink+ L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+ L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
@ -30,7 +23,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
github.com/matttproud/golang_protobuf_extensions/pbutil from github.com/prometheus/common/expfmt github.com/matttproud/golang_protobuf_extensions/pbutil from github.com/prometheus/common/expfmt
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+ L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+ L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket 💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
💣 github.com/prometheus/client_golang/prometheus from tailscale.com/tsweb/promvarz 💣 github.com/prometheus/client_golang/prometheus from tailscale.com/tsweb/promvarz
@ -42,9 +34,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
LD github.com/prometheus/procfs from github.com/prometheus/client_golang/prometheus LD github.com/prometheus/procfs from github.com/prometheus/client_golang/prometheus
LD github.com/prometheus/procfs/internal/fs from github.com/prometheus/procfs LD github.com/prometheus/procfs/internal/fs from github.com/prometheus/procfs
LD github.com/prometheus/procfs/internal/util from github.com/prometheus/procfs LD github.com/prometheus/procfs/internal/util from github.com/prometheus/procfs
L 💣 github.com/tailscale/netlink from tailscale.com/util/linuxfw
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
L github.com/vishvananda/netns from github.com/tailscale/netlink+
github.com/x448/float16 from github.com/fxamacker/cbor/v2 github.com/x448/float16 from github.com/fxamacker/cbor/v2
💣 go4.org/mem from tailscale.com/client/tailscale+ 💣 go4.org/mem from tailscale.com/client/tailscale+
go4.org/netipx from tailscale.com/wgengine/filter go4.org/netipx from tailscale.com/wgengine/filter
@ -77,20 +66,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
google.golang.org/protobuf/runtime/protoimpl from github.com/golang/protobuf/proto+ google.golang.org/protobuf/runtime/protoimpl from github.com/golang/protobuf/proto+
google.golang.org/protobuf/types/descriptorpb from google.golang.org/protobuf/reflect/protodesc google.golang.org/protobuf/types/descriptorpb from google.golang.org/protobuf/reflect/protodesc
google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+ google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+
L gvisor.dev/gvisor/pkg/abi from gvisor.dev/gvisor/pkg/abi/linux
L 💣 gvisor.dev/gvisor/pkg/abi/linux from tailscale.com/util/linuxfw
L gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/abi/linux
L gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/abi/linux
L 💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/abi/linux+
L 💣 gvisor.dev/gvisor/pkg/hostarch from gvisor.dev/gvisor/pkg/abi/linux+
L gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
L gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context
L gvisor.dev/gvisor/pkg/marshal from gvisor.dev/gvisor/pkg/abi/linux+
L 💣 gvisor.dev/gvisor/pkg/marshal/primitive from gvisor.dev/gvisor/pkg/abi/linux
L 💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/abi/linux+
L gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
L 💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/linewriter+
L gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context
nhooyr.io/websocket from tailscale.com/cmd/derper+ nhooyr.io/websocket from tailscale.com/cmd/derper+
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
@ -118,7 +93,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/net/packet from tailscale.com/wgengine/filter tailscale.com/net/packet from tailscale.com/wgengine/filter
tailscale.com/net/sockstats from tailscale.com/derp/derphttp tailscale.com/net/sockstats from tailscale.com/derp/derphttp
tailscale.com/net/stun from tailscale.com/cmd/derper tailscale.com/net/stun from tailscale.com/cmd/derper
L tailscale.com/net/tcpinfo from tailscale.com/derp
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
tailscale.com/net/tsaddr from tailscale.com/ipn+ tailscale.com/net/tsaddr from tailscale.com/ipn+
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+ 💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
@ -151,14 +125,12 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
W tailscale.com/util/clientmetric from tailscale.com/net/tshttpproxy W tailscale.com/util/clientmetric from tailscale.com/net/tshttpproxy
tailscale.com/util/cloudenv from tailscale.com/hostinfo+ tailscale.com/util/cloudenv from tailscale.com/hostinfo+
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy 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 L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/hostinfo+ tailscale.com/util/dnsname from tailscale.com/hostinfo+
tailscale.com/util/httpm from tailscale.com/client/tailscale tailscale.com/util/httpm from tailscale.com/client/tailscale
tailscale.com/util/lineread from tailscale.com/hostinfo+ tailscale.com/util/lineread from tailscale.com/hostinfo+
L 💣 tailscale.com/util/linuxfw from tailscale.com/net/netns
tailscale.com/util/mak from tailscale.com/syncs+ tailscale.com/util/mak from tailscale.com/syncs+
tailscale.com/util/multierr from tailscale.com/health+ tailscale.com/util/multierr from tailscale.com/health
tailscale.com/util/set from tailscale.com/health+ tailscale.com/util/set from tailscale.com/health+
tailscale.com/util/singleflight from tailscale.com/net/dnscache tailscale.com/util/singleflight from tailscale.com/net/dnscache
tailscale.com/util/slicesx from tailscale.com/cmd/derper+ tailscale.com/util/slicesx from tailscale.com/cmd/derper+
@ -182,7 +154,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+ golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/exp/constraints from golang.org/x/exp/slices golang.org/x/exp/constraints from golang.org/x/exp/slices
golang.org/x/exp/maps from tailscale.com/types/views
golang.org/x/exp/slices from tailscale.com/net/tsaddr+ golang.org/x/exp/slices from tailscale.com/net/tsaddr+
L golang.org/x/net/bpf from github.com/mdlayher/netlink+ L golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/dns/dnsmessage from net+ golang.org/x/net/dns/dnsmessage from net+
@ -208,7 +179,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
bytes from bufio+ bytes from bufio+
compress/flate from compress/gzip+ compress/flate from compress/gzip+
compress/gzip from internal/profile+ compress/gzip from internal/profile+
L compress/zlib from debug/elf
container/list from crypto/tls+ container/list from crypto/tls+
context from crypto/tls+ context from crypto/tls+
crypto from crypto/ecdsa+ crypto from crypto/ecdsa+
@ -232,8 +202,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
crypto/tls from golang.org/x/crypto/acme+ crypto/tls from golang.org/x/crypto/acme+
crypto/x509 from crypto/tls+ crypto/x509 from crypto/tls+
crypto/x509/pkix from crypto/x509+ 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+ embed from crypto/internal/nistec+
encoding from encoding/json+ encoding from encoding/json+
encoding/asn1 from crypto/x509+ encoding/asn1 from crypto/x509+
@ -249,7 +217,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
fmt from compress/flate+ fmt from compress/flate+
go/token from google.golang.org/protobuf/internal/strs go/token from google.golang.org/protobuf/internal/strs
hash from crypto+ hash from crypto+
L hash/adler32 from compress/zlib
hash/crc32 from compress/gzip+ hash/crc32 from compress/gzip+
hash/fnv from google.golang.org/protobuf/internal/detrand hash/fnv from google.golang.org/protobuf/internal/detrand
hash/maphash from go4.org/mem hash/maphash from go4.org/mem
@ -258,7 +225,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
io/fs from crypto/x509+ io/fs from crypto/x509+
io/ioutil from github.com/mitchellh/go-ps+ io/ioutil from github.com/mitchellh/go-ps+
log from expvar+ log from expvar+
log/internal from log
math from compress/flate+ math from compress/flate+
math/big from crypto/dsa+ math/big from crypto/dsa+
math/bits from compress/flate+ math/bits from compress/flate+

View File

@ -12,15 +12,19 @@ import (
"expvar" "expvar"
"flag" "flag"
"fmt" "fmt"
"html"
"io" "io"
"log" "log"
"math" "math"
"net" "net"
"net/http" "net/http"
"net/http/pprof"
"net/netip" "net/netip"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime"
"strconv"
"strings" "strings"
"time" "time"
@ -33,7 +37,7 @@ import (
"tailscale.com/net/stun" "tailscale.com/net/stun"
"tailscale.com/tsweb" "tailscale.com/tsweb"
"tailscale.com/types/key" "tailscale.com/types/key"
"tailscale.com/util/cmpx" "tailscale.com/version"
) )
var ( var (
@ -56,6 +60,8 @@ var (
acceptConnLimit = flag.Float64("accept-connection-limit", math.Inf(+1), "rate limit for accepting new connection") acceptConnLimit = flag.Float64("accept-connection-limit", math.Inf(+1), "rate limit for accepting new connection")
acceptConnBurst = flag.Int("accept-connection-burst", math.MaxInt, "burst limit for accepting new connection") acceptConnBurst = flag.Int("accept-connection-burst", math.MaxInt, "burst limit for accepting new connection")
debugAddr = flag.String("debug_addr", "", "listening address for debug server")
) )
var ( var (
@ -137,6 +143,7 @@ func main() {
if *dev { if *dev {
*addr = ":3340" // above the keys DERP *addr = ":3340" // above the keys DERP
*debugAddr = "localhost:8383"
log.Printf("Running in dev mode.") log.Printf("Running in dev mode.")
tsweb.DevMode = true tsweb.DevMode = true
} }
@ -223,6 +230,11 @@ func main() {
go serveSTUN(listenHost, *stunPort) go serveSTUN(listenHost, *stunPort)
} }
if *debugAddr != "" {
log.Printf("running debug server on %s", *debugAddr)
go runDebugServer(*debugAddr, s)
}
quietLogger := log.New(logFilter{}, "", 0) quietLogger := log.New(logFilter{}, "", 0)
httpsrv := &http.Server{ httpsrv := &http.Server{
Addr: *addr, Addr: *addr,
@ -437,7 +449,11 @@ func defaultMeshPSKFile() string {
} }
func rateLimitedListenAndServeTLS(srv *http.Server) error { 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 { if err != nil {
return err return err
} }
@ -510,3 +526,79 @@ func (logFilter) Write(p []byte) (int, error) {
log.Printf("%s", p) log.Printf("%s", p)
return len(p), nil return len(p), nil
} }
func runDebugServer(addr string, s *derp.Server) {
mux := http.NewServeMux()
mux.Handle("/debug/vars", http.DefaultServeMux) // serve out expvar
mux.HandleFunc("/debug/varz", tsweb.VarzHandler) // expvar but in prometheus format
mux.HandleFunc("/debug/pprof/", pprof.Index)
mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
mux.HandleFunc("/debug/pprof/profile", pprof.Profile)
mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
mux.HandleFunc("/debug/pprof/trace", pprof.Trace)
mux.Handle("/debug/pprofrate", http.HandlerFunc(handlePprofRate))
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(200)
fmt.Fprintf(w, `<html><body>
<h1>DERP</h1>
<p>
This is a Tailscale DERP debug server.
</p>
<ul>
<li>Version: <pre style="display: inline">%s</pre>
<li><a href="/debug/vars">/debug/vars</a> (JSON)</li>
<li><a href="/debug/varz">/debug/varz</a> (Prometheus)</li>
<li><a href="/debug/pprof">/debug/pprof</a></li>
<li><a href="/debug/pprof/goroutine?debug=1">/debug/pprof/goroutine?debug=1</a> (goroutine summary)</li>
</ul>
`, html.EscapeString(version.Long()))
}))
srv := &http.Server{
Addr: addr,
Handler: mux,
}
if err := srv.ListenAndServe(); err != nil {
if err != http.ErrServerClosed {
log.Fatal(err)
}
}
}
// handlePprofRate adjusts the pprof rate.
//
// $ curl -d block=0 http://localhost:8383/debug/pprofrate
// $ curl -d mutex=0 http://localhost:8383/debug/pprofrate
//
// See:
//
// - https://pkg.go.dev/runtime#SetMutexProfileFraction
// - https://pkg.go.dev/runtime#SetBlockProfileRate
//
// And corp#5277.
func handlePprofRate(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "method not allowed; want POST", http.StatusMethodNotAllowed)
return
}
if s := r.FormValue("block"); s != "" {
v, err := strconv.Atoi(s)
if err != nil {
http.Error(w, "bad block value", http.StatusBadRequest)
return
}
runtime.SetBlockProfileRate(v)
fmt.Fprintf(w, "block set to %v\n", v)
}
if s := r.FormValue("mutex"); s != "" {
v, err := strconv.Atoi(s)
if err != nil {
http.Error(w, "bad mutex value", http.StatusBadRequest)
return
}
old := runtime.SetMutexProfileFraction(v)
fmt.Fprintf(w, "mutex changed from %v to %v\n", old, v)
}
}

View File

@ -16,7 +16,6 @@ import (
"golang.org/x/oauth2/clientcredentials" "golang.org/x/oauth2/clientcredentials"
"tailscale.com/client/tailscale" "tailscale.com/client/tailscale"
"tailscale.com/util/cmpx"
) )
func main() { func main() {
@ -40,7 +39,10 @@ func main() {
log.Fatal("at least one tag must be specified") 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{ credentials := clientcredentials.Config{
ClientID: clientID, ClientID: clientID,

View File

@ -26,7 +26,6 @@ import (
var ( var (
ports = flag.String("ports", "443", "comma-separated list of ports to proxy") ports = flag.String("ports", "443", "comma-separated list of ports to proxy")
wgPort = flag.Int("wg-listen-port", 0, "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
promoteHTTPS = flag.Bool("promote-https", true, "promote HTTP to HTTPS") promoteHTTPS = flag.Bool("promote-https", true, "promote HTTP to HTTPS")
) )
@ -41,7 +40,6 @@ func main() {
hostinfo.SetApp("sniproxy") hostinfo.SetApp("sniproxy")
var s server var s server
s.ts.Port = uint16(*wgPort)
defer s.ts.Close() defer s.ts.Close()
lc, err := s.ts.LocalClient() lc, err := s.ts.LocalClient()

View File

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

View File

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

View File

@ -465,16 +465,7 @@ func runNetworkLockSign(ctx context.Context, args []string) error {
} }
} }
err := localClient.NetworkLockSign(ctx, nodeKey, []byte(rotationKey.Verifier())) return localClient.NetworkLockSign(ctx, nodeKey, []byte(rotationKey.Verifier()))
// Provide a better help message for when someone clicks through the signing flow
// on the wrong device.
if err != nil && strings.Contains(err.Error(), "this node is not trusted by network lock") {
fmt.Fprintln(os.Stderr, "Error: Signing is not available on this device because it does not have a trusted tailnet lock key.")
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, "Try again on a signing device instead. Tailnet admins can see signing devices on the admin panel.")
fmt.Fprintln(os.Stderr)
}
return err
} }
var nlDisableCmd = &ffcli.Command{ var nlDisableCmd = &ffcli.Command{

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.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.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.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") fs.DurationVar(&pingArgs.timeout, "timeout", 5*time.Second, "timeout before giving up on a ping")
return fs return fs
})(), })(),

View File

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

View File

@ -29,7 +29,6 @@ import (
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/ipn/ipnstate" "tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/util/cmpx"
"tailscale.com/util/groupmember" "tailscale.com/util/groupmember"
"tailscale.com/version/distro" "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 // urlOfListenAddr parses a given listen address into a formatted URL
func urlOfListenAddr(addr string) string { func urlOfListenAddr(addr string) string {
host, port, _ := net.SplitHostPort(addr) 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 // authorize returns the name of the user accessing the web UI after verifying

View File

@ -10,15 +10,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+ W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
github.com/fxamacker/cbor/v2 from tailscale.com/tka github.com/fxamacker/cbor/v2 from tailscale.com/tka
github.com/golang/groupcache/lru from tailscale.com/net/dnscache github.com/golang/groupcache/lru from tailscale.com/net/dnscache
L github.com/google/nftables from tailscale.com/util/linuxfw
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
L github.com/google/nftables/expr from github.com/google/nftables+
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
L github.com/google/nftables/xt from github.com/google/nftables/expr+
github.com/google/uuid from tailscale.com/util/quarantine+ github.com/google/uuid from tailscale.com/util/quarantine+
github.com/hdevalence/ed25519consensus from tailscale.com/tka github.com/hdevalence/ed25519consensus from tailscale.com/tka
L github.com/josharian/native from github.com/mdlayher/netlink+ L github.com/josharian/native from github.com/mdlayher/netlink+
@ -30,7 +23,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
💣 github.com/mattn/go-isatty from github.com/mattn/go-colorable+ 💣 github.com/mattn/go-isatty from github.com/mattn/go-colorable+
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+ L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+ L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+ 💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
github.com/peterbourgon/ff/v3 from github.com/peterbourgon/ff/v3/ffcli github.com/peterbourgon/ff/v3 from github.com/peterbourgon/ff/v3/ffcli
@ -44,30 +36,13 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+ github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
L 💣 github.com/tailscale/netlink from tailscale.com/util/linuxfw
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
L github.com/vishvananda/netns from github.com/tailscale/netlink+
github.com/x448/float16 from github.com/fxamacker/cbor/v2 github.com/x448/float16 from github.com/fxamacker/cbor/v2
💣 go4.org/mem from tailscale.com/derp+ 💣 go4.org/mem from tailscale.com/derp+
go4.org/netipx from tailscale.com/wgengine/filter go4.org/netipx from tailscale.com/wgengine/filter
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+ W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
gopkg.in/yaml.v2 from sigs.k8s.io/yaml gopkg.in/yaml.v2 from sigs.k8s.io/yaml
L gvisor.dev/gvisor/pkg/abi from gvisor.dev/gvisor/pkg/abi/linux
L 💣 gvisor.dev/gvisor/pkg/abi/linux from tailscale.com/util/linuxfw
L gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/abi/linux
L gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/abi/linux
L 💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/abi/linux+
L 💣 gvisor.dev/gvisor/pkg/hostarch from gvisor.dev/gvisor/pkg/abi/linux+
L gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
L gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context
L gvisor.dev/gvisor/pkg/marshal from gvisor.dev/gvisor/pkg/abi/linux+
L 💣 gvisor.dev/gvisor/pkg/marshal/primitive from gvisor.dev/gvisor/pkg/abi/linux
L 💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/abi/linux+
L gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
L 💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/linewriter+
L gvisor.dev/gvisor/pkg/waiter from gvisor.dev/gvisor/pkg/context
k8s.io/client-go/util/homedir from tailscale.com/cmd/tailscale/cli k8s.io/client-go/util/homedir from tailscale.com/cmd/tailscale/cli
nhooyr.io/websocket from tailscale.com/derp/derphttp+ nhooyr.io/websocket from tailscale.com/derp/derphttp+
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
@ -109,7 +84,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/net/portmapper from tailscale.com/net/netcheck+ tailscale.com/net/portmapper from tailscale.com/net/netcheck+
tailscale.com/net/sockstats from tailscale.com/control/controlhttp+ tailscale.com/net/sockstats from tailscale.com/control/controlhttp+
tailscale.com/net/stun from tailscale.com/net/netcheck tailscale.com/net/stun from tailscale.com/net/netcheck
L tailscale.com/net/tcpinfo from tailscale.com/derp
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp+ tailscale.com/net/tlsdial from tailscale.com/derp/derphttp+
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+ tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+ 💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
@ -140,13 +114,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/util/clientmetric from tailscale.com/net/netcheck+ tailscale.com/util/clientmetric from tailscale.com/net/netcheck+
tailscale.com/util/cloudenv from tailscale.com/net/dnscache+ tailscale.com/util/cloudenv from tailscale.com/net/dnscache+
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy 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 L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+ tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
tailscale.com/util/httpm from tailscale.com/client/tailscale tailscale.com/util/httpm from tailscale.com/client/tailscale
tailscale.com/util/lineread from tailscale.com/net/interfaces+ tailscale.com/util/lineread from tailscale.com/net/interfaces+
L 💣 tailscale.com/util/linuxfw from tailscale.com/net/netns
tailscale.com/util/mak from tailscale.com/net/netcheck+ tailscale.com/util/mak from tailscale.com/net/netcheck+
tailscale.com/util/multierr from tailscale.com/control/controlhttp+ tailscale.com/util/multierr from tailscale.com/control/controlhttp+
tailscale.com/util/must from tailscale.com/cmd/tailscale/cli tailscale.com/util/must from tailscale.com/cmd/tailscale/cli
@ -173,7 +145,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/crypto/pbkdf2 from software.sslmate.com/src/go-pkcs12 golang.org/x/crypto/pbkdf2 from software.sslmate.com/src/go-pkcs12
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+ golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/exp/constraints from golang.org/x/exp/slices golang.org/x/exp/constraints from golang.org/x/exp/slices
golang.org/x/exp/maps from tailscale.com/types/views
golang.org/x/exp/slices from tailscale.com/net/tsaddr+ golang.org/x/exp/slices from tailscale.com/net/tsaddr+
golang.org/x/net/bpf from github.com/mdlayher/netlink+ golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/dns/dnsmessage from net+ golang.org/x/net/dns/dnsmessage from net+
@ -205,7 +176,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
bytes from bufio+ bytes from bufio+
compress/flate from compress/gzip+ compress/flate from compress/gzip+
compress/gzip from net/http compress/gzip from net/http
compress/zlib from image/png+ compress/zlib from image/png
container/list from crypto/tls+ container/list from crypto/tls+
context from crypto/tls+ context from crypto/tls+
crypto from crypto/ecdsa+ crypto from crypto/ecdsa+
@ -230,8 +201,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
crypto/x509 from crypto/tls+ crypto/x509 from crypto/tls+
crypto/x509/pkix from crypto/x509+ crypto/x509/pkix from crypto/x509+
database/sql/driver from github.com/google/uuid 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+ embed from tailscale.com/cmd/tailscale/cli+
encoding from encoding/json+ encoding from encoding/json+
encoding/asn1 from crypto/x509+ encoding/asn1 from crypto/x509+
@ -259,7 +228,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
io/fs from crypto/x509+ io/fs from crypto/x509+
io/ioutil from golang.org/x/sys/cpu+ io/ioutil from golang.org/x/sys/cpu+
log from expvar+ log from expvar+
log/internal from log
math from compress/flate+ math from compress/flate+
math/big from crypto/dsa+ math/big from crypto/dsa+
math/bits from compress/flate+ math/bits from compress/flate+

View File

@ -75,7 +75,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/smithy-go/transport/http from github.com/aws/aws-sdk-go-v2/aws/middleware+ L github.com/aws/smithy-go/transport/http from github.com/aws/aws-sdk-go-v2/aws/middleware+
L github.com/aws/smithy-go/transport/http/internal/io from github.com/aws/smithy-go/transport/http L github.com/aws/smithy-go/transport/http/internal/io from github.com/aws/smithy-go/transport/http
L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
LD 💣 github.com/creack/pty from tailscale.com/ssh/tailssh LD 💣 github.com/creack/pty from tailscale.com/ssh/tailssh
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com
W 💣 github.com/dblohm7/wingoes/com from tailscale.com/cmd/tailscaled W 💣 github.com/dblohm7/wingoes/com from tailscale.com/cmd/tailscaled
@ -86,12 +86,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns+ L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns+
github.com/golang/groupcache/lru from tailscale.com/net/dnscache github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header+ github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header+
L github.com/google/nftables from tailscale.com/util/linuxfw
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
L github.com/google/nftables/expr from github.com/google/nftables+
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
L github.com/google/nftables/xt from github.com/google/nftables/expr+
github.com/hdevalence/ed25519consensus from tailscale.com/tka github.com/hdevalence/ed25519consensus from tailscale.com/tka
L 💣 github.com/illarion/gonotify from tailscale.com/net/dns L 💣 github.com/illarion/gonotify from tailscale.com/net/dns
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
@ -115,7 +109,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/mdlayher/genetlink from tailscale.com/net/tstun L github.com/mdlayher/genetlink from tailscale.com/net/tstun
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+ L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+ L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket 💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
@ -160,18 +153,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
go4.org/netipx from tailscale.com/ipn/ipnlocal+ go4.org/netipx from tailscale.com/ipn/ipnlocal+
W 💣 golang.zx2c4.com/wintun from github.com/tailscale/wireguard-go/tun+ W 💣 golang.zx2c4.com/wintun from github.com/tailscale/wireguard-go/tun+
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/dns+ W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/dns+
L gvisor.dev/gvisor/pkg/abi from gvisor.dev/gvisor/pkg/abi/linux
L 💣 gvisor.dev/gvisor/pkg/abi/linux from tailscale.com/util/linuxfw
gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/tcpip+ gvisor.dev/gvisor/pkg/atomicbitops from gvisor.dev/gvisor/pkg/tcpip+
gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/bufferv2+ gvisor.dev/gvisor/pkg/bits from gvisor.dev/gvisor/pkg/bufferv2
💣 gvisor.dev/gvisor/pkg/bufferv2 from gvisor.dev/gvisor/pkg/tcpip+ 💣 gvisor.dev/gvisor/pkg/bufferv2 from gvisor.dev/gvisor/pkg/tcpip+
gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs+ gvisor.dev/gvisor/pkg/context from gvisor.dev/gvisor/pkg/refs
💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire+ 💣 gvisor.dev/gvisor/pkg/gohacks from gvisor.dev/gvisor/pkg/state/wire+
L 💣 gvisor.dev/gvisor/pkg/hostarch from gvisor.dev/gvisor/pkg/abi/linux+
gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log gvisor.dev/gvisor/pkg/linewriter from gvisor.dev/gvisor/pkg/log
gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context+ gvisor.dev/gvisor/pkg/log from gvisor.dev/gvisor/pkg/context+
L gvisor.dev/gvisor/pkg/marshal from gvisor.dev/gvisor/pkg/abi/linux+
L 💣 gvisor.dev/gvisor/pkg/marshal/primitive from gvisor.dev/gvisor/pkg/abi/linux
gvisor.dev/gvisor/pkg/rand from gvisor.dev/gvisor/pkg/tcpip/network/hash+ gvisor.dev/gvisor/pkg/rand from gvisor.dev/gvisor/pkg/tcpip/network/hash+
gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/bufferv2+ gvisor.dev/gvisor/pkg/refs from gvisor.dev/gvisor/pkg/bufferv2+
💣 gvisor.dev/gvisor/pkg/sleep from gvisor.dev/gvisor/pkg/tcpip/transport/tcp 💣 gvisor.dev/gvisor/pkg/sleep from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
@ -276,7 +264,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
tailscale.com/net/sockstats from tailscale.com/control/controlclient+ tailscale.com/net/sockstats from tailscale.com/control/controlclient+
tailscale.com/net/stun from tailscale.com/net/netcheck+ tailscale.com/net/stun from tailscale.com/net/netcheck+
L tailscale.com/net/tcpinfo from tailscale.com/derp
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+ tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
tailscale.com/net/tsaddr from tailscale.com/ipn+ tailscale.com/net/tsaddr from tailscale.com/ipn+
tailscale.com/net/tsdial from tailscale.com/control/controlclient+ tailscale.com/net/tsdial from tailscale.com/control/controlclient+
@ -321,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/clientmetric from tailscale.com/control/controlclient+
tailscale.com/util/cloudenv from tailscale.com/net/dns/resolver+ tailscale.com/util/cloudenv from tailscale.com/net/dns/resolver+
LW tailscale.com/util/cmpver from tailscale.com/net/dns+ 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+ 💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics+ L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics+
tailscale.com/util/dnsname from tailscale.com/hostinfo+ tailscale.com/util/dnsname from tailscale.com/hostinfo+
@ -330,7 +316,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 tailscale.com/util/hashx from tailscale.com/util/deephash 💣 tailscale.com/util/hashx from tailscale.com/util/deephash
tailscale.com/util/httpm from tailscale.com/client/tailscale+ tailscale.com/util/httpm from tailscale.com/client/tailscale+
tailscale.com/util/lineread from tailscale.com/hostinfo+ tailscale.com/util/lineread from tailscale.com/hostinfo+
L 💣 tailscale.com/util/linuxfw from tailscale.com/net/netns+
tailscale.com/util/mak from tailscale.com/control/controlclient+ tailscale.com/util/mak from tailscale.com/control/controlclient+
tailscale.com/util/multierr from tailscale.com/control/controlclient+ tailscale.com/util/multierr from tailscale.com/control/controlclient+
tailscale.com/util/must from tailscale.com/logpolicy tailscale.com/util/must from tailscale.com/logpolicy
@ -379,7 +364,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+ golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+ LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+
golang.org/x/exp/constraints from golang.org/x/exp/slices+ golang.org/x/exp/constraints from golang.org/x/exp/slices+
golang.org/x/exp/maps from tailscale.com/wgengine+ golang.org/x/exp/maps from tailscale.com/wgengine
golang.org/x/exp/slices from tailscale.com/ipn/ipnlocal+ golang.org/x/exp/slices from tailscale.com/ipn/ipnlocal+
golang.org/x/net/bpf from github.com/mdlayher/genetlink+ golang.org/x/net/bpf from github.com/mdlayher/genetlink+
golang.org/x/net/dns/dnsmessage from net+ golang.org/x/net/dns/dnsmessage from net+
@ -412,7 +397,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
bytes from bufio+ bytes from bufio+
compress/flate from compress/gzip+ compress/flate from compress/gzip+
compress/gzip from golang.org/x/net/http2+ 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/heap from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
container/list from crypto/tls+ container/list from crypto/tls+
context from crypto/tls+ context from crypto/tls+
@ -437,8 +421,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
crypto/tls from github.com/tcnksm/go-httpstat+ crypto/tls from github.com/tcnksm/go-httpstat+
crypto/x509 from crypto/tls+ crypto/x509 from crypto/tls+
crypto/x509/pkix from crypto/x509+ 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+ embed from tailscale.com+
encoding from encoding/json+ encoding from encoding/json+
encoding/asn1 from crypto/x509+ encoding/asn1 from crypto/x509+
@ -454,7 +436,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
flag from net/http/httptest+ flag from net/http/httptest+
fmt from compress/flate+ fmt from compress/flate+
hash from crypto+ hash from crypto+
hash/adler32 from tailscale.com/ipn/ipnlocal+ hash/adler32 from tailscale.com/ipn/ipnlocal
hash/crc32 from compress/gzip+ hash/crc32 from compress/gzip+
hash/fnv from tailscale.com/wgengine/magicsock+ hash/fnv from tailscale.com/wgengine/magicsock+
hash/maphash from go4.org/mem hash/maphash from go4.org/mem
@ -463,7 +445,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
io/fs from crypto/x509+ io/fs from crypto/x509+
io/ioutil from github.com/godbus/dbus/v5+ io/ioutil from github.com/godbus/dbus/v5+
log from expvar+ log from expvar+
log/internal from log
LD log/syslog from tailscale.com/ssh/tailssh LD log/syslog from tailscale.com/ssh/tailssh
math from compress/flate+ math from compress/flate+
math/big from crypto/dsa+ math/big from crypto/dsa+

View File

@ -7,20 +7,16 @@
package flakytest package flakytest
import ( import (
"fmt"
"os" "os"
"regexp" "regexp"
"testing" "testing"
) )
// FlakyTestLogMessage is a sentinel value that is printed to stderr when a // InTestWrapper returns whether or not this binary is running under our test
// flaky test is marked. This is used by cmd/testwrapper to detect flaky tests // wrapper.
// and retry them. func InTestWrapper() bool {
const FlakyTestLogMessage = "flakytest: this is a known flaky test" return os.Getenv("TS_IN_TESTWRAPPER") != ""
}
// FlakeAttemptEnv is an environment variable that is set by cmd/testwrapper
// when a flaky test is retried. It contains the attempt number, starting at 1.
const FlakeAttemptEnv = "TS_TESTWRAPPER_ATTEMPT"
var issueRegexp = regexp.MustCompile(`\Ahttps://github\.com/tailscale/[a-zA-Z0-9_.-]+/issues/\d+\z`) var issueRegexp = regexp.MustCompile(`\Ahttps://github\.com/tailscale/[a-zA-Z0-9_.-]+/issues/\d+\z`)
@ -34,6 +30,16 @@ func Mark(t testing.TB, issue string) {
t.Fatalf("bad issue format: %q", issue) t.Fatalf("bad issue format: %q", issue)
} }
fmt.Fprintln(os.Stderr, FlakyTestLogMessage) // sentinel value for testwrapper if !InTestWrapper() {
t.Logf("flakytest: issue tracking this flaky test: %s", issue) return
}
t.Cleanup(func() {
if t.Failed() {
t.Logf("flakytest: signaling test wrapper to retry test")
// Signal to test wrapper that we should restart.
os.Exit(123)
}
})
} }

View File

@ -3,10 +3,7 @@
package flakytest package flakytest
import ( import "testing"
"os"
"testing"
)
func TestIssueFormat(t *testing.T) { func TestIssueFormat(t *testing.T) {
testCases := []struct { testCases := []struct {
@ -27,17 +24,3 @@ func TestIssueFormat(t *testing.T) {
} }
} }
} }
// TestFlakeRun is a test that fails when run in the testwrapper
// for the first time, but succeeds on the second run.
// It's used to test whether the testwrapper retries flaky tests.
func TestFlakeRun(t *testing.T) {
Mark(t, "https://github.com/tailscale/tailscale/issues/0") // random issue
e := os.Getenv(FlakeAttemptEnv)
if e == "" {
t.Skip("not running in testwrapper")
}
if e == "1" {
t.Fatal("First run in testwrapper, failing so that test is retried. This is expected.")
}
}

View File

@ -1,278 +1,62 @@
// Copyright (c) Tailscale Inc & AUTHORS // Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
// testwrapper is a wrapper for retrying flaky tests. It is an alternative to // testwrapper is a wrapper for retrying flaky tests, using the -exec flag of
// `go test` and re-runs failed marked flaky tests (using the flakytest pkg). It // 'go test'. Tests that are flaky can use the 'flakytest' subpackage to mark
// takes different arguments than go test and requires the first positional // themselves as flaky and be retried on failure.
// argument to be the pattern to test.
package main package main
import ( import (
"bytes"
"context" "context"
"encoding/json"
"errors" "errors"
"flag"
"fmt"
"io"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"sort"
"strings"
"time"
"golang.org/x/exp/maps"
"tailscale.com/cmd/testwrapper/flakytest"
) )
const maxAttempts = 3 const (
retryStatus = 123
type testAttempt struct { maxIterations = 3
name testName )
outcome string // "pass", "fail", "skip"
logs bytes.Buffer
isMarkedFlaky bool // set if the test is marked as flaky
pkgFinished bool
}
type testName struct {
pkg string // "tailscale.com/types/key"
name string // "TestFoo"
}
type packageTests struct {
// pattern is the package pattern to run.
// Must be a single pattern, not a list of patterns.
pattern string // "./...", "./types/key"
// tests is a list of tests to run. If empty, all tests in the package are
// run.
tests []string // ["TestFoo", "TestBar"]
}
type goTestOutput struct {
Time time.Time
Action string
Package string
Test string
Output string
}
var debug = os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
// runTests runs the tests in pt and sends the results on ch. It sends a
// testAttempt for each test and a final testAttempt per pkg with pkgFinished
// set to true.
// It calls close(ch) when it's done.
func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []string, ch chan<- *testAttempt) {
defer close(ch)
args := []string{"test", "-json", pt.pattern}
args = append(args, otherArgs...)
if len(pt.tests) > 0 {
runArg := strings.Join(pt.tests, "|")
args = append(args, "-run", runArg)
}
if debug {
fmt.Println("running", strings.Join(args, " "))
}
cmd := exec.CommandContext(ctx, "go", args...)
r, err := cmd.StdoutPipe()
if err != nil {
log.Printf("error creating stdout pipe: %v", err)
}
cmd.Stderr = os.Stderr
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", flakytest.FlakeAttemptEnv, attempt))
if err := cmd.Start(); err != nil {
log.Printf("error starting test: %v", err)
os.Exit(1)
}
done := make(chan struct{})
go func() {
defer close(done)
cmd.Wait()
}()
jd := json.NewDecoder(r)
resultMap := make(map[testName]*testAttempt)
for {
var goOutput goTestOutput
if err := jd.Decode(&goOutput); err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, os.ErrClosed) {
break
}
panic(err)
}
if goOutput.Test == "" {
switch goOutput.Action {
case "fail", "pass", "skip":
ch <- &testAttempt{
name: testName{
pkg: goOutput.Package,
},
outcome: goOutput.Action,
pkgFinished: true,
}
}
continue
}
name := testName{
pkg: goOutput.Package,
name: goOutput.Test,
}
if test, _, isSubtest := strings.Cut(goOutput.Test, "/"); isSubtest {
name.name = test
if goOutput.Action == "output" {
resultMap[name].logs.WriteString(goOutput.Output)
}
continue
}
switch goOutput.Action {
case "start":
// ignore
case "run":
resultMap[name] = &testAttempt{
name: name,
}
case "skip", "pass", "fail":
resultMap[name].outcome = goOutput.Action
ch <- resultMap[name]
case "output":
if strings.TrimSpace(goOutput.Output) == flakytest.FlakyTestLogMessage {
resultMap[name].isMarkedFlaky = true
} else {
resultMap[name].logs.WriteString(goOutput.Output)
}
}
}
<-done
}
func main() { func main() {
ctx := context.Background() ctx := context.Background()
debug := os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
// We only need to parse the -v flag to figure out whether to print the logs log.SetPrefix("testwrapper: ")
// for a test. We don't need to parse any other flags, so we just use the if !debug {
// flag package to parse the -v flag and then pass the rest of the args log.SetFlags(0)
// through to 'go test'.
// We run `go test -json` which returns the same information as `go test -v`,
// but in a machine-readable format. So this flag is only for testwrapper's
// output.
v := flag.Bool("v", false, "verbose")
flag.Usage = func() {
fmt.Println("usage: testwrapper [testwrapper-flags] [pattern] [build/test flags & test binary flags]")
fmt.Println()
fmt.Println("testwrapper-flags:")
flag.CommandLine.PrintDefaults()
fmt.Println()
fmt.Println("examples:")
fmt.Println("\ttestwrapper -v ./... -count=1")
fmt.Println("\ttestwrapper ./pkg/foo -run TestBar -count=1")
fmt.Println()
fmt.Println("Unlike 'go test', testwrapper requires a package pattern as the first positional argument and only supports a single pattern.")
}
flag.Parse()
args := flag.Args()
if len(args) < 1 || strings.HasPrefix(args[0], "-") {
fmt.Println("no pattern specified")
flag.Usage()
os.Exit(1)
} else if len(args) > 1 && !strings.HasPrefix(args[1], "-") {
fmt.Println("expected single pattern")
flag.Usage()
os.Exit(1)
}
pattern, otherArgs := args[0], args[1:]
type nextRun struct {
tests []*packageTests
attempt int
} }
toRun := []*nextRun{ for i := 1; i <= maxIterations; i++ {
{ if i > 1 {
tests: []*packageTests{{pattern: pattern}}, log.Printf("retrying flaky tests (%d of %d)", i, maxIterations)
attempt: 1,
},
} }
printPkgOutcome := func(pkg, outcome string, attempt int) { cmd := exec.CommandContext(ctx, os.Args[1], os.Args[2:]...)
if outcome == "skip" { cmd.Stdout = os.Stdout
fmt.Printf("?\t%s [skipped/no tests] \n", pkg) cmd.Stderr = os.Stderr
cmd.Env = append(os.Environ(), "TS_IN_TESTWRAPPER=1")
err := cmd.Run()
if err == nil {
return return
} }
if outcome == "pass" {
outcome = "ok"
}
if outcome == "fail" {
outcome = "FAIL"
}
if attempt > 1 {
fmt.Printf("%s\t%s [attempt=%d]\n", outcome, pkg, attempt)
return
}
fmt.Printf("%s\t%s\n", outcome, pkg)
}
for len(toRun) > 0 { var exitErr *exec.ExitError
var thisRun *nextRun if !errors.As(err, &exitErr) {
thisRun, toRun = toRun[0], toRun[1:] if debug {
log.Printf("error isn't an ExitError")
if thisRun.attempt >= maxAttempts { }
fmt.Println("max attempts reached")
os.Exit(1) os.Exit(1)
} }
if thisRun.attempt > 1 {
fmt.Printf("\n\nAttempt #%d: Retrying flaky tests:\n\n", thisRun.attempt) if code := exitErr.ExitCode(); code != retryStatus {
if debug {
log.Printf("code (%d) != retryStatus (%d)", code, retryStatus)
}
os.Exit(code)
}
} }
failed := false log.Printf("test did not pass in %d iterations", maxIterations)
toRetry := make(map[string][]string) // pkg -> tests to retry
for _, pt := range thisRun.tests {
ch := make(chan *testAttempt)
go runTests(ctx, thisRun.attempt, pt, otherArgs, ch)
for tr := range ch {
if tr.pkgFinished {
printPkgOutcome(tr.name.pkg, tr.outcome, thisRun.attempt)
continue
}
if *v || tr.outcome == "fail" {
io.Copy(os.Stdout, &tr.logs)
}
if tr.outcome != "fail" {
continue
}
if tr.isMarkedFlaky {
toRetry[tr.name.pkg] = append(toRetry[tr.name.pkg], tr.name.name)
} else {
failed = true
}
}
}
if failed {
fmt.Println("\n\nNot retrying flaky tests because non-flaky tests failed.")
os.Exit(1) os.Exit(1)
} }
if len(toRetry) == 0 {
continue
}
pkgs := maps.Keys(toRetry)
sort.Strings(pkgs)
nextRun := &nextRun{
attempt: thisRun.attempt + 1,
}
for _, pkg := range pkgs {
tests := toRetry[pkg]
sort.Strings(tests)
nextRun.tests = append(nextRun.tests, &packageTests{
pattern: pkg,
tests: tests,
})
}
toRun = append(toRun, nextRun)
}
}

View File

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

View File

@ -287,25 +287,6 @@ func (nc *NoiseClient) GetSingleUseRoundTripper(ctx context.Context) (http.Round
return nil, nil, errors.New("[unexpected] failed to reserve a request on a connection") return nil, nil, errors.New("[unexpected] failed to reserve a request on a connection")
} }
// contextErr is an error that wraps another error and is used to indicate that
// the error was because a context expired.
type contextErr struct {
err error
}
func (e contextErr) Error() string {
return e.err.Error()
}
func (e contextErr) Unwrap() error {
return e.err
}
// getConn returns a noiseConn that can be used to make requests to the
// coordination server. It may return a cached connection or create a new one.
// Dials are singleflighted, so concurrent calls to getConn may only dial once.
// As such, context values may not be respected as there are no guarantees that
// the context passed to getConn is the same as the context passed to dial.
func (nc *NoiseClient) getConn(ctx context.Context) (*noiseConn, error) { func (nc *NoiseClient) getConn(ctx context.Context) (*noiseConn, error) {
nc.mu.Lock() nc.mu.Lock()
if last := nc.last; last != nil && last.canTakeNewRequest() { if last := nc.last; last != nil && last.canTakeNewRequest() {
@ -314,35 +295,11 @@ func (nc *NoiseClient) getConn(ctx context.Context) (*noiseConn, error) {
} }
nc.mu.Unlock() nc.mu.Unlock()
for { conn, err, _ := nc.sfDial.Do(struct{}{}, nc.dial)
// We singeflight the dial to avoid making multiple connections, however
// that means that we can't simply cancel the dial if the context is
// canceled. Instead, we have to additionally check that the context
// which was canceled is our context and retry if our context is still
// valid.
conn, err, _ := nc.sfDial.Do(struct{}{}, func() (*noiseConn, error) {
c, err := nc.dial(ctx)
if err != nil { if err != nil {
if ctx.Err() != nil {
return nil, contextErr{ctx.Err()}
}
return nil, err return nil, err
} }
return c, nil return conn, nil
})
var ce contextErr
if err == nil || !errors.As(err, &ce) {
return conn, err
}
if ctx.Err() == nil {
// The dial failed because of a context error, but our context
// is still valid. Retry.
continue
}
// The dial failed because our context was canceled. Return the
// underlying error.
return nil, ce.Unwrap()
}
} }
func (nc *NoiseClient) RoundTrip(req *http.Request) (*http.Response, error) { func (nc *NoiseClient) RoundTrip(req *http.Request) (*http.Response, error) {
@ -387,7 +344,7 @@ func (nc *NoiseClient) Close() error {
// dial opens a new connection to tailcontrol, fetching the server noise key // dial opens a new connection to tailcontrol, fetching the server noise key
// if not cached. // if not cached.
func (nc *NoiseClient) dial(ctx context.Context) (*noiseConn, error) { func (nc *NoiseClient) dial() (*noiseConn, error) {
nc.mu.Lock() nc.mu.Lock()
connID := nc.nextID connID := nc.nextID
nc.nextID++ nc.nextID++
@ -435,7 +392,7 @@ func (nc *NoiseClient) dial(ctx context.Context) (*noiseConn, error) {
} }
timeout := time.Duration(timeoutSec * float64(time.Second)) timeout := time.Duration(timeoutSec * float64(time.Second))
ctx, cancel := context.WithTimeout(ctx, timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel() defer cancel()
clientConn, err := (&controlhttp.Dialer{ clientConn, err := (&controlhttp.Dialer{

View File

@ -583,20 +583,19 @@ func TestDialPlan(t *testing.T) {
}}, }},
want: goodAddr, want: goodAddr,
}, },
// TODO(#8442): fix this test {
// { name: "multiple-priority-fast-path",
// name: "multiple-priority-fast-path", plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
// plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{ // Dials some good IPs and our bad one (which
// // Dials some good IPs and our bad one (which // hangs forever), which then hits the fast
// // hangs forever), which then hits the fast // path where we bail without waiting.
// // path where we bail without waiting. {IP: brokenAddr, Priority: 1, DialTimeoutSec: 10},
// {IP: brokenAddr, Priority: 1, DialTimeoutSec: 10}, {IP: goodAddr, Priority: 1, DialTimeoutSec: 10},
// {IP: goodAddr, Priority: 1, DialTimeoutSec: 10}, {IP: other2Addr, Priority: 1, DialTimeoutSec: 10},
// {IP: other2Addr, Priority: 1, DialTimeoutSec: 10}, {IP: otherAddr, Priority: 2, DialTimeoutSec: 10},
// {IP: otherAddr, Priority: 2, DialTimeoutSec: 10}, }},
// }}, want: otherAddr,
// want: otherAddr, },
// },
{ {
name: "multiple-priority-slow-path", name: "multiple-priority-slow-path",
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{ plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{

View File

@ -9,18 +9,19 @@ import (
"net" "net"
"time" "time"
"tailscale.com/net/tcpinfo" "golang.org/x/sys/unix"
) )
func (c *sclient) statsLoop(ctx context.Context) error { func (c *sclient) statsLoop(ctx context.Context) error {
// Get the RTT initially to verify it's supported. // If we can't get a TCP socket, then we can't send stats.
conn := c.tcpConn() tcpConn := c.tcpConn()
if conn == nil { if tcpConn == nil {
c.s.tcpRtt.Add("non-tcp", 1) c.s.tcpRtt.Add("non-tcp", 1)
return nil return nil
} }
if _, err := tcpinfo.RTT(conn); err != nil { rawConn, err := tcpConn.SyscallConn()
c.logf("error fetching initial RTT: %v", err) if err != nil {
c.logf("error getting SyscallConn: %v", err)
c.s.tcpRtt.Add("error", 1) c.s.tcpRtt.Add("error", 1)
return nil return nil
} }
@ -30,16 +31,23 @@ func (c *sclient) statsLoop(ctx context.Context) error {
ticker := time.NewTicker(statsInterval) ticker := time.NewTicker(statsInterval)
defer ticker.Stop() defer ticker.Stop()
var (
tcpInfo *unix.TCPInfo
sysErr error
)
statsLoop: statsLoop:
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
rtt, err := tcpinfo.RTT(conn) err = rawConn.Control(func(fd uintptr) {
if err != nil { tcpInfo, sysErr = unix.GetsockoptTCPInfo(int(fd), unix.IPPROTO_TCP, unix.TCP_INFO)
})
if err != nil || sysErr != nil {
continue statsLoop continue statsLoop
} }
// TODO(andrew): more metrics? // TODO(andrew): more metrics?
rtt := time.Duration(tcpInfo.Rtt) * time.Microsecond
c.s.tcpRtt.Add(durationToLabel(rtt), 1) c.s.tcpRtt.Add(durationToLabel(rtt), 1)
case <-ctx.Done(): case <-ctx.Done():

View File

@ -40,7 +40,6 @@ import (
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/types/key" "tailscale.com/types/key"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/cmpx"
) )
// Client is a DERP-over-HTTP client. // 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 // Start v4 dial
} }
} }
dst := cmpx.Or(dstPrimary, n.HostName) dst := dstPrimary
if dst == "" {
dst = n.HostName
}
port := "443" port := "443"
if n.DERPPort != 0 { if n.DERPPort != 0 {
port = fmt.Sprint(n.DERPPort) port = fmt.Sprint(n.DERPPort)

View File

@ -6,20 +6,22 @@ SA_NAME ?= tailscale
TS_KUBE_SECRET ?= tailscale TS_KUBE_SECRET ?= tailscale
rbac: rbac:
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" role.yaml @sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" role.yaml | kubectl apply -f -
@echo "---" @sed -e "s;{{SA_NAME}};$(SA_NAME);g" rolebinding.yaml | kubectl apply -f -
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" rolebinding.yaml @sed -e "s;{{SA_NAME}};$(SA_NAME);g" sa.yaml | kubectl apply -f -
@echo "---"
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" sa.yaml
sidecar: sidecar:
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" @kubectl delete -f sidecar.yaml --ignore-not-found --grace-period=0
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | kubectl create -f-
userspace-sidecar: userspace-sidecar:
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" userspace-sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" @kubectl delete -f userspace-sidecar.yaml --ignore-not-found --grace-period=0
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" userspace-sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | kubectl create -f-
proxy: proxy:
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" proxy.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{TS_DEST_IP}};$(TS_DEST_IP);g" kubectl delete -f proxy.yaml --ignore-not-found --grace-period=0
sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" proxy.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{TS_DEST_IP}};$(TS_DEST_IP);g" | kubectl create -f-
subnet-router: subnet-router:
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" subnet.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{TS_ROUTES}};$(TS_ROUTES);g" @kubectl delete -f subnet.yaml --ignore-not-found --grace-period=0
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" subnet.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{TS_ROUTES}};$(TS_ROUTES);g" | kubectl create -f-

View File

@ -26,7 +26,7 @@ There are quite a few ways of running Tailscale inside a Kubernetes Cluster, som
```bash ```bash
export SA_NAME=tailscale export SA_NAME=tailscale
export TS_KUBE_SECRET=tailscale-auth export TS_KUBE_SECRET=tailscale-auth
make rbac | kubectl apply -f- make rbac
``` ```
### Sample Sidecar ### Sample Sidecar
@ -36,7 +36,7 @@ Running as a sidecar allows you to directly expose a Kubernetes pod over Tailsca
1. Create and login to the sample nginx pod with a Tailscale sidecar 1. Create and login to the sample nginx pod with a Tailscale sidecar
```bash ```bash
make sidecar | kubectl apply -f- make sidecar
# If not using an auth key, authenticate by grabbing the Login URL here: # If not using an auth key, authenticate by grabbing the Login URL here:
kubectl logs nginx ts-sidecar kubectl logs nginx ts-sidecar
``` ```
@ -60,7 +60,7 @@ You can also run the sidecar in userspace mode. The obvious benefit is reducing
1. Create and login to the sample nginx pod with a Tailscale sidecar 1. Create and login to the sample nginx pod with a Tailscale sidecar
```bash ```bash
make userspace-sidecar | kubectl apply -f- make userspace-sidecar
# If not using an auth key, authenticate by grabbing the Login URL here: # If not using an auth key, authenticate by grabbing the Login URL here:
kubectl logs nginx ts-sidecar kubectl logs nginx ts-sidecar
``` ```
@ -100,7 +100,7 @@ Running a Tailscale proxy allows you to provide inbound connectivity to a Kubern
1. Deploy the proxy pod 1. Deploy the proxy pod
```bash ```bash
make proxy | kubectl apply -f- make proxy
# If not using an auth key, authenticate by grabbing the Login URL here: # If not using an auth key, authenticate by grabbing the Login URL here:
kubectl logs proxy kubectl logs proxy
``` ```
@ -133,7 +133,7 @@ the entire Kubernetes cluster network (assuming NetworkPolicies allow) over Tail
1. Deploy the subnet-router pod. 1. Deploy the subnet-router pod.
```bash ```bash
make subnet-router | kubectl apply -f- make subnet-router
# If not using an auth key, authenticate by grabbing the Login URL here: # If not using an auth key, authenticate by grabbing the Login URL here:
kubectl logs subnet-router kubectl logs subnet-router
``` ```

View File

@ -115,4 +115,4 @@
in in
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system); 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/genetlink v1.3.2
github.com/mdlayher/netlink v1.7.2 github.com/mdlayher/netlink v1.7.2
github.com/mdlayher/sdnotify v1.0.0 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/mitchellh/go-ps v1.0.0
github.com/peterbourgon/ff/v3 v3.3.0 github.com/peterbourgon/ff/v3 v3.3.0
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
@ -79,7 +79,7 @@ require (
golang.org/x/net v0.10.0 golang.org/x/net v0.10.0
golang.org/x/oauth2 v0.7.0 golang.org/x/oauth2 v0.7.0
golang.org/x/sync v0.2.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/term v0.8.0
golang.org/x/time v0.3.0 golang.org/x/time v0.3.0
golang.org/x/tools v0.9.1 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 h1:OlQkcH40IB2cGuprTPcjB0iIUddgVZgGmDX3IAMR8D4=
github.com/mgechev/revive v1.3.1/go.mod h1:YlD6TTWl2B8A103R9KWJSPVI9DrEf+oqr15q21Ld+5I= 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.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= github.com/miekg/dns v1.1.54 h1:5jon9mWcb0sFJGpnI99tOMhCPyJ+RPVz5b63MQG0VWI=
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= 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/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 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.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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.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.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.1-0.20230609144347-5059a07aa46a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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-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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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 ( import (
"bufio" "bufio"
"bytes"
"io" "io"
"os" "os"
"os/exec"
"runtime" "runtime"
"runtime/debug" "runtime/debug"
"strings" "strings"
@ -283,7 +281,7 @@ func inContainer() opt.Bool {
return nil return nil
}) })
lineread.File("/proc/mounts", func(line []byte) error { lineread.File("/proc/mounts", func(line []byte) error {
if mem.Contains(mem.B(line), mem.S("lxcfs /proc/cpuinfo fuse.lxcfs")) { if mem.Contains(mem.B(line), mem.S("fuse.lxcfs")) {
ret.Set(true) ret.Set(true)
return io.EOF return io.EOF
} }
@ -436,12 +434,3 @@ func etcAptSourceFileIsDisabled(r io.Reader) bool {
} }
return disabled 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. // A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _TCPPortHandlerCloneNeedsRegeneration = TCPPortHandler(struct { var _TCPPortHandlerCloneNeedsRegeneration = TCPPortHandler(struct {
HTTPS bool HTTPS bool
HTTP bool
TCPForward string TCPForward string
TerminateTLS 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) HTTPS() bool { return v.ж.HTTPS }
func (v TCPPortHandlerView) HTTP() bool { return v.ж.HTTP }
func (v TCPPortHandlerView) TCPForward() string { return v.ж.TCPForward } func (v TCPPortHandlerView) TCPForward() string { return v.ж.TCPForward }
func (v TCPPortHandlerView) TerminateTLS() string { return v.ж.TerminateTLS } 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. // A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _TCPPortHandlerViewNeedsRegeneration = TCPPortHandler(struct { var _TCPPortHandlerViewNeedsRegeneration = TCPPortHandler(struct {
HTTPS bool HTTPS bool
HTTP bool
TCPForward string TCPForward string
TerminateTLS string TerminateTLS string
}{}) }{})

View File

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

View File

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

View File

@ -4,6 +4,7 @@
package ipnlocal package ipnlocal
import ( import (
"bytes"
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
@ -17,6 +18,7 @@ import (
"net/netip" "net/netip"
"net/url" "net/url"
"os" "os"
"os/exec"
"os/user" "os/user"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -30,7 +32,6 @@ import (
"go4.org/mem" "go4.org/mem"
"go4.org/netipx" "go4.org/netipx"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
"gvisor.dev/gvisor/pkg/tcpip"
"tailscale.com/client/tailscale/apitype" "tailscale.com/client/tailscale/apitype"
"tailscale.com/control/controlclient" "tailscale.com/control/controlclient"
"tailscale.com/doctor" "tailscale.com/doctor"
@ -70,7 +71,6 @@ import (
"tailscale.com/types/preftype" "tailscale.com/types/preftype"
"tailscale.com/types/ptr" "tailscale.com/types/ptr"
"tailscale.com/types/views" "tailscale.com/types/views"
"tailscale.com/util/cmpx"
"tailscale.com/util/deephash" "tailscale.com/util/deephash"
"tailscale.com/util/dnsname" "tailscale.com/util/dnsname"
"tailscale.com/util/mak" "tailscale.com/util/mak"
@ -742,6 +742,7 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
HostName: p.Hostinfo.Hostname(), HostName: p.Hostinfo.Hostname(),
DNSName: p.Name, DNSName: p.Name,
OS: p.Hostinfo.OS(), OS: p.Hostinfo.OS(),
KeepAlive: p.KeepAlive,
LastSeen: lastSeen, LastSeen: lastSeen,
Online: p.Online != nil && *p.Online, Online: p.Online != nil && *p.Online,
ShareeNode: p.Hostinfo.ShareeNode(), ShareeNode: p.Hostinfo.ShareeNode(),
@ -2580,7 +2581,7 @@ func (b *LocalBackend) checkSSHPrefsLocked(p *ipn.Prefs) error {
if distro.Get() == distro.QNAP && !envknob.UseWIPCode() { if distro.Get() == distro.QNAP && !envknob.UseWIPCode() {
return errors.New("The Tailscale SSH server does not run on QNAP.") return errors.New("The Tailscale SSH server does not run on QNAP.")
} }
b.updateSELinuxHealthWarning() checkSELinux()
// otherwise okay // otherwise okay
case "darwin": case "darwin":
// okay only in tailscaled mode for now. // okay only in tailscaled mode for now.
@ -2826,14 +2827,14 @@ func (b *LocalBackend) GetPeerAPIPort(ip netip.Addr) (port uint16, ok bool) {
return 0, false 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 remote parameter is the remote address.
// The local parameter is the local address (either a Tailscale IPv4 // The local parameter is the local address (either a Tailscale IPv4
// or IPv6 IP and the peerapi port for that address). // or IPv6 IP and the peerapi port for that address).
// //
// The connection will be closed by handlePeerAPIConn. // The connection will be closed by ServePeerAPIConnection.
func (b *LocalBackend) handlePeerAPIConn(remote, local netip.AddrPort, c net.Conn) { func (b *LocalBackend) ServePeerAPIConnection(remote, local netip.AddrPort, c net.Conn) {
b.mu.Lock() b.mu.Lock()
defer b.mu.Unlock() defer b.mu.Unlock()
for _, pln := range b.peerAPIListeners { for _, pln := range b.peerAPIListeners {
@ -2847,48 +2848,6 @@ func (b *LocalBackend) handlePeerAPIConn(remote, local netip.AddrPort, c net.Con
return 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) { func (b *LocalBackend) peerAPIServicesLocked() (ret []tailcfg.Service) {
for _, pln := range b.peerAPIListeners { for _, pln := range b.peerAPIListeners {
proto := tailcfg.PeerAPI4 proto := tailcfg.PeerAPI4
@ -3973,7 +3932,10 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
b.dialer.SetNetMap(nm) b.dialer.SetNetMap(nm)
var login string var login string
if nm != nil { 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 b.netMap = nm
if login != b.activeLogin { if login != b.activeLogin {
@ -4128,10 +4090,6 @@ func (b *LocalBackend) setServeProxyHandlersLocked() {
b.serveConfig.Web().Range(func(_ ipn.HostPort, conf ipn.WebServerConfigView) (cont bool) { b.serveConfig.Web().Range(func(_ ipn.HostPort, conf ipn.WebServerConfigView) (cont bool) {
conf.Handlers().Range(func(_ string, h ipn.HTTPHandlerView) (cont bool) { conf.Handlers().Range(func(_ string, h ipn.HTTPHandlerView) (cont bool) {
backend := h.Proxy() backend := h.Proxy()
if backend == "" {
// Only create proxy handlers for servers with a proxy backend.
return true
}
mak.Set(&backends, backend, true) mak.Set(&backends, backend, true)
if _, ok := b.serveProxyHandlers.Load(backend); ok { if _, ok := b.serveProxyHandlers.Load(backend); ok {
return true return true
@ -4706,29 +4664,33 @@ func (b *LocalBackend) sshServerOrInit() (_ SSHServer, err error) {
var warnSSHSELinux = health.NewWarnable() var warnSSHSELinux = health.NewWarnable()
func (b *LocalBackend) updateSELinuxHealthWarning() { func checkSELinux() {
if hostinfo.IsSELinuxEnforcing() { 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")) warnSSHSELinux.Set(errors.New("SELinux is enabled; Tailscale SSH may not work. See https://tailscale.com/s/ssh-selinux"))
} else { } else {
warnSSHSELinux.Set(nil) 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() s, err := b.sshServerOrInit()
if err != nil { if err != nil {
return err return err
} }
b.updateSELinuxHealthWarning() checkSELinux()
return s.HandleSSHConn(c) return s.HandleSSHConn(c)
} }
// HandleQuad100Port80Conn serves http://100.100.100.100/ on port 80 (and // HandleQuad100Port80Conn serves http://100.100.100.100/ on port 80 (and
// the equivalent tsaddr.TailscaleServiceIPv6 address). // the equivalent tsaddr.TailscaleServiceIPv6 address).
func (b *LocalBackend) HandleQuad100Port80Conn(c net.Conn) error { func (b *LocalBackend) HandleQuad100Port80Conn(c net.Conn) {
var s http.Server var s http.Server
s.Handler = http.HandlerFunc(b.handleQuad100Port80Conn) s.Handler = http.HandlerFunc(b.handleQuad100Port80Conn)
return s.Serve(netutil.NewOneConnListener(c, nil)) s.Serve(netutil.NewOneConnListener(c, nil))
} }
func validQuad100Host(h string) bool { func validQuad100Host(h string) bool {

View File

@ -158,9 +158,7 @@ func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap, prefs ipn.PrefsVie
return nil 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() ourNodeKey := prefs.Persist().PublicNodeKey()
@ -199,7 +197,7 @@ func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap, prefs ipn.PrefsVie
health.SetTKAHealth(nil) health.SetTKAHealth(nil)
} }
} else { } 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 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) { func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeKeySignature, error) {
p, err := nodeInfo.NodePublic.MarshalBinary() p, err := nodeInfo.NodePublic.MarshalBinary()
if err != nil { if err != nil {

View File

@ -780,7 +780,7 @@ func (h *peerAPIHandler) handleServeIngress(w http.ResponseWriter, r *http.Reque
return return
} }
getConnOrReset := func() (net.Conn, bool) { getConn := func() (net.Conn, bool) {
conn, _, err := w.(http.Hijacker).Hijack() conn, _, err := w.(http.Hijacker).Hijack()
if err != nil { if err != nil {
h.logf("ingress: failed hijacking conn") 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) 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) { 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 return err
} }
srcAddr := conn.RemoteAddr().(*net.TCPAddr).AddrPort() srcAddr := conn.RemoteAddr().(*net.TCPAddr).AddrPort()
handler := s.b.tcpHandlerForServe(s.ap.Port(), srcAddr) getConn := func() (net.Conn, bool) { return conn, true }
if handler == nil { sendRST := func() {
s.b.logf("serve RST for %v", srcAddr) s.b.logf("serve RST for %v", srcAddr)
conn.Close() 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 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() b.mu.Lock()
sc := b.serveConfig sc := b.serveConfig
b.mu.Unlock() b.mu.Unlock()
@ -290,7 +289,7 @@ func (b *LocalBackend) HandleIngressTCPConn(ingressPeer *tailcfg.Node, target ip
if b.getTCPHandlerForFunnelFlow != nil { if b.getTCPHandlerForFunnelFlow != nil {
handler := b.getTCPHandlerForFunnelFlow(srcAddr, dport) handler := b.getTCPHandlerForFunnelFlow(srcAddr, dport)
if handler != nil { if handler != nil {
c, ok := getConnOrReset() c, ok := getConn()
if !ok { if !ok {
b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport) b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport)
return return
@ -299,41 +298,39 @@ func (b *LocalBackend) HandleIngressTCPConn(ingressPeer *tailcfg.Node, target ip
return return
} }
} }
// TODO(bradfitz): pass ingressPeer etc in context to tcpHandlerForServe, // TODO(bradfitz): pass ingressPeer etc in context to HandleInterceptedTCPConn,
// extend serveHTTPContext or similar. // extend serveHTTPContext or similar.
handler := b.tcpHandlerForServe(dport, srcAddr) b.HandleInterceptedTCPConn(dport, srcAddr, getConn, sendRST)
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)
} }
// tcpHandlerForServe returns a handler for a TCP connection to be served via func (b *LocalBackend) HandleInterceptedTCPConn(dport uint16, srcAddr netip.AddrPort, getConn func() (net.Conn, bool), sendRST func()) {
// the ipn.ServeConfig.
func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort) (handler func(net.Conn) error) {
b.mu.Lock() b.mu.Lock()
sc := b.serveConfig sc := b.serveConfig
b.mu.Unlock() b.mu.Unlock()
if !sc.Valid() { if !sc.Valid() {
b.logf("[unexpected] localbackend: got TCP conn w/o serveConfig; from %v to port %v", srcAddr, dport) 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) tcph, ok := sc.TCP().GetOk(dport)
if !ok { if !ok {
b.logf("[unexpected] localbackend: got TCP conn without TCP config for port %v; from %v", dport, srcAddr) 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{ hs := &http.Server{
TLSConfig: &tls.Config{
GetCertificate: b.getTLSServeCertForPort(dport),
},
Handler: http.HandlerFunc(b.serveWebHandler), Handler: http.HandlerFunc(b.serveWebHandler),
BaseContext: func(_ net.Listener) context.Context { BaseContext: func(_ net.Listener) context.Context {
return context.WithValue(context.Background(), serveHTTPContextKey{}, &serveHTTPContext{ return context.WithValue(context.Background(), serveHTTPContextKey{}, &serveHTTPContext{
@ -342,31 +339,28 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort)
}) })
}, },
} }
if tcph.HTTPS() { hs.ServeTLS(netutil.NewOneConnListener(conn, nil), "", "")
hs.TLSConfig = &tls.Config{ return
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))
}
} }
if backDst := tcph.TCPForward(); backDst != "" { if backDst := tcph.TCPForward(); backDst != "" {
return func(conn net.Conn) error {
defer conn.Close()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
backConn, err := b.dialer.SystemDial(ctx, "tcp", backDst) backConn, err := b.dialer.SystemDial(ctx, "tcp", backDst)
cancel() cancel()
if err != nil { if err != nil {
b.logf("localbackend: failed to TCP proxy port %v (from %v) to %s: %v", dport, srcAddr, backDst, err) b.logf("localbackend: failed to TCP proxy port %v (from %v) to %s: %v", dport, srcAddr, backDst, err)
return nil 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() defer backConn.Close()
if sni := tcph.TerminateTLS(); sni != "" { if sni := tcph.TerminateTLS(); sni != "" {
conn = tls.Server(conn, &tls.Config{ conn = tls.Server(conn, &tls.Config{
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) { GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
@ -387,6 +381,7 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort)
// TODO(bradfitz): do the RegisterIPPortIdentity and // TODO(bradfitz): do the RegisterIPPortIdentity and
// UnregisterIPPortIdentity stuff that netstack does // UnregisterIPPortIdentity stuff that netstack does
errc := make(chan error, 1) errc := make(chan error, 1)
go func() { go func() {
_, err := io.Copy(backConn, conn) _, err := io.Copy(backConn, conn)
@ -396,38 +391,27 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort)
_, err := io.Copy(conn, backConn) _, err := io.Copy(conn, backConn)
errc <- err errc <- err
}() }()
return <-errc <-errc
} return
} }
b.logf("closing TCP conn to port %v (from %v) with actionless TCPPortHandler", dport, srcAddr) b.logf("closing TCP conn to port %v (from %v) with actionless TCPPortHandler", dport, srcAddr)
return nil sendRST()
}
func getServeHTTPContext(r *http.Request) (c *serveHTTPContext, ok bool) {
c, ok = r.Context().Value(serveHTTPContextKey{}).(*serveHTTPContext)
return c, ok
} }
func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView, at string, ok bool) { func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView, at string, ok bool) {
var z ipn.HTTPHandlerView // zero value var z ipn.HTTPHandlerView // zero value
hostname := r.Host
if r.TLS == nil { if r.TLS == nil {
tcd := "." + b.Status().CurrentTailnet.MagicDNSSuffix return z, "", false
if !strings.HasSuffix(hostname, tcd) {
hostname += tcd
}
} else {
hostname = r.TLS.ServerName
} }
sctx, ok := getServeHTTPContext(r) sctx, ok := r.Context().Value(serveHTTPContextKey{}).(*serveHTTPContext)
if !ok { if !ok {
b.logf("[unexpected] localbackend: no serveHTTPContext in request") b.logf("[unexpected] localbackend: no serveHTTPContext in request")
return z, "", false return z, "", false
} }
wsc, ok := b.webServerConfig(hostname, sctx.DestPort) wsc, ok := b.webServerConfig(r.TLS.ServerName, sctx.DestPort)
if !ok { if !ok {
return z, "", false return z, "", false
} }
@ -463,8 +447,11 @@ func (b *LocalBackend) proxyHandlerForBackend(backend string) (*httputil.Reverse
Rewrite: func(r *httputil.ProxyRequest) { Rewrite: func(r *httputil.ProxyRequest) {
r.SetURL(u) r.SetURL(u)
r.Out.Host = r.In.Host r.Out.Host = r.In.Host
addProxyForwardedHeaders(r) r.Out.Header.Set("X-Forwarded-Host", r.In.Host)
b.addTailscaleIdentityHeaders(r) 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{ Transport: &http.Transport{
DialContext: b.dialer.SystemDial, DialContext: b.dialer.SystemDial,
@ -482,40 +469,6 @@ func (b *LocalBackend) proxyHandlerForBackend(backend string) (*httputil.Reverse
return rp, nil 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) { func (b *LocalBackend) serveWebHandler(w http.ResponseWriter, r *http.Request) {
h, mountPoint, ok := b.getServeHandler(r) h, mountPoint, ok := b.getServeHandler(r)
if !ok { if !ok {
@ -648,8 +601,8 @@ func allNumeric(s string) bool {
return s != "" return s != ""
} }
func (b *LocalBackend) webServerConfig(hostname string, port uint16) (c ipn.WebServerConfigView, ok bool) { func (b *LocalBackend) webServerConfig(sniName string, port uint16) (c ipn.WebServerConfigView, ok bool) {
key := ipn.HostPort(fmt.Sprintf("%s:%v", hostname, port)) key := ipn.HostPort(fmt.Sprintf("%s:%v", sniName, port))
b.mu.Lock() b.mu.Lock()
defer b.mu.Unlock() defer b.mu.Unlock()

View File

@ -10,22 +10,12 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/netip"
"net/url" "net/url"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"tailscale.com/ipn" "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) { func TestExpandProxyArg(t *testing.T) {
@ -150,7 +140,10 @@ func TestGetServeHandler(t *testing.T) {
}, },
TLS: &tls.ConnectionState{ServerName: serverName}, 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{ req = req.WithContext(context.WithValue(req.Context(), serveHTTPContextKey{}, &serveHTTPContext{
DestPort: port, 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) { func TestServeFileOrDirectory(t *testing.T) {
td := t.TempDir() td := t.TempDir()
writeFile := func(suffix, contents string) { writeFile := func(suffix, contents string) {

View File

@ -223,6 +223,7 @@ type PeerStatus struct {
LastSeen time.Time // last seen to tailcontrol; only present if offline LastSeen time.Time // last seen to tailcontrol; only present if offline
LastHandshake time.Time // with local wireguard LastHandshake time.Time // with local wireguard
Online bool // whether node is connected to the control plane Online bool // whether node is connected to the control plane
KeepAlive bool
ExitNode bool // true if this is the currently selected exit node. ExitNode bool // true if this is the currently selected exit node.
ExitNodeOption bool // true if this node can be an exit node (offered && approved) ExitNodeOption bool // true if this node can be an exit node (offered && approved)
@ -436,6 +437,9 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
if st.InEngine { if st.InEngine {
e.InEngine = true e.InEngine = true
} }
if st.KeepAlive {
e.KeepAlive = true
}
if st.ExitNode { if st.ExitNode {
e.ExitNode = true e.ExitNode = true
} }

View File

@ -104,7 +104,6 @@ var handler = map[string]localAPIHandler{
"tka/force-local-disable": (*Handler).serveTKALocalDisable, "tka/force-local-disable": (*Handler).serveTKALocalDisable,
"tka/affected-sigs": (*Handler).serveTKAAffectedSigs, "tka/affected-sigs": (*Handler).serveTKAAffectedSigs,
"tka/wrap-preauth-key": (*Handler).serveTKAWrapPreauthKey, "tka/wrap-preauth-key": (*Handler).serveTKAWrapPreauthKey,
"tka/verify-deeplink": (*Handler).serveTKAVerifySigningDeeplink,
"upload-client-metrics": (*Handler).serveUploadClientMetrics, "upload-client-metrics": (*Handler).serveUploadClientMetrics,
"watch-ipn-bus": (*Handler).serveWatchIPNBus, "watch-ipn-bus": (*Handler).serveWatchIPNBus,
"whois": (*Handler).serveWhoIs, "whois": (*Handler).serveWhoIs,
@ -1331,7 +1330,7 @@ func (h *Handler) servePing(w http.ResponseWriter, r *http.Request) {
return return
} }
pingTypeStr := r.FormValue("type") pingTypeStr := r.FormValue("type")
if pingTypeStr == "" { if ipStr == "" {
http.Error(w, "missing 'type' parameter", 400) http.Error(w, "missing 'type' parameter", 400)
return return
} }
@ -1611,35 +1610,6 @@ func (h *Handler) serveTKAWrapPreauthKey(w http.ResponseWriter, r *http.Request)
w.Write([]byte(wrappedKey)) 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) { func (h *Handler) serveTKADisable(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite { if !h.PermitWrite {
http.Error(w, "network-lock modify access denied", http.StatusForbidden) 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. // It is mutually exclusive with TCPForward.
HTTPS bool `json:",omitempty"` 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. // TCPForward is the IP:port to forward TCP connections to.
// Whether or not TLS is terminated by tailscaled depends on // Whether or not TLS is terminated by tailscaled depends on
// TerminateTLS. // TerminateTLS.
@ -109,7 +103,7 @@ type HTTPHandler struct {
// temporary ones? Error codes? Redirects? // 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. // the given host:port and mount point.
func (sc *ServeConfig) WebHandlerExists(hp HostPort, mount string) bool { func (sc *ServeConfig) WebHandlerExists(hp HostPort, mount string) bool {
h := sc.GetWebHandler(hp, mount) h := sc.GetWebHandler(hp, mount)
@ -134,8 +128,9 @@ func (sc *ServeConfig) GetTCPPortHandler(port uint16) *TCPPortHandler {
return sc.TCP[port] return sc.TCP[port]
} }
// IsTCPForwardingAny reports whether ServeConfig is currently forwarding in // IsTCPForwardingAny checks if ServeConfig is currently forwarding
// TCPForward mode on any port. This is exclusive of Web/HTTPS serving. // in TCPForward mode on any port.
// This is exclusive of Web/HTTPS serving.
func (sc *ServeConfig) IsTCPForwardingAny() bool { func (sc *ServeConfig) IsTCPForwardingAny() bool {
if sc == nil || len(sc.TCP) == 0 { if sc == nil || len(sc.TCP) == 0 {
return false return false
@ -148,47 +143,34 @@ func (sc *ServeConfig) IsTCPForwardingAny() bool {
return false return false
} }
// IsTCPForwardingOnPort reports whether if ServeConfig is currently forwarding // IsTCPForwardingOnPort checks if ServeConfig is currently forwarding
// in TCPForward mode on the given port. This is exclusive of Web/HTTPS serving. // in TCPForward mode on the given port.
// This is exclusive of Web/HTTPS serving.
func (sc *ServeConfig) IsTCPForwardingOnPort(port uint16) bool { func (sc *ServeConfig) IsTCPForwardingOnPort(port uint16) bool {
if sc == nil || sc.TCP[port] == nil { if sc == nil || sc.TCP[port] == nil {
return false return false
} }
return !sc.IsServingWeb(port) return !sc.TCP[port].HTTPS
} }
// IsServingWeb reports whether if ServeConfig is currently serving Web // IsServingWeb checks if ServeConfig is currently serving
// (HTTP/HTTPS) on the given port. This is exclusive of TCPForwarding. // Web/HTTPS on the given port.
// This is exclusive of TCPForwarding.
func (sc *ServeConfig) IsServingWeb(port uint16) bool { 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 { if sc == nil || sc.TCP[port] == nil {
return false return false
} }
return sc.TCP[port].HTTPS return sc.TCP[port].HTTPS
} }
// IsServingHTTP reports whether if ServeConfig is currently serving HTTP on the // IsFunnelOn checks if ServeConfig is currently allowing
// given port. This is exclusive of HTTPS and TCPForwarding. // funnel traffic for any host:port.
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.
// //
// View version of ServeConfig.IsFunnelOn. // View version of ServeConfig.IsFunnelOn.
func (v ServeConfigView) IsFunnelOn() bool { return v.ж.IsFunnelOn() } func (v ServeConfigView) IsFunnelOn() bool { return v.ж.IsFunnelOn() }
// IsFunnelOn reports whether if ServeConfig is currently allowing funnel // IsFunnelOn checks if ServeConfig is currently allowing
// traffic for any host:port. // funnel traffic for any host:port.
func (sc *ServeConfig) IsFunnelOn() bool { func (sc *ServeConfig) IsFunnelOn() bool {
if sc == nil { if sc == nil {
return false return false

View File

@ -69,14 +69,14 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/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)) - [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/f1b76eb4bb35/LICENSE))
- [go4.org/unsafe/assume-no-moving-gc](https://pkg.go.dev/go4.org/unsafe/assume-no-moving-gc) ([BSD-3-Clause](https://github.com/go4org/unsafe-assume-no-moving-gc/blob/ee73d164e760/LICENSE)) - [go4.org/unsafe/assume-no-moving-gc](https://pkg.go.dev/go4.org/unsafe/assume-no-moving-gc) ([BSD-3-Clause](https://github.com/go4org/unsafe-assume-no-moving-gc/blob/ee73d164e760/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.9.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/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/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/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/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/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/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)) - [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)) - [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

@ -31,7 +31,6 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/v5.1.0/LICENSE)) - [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/v5.1.0/LICENSE))
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE)) - [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE)) - [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
- [github.com/google/nftables](https://pkg.go.dev/github.com/google/nftables) ([Apache-2.0](https://github.com/google/nftables/blob/9aa6fdf5a28c/LICENSE))
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.1.0/LICENSE)) - [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.1.0/LICENSE))
- [github.com/illarion/gonotify](https://pkg.go.dev/github.com/illarion/gonotify) ([MIT](https://github.com/illarion/gonotify/blob/v1.0.1/LICENSE)) - [github.com/illarion/gonotify](https://pkg.go.dev/github.com/illarion/gonotify) ([MIT](https://github.com/illarion/gonotify/blob/v1.0.1/LICENSE))
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/974c6f05fe16/LICENSE)) - [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/974c6f05fe16/LICENSE))
@ -59,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)) - [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/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)) - [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/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/9a58c47922fd/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/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/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.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.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.10.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)) - [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)) - [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)) - [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/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/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/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/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/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)) - [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

@ -14,12 +14,10 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/alexbrainman/sspi](https://pkg.go.dev/github.com/alexbrainman/sspi) ([BSD-3-Clause](https://github.com/alexbrainman/sspi/blob/909beea2cc74/LICENSE)) - [github.com/alexbrainman/sspi](https://pkg.go.dev/github.com/alexbrainman/sspi) ([BSD-3-Clause](https://github.com/alexbrainman/sspi/blob/909beea2cc74/LICENSE))
- [github.com/apenwarr/fixconsole](https://pkg.go.dev/github.com/apenwarr/fixconsole) ([Apache-2.0](https://github.com/apenwarr/fixconsole/blob/5a9f6489cc29/LICENSE)) - [github.com/apenwarr/fixconsole](https://pkg.go.dev/github.com/apenwarr/fixconsole) ([Apache-2.0](https://github.com/apenwarr/fixconsole/blob/5a9f6489cc29/LICENSE))
- [github.com/apenwarr/w32](https://pkg.go.dev/github.com/apenwarr/w32) ([BSD-3-Clause](https://github.com/apenwarr/w32/blob/aa00fece76ab/LICENSE)) - [github.com/apenwarr/w32](https://pkg.go.dev/github.com/apenwarr/w32) ([BSD-3-Clause](https://github.com/apenwarr/w32/blob/aa00fece76ab/LICENSE))
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.6.0/LICENSE))
- [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/111c8c3b57c8/LICENSE)) - [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/111c8c3b57c8/LICENSE))
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.4.0/LICENSE)) - [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.4.0/LICENSE))
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE)) - [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE)) - [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
- [github.com/google/nftables](https://pkg.go.dev/github.com/google/nftables) ([Apache-2.0](https://github.com/google/nftables/blob/9aa6fdf5a28c/LICENSE))
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.3.0/LICENSE)) - [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.3.0/LICENSE))
- [github.com/gregjones/httpcache](https://pkg.go.dev/github.com/gregjones/httpcache) ([MIT](https://github.com/gregjones/httpcache/blob/901d90724c79/LICENSE.txt)) - [github.com/gregjones/httpcache](https://pkg.go.dev/github.com/gregjones/httpcache) ([MIT](https://github.com/gregjones/httpcache/blob/901d90724c79/LICENSE.txt))
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.1.0/LICENSE)) - [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/v0.1.0/LICENSE))
@ -34,29 +32,24 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/nfnt/resize](https://pkg.go.dev/github.com/nfnt/resize) ([ISC](https://github.com/nfnt/resize/blob/83c6a9932646/LICENSE)) - [github.com/nfnt/resize](https://pkg.go.dev/github.com/nfnt/resize) ([ISC](https://github.com/nfnt/resize/blob/83c6a9932646/LICENSE))
- [github.com/peterbourgon/diskv](https://pkg.go.dev/github.com/peterbourgon/diskv) ([MIT](https://github.com/peterbourgon/diskv/blob/v2.0.1/LICENSE)) - [github.com/peterbourgon/diskv](https://pkg.go.dev/github.com/peterbourgon/diskv) ([MIT](https://github.com/peterbourgon/diskv/blob/v2.0.1/LICENSE))
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE)) - [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/f63dace725d8/LICENSE)) - [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/f63dace725d8/LICENSE))
- [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/59dfb47dfef1/LICENSE)) - [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/59dfb47dfef1/LICENSE))
- [github.com/tc-hib/winres](https://pkg.go.dev/github.com/tc-hib/winres) ([0BSD](https://github.com/tc-hib/winres/blob/v0.2.0/LICENSE)) - [github.com/tc-hib/winres](https://pkg.go.dev/github.com/tc-hib/winres) ([0BSD](https://github.com/tc-hib/winres/blob/v0.2.0/LICENSE))
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/v1.2.1-beta.2/LICENSE))
- [github.com/vishvananda/netns](https://pkg.go.dev/github.com/vishvananda/netns) ([Apache-2.0](https://github.com/vishvananda/netns/blob/v0.0.4/LICENSE))
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE)) - [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/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)) - [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/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/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/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/9a58c47922fd/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/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/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.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.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.10.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))
- [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/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)) - [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)) - [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))
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/7b0a1988a28f/LICENSE))
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE)) - [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE))
## Additional Dependencies ## Additional Dependencies

View File

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

View File

@ -45,14 +45,6 @@ func (m *LabelMap) Get(key string) *expvar.Int {
return m.Map.Get(key).(*expvar.Int) return m.Map.Get(key).(*expvar.Int)
} }
// GetIncrFunc returns a function that increments the expvar.Int named by key.
//
// Most callers should not need this; it exists to satisfy an
// interface elsewhere.
func (m *LabelMap) GetIncrFunc(key string) func(delta int64) {
return m.Get(key).Add
}
// GetFloat returns a direct pointer to the expvar.Float for key, creating it // GetFloat returns a direct pointer to the expvar.Float for key, creating it
// if necessary. // if necessary.
func (m *LabelMap) GetFloat(key string) *expvar.Float { func (m *LabelMap) GetFloat(key string) *expvar.Float {

View File

@ -11,18 +11,6 @@ import (
"tailscale.com/tstest" "tailscale.com/tstest"
) )
func TestLabelMap(t *testing.T) {
var m LabelMap
m.GetIncrFunc("foo")(1)
m.GetIncrFunc("bar")(2)
if g, w := m.Get("foo").Value(), int64(1); g != w {
t.Errorf("foo = %v; want %v", g, w)
}
if g, w := m.Get("bar").Value(), int64(2); g != w {
t.Errorf("bar = %v; want %v", g, w)
}
}
func TestCurrentFileDescriptors(t *testing.T) { func TestCurrentFileDescriptors(t *testing.T) {
if runtime.GOOS != "linux" { if runtime.GOOS != "linux" {
t.Skipf("skipping on %v", runtime.GOOS) t.Skipf("skipping on %v", runtime.GOOS)

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.NewClock(tstest.ClockOpts{
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

@ -724,7 +724,7 @@ func (r *Resolver) parseViaDomain(domain dnsname.FQDN, typ dns.Type) (netip.Addr
return netip.Addr{}, false // badly formed, don't respond return netip.Addr{}, false // badly formed, don't respond
} }
// MapVia will never error when given an IPv4 netip.Prefix. // MapVia will never error when given an ipv4 netip.Prefix.
out, _ := tsaddr.MapVia(uint32(prefix), netip.PrefixFrom(ip4, ip4.BitLen())) out, _ := tsaddr.MapVia(uint32(prefix), netip.PrefixFrom(ip4, ip4.BitLen()))
return out.Addr(), true return out.Addr(), true
} }

View File

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

View File

@ -18,9 +18,9 @@ import (
) )
func TestMessageCache(t *testing.T) { func TestMessageCache(t *testing.T) {
clock := tstest.NewClock(tstest.ClockOpts{ clock := &tstest.Clock{
Start: time.Date(1987, 11, 1, 0, 0, 0, 0, time.UTC), Start: time.Date(1987, 11, 1, 0, 0, 0, 0, time.UTC),
}) }
mc := &MessageCache{Clock: clock.Now} mc := &MessageCache{Clock: clock.Now}
mc.SetMaxCacheSize(2) mc.SetMaxCacheSize(2)
clock.Advance(time.Second) clock.Advance(time.Second)

View File

@ -22,10 +22,6 @@ type Listener struct {
ch chan Conn ch chan Conn
closeOnce sync.Once closeOnce sync.Once
closed chan struct{} 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. // 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, Addr: addr,
} }
} }
c, s := NewConn(addr, bufferSize)
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)
defer func() { defer func() {
if err != nil { if err != nil {
c.Close() c.Close()

View File

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

View File

@ -100,7 +100,7 @@ func (c *nlConn) Receive() (message, error) {
typ = "RTM_DELADDR" typ = "RTM_DELADDR"
} }
// label attributes are seemingly only populated for IPv4 addresses in the wild. // label attributes are seemingly only populated for ipv4 addresses in the wild.
label := rmsg.Attributes.Label label := rmsg.Attributes.Label
if label == "" { if label == "" {
itf, err := net.InterfaceByIndex(int(rmsg.Index)) itf, err := net.InterfaceByIndex(int(rmsg.Index))

View File

@ -17,9 +17,16 @@ import (
"tailscale.com/net/interfaces" "tailscale.com/net/interfaces"
"tailscale.com/net/netmon" "tailscale.com/net/netmon"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/util/linuxfw"
) )
// tailscaleBypassMark is the mark indicating that packets originating
// from a socket should bypass Tailscale-managed routes during routing
// table lookups.
//
// Keep this in sync with tailscaleBypassMark in
// wgengine/router/router_linux.go.
const tailscaleBypassMark = 0x80000
// socketMarkWorksOnce is the sync.Once & cached value for useSocketMark. // socketMarkWorksOnce is the sync.Once & cached value for useSocketMark.
var socketMarkWorksOnce struct { var socketMarkWorksOnce struct {
sync.Once sync.Once
@ -112,7 +119,7 @@ func controlC(network, address string, c syscall.RawConn) error {
} }
func setBypassMark(fd uintptr) error { func setBypassMark(fd uintptr) error {
if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_MARK, linuxfw.TailscaleBypassMarkNum); err != nil { if err := unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_MARK, tailscaleBypassMark); err != nil {
return fmt.Errorf("setting SO_MARK bypass: %w", err) return fmt.Errorf("setting SO_MARK bypass: %w", err)
} }
return nil return nil

View File

@ -4,9 +4,51 @@
package netns package netns
import ( import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"testing" "testing"
) )
// verifies tailscaleBypassMark is in sync with wgengine.
func TestBypassMarkInSync(t *testing.T) {
want := fmt.Sprintf("%q", fmt.Sprintf("0x%x", tailscaleBypassMark))
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "../../wgengine/router/router_linux.go", nil, 0)
if err != nil {
t.Fatal(err)
}
for _, decl := range f.Decls {
gd, ok := decl.(*ast.GenDecl)
if !ok || gd.Tok != token.CONST {
continue
}
for _, spec := range gd.Specs {
vs, ok := spec.(*ast.ValueSpec)
if !ok {
continue
}
for i, ident := range vs.Names {
if ident.Name != "tailscaleBypassMark" {
continue
}
valExpr := vs.Values[i]
lit, ok := valExpr.(*ast.BasicLit)
if !ok {
t.Errorf("tailscaleBypassMark = %T, expected *ast.BasicLit", valExpr)
}
if lit.Value == want {
// Pass.
return
}
t.Fatalf("router_linux.go's tailscaleBypassMark = %s; not in sync with netns's %s", lit.Value, want)
}
}
}
t.Errorf("tailscaleBypassMark not found in router_linux.go")
}
func TestSocketMarkWorks(t *testing.T) { func TestSocketMarkWorks(t *testing.T) {
_ = socketMarkWorks() _ = socketMarkWorks()
// we cannot actually assert whether the test runner has SO_MARK available // we cannot actually assert whether the test runner has SO_MARK available

View File

@ -212,16 +212,9 @@ func ipForwardingEnabledLinux(p protocol, iface string) (bool, error) {
} }
return false, err return false, err
} }
on, err := strconv.ParseBool(string(bytes.TrimSpace(bs)))
val, err := strconv.ParseInt(string(bytes.TrimSpace(bs)), 10, 32)
if err != nil { if err != nil {
return false, fmt.Errorf("couldn't parse %s: %w", k, err) return false, fmt.Errorf("couldn't parse %s: %w", k, err)
} }
// 0 = disabled, 1 = enabled, 2 = enabled (but uncommon)
// https://github.com/tailscale/tailscale/issues/8375
if val < 0 || val > 2 {
return false, fmt.Errorf("unexpected value %d for %s", val, k)
}
on := val == 1 || val == 2
return on, nil return on, nil
} }

View File

@ -1,51 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package tcpinfo provides platform-agnostic accessors to information about a
// TCP connection (e.g. RTT, MSS, etc.).
package tcpinfo
import (
"errors"
"net"
"time"
)
var (
ErrNotTCP = errors.New("tcpinfo: not a TCP conn")
ErrUnimplemented = errors.New("tcpinfo: unimplemented")
)
// RTT returns the RTT for the given net.Conn.
//
// If the net.Conn is not a *net.TCPConn and cannot be unwrapped into one, then
// ErrNotTCP will be returned. If retrieving the RTT is not supported on the
// current platform, ErrUnimplemented will be returned.
func RTT(conn net.Conn) (time.Duration, error) {
tcpConn, err := unwrap(conn)
if err != nil {
return 0, err
}
return rttImpl(tcpConn)
}
// netConner is implemented by crypto/tls.Conn to unwrap into an underlying
// net.Conn.
type netConner interface {
NetConn() net.Conn
}
// unwrap attempts to unwrap a net.Conn into an underlying *net.TCPConn
func unwrap(nc net.Conn) (*net.TCPConn, error) {
for {
switch v := nc.(type) {
case *net.TCPConn:
return v, nil
case netConner:
nc = v.NetConn()
default:
return nil, ErrNotTCP
}
}
}

View File

@ -1,33 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tcpinfo
import (
"net"
"time"
"golang.org/x/sys/unix"
)
func rttImpl(conn *net.TCPConn) (time.Duration, error) {
rawConn, err := conn.SyscallConn()
if err != nil {
return 0, err
}
var (
tcpInfo *unix.TCPConnectionInfo
sysErr error
)
err = rawConn.Control(func(fd uintptr) {
tcpInfo, sysErr = unix.GetsockoptTCPConnectionInfo(int(fd), unix.IPPROTO_TCP, unix.TCP_CONNECTION_INFO)
})
if err != nil {
return 0, err
} else if sysErr != nil {
return 0, sysErr
}
return time.Duration(tcpInfo.Rttcur) * time.Millisecond, nil
}

View File

@ -1,33 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tcpinfo
import (
"net"
"time"
"golang.org/x/sys/unix"
)
func rttImpl(conn *net.TCPConn) (time.Duration, error) {
rawConn, err := conn.SyscallConn()
if err != nil {
return 0, err
}
var (
tcpInfo *unix.TCPInfo
sysErr error
)
err = rawConn.Control(func(fd uintptr) {
tcpInfo, sysErr = unix.GetsockoptTCPInfo(int(fd), unix.IPPROTO_TCP, unix.TCP_INFO)
})
if err != nil {
return 0, err
} else if sysErr != nil {
return 0, sysErr
}
return time.Duration(tcpInfo.Rtt) * time.Microsecond, nil
}

View File

@ -1,15 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !linux && !darwin
package tcpinfo
import (
"net"
"time"
)
func rttImpl(conn *net.TCPConn) (time.Duration, error) {
return 0, ErrUnimplemented
}

View File

@ -1,64 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tcpinfo
import (
"bytes"
"io"
"net"
"runtime"
"testing"
)
func TestRTT(t *testing.T) {
switch runtime.GOOS {
case "linux", "darwin":
default:
t.Skipf("not currently supported on %s", runtime.GOOS)
}
ln, err := net.Listen("tcp4", "localhost:0")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
go func() {
for {
c, err := ln.Accept()
if err != nil {
return
}
t.Cleanup(func() { c.Close() })
// Copy from the client to nowhere
go io.Copy(io.Discard, c)
}
}()
conn, err := net.Dial("tcp4", ln.Addr().String())
if err != nil {
t.Fatal(err)
}
// Write a bunch of data to the conn to force TCP session establishment
// and a few packets.
junkData := bytes.Repeat([]byte("hello world\n"), 1024*1024)
for i := 0; i < 10; i++ {
if _, err := conn.Write(junkData); err != nil {
t.Fatalf("error writing junk data [%d]: %v", i, err)
}
}
// Get the RTT now
rtt, err := RTT(conn)
if err != nil {
t.Fatalf("error getting RTT: %v", err)
}
if rtt == 0 {
t.Errorf("expected RTT > 0")
}
t.Logf("TCP rtt: %v", rtt)
}

View File

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

View File

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

View File

@ -188,9 +188,6 @@ func (d *derpProber) updateMap(ctx context.Context) error {
if existing, ok := d.nodes[n.HostName]; ok { if existing, ok := d.nodes[n.HostName]; ok {
return fmt.Errorf("derpmap has duplicate nodes: %+v and %+v", existing, n) 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 d.nodes[n.HostName] = n
} }
} }

25
release/dist/dist.go vendored
View File

@ -184,17 +184,6 @@ func (b *Build) TmpDir() string {
// binary. Builds are cached by path and env, so each build only happens once // binary. Builds are cached by path and env, so each build only happens once
// per process execution. // per process execution.
func (b *Build) BuildGoBinary(path string, env map[string]string) (string, error) { func (b *Build) BuildGoBinary(path string, env map[string]string) (string, error) {
return b.BuildGoBinaryWithTags(path, env, nil)
}
// BuildGoBinaryWithTags builds the Go binary at path and returns the
// path to the binary. Builds are cached by path, env and tags, so
// each build only happens once per process execution.
//
// The passed in tags override gocross's automatic selection of build
// tags, so you will have to figure out and specify all the tags
// relevant to your build.
func (b *Build) BuildGoBinaryWithTags(path string, env map[string]string, tags []string) (string, error) {
err := b.Once("init-go", func() error { err := b.Once("init-go", func() error {
log.Printf("Initializing Go toolchain") log.Printf("Initializing Go toolchain")
// If the build is using a tool/go, it may need to download a toolchain // If the build is using a tool/go, it may need to download a toolchain
@ -208,7 +197,7 @@ func (b *Build) BuildGoBinaryWithTags(path string, env map[string]string, tags [
return "", err return "", err
} }
buildKey := []any{"go-build", path, env, tags} buildKey := []any{"go-build", path, env}
return b.goBuilds.Do(buildKey, func() (string, error) { return b.goBuilds.Do(buildKey, func() (string, error) {
b.goBuildLimit <- struct{}{} b.goBuildLimit <- struct{}{}
defer func() { <-b.goBuildLimit }() defer func() { <-b.goBuildLimit }()
@ -218,17 +207,9 @@ func (b *Build) BuildGoBinaryWithTags(path string, env map[string]string, tags [
envStrs = append(envStrs, k+"="+v) envStrs = append(envStrs, k+"="+v)
} }
sort.Strings(envStrs) sort.Strings(envStrs)
buildDir := b.TmpDir()
args := []string{"build", "-v", "-o", buildDir}
if len(tags) > 0 {
tagsStr := strings.Join(tags, ",")
log.Printf("Building %s (with env %s, tags %s)", path, strings.Join(envStrs, " "), tagsStr)
args = append(args, "-tags="+tagsStr)
} else {
log.Printf("Building %s (with env %s)", path, strings.Join(envStrs, " ")) log.Printf("Building %s (with env %s)", path, strings.Join(envStrs, " "))
} buildDir := b.TmpDir()
args = append(args, path) cmd := b.Command(b.Repo, b.Go, "build", "-v", "-o", buildDir, path)
cmd := b.Command(b.Repo, b.Go, args...)
for k, v := range env { for k, v := range env {
cmd.Cmd.Env = append(cmd.Cmd.Env, k+"="+v) cmd.Cmd.Env = append(cmd.Cmd.Env, k+"="+v)
} }

View File

@ -2,6 +2,7 @@
".url": { ".url": {
"SYNO.SDS.Tailscale": { "SYNO.SDS.Tailscale": {
"type": "url", "type": "url",
"version": "1.8.3",
"title": "Tailscale", "title": "Tailscale",
"icon": "PACKAGE_ICON_256.PNG", "icon": "PACKAGE_ICON_256.PNG",
"url": "webman/3rdparty/Tailscale/", "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), static(fmt.Sprintf("logrotate-dsm%d", dsmVersion), "conf/logrotate.conf", 0644),
dir("ui"), dir("ui"),
static("PACKAGE_ICON_256.PNG", "ui/PACKAGE_ICON_256.PNG", 0644), 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)) static("index.cgi", "ui/index.cgi", 0755))
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -16,4 +16,4 @@
) { ) {
src = ./.; src = ./.;
}).shellNix }).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/exp/slices"
"golang.org/x/sys/unix" "golang.org/x/sys/unix"
"tailscale.com/cmd/tailscaled/childproc" "tailscale.com/cmd/tailscaled/childproc"
"tailscale.com/hostinfo"
"tailscale.com/tempfork/gliderlabs/ssh" "tailscale.com/tempfork/gliderlabs/ssh"
"tailscale.com/types/logger" "tailscale.com/types/logger"
"tailscale.com/version/distro" "tailscale.com/version/distro"
@ -121,18 +120,10 @@ func (ss *sshSession) newIncubatorCommand() (cmd *exec.Cmd) {
if isShell { if isShell {
incubatorArgs = append(incubatorArgs, "--shell") incubatorArgs = append(incubatorArgs, "--shell")
} }
if isShell || runtime.GOOS == "darwin" {
// Only the macOS version of the login command supports executing a // Only the macOS version of the login command supports executing a
// command, all other versions only support launching a shell // command, all other versions only support launching a shell
// without taking any arguments. // 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 lp, err := exec.LookPath("login"); err == nil { if lp, err := exec.LookPath("login"); err == nil {
incubatorArgs = append(incubatorArgs, "--login-cmd="+lp) incubatorArgs = append(incubatorArgs, "--login-cmd="+lp)
} }
@ -476,10 +467,10 @@ func (ss *sshSession) launchProcess() error {
} }
go resizeWindow(ptyDup /* arbitrary fd */, winCh) go resizeWindow(ptyDup /* arbitrary fd */, winCh)
ss.wrStdin = pty ss.tty = tty
ss.rdStdout = os.NewFile(uintptr(ptyDup), pty.Name()) ss.stdin = pty
ss.rdStderr = nil // not available for pty ss.stdout = os.NewFile(uintptr(ptyDup), pty.Name())
ss.childPipes = []io.Closer{tty} ss.stderr = nil // not available for pty
return nil 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. // startWithStdPipes starts cmd with os.Pipe for Stdin, Stdout and Stderr.
func (ss *sshSession) startWithStdPipes() (err error) { func (ss *sshSession) startWithStdPipes() (err error) {
var rdStdin, wrStdout, wrStderr io.ReadWriteCloser var stdin io.WriteCloser
var stdout, stderr io.ReadCloser
defer func() { defer func() {
if err != nil { 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") return errors.New("nil cmd")
} }
if rdStdin, ss.wrStdin, err = os.Pipe(); err != nil { stdin, err = cmd.StdinPipe()
if err != nil {
return err return err
} }
if ss.rdStdout, wrStdout, err = os.Pipe(); err != nil { stdout, err = cmd.StdoutPipe()
if err != nil {
return err return err
} }
if ss.rdStderr, wrStderr, err = os.Pipe(); err != nil { stderr, err = cmd.StderrPipe()
if err != nil {
return err return err
} }
ss.cmd.Stdin = rdStdin if err := cmd.Start(); err != nil {
ss.cmd.Stdout = wrStdout return err
ss.cmd.Stderr = wrStderr }
ss.childPipes = []io.Closer{rdStdin, wrStdout, wrStderr} ss.stdin = stdin
return ss.cmd.Start() ss.stdout = stdout
ss.stderr = stderr
return nil
} }
func envForUser(u *userMeta) []string { func envForUser(u *userMeta) []string {

View File

@ -422,7 +422,6 @@ func (srv *server) newConn() (*conn, error) {
c := &conn{srv: srv} c := &conn{srv: srv}
now := srv.now() now := srv.now()
c.connID = fmt.Sprintf("ssh-conn-%s-%02x", now.UTC().Format("20060102T150405"), randBytes(5)) c.connID = fmt.Sprintf("ssh-conn-%s-%02x", now.UTC().Format("20060102T150405"), randBytes(5))
fwdHandler := &ssh.ForwardedTCPHandler{}
c.Server = &ssh.Server{ c.Server = &ssh.Server{
Version: "Tailscale", Version: "Tailscale",
ServerConfigCallback: c.ServerConfig, ServerConfigCallback: c.ServerConfig,
@ -433,7 +432,6 @@ func (srv *server) newConn() (*conn, error) {
Handler: c.handleSessionPostSSHAuth, Handler: c.handleSessionPostSSHAuth,
LocalPortForwardingCallback: c.mayForwardLocalPortTo, LocalPortForwardingCallback: c.mayForwardLocalPortTo,
ReversePortForwardingCallback: c.mayReversePortForwardTo,
SubsystemHandlers: map[string]ssh.SubsystemHandler{ SubsystemHandlers: map[string]ssh.SubsystemHandler{
"sftp": c.handleSessionPostSSHAuth, "sftp": c.handleSessionPostSSHAuth,
}, },
@ -443,10 +441,7 @@ func (srv *server) newConn() (*conn, error) {
ChannelHandlers: map[string]ssh.ChannelHandler{ ChannelHandlers: map[string]ssh.ChannelHandler{
"direct-tcpip": ssh.DirectTCPIPHandler, "direct-tcpip": ssh.DirectTCPIPHandler,
}, },
RequestHandlers: map[string]ssh.RequestHandler{ RequestHandlers: map[string]ssh.RequestHandler{},
"tcpip-forward": fwdHandler.HandleSSHRequest,
"cancel-tcpip-forward": fwdHandler.HandleSSHRequest,
},
} }
ss := c.Server ss := c.Server
for k, v := range ssh.DefaultRequestHandlers { for k, v := range ssh.DefaultRequestHandlers {
@ -468,17 +463,6 @@ func (srv *server) newConn() (*conn, error) {
return c, nil 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 // mayForwardLocalPortTo reports whether the ctx should be allowed to port forward
// to the specified host and port. // to the specified host and port.
// TODO(bradfitz/maisem): should we have more checks on host/port? // TODO(bradfitz/maisem): should we have more checks on host/port?
@ -824,15 +808,11 @@ type sshSession struct {
// initialized by launchProcess: // initialized by launchProcess:
cmd *exec.Cmd cmd *exec.Cmd
wrStdin io.WriteCloser stdin io.WriteCloser
rdStdout io.ReadCloser stdout io.ReadCloser
rdStderr io.ReadCloser // rdStderr is nil for pty sessions stderr io.Reader // nil for pty sessions
ptyReq *ssh.Pty // non-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
// 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
// We use this sync.Once to ensure that we only terminate the process once, // We use this sync.Once to ensure that we only terminate the process once,
// either it exits itself or is terminated // either it exits itself or is terminated
@ -1111,22 +1091,21 @@ func (ss *sshSession) run() {
var processDone atomic.Bool var processDone atomic.Bool
go func() { go func() {
defer ss.wrStdin.Close() defer ss.stdin.Close()
if _, err := io.Copy(rec.writer("i", ss.wrStdin), ss); err != nil { if _, err := io.Copy(rec.writer("i", ss.stdin), ss); err != nil {
logf("stdin copy: %v", err) logf("stdin copy: %v", err)
ss.cancelCtx(err) ss.cancelCtx(err)
} }
}() }()
outputDone := make(chan struct{})
var openOutputStreams atomic.Int32 var openOutputStreams atomic.Int32
if ss.rdStderr != nil { if ss.stderr != nil {
openOutputStreams.Store(2) openOutputStreams.Store(2)
} else { } else {
openOutputStreams.Store(1) openOutputStreams.Store(1)
} }
go func() { go func() {
defer ss.rdStdout.Close() defer ss.stdout.Close()
_, err := io.Copy(rec.writer("o", ss), ss.rdStdout) _, err := io.Copy(rec.writer("o", ss), ss.stdout)
if err != nil && !errors.Is(err, io.EOF) { if err != nil && !errors.Is(err, io.EOF) {
isErrBecauseProcessExited := processDone.Load() && errors.Is(err, syscall.EIO) isErrBecauseProcessExited := processDone.Load() && errors.Is(err, syscall.EIO)
if !isErrBecauseProcessExited { if !isErrBecauseProcessExited {
@ -1136,41 +1115,32 @@ func (ss *sshSession) run() {
} }
if openOutputStreams.Add(-1) == 0 { if openOutputStreams.Add(-1) == 0 {
ss.CloseWrite() ss.CloseWrite()
close(outputDone)
} }
}() }()
// rdStderr is nil for ptys. // stderr is nil for ptys.
if ss.rdStderr != nil { if ss.stderr != nil {
go func() { go func() {
defer ss.rdStderr.Close() _, err := io.Copy(ss.Stderr(), ss.stderr)
_, err := io.Copy(ss.Stderr(), ss.rdStderr)
if err != nil { if err != nil {
logf("stderr copy: %v", err) logf("stderr copy: %v", err)
} }
if openOutputStreams.Add(-1) == 0 { if openOutputStreams.Add(-1) == 0 {
ss.CloseWrite() 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() err = ss.cmd.Wait()
processDone.Store(true) processDone.Store(true)
// This will either make the SSH Termination goroutine be a no-op, // 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 // or itself will be a no-op because the process was killed by the
// aforementioned goroutine. // aforementioned goroutine.
ss.exitOnce.Do(func() {}) 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 { if err == nil {
ss.logf("Session complete") ss.logf("Session complete")
ss.Exit(0) ss.Exit(0)
@ -1890,7 +1860,6 @@ var (
metricPolicyChangeKick = clientmetric.NewCounter("ssh_policy_change_kick") metricPolicyChangeKick = clientmetric.NewCounter("ssh_policy_change_kick")
metricSFTP = clientmetric.NewCounter("ssh_sftp_requests") metricSFTP = clientmetric.NewCounter("ssh_sftp_requests")
metricLocalPortForward = clientmetric.NewCounter("ssh_local_port_forward_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 // userVisibleError is a wrapper around an error that implements
@ -1908,11 +1877,3 @@ type SSHTerminationError interface {
error error
SSHTerminationMessage() string 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" "os/user"
"reflect" "reflect"
"runtime" "runtime"
"strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@ -948,19 +947,6 @@ func TestSSH(t *testing.T) {
// "foo\n" and "bar\n", not "\n" and "bar\n". // "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) { t.Run("stdin", func(t *testing.T) {
if cibuild.On() { if cibuild.On() {
t.Skip("Skipping for now; see https://github.com/tailscale/tailscale/issues/4051") t.Skip("Skipping for now; see https://github.com/tailscale/tailscale/issues/4051")

View File

@ -1,138 +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
}
// Mutate atomically mutates m[k] by calling mutator.
//
// The mutator function is called with the old value (or its zero value) and
// whether it existed in the map and it returns the new value and whether it
// should be set in the map (true) or deleted from the map (false).
//
// It returns the change in size of the map as a result of the mutation, one of
// -1 (delete), 0 (change), or 1 (addition).
func (m *ShardedMap[K, V]) Mutate(key K, mutator func(oldValue V, oldValueExisted bool) (newValue V, keep bool)) (sizeDelta int) {
shard := m.shard(key)
shard.mu.Lock()
defer shard.mu.Unlock()
oldV, oldOK := shard.m[key]
newV, newOK := mutator(oldV, oldOK)
if newOK {
shard.m[key] = newV
if oldOK {
return 0
}
return 1
}
delete(shard.m, key)
if oldOK {
return -1
}
return 0
}
// Set sets m[key] = value.
//
// 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,81 +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)
}
// Mutation adding an entry.
if v := m.Mutate(1, func(was string, ok bool) (string, bool) {
if ok {
t.Fatal("was okay")
}
return "ONE", true
}); v != 1 {
t.Errorf("Mutate = %v; want 1", v)
}
if g, w := m.Get(1), "ONE"; g != w {
t.Errorf("got %q; want %q", g, w)
}
// Mutation changing an entry.
if v := m.Mutate(1, func(was string, ok bool) (string, bool) {
if !ok {
t.Fatal("wasn't okay")
}
return was + "-" + was, true
}); v != 0 {
t.Errorf("Mutate = %v; want 0", v)
}
if g, w := m.Get(1), "ONE-ONE"; g != w {
t.Errorf("got %q; want %q", g, w)
}
// Mutation removing an entry.
if v := m.Mutate(1, func(was string, ok bool) (string, bool) {
if !ok {
t.Fatal("wasn't okay")
}
return "", false
}); v != -1 {
t.Errorf("Mutate = %v; want -1", v)
}
if g, w := m.Get(1), ""; g != w {
t.Errorf("got %q; want %q", g, w)
}
}

View File

@ -3,7 +3,7 @@
package tailcfg 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 ( import (
"bytes" "bytes"
@ -99,8 +99,7 @@ type CapabilityVersion int
// - 60: 2023-04-06: Client understands IsWireGuardOnly // - 60: 2023-04-06: Client understands IsWireGuardOnly
// - 61: 2023-04-18: Client understand SSHAction.SSHRecorderFailureAction // - 61: 2023-04-18: Client understand SSHAction.SSHRecorderFailureAction
// - 62: 2023-05-05: Client can notify control over noise for SSHEventNotificationRequest recording failure events // - 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 = 62
const CurrentCapabilityVersion CapabilityVersion = 63
type StableID string type StableID string
@ -242,6 +241,8 @@ type Node struct {
// current node doesn't have permission to know. // current node doesn't have permission to know.
Online *bool `json:",omitempty"` Online *bool `json:",omitempty"`
KeepAlive bool `json:",omitempty"` // open and keep open a connection to this peer
MachineAuthorized bool `json:",omitempty"` // TODO(crawshaw): replace with MachineStatus MachineAuthorized bool `json:",omitempty"` // TODO(crawshaw): replace with MachineStatus
// Capabilities are capabilities that the node has. // Capabilities are capabilities that the node has.
@ -529,31 +530,6 @@ type Service struct {
// TODO(apenwarr): add "tags" here for each service? // 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 is a short code representing the city in upper case.
// CityCode is used to disambiguate a city from another location
// with the same city name. It uniquely identifies a particular
// geographical location, within the tailnet.
// IATA, ICAO or ISO 3166-2 codes are recommended ("YSE")
CityCode string `json:",omitempty"`
// Priority determines the order of use of an exit node when a
// location based preference matches more than one exit node,
// the node with the highest priority wins. Nodes of equal
// probability may be selected arbitrarily.
//
// 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. // Hostinfo contains a summary of a Tailscale host.
// //
// Because it contains pointers (slices), this type should not be used // Because it contains pointers (slices), this type should not be used
@ -608,11 +584,6 @@ type Hostinfo struct {
Userspace opt.Bool `json:",omitempty"` // if the client is running in userspace (netstack) mode 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 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 // NOTE: any new fields containing pointers in this type
// require changes to Hostinfo.Equal. // require changes to Hostinfo.Equal.
} }
@ -1282,7 +1253,7 @@ type DNSConfig struct {
// match. // match.
// //
// Matches are case insensitive. // Matches are case insensitive.
ExitNodeFilteredSet []string `json:",omitempty"` ExitNodeFilteredSet []string
} }
// DNSRecord is an extra DNS record to add to MagicDNS. // DNSRecord is an extra DNS record to add to MagicDNS.
@ -2077,10 +2048,6 @@ type SSHAction struct {
// to use local port forwarding if requested. // to use local port forwarding if requested.
AllowLocalPortForwarding bool `json:"allowLocalPortForwarding,omitempty"` 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. // Recorders defines the destinations of the SSH session recorders.
// The recording will be uploaded to http://addr:port/record. // The recording will be uploaded to http://addr:port/record.
Recorders []netip.AddrPort `json:"recorders,omitempty"` Recorders []netip.AddrPort `json:"recorders,omitempty"`

View File

@ -93,6 +93,7 @@ var _NodeCloneNeedsRegeneration = Node(struct {
PrimaryRoutes []netip.Prefix PrimaryRoutes []netip.Prefix
LastSeen *time.Time LastSeen *time.Time
Online *bool Online *bool
KeepAlive bool
MachineAuthorized bool MachineAuthorized bool
Capabilities []string Capabilities []string
UnsignedPeerAPIOnly bool UnsignedPeerAPIOnly bool
@ -118,10 +119,6 @@ func (src *Hostinfo) Clone() *Hostinfo {
dst.Services = append(src.Services[:0:0], src.Services...) dst.Services = append(src.Services[:0:0], src.Services...)
dst.NetInfo = src.NetInfo.Clone() dst.NetInfo = src.NetInfo.Clone()
dst.SSH_HostKeys = append(src.SSH_HostKeys[:0:0], src.SSH_HostKeys...) 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 return dst
} }
@ -160,7 +157,6 @@ var _HostinfoCloneNeedsRegeneration = Hostinfo(struct {
Cloud string Cloud string
Userspace opt.Bool Userspace opt.Bool
UserspaceRouter opt.Bool UserspaceRouter opt.Bool
Location *Location
}{}) }{})
// Clone makes a deep copy of NetInfo. // Clone makes a deep copy of NetInfo.
@ -419,7 +415,6 @@ var _SSHActionCloneNeedsRegeneration = SSHAction(struct {
AllowAgentForwarding bool AllowAgentForwarding bool
HoldAndDelegate string HoldAndDelegate string
AllowLocalPortForwarding bool AllowLocalPortForwarding bool
AllowRemotePortForwarding bool
Recorders []netip.AddrPort Recorders []netip.AddrPort
OnRecordingFailure *SSHRecorderFailureAction OnRecordingFailure *SSHRecorderFailureAction
}{}) }{})
@ -462,29 +457,9 @@ var _ControlDialPlanCloneNeedsRegeneration = ControlDialPlan(struct {
Candidates []ControlIPCandidate 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. // Clone duplicates src into dst and reports whether it succeeded.
// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>, // 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 { func Clone(dst, src any) bool {
switch src := src.(type) { switch src := src.(type) {
case *User: case *User:
@ -613,15 +588,6 @@ func Clone(dst, src any) bool {
*dst = src.Clone() *dst = src.Clone()
return true return true
} }
case *Location:
switch dst := dst.(type) {
case *Location:
*dst = *src.Clone()
return true
case **Location:
*dst = src.Clone()
return true
}
} }
return false return false
} }

View File

@ -65,7 +65,6 @@ func TestHostinfoEqual(t *testing.T) {
"Cloud", "Cloud",
"Userspace", "Userspace",
"UserspaceRouter", "UserspaceRouter",
"Location",
} }
if have := fieldsOf(reflect.TypeOf(Hostinfo{})); !reflect.DeepEqual(have, hiHandles) { 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", t.Errorf("Hostinfo.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
@ -347,7 +346,7 @@ func TestNodeEqual(t *testing.T) {
"Key", "KeyExpiry", "KeySignature", "Machine", "DiscoKey", "Key", "KeyExpiry", "KeySignature", "Machine", "DiscoKey",
"Addresses", "AllowedIPs", "Endpoints", "DERP", "Hostinfo", "Addresses", "AllowedIPs", "Endpoints", "DERP", "Hostinfo",
"Created", "Cap", "Tags", "PrimaryRoutes", "Created", "Cap", "Tags", "PrimaryRoutes",
"LastSeen", "Online", "MachineAuthorized", "LastSeen", "Online", "KeepAlive", "MachineAuthorized",
"Capabilities", "Capabilities",
"UnsignedPeerAPIOnly", "UnsignedPeerAPIOnly",
"ComputedName", "computedHostIfDifferent", "ComputedNameWithHost", "ComputedName", "computedHostIfDifferent", "ComputedNameWithHost",

View File

@ -20,7 +20,7 @@ import (
"tailscale.com/types/views" "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. // View returns a readonly view of User.
func (p *User) View() UserView { func (p *User) View() UserView {
@ -168,6 +168,7 @@ func (v NodeView) Online() *bool {
return &x return &x
} }
func (v NodeView) KeepAlive() bool { return v.ж.KeepAlive }
func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized } func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized }
func (v NodeView) Capabilities() views.Slice[string] { return views.SliceOf(v.ж.Capabilities) } func (v NodeView) Capabilities() views.Slice[string] { return views.SliceOf(v.ж.Capabilities) }
func (v NodeView) UnsignedPeerAPIOnly() bool { return v.ж.UnsignedPeerAPIOnly } func (v NodeView) UnsignedPeerAPIOnly() bool { return v.ж.UnsignedPeerAPIOnly }
@ -209,6 +210,7 @@ var _NodeViewNeedsRegeneration = Node(struct {
PrimaryRoutes []netip.Prefix PrimaryRoutes []netip.Prefix
LastSeen *time.Time LastSeen *time.Time
Online *bool Online *bool
KeepAlive bool
MachineAuthorized bool MachineAuthorized bool
Capabilities []string Capabilities []string
UnsignedPeerAPIOnly bool UnsignedPeerAPIOnly bool
@ -301,14 +303,6 @@ func (v HostinfoView) SSH_HostKeys() views.Slice[string] { return views.SliceOf(
func (v HostinfoView) Cloud() string { return v.ж.Cloud } func (v HostinfoView) Cloud() string { return v.ж.Cloud }
func (v HostinfoView) Userspace() opt.Bool { return v.ж.Userspace } func (v HostinfoView) Userspace() opt.Bool { return v.ж.Userspace }
func (v HostinfoView) UserspaceRouter() opt.Bool { return v.ж.UserspaceRouter } 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. // A compilation failure here means this code must be regenerated, with the command at the top of this file.
@ -346,7 +340,6 @@ var _HostinfoViewNeedsRegeneration = Hostinfo(struct {
Cloud string Cloud string
Userspace opt.Bool Userspace opt.Bool
UserspaceRouter opt.Bool UserspaceRouter opt.Bool
Location *Location
}{}) }{})
// View returns a readonly view of NetInfo. // View returns a readonly view of NetInfo.
@ -947,7 +940,6 @@ func (v SSHActionView) SessionDuration() time.Duration { return v.ж.Ses
func (v SSHActionView) AllowAgentForwarding() bool { return v.ж.AllowAgentForwarding } func (v SSHActionView) AllowAgentForwarding() bool { return v.ж.AllowAgentForwarding }
func (v SSHActionView) HoldAndDelegate() string { return v.ж.HoldAndDelegate } func (v SSHActionView) HoldAndDelegate() string { return v.ж.HoldAndDelegate }
func (v SSHActionView) AllowLocalPortForwarding() bool { return v.ж.AllowLocalPortForwarding } 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) Recorders() views.Slice[netip.AddrPort] { return views.SliceOf(v.ж.Recorders) }
func (v SSHActionView) OnRecordingFailure() *SSHRecorderFailureAction { func (v SSHActionView) OnRecordingFailure() *SSHRecorderFailureAction {
if v.ж.OnRecordingFailure == nil { if v.ж.OnRecordingFailure == nil {
@ -966,7 +958,6 @@ var _SSHActionViewNeedsRegeneration = SSHAction(struct {
AllowAgentForwarding bool AllowAgentForwarding bool
HoldAndDelegate string HoldAndDelegate string
AllowLocalPortForwarding bool AllowLocalPortForwarding bool
AllowRemotePortForwarding bool
Recorders []netip.AddrPort Recorders []netip.AddrPort
OnRecordingFailure *SSHRecorderFailureAction OnRecordingFailure *SSHRecorderFailureAction
}{}) }{})
@ -1084,63 +1075,3 @@ func (v ControlDialPlanView) Candidates() views.Slice[ControlIPCandidate] {
var _ControlDialPlanViewNeedsRegeneration = ControlDialPlan(struct { var _ControlDialPlanViewNeedsRegeneration = ControlDialPlan(struct {
Candidates []ControlIPCandidate 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,121 +0,0 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package heap provides heap operations for any type that implements
// heap.Interface. A heap is a tree with the property that each node is the
// minimum-valued node in its subtree.
//
// The minimum element in the tree is the root, at index 0.
//
// A heap is a common way to implement a priority queue. To build a priority
// queue, implement the Heap interface with the (negative) priority as the
// ordering for the Less method, so Push adds items while Pop removes the
// highest-priority item from the queue. The Examples include such an
// implementation; the file example_pq_test.go has the complete source.
//
// This package is a copy of the Go standard library's
// container/heap, but using generics.
package heap
import "sort"
// The Interface type describes the requirements
// for a type using the routines in this package.
// Any type that implements it may be used as a
// min-heap with the following invariants (established after
// Init has been called or if the data is empty or sorted):
//
// !h.Less(j, i) for 0 <= i < h.Len() and 2*i+1 <= j <= 2*i+2 and j < h.Len()
//
// Note that Push and Pop in this interface are for package heap's
// implementation to call. To add and remove things from the heap,
// use heap.Push and heap.Pop.
type Interface[V any] interface {
sort.Interface
Push(x V) // add x as element Len()
Pop() V // remove and return element Len() - 1.
}
// Init establishes the heap invariants required by the other routines in this package.
// Init is idempotent with respect to the heap invariants
// and may be called whenever the heap invariants may have been invalidated.
// The complexity is O(n) where n = h.Len().
func Init[V any](h Interface[V]) {
// heapify
n := h.Len()
for i := n/2 - 1; i >= 0; i-- {
down(h, i, n)
}
}
// Push pushes the element x onto the heap.
// The complexity is O(log n) where n = h.Len().
func Push[V any](h Interface[V], x V) {
h.Push(x)
up(h, h.Len()-1)
}
// Pop removes and returns the minimum element (according to Less) from the heap.
// The complexity is O(log n) where n = h.Len().
// Pop is equivalent to Remove(h, 0).
func Pop[V any](h Interface[V]) V {
n := h.Len() - 1
h.Swap(0, n)
down(h, 0, n)
return h.Pop()
}
// Remove removes and returns the element at index i from the heap.
// The complexity is O(log n) where n = h.Len().
func Remove[V any](h Interface[V], i int) V {
n := h.Len() - 1
if n != i {
h.Swap(i, n)
if !down(h, i, n) {
up(h, i)
}
}
return h.Pop()
}
// Fix re-establishes the heap ordering after the element at index i has changed its value.
// Changing the value of the element at index i and then calling Fix is equivalent to,
// but less expensive than, calling Remove(h, i) followed by a Push of the new value.
// The complexity is O(log n) where n = h.Len().
func Fix[V any](h Interface[V], i int) {
if !down(h, i, h.Len()) {
up(h, i)
}
}
func up[V any](h Interface[V], j int) {
for {
i := (j - 1) / 2 // parent
if i == j || !h.Less(j, i) {
break
}
h.Swap(i, j)
j = i
}
}
func down[V any](h Interface[V], i0, n int) bool {
i := i0
for {
j1 := 2*i + 1
if j1 >= n || j1 < 0 { // j1 < 0 after int overflow
break
}
j := j1 // left child
if j2 := j1 + 1; j2 < n && h.Less(j2, j1) {
j = j2 // = 2*i + 2 // right child
}
if !h.Less(j, i) {
break
}
h.Swap(i, j)
i = j
}
return i > i0
}

View File

@ -1,216 +0,0 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package heap
import (
"math/rand"
"testing"
"golang.org/x/exp/constraints"
)
type myHeap[T constraints.Ordered] []T
func (h *myHeap[T]) Less(i, j int) bool {
return (*h)[i] < (*h)[j]
}
func (h *myHeap[T]) Swap(i, j int) {
(*h)[i], (*h)[j] = (*h)[j], (*h)[i]
}
func (h *myHeap[T]) Len() int {
return len(*h)
}
func (h *myHeap[T]) Pop() (v T) {
*h, v = (*h)[:h.Len()-1], (*h)[h.Len()-1]
return
}
func (h *myHeap[T]) Push(v T) {
*h = append(*h, v)
}
func (h myHeap[T]) verify(t *testing.T, i int) {
t.Helper()
n := h.Len()
j1 := 2*i + 1
j2 := 2*i + 2
if j1 < n {
if h.Less(j1, i) {
t.Errorf("heap invariant invalidated [%d] = %v > [%d] = %v", i, h[i], j1, h[j1])
return
}
h.verify(t, j1)
}
if j2 < n {
if h.Less(j2, i) {
t.Errorf("heap invariant invalidated [%d] = %v > [%d] = %v", i, h[i], j1, h[j2])
return
}
h.verify(t, j2)
}
}
func TestInit0(t *testing.T) {
h := new(myHeap[int])
for i := 20; i > 0; i-- {
h.Push(0) // all elements are the same
}
Init[int](h)
h.verify(t, 0)
for i := 1; h.Len() > 0; i++ {
x := Pop[int](h)
h.verify(t, 0)
if x != 0 {
t.Errorf("%d.th pop got %d; want %d", i, x, 0)
}
}
}
func TestInit1(t *testing.T) {
h := new(myHeap[int])
for i := 20; i > 0; i-- {
h.Push(i) // all elements are different
}
Init[int](h)
h.verify(t, 0)
for i := 1; h.Len() > 0; i++ {
x := Pop[int](h)
h.verify(t, 0)
if x != i {
t.Errorf("%d.th pop got %d; want %d", i, x, i)
}
}
}
func Test(t *testing.T) {
h := new(myHeap[int])
h.verify(t, 0)
for i := 20; i > 10; i-- {
h.Push(i)
}
Init[int](h)
h.verify(t, 0)
for i := 10; i > 0; i-- {
Push[int](h, i)
h.verify(t, 0)
}
for i := 1; h.Len() > 0; i++ {
x := Pop[int](h)
if i < 20 {
Push[int](h, 20+i)
}
h.verify(t, 0)
if x != i {
t.Errorf("%d.th pop got %d; want %d", i, x, i)
}
}
}
func TestRemove0(t *testing.T) {
h := new(myHeap[int])
for i := 0; i < 10; i++ {
h.Push(i)
}
h.verify(t, 0)
for h.Len() > 0 {
i := h.Len() - 1
x := Remove[int](h, i)
if x != i {
t.Errorf("Remove(%d) got %d; want %d", i, x, i)
}
h.verify(t, 0)
}
}
func TestRemove1(t *testing.T) {
h := new(myHeap[int])
for i := 0; i < 10; i++ {
h.Push(i)
}
h.verify(t, 0)
for i := 0; h.Len() > 0; i++ {
x := Remove[int](h, 0)
if x != i {
t.Errorf("Remove(0) got %d; want %d", x, i)
}
h.verify(t, 0)
}
}
func TestRemove2(t *testing.T) {
N := 10
h := new(myHeap[int])
for i := 0; i < N; i++ {
h.Push(i)
}
h.verify(t, 0)
m := make(map[int]bool)
for h.Len() > 0 {
m[Remove[int](h, (h.Len()-1)/2)] = true
h.verify(t, 0)
}
if len(m) != N {
t.Errorf("len(m) = %d; want %d", len(m), N)
}
for i := 0; i < len(m); i++ {
if !m[i] {
t.Errorf("m[%d] doesn't exist", i)
}
}
}
func BenchmarkDup(b *testing.B) {
const n = 10000
h := make(myHeap[int], 0, n)
for i := 0; i < b.N; i++ {
for j := 0; j < n; j++ {
Push[int](&h, 0) // all elements are the same
}
for h.Len() > 0 {
Pop[int](&h)
}
}
}
func TestFix(t *testing.T) {
h := new(myHeap[int])
h.verify(t, 0)
for i := 200; i > 0; i -= 10 {
Push[int](h, i)
}
h.verify(t, 0)
if (*h)[0] != 10 {
t.Fatalf("Expected head to be 10, was %d", (*h)[0])
}
(*h)[0] = 210
Fix[int](h, 0)
h.verify(t, 0)
for i := 100; i > 0; i-- {
elem := rand.Intn(h.Len())
if i&1 == 0 {
(*h)[elem] *= 2
} else {
(*h)[elem] /= 2
}
Fix[int](h, elem)
h.verify(t, 0)
}
}

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)
}
}

Some files were not shown because too many files have changed in this diff Show More