Compare commits

..

3 Commits

Author SHA1 Message Date
Rhea Ghosh 2637f93cff
update DNAT routing rules 2023-06-30 12:56:20 -05:00
Maisem Ali 731fecf3af cmd/k8s-operator: draw more owl
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-06-13 20:47:58 -07:00
Maisem Ali 2e60c3c684 cmd/k8s-operator: WIP add support for egress proxy
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-06-13 12:50:43 -07:00
137 changed files with 1447 additions and 8978 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

@ -16,6 +16,8 @@
// - TS_ROUTES: subnet routes to advertise. // - TS_ROUTES: subnet routes to advertise.
// - TS_DEST_IP: proxy all incoming Tailscale traffic to the given // - TS_DEST_IP: proxy all incoming Tailscale traffic to the given
// destination. // destination.
// - TS_EGRESS_IP: proxy all incoming non-Tailscale traffic to the given
// destination.
// - TS_TAILSCALED_EXTRA_ARGS: extra arguments to 'tailscaled'. // - TS_TAILSCALED_EXTRA_ARGS: extra arguments to 'tailscaled'.
// - TS_EXTRA_ARGS: extra arguments to 'tailscale up'. // - TS_EXTRA_ARGS: extra arguments to 'tailscale up'.
// - TS_USERSPACE: run with userspace networking (the default) // - TS_USERSPACE: run with userspace networking (the default)
@ -76,7 +78,8 @@ func main() {
AuthKey: defaultEnvs([]string{"TS_AUTHKEY", "TS_AUTH_KEY"}, ""), AuthKey: defaultEnvs([]string{"TS_AUTHKEY", "TS_AUTH_KEY"}, ""),
Hostname: defaultEnv("TS_HOSTNAME", ""), Hostname: defaultEnv("TS_HOSTNAME", ""),
Routes: defaultEnv("TS_ROUTES", ""), Routes: defaultEnv("TS_ROUTES", ""),
ProxyTo: defaultEnv("TS_DEST_IP", ""), IngressProxyTo: defaultEnv("TS_DEST_IP", ""),
EgressProxyTo: defaultEnv("TS_EGRESS_IP", ""),
DaemonExtraArgs: defaultEnv("TS_TAILSCALED_EXTRA_ARGS", ""), DaemonExtraArgs: defaultEnv("TS_TAILSCALED_EXTRA_ARGS", ""),
ExtraArgs: defaultEnv("TS_EXTRA_ARGS", ""), ExtraArgs: defaultEnv("TS_EXTRA_ARGS", ""),
InKubernetes: os.Getenv("KUBERNETES_SERVICE_HOST") != "", InKubernetes: os.Getenv("KUBERNETES_SERVICE_HOST") != "",
@ -91,7 +94,7 @@ func main() {
Root: defaultEnv("TS_TEST_ONLY_ROOT", "/"), Root: defaultEnv("TS_TEST_ONLY_ROOT", "/"),
} }
if cfg.ProxyTo != "" && cfg.UserspaceMode { if cfg.IngressProxyTo != "" && cfg.UserspaceMode {
log.Fatal("TS_DEST_IP is not supported with TS_USERSPACE") log.Fatal("TS_DEST_IP is not supported with TS_USERSPACE")
} }
@ -99,8 +102,8 @@ func main() {
if err := ensureTunFile(cfg.Root); err != nil { if err := ensureTunFile(cfg.Root); err != nil {
log.Fatalf("Unable to create tuntap device file: %v", err) log.Fatalf("Unable to create tuntap device file: %v", err)
} }
if cfg.ProxyTo != "" || cfg.Routes != "" { if cfg.IngressProxyTo != "" || cfg.Routes != "" {
if err := ensureIPForwarding(cfg.Root, cfg.ProxyTo, cfg.Routes); err != nil { if err := ensureIPForwarding(cfg.Root, cfg.IngressProxyTo, cfg.Routes); err != nil {
log.Printf("Failed to enable IP forwarding: %v", err) log.Printf("Failed to enable IP forwarding: %v", err)
log.Printf("To run tailscale as a proxy or router container, IP forwarding must be enabled.") log.Printf("To run tailscale as a proxy or router container, IP forwarding must be enabled.")
if cfg.InKubernetes { if cfg.InKubernetes {
@ -240,11 +243,13 @@ authLoop:
} }
var ( var (
wantProxy = cfg.ProxyTo != "" wantProxy = cfg.IngressProxyTo != "" || cfg.EgressProxyTo != ""
wantDeviceInfo = cfg.InKubernetes && cfg.KubeSecret != "" && cfg.KubernetesCanPatch wantDeviceInfo = cfg.InKubernetes && cfg.KubeSecret != "" && cfg.KubernetesCanPatch
startupTasksDone = false startupTasksDone = false
currentIPs deephash.Sum // tailscale IPs assigned to device currentIPs deephash.Sum // tailscale IPs assigned to device
currentDeviceInfo deephash.Sum // device ID and fqdn currentDeviceInfo deephash.Sum // device ID and fqdn
installedEgressProxy = false
) )
for { for {
n, err := w.Next() n, err := w.Next()
@ -261,11 +266,17 @@ authLoop:
log.Fatalf("tailscaled left running state (now in state %q), exiting", *n.State) log.Fatalf("tailscaled left running state (now in state %q), exiting", *n.State)
} }
if n.NetMap != nil { if n.NetMap != nil {
if cfg.ProxyTo != "" && len(n.NetMap.Addresses) > 0 && deephash.Update(&currentIPs, &n.NetMap.Addresses) { if cfg.IngressProxyTo != "" && len(n.NetMap.Addresses) > 0 && deephash.Update(&currentIPs, &n.NetMap.Addresses) {
if err := installIPTablesRule(ctx, cfg.ProxyTo, n.NetMap.Addresses); err != nil { if err := installIngressForwardingRule(ctx, cfg.IngressProxyTo, n.NetMap.Addresses); err != nil {
log.Fatalf("installing proxy rules: %v", err) log.Fatalf("installing proxy rules: %v", err)
} }
} }
if cfg.EgressProxyTo != "" && !installedEgressProxy {
if err := installEgressForwardingRule(ctx, cfg.EgressProxyTo); err != nil {
log.Fatalf("installing proxy rules: %v", err)
}
installedEgressProxy = true
}
deviceInfo := []any{n.NetMap.SelfNode.StableID, n.NetMap.SelfNode.Name} deviceInfo := []any{n.NetMap.SelfNode.StableID, n.NetMap.SelfNode.Name}
if cfg.InKubernetes && cfg.KubernetesCanPatch && cfg.KubeSecret != "" && deephash.Update(&currentDeviceInfo, &deviceInfo) { if cfg.InKubernetes && cfg.KubernetesCanPatch && cfg.KubeSecret != "" && deephash.Update(&currentDeviceInfo, &deviceInfo) {
if err := storeDeviceInfo(ctx, cfg.KubeSecret, n.NetMap.SelfNode.StableID, n.NetMap.SelfNode.Name); err != nil { if err := storeDeviceInfo(ctx, cfg.KubeSecret, n.NetMap.SelfNode.StableID, n.NetMap.SelfNode.Name); err != nil {
@ -492,7 +503,28 @@ func ensureIPForwarding(root, proxyTo, routes string) error {
return nil return nil
} }
func installIPTablesRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix) error { func installEgressForwardingRule(ctx context.Context, dstStr string) error {
dst, err := netip.ParseAddr(dstStr)
if err != nil {
return err
}
argv0 := "iptables"
if dst.Is6() {
argv0 = "ip6tables"
}
// Technically, if the control server ever changes the IPs assigned to this
// node, we'll slowly accumulate iptables rules. This shouldn't happen, so
// for now we'll live with it.
cmd := exec.CommandContext(ctx, argv0, "-t", "nat", "-I", "PREROUTING", "1", "!", "-i", "tailscale0", "-j", "DNAT", "--to-destination", dstStr)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("executing iptables failed: %w", err)
}
return nil
}
func installIngressForwardingRule(ctx context.Context, dstStr string, tsIPs []netip.Prefix) error {
dst, err := netip.ParseAddr(dstStr) dst, err := netip.ParseAddr(dstStr)
if err != nil { if err != nil {
return err return err
@ -532,7 +564,17 @@ type settings struct {
AuthKey string AuthKey string
Hostname string Hostname string
Routes string Routes string
ProxyTo string
// IngressProxyTo is the destination IP to which all incoming
// Tailscale traffic should be proxied. If empty, no proxying
// is done. This is typically a locally reachable IP.
IngressProxyTo string
// EgressProxyTo is the destination IP to which all incoming
// non-Tailscale traffic should be proxied. If empty, no
// proxying is done. This is typically a Tailscale IP.
EgressProxyTo string
DaemonExtraArgs string DaemonExtraArgs string
ExtraArgs string ExtraArgs string
InKubernetes bool InKubernetes bool

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+
@ -156,9 +130,8 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
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 +155,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 +180,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 +203,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 +218,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 +226,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

@ -274,6 +274,8 @@ const (
AnnotationExpose = "tailscale.com/expose" AnnotationExpose = "tailscale.com/expose"
AnnotationTags = "tailscale.com/tags" AnnotationTags = "tailscale.com/tags"
AnnotationHostname = "tailscale.com/hostname" AnnotationHostname = "tailscale.com/hostname"
AnnotationTargetIP = "tailscale.com/target-ip"
) )
// ServiceReconciler is a simple ControllerManagedBy example implementation. // ServiceReconciler is a simple ControllerManagedBy example implementation.
@ -321,7 +323,7 @@ func (a *ServiceReconciler) Reconcile(ctx context.Context, req reconcile.Request
} else if err != nil { } else if err != nil {
return reconcile.Result{}, fmt.Errorf("failed to get svc: %w", err) return reconcile.Result{}, fmt.Errorf("failed to get svc: %w", err)
} }
if !svc.DeletionTimestamp.IsZero() || !a.shouldExpose(svc) { if !svc.DeletionTimestamp.IsZero() || (!a.shouldExpose(svc) && !a.hasTargetAnnotation(svc)) {
logger.Debugf("service is being deleted or should not be exposed, cleaning up") logger.Debugf("service is being deleted or should not be exposed, cleaning up")
return reconcile.Result{}, a.maybeCleanup(ctx, logger, svc) return reconcile.Result{}, a.maybeCleanup(ctx, logger, svc)
} }
@ -402,6 +404,7 @@ func (a *ServiceReconciler) maybeCleanup(ctx context.Context, logger *zap.Sugare
return nil return nil
} }
// TODO(maisem): XXXXXXXXXXXXXXXXXXX update docs
// maybeProvision ensures that svc is exposed over tailscale, taking any actions // maybeProvision ensures that svc is exposed over tailscale, taking any actions
// necessary to reach that state. // necessary to reach that state.
// //
@ -444,6 +447,19 @@ func (a *ServiceReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
return fmt.Errorf("failed to reconcile statefulset: %w", err) return fmt.Errorf("failed to reconcile statefulset: %w", err)
} }
if a.hasTargetAnnotation(svc) {
headlessSvcName := hsvc.Name + "." + hsvc.Namespace + ".svc"
if svc.Spec.ExternalName != headlessSvcName || svc.Spec.Type != corev1.ServiceTypeExternalName {
svc.Spec.ExternalName = headlessSvcName
svc.Spec.Selector = nil
svc.Spec.Type = corev1.ServiceTypeExternalName
if err := a.Update(ctx, svc); err != nil {
return fmt.Errorf("failed to update service: %w", err)
}
}
return nil
}
if !a.hasLoadBalancerClass(svc) { if !a.hasLoadBalancerClass(svc) {
logger.Debugf("service is not a LoadBalancer, so not updating ingress") logger.Debugf("service is not a LoadBalancer, so not updating ingress")
return nil return nil
@ -482,7 +498,7 @@ func (a *ServiceReconciler) shouldExpose(svc *corev1.Service) bool {
return false return false
} }
return a.hasLoadBalancerClass(svc) || a.hasAnnotation(svc) return a.hasLoadBalancerClass(svc) || a.hasExposeAnnotation(svc)
} }
func (a *ServiceReconciler) hasLoadBalancerClass(svc *corev1.Service) bool { func (a *ServiceReconciler) hasLoadBalancerClass(svc *corev1.Service) bool {
@ -492,9 +508,12 @@ func (a *ServiceReconciler) hasLoadBalancerClass(svc *corev1.Service) bool {
*svc.Spec.LoadBalancerClass == "tailscale" *svc.Spec.LoadBalancerClass == "tailscale"
} }
func (a *ServiceReconciler) hasAnnotation(svc *corev1.Service) bool { func (a *ServiceReconciler) hasExposeAnnotation(svc *corev1.Service) bool {
return svc != nil && return svc != nil && svc.Annotations[AnnotationExpose] == "true"
svc.Annotations[AnnotationExpose] == "true" }
func (a *ServiceReconciler) hasTargetAnnotation(svc *corev1.Service) bool {
return svc != nil && svc.Annotations[AnnotationTargetIP] != ""
} }
func (a *ServiceReconciler) reconcileHeadlessService(ctx context.Context, logger *zap.SugaredLogger, svc *corev1.Service) (*corev1.Service, error) { func (a *ServiceReconciler) reconcileHeadlessService(ctx context.Context, logger *zap.SugaredLogger, svc *corev1.Service) (*corev1.Service, error) {
@ -612,11 +631,22 @@ func (a *ServiceReconciler) reconcileSTS(ctx context.Context, logger *zap.Sugare
} }
container := &ss.Spec.Template.Spec.Containers[0] container := &ss.Spec.Template.Spec.Containers[0]
container.Image = a.proxyImage container.Image = a.proxyImage
if ip := parentSvc.Annotations[AnnotationTargetIP]; ip != "" {
container.Env = append(container.Env,
corev1.EnvVar{
Name: "TS_EGRESS_IP",
Value: ip,
},
)
} else {
container.Env = append(container.Env, container.Env = append(container.Env,
corev1.EnvVar{ corev1.EnvVar{
Name: "TS_DEST_IP", Name: "TS_DEST_IP",
Value: parentSvc.Spec.ClusterIP, Value: parentSvc.Spec.ClusterIP,
}, },
)
}
container.Env = append(container.Env,
corev1.EnvVar{ corev1.EnvVar{
Name: "TS_KUBE_SECRET", Name: "TS_KUBE_SECRET",
Value: authKeySecret, Value: authKeySecret,

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

@ -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

@ -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+
@ -146,7 +120,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
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 +146,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 +177,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 +202,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 +229,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+
@ -330,7 +317,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 +365,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 +398,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 +422,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 +437,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 +446,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

@ -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 40dc4d834a5fde9872bcf470be50069f56c3e3b3

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

@ -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"
@ -742,6 +743,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 +2582,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 +2828,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 +2849,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
@ -4128,10 +4088,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 +4662,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,13 @@ 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/cmpx"
"tailscale.com/util/must"
"tailscale.com/wgengine"
) )
func TestExpandProxyArg(t *testing.T) { func TestExpandProxyArg(t *testing.T) {
@ -169,142 +160,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

@ -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

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

View File

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

View File

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

View File

@ -22,7 +22,6 @@ import (
"net/netip" "net/netip"
"os" "os"
"path/filepath" "path/filepath"
"runtime"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -61,7 +60,7 @@ func inTest() bool { return flag.Lookup("test.v") != nil }
// Server is an embedded Tailscale server. // Server is an embedded Tailscale server.
// //
// Its exported fields may be changed until the first method call. // Its exported fields may be changed until the first call to Listen.
type Server struct { type Server struct {
// Dir specifies the name of the directory to use for // Dir specifies the name of the directory to use for
// state. If empty, a directory is selected automatically // state. If empty, a directory is selected automatically
@ -80,7 +79,7 @@ type Server struct {
// If nil, a new FileStore is initialized at `Dir/tailscaled.state`. // If nil, a new FileStore is initialized at `Dir/tailscaled.state`.
// See tailscale.com/ipn/store for supported stores. // See tailscale.com/ipn/store for supported stores.
// //
// Logs will automatically be uploaded to log.tailscale.io, // Logs will automatically be uploaded to uploaded to log.tailscale.io,
// where the configuration file for logging will be saved at // where the configuration file for logging will be saved at
// `Dir/tailscaled.log.conf`. // `Dir/tailscaled.log.conf`.
Store ipn.StateStore Store ipn.StateStore
@ -108,11 +107,6 @@ type Server struct {
// If empty, the Tailscale default is used. // If empty, the Tailscale default is used.
ControlURL string ControlURL string
// Port is the UDP port to listen on for WireGuard and peer-to-peer
// traffic. If zero, a port is automatically selected. Leave this
// field at zero unless you know what you are doing.
Port uint16
getCertForTesting func(*tls.ClientHelloInfo) (*tls.Certificate, error) getCertForTesting func(*tls.ClientHelloInfo) (*tls.Certificate, error)
initOnce sync.Once initOnce sync.Once
@ -447,17 +441,8 @@ func (s *Server) start() (reterr error) {
exe, err := os.Executable() exe, err := os.Executable()
if err != nil { if err != nil {
switch runtime.GOOS {
case "js", "wasip1":
// These platforms don't implement os.Executable (at least as of Go
// 1.21), but we don't really care much: it's only used as a default
// directory and hostname when they're not supplied. But we can fall
// back to "tsnet" as well.
exe = "tsnet"
default:
return err return err
} }
}
prog := strings.TrimSuffix(strings.ToLower(filepath.Base(exe)), ".exe") prog := strings.TrimSuffix(strings.ToLower(filepath.Base(exe)), ".exe")
s.hostname = s.Hostname s.hostname = s.Hostname
@ -507,7 +492,7 @@ func (s *Server) start() (reterr error) {
sys := new(tsd.System) sys := new(tsd.System)
s.dialer = &tsdial.Dialer{Logf: logf} // mutated below (before used) s.dialer = &tsdial.Dialer{Logf: logf} // mutated below (before used)
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{ eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
ListenPort: s.Port, ListenPort: 0,
NetMon: s.netMon, NetMon: s.netMon,
Dialer: s.dialer, Dialer: s.dialer,
SetSubsystem: sys.Set, SetSubsystem: sys.Set,

View File

@ -30,7 +30,6 @@ import (
"time" "time"
"golang.org/x/net/proxy" "golang.org/x/net/proxy"
"tailscale.com/cmd/testwrapper/flakytest"
"tailscale.com/ipn" "tailscale.com/ipn"
"tailscale.com/ipn/store/mem" "tailscale.com/ipn/store/mem"
"tailscale.com/net/netns" "tailscale.com/net/netns"
@ -283,7 +282,6 @@ func TestConn(t *testing.T) {
} }
func TestLoopbackLocalAPI(t *testing.T) { func TestLoopbackLocalAPI(t *testing.T) {
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/8557")
tstest.ResourceCheck(t) tstest.ResourceCheck(t)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()
@ -358,7 +356,6 @@ func TestLoopbackLocalAPI(t *testing.T) {
} }
func TestLoopbackSOCKS5(t *testing.T) { func TestLoopbackSOCKS5(t *testing.T) {
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/8198")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel() defer cancel()

View File

@ -4,686 +4,57 @@
package tstest package tstest
import ( import (
"container/heap"
"sync" "sync"
"time" "time"
"tailscale.com/tstime"
"tailscale.com/util/mak"
) )
// ClockOpts is used to configure the initial settings for a Clock. Once the
// settings are configured as desired, call NewClock to get the resulting Clock.
type ClockOpts struct {
// Start is the starting time for the Clock. When FollowRealTime is false,
// Start is also the value that will be returned by the first call
// to Clock.Now.
Start time.Time
// Step is the amount of time the Clock will advance whenever Clock.Now is
// called. If set to zero, the Clock will only advance when Clock.Advance is
// called and/or if FollowRealTime is true.
//
// FollowRealTime and Step cannot be enabled at the same time.
Step time.Duration
// TimerChannelSize configures the maximum buffered ticks that are
// permitted in the channel of any Timer and Ticker created by this Clock.
// The special value 0 means to use the default of 1. The buffer may need to
// be increased if time is advanced by more than a single tick and proper
// functioning of the test requires that the ticks are not lost.
TimerChannelSize int
// FollowRealTime makes the simulated time increment along with real time.
// It is a compromise between determinism and the difficulty of explicitly
// managing the simulated time via Step or Clock.Advance. When
// FollowRealTime is set, calls to Now() and PeekNow() will add the
// elapsed real-world time to the simulated time.
//
// FollowRealTime and Step cannot be enabled at the same time.
FollowRealTime bool
}
// NewClock creates a Clock with the specified settings. To create a
// Clock with only the default settings, new(Clock) is equivalent, except that
// the start time will not be computed until one of the receivers is called.
func NewClock(co ClockOpts) *Clock {
if co.FollowRealTime && co.Step != 0 {
panic("only one of FollowRealTime and Step are allowed in NewClock")
}
return newClockInternal(co, nil)
}
// newClockInternal creates a Clock with the specified settings and allows
// specifying a non-standard realTimeClock.
func newClockInternal(co ClockOpts, rtClock tstime.Clock) *Clock {
if !co.FollowRealTime && rtClock != nil {
panic("rtClock can only be set with FollowRealTime enabled")
}
if co.FollowRealTime && rtClock == nil {
rtClock = new(tstime.StdClock)
}
c := &Clock{
start: co.Start,
realTimeClock: rtClock,
step: co.Step,
timerChannelSize: co.TimerChannelSize,
}
c.init() // init now to capture the current time when co.Start.IsZero()
return c
}
// Clock is a testing clock that advances every time its Now method is // Clock is a testing clock that advances every time its Now method is
// called, beginning at its start time. If no start time is specified using // called, beginning at Start.
// ClockBuilder, an arbitrary start time will be selected when the Clock is //
// created and can be retrieved by calling Clock.Start(). // The zero value starts virtual time at an arbitrary value recorded
// in Start on the first call to Now, and time never advances.
type Clock struct { type Clock struct {
// start is the first value returned by Now. It must not be modified after // Start is the first value returned by Now.
// init is called. Start time.Time
start time.Time // Step is how much to advance with each Now call.
Step time.Duration
// Present is the time that the next Now call will receive.
Present time.Time
// realTimeClock, if not nil, indicates that the Clock shall move forward sync.Mutex
// according to realTimeClock + the accumulated calls to Advance. This can
// make writing tests easier that require some control over the clock but do
// not need exact control over the clock. While step can also be used for
// this purpose, it is harder to control how quickly time moves using step.
realTimeClock tstime.Clock
initOnce sync.Once
mu sync.Mutex
// step is how much to advance with each Now call.
step time.Duration
// present is the last value returned by Now (and will be returned again by
// PeekNow).
present time.Time
// realTime is the time from realTimeClock corresponding to the current
// value of present.
realTime time.Time
// skipStep indicates that the next call to Now should not add step to
// present. This occurs after initialization and after Advance.
skipStep bool
// timerChannelSize is the buffer size to use for channels created by
// NewTimer and NewTicker.
timerChannelSize int
events eventManager
}
func (c *Clock) init() {
c.initOnce.Do(func() {
if c.realTimeClock != nil {
c.realTime = c.realTimeClock.Now()
}
if c.start.IsZero() {
if c.realTime.IsZero() {
c.start = time.Now()
} else {
c.start = c.realTime
}
}
if c.timerChannelSize == 0 {
c.timerChannelSize = 1
}
c.present = c.start
c.skipStep = true
c.events.AdvanceTo(c.present)
})
} }
// Now returns the virtual clock's current time, and advances it // Now returns the virtual clock's current time, and advances it
// according to its step configuration. // according to its step configuration.
func (c *Clock) Now() time.Time { func (c *Clock) Now() time.Time {
c.init() c.Lock()
rt := c.maybeGetRealTime() defer c.Unlock()
c.initLocked()
c.mu.Lock() step := c.Step
defer c.mu.Unlock() ret := c.Present
c.Present = c.Present.Add(step)
step := c.step return ret
if c.skipStep {
step = 0
c.skipStep = false
}
c.advanceLocked(rt, step)
return c.present
}
func (c *Clock) maybeGetRealTime() time.Time {
if c.realTimeClock == nil {
return time.Time{}
}
return c.realTimeClock.Now()
} }
func (c *Clock) advanceLocked(now time.Time, add time.Duration) { func (c *Clock) Advance(d time.Duration) {
if !now.IsZero() { c.Lock()
add += now.Sub(c.realTime) defer c.Unlock()
c.realTime = now c.initLocked()
} c.Present = c.Present.Add(d)
if add == 0 {
return
}
c.present = c.present.Add(add)
c.events.AdvanceTo(c.present)
} }
// PeekNow returns the last time reported by Now. If Now has never been called, func (c *Clock) initLocked() {
// PeekNow returns the same value as GetStart. if c.Start.IsZero() {
func (c *Clock) PeekNow() time.Time { c.Start = time.Now()
c.init()
c.mu.Lock()
defer c.mu.Unlock()
return c.present
} }
if c.Present.Before(c.Start) {
// Advance moves simulated time forward or backwards by a relative amount. Any c.Present = c.Start
// Timer or Ticker that is waiting will fire at the requested point in simulated
// time. Advance returns the new simulated time. If this Clock follows real time
// then the next call to Now will equal the return value of Advance + the
// elapsed time since calling Advance. Otherwise, the next call to Now will
// equal the return value of Advance, regardless of the current step.
func (c *Clock) Advance(d time.Duration) time.Time {
c.init()
rt := c.maybeGetRealTime()
c.mu.Lock()
defer c.mu.Unlock()
c.skipStep = true
c.advanceLocked(rt, d)
return c.present
}
// AdvanceTo moves simulated time to a new absolute value. Any Timer or Ticker
// that is waiting will fire at the requested point in simulated time. If this
// Clock follows real time then the next call to Now will equal t + the elapsed
// time since calling Advance. Otherwise, the next call to Now will equal t,
// regardless of the configured step.
func (c *Clock) AdvanceTo(t time.Time) {
c.init()
rt := c.maybeGetRealTime()
c.mu.Lock()
defer c.mu.Unlock()
c.skipStep = true
c.realTime = rt
c.present = t
c.events.AdvanceTo(c.present)
}
// GetStart returns the initial simulated time when this Clock was created.
func (c *Clock) GetStart() time.Time {
c.init()
c.mu.Lock()
defer c.mu.Unlock()
return c.start
}
// GetStep returns the amount that simulated time advances on every call to Now.
func (c *Clock) GetStep() time.Duration {
c.init()
c.mu.Lock()
defer c.mu.Unlock()
return c.step
}
// SetStep updates the amount that simulated time advances on every call to Now.
func (c *Clock) SetStep(d time.Duration) {
c.init()
c.mu.Lock()
defer c.mu.Unlock()
c.step = d
}
// SetTimerChannelSize changes the channel size for any Timer or Ticker created
// in the future. It does not affect those that were already created.
func (c *Clock) SetTimerChannelSize(n int) {
c.init()
c.mu.Lock()
defer c.mu.Unlock()
c.timerChannelSize = n
}
// NewTicker returns a Ticker that uses this Clock for accessing the current
// time.
func (c *Clock) NewTicker(d time.Duration) (tstime.TickerController, <-chan time.Time) {
c.init()
rt := c.maybeGetRealTime()
c.mu.Lock()
defer c.mu.Unlock()
c.advanceLocked(rt, 0)
t := &Ticker{
nextTrigger: c.present.Add(d),
period: d,
em: &c.events,
}
t.init(c.timerChannelSize)
return t, t.C
}
// NewTimer returns a Timer that uses this Clock for accessing the current
// time.
func (c *Clock) NewTimer(d time.Duration) (tstime.TimerController, <-chan time.Time) {
c.init()
rt := c.maybeGetRealTime()
c.mu.Lock()
defer c.mu.Unlock()
c.advanceLocked(rt, 0)
t := &Timer{
nextTrigger: c.present.Add(d),
em: &c.events,
}
t.init(c.timerChannelSize, nil)
return t, t.C
}
// AfterFunc returns a Timer that calls f when it fires, using this Clock for
// accessing the current time.
func (c *Clock) AfterFunc(d time.Duration, f func()) tstime.TimerController {
c.init()
rt := c.maybeGetRealTime()
c.mu.Lock()
defer c.mu.Unlock()
c.advanceLocked(rt, 0)
t := &Timer{
nextTrigger: c.present.Add(d),
em: &c.events,
} }
t.init(c.timerChannelSize, f)
return t
}
// eventHandler offers a common interface for Timer and Ticker events to avoid
// code duplication in eventManager.
type eventHandler interface {
// Fire signals the event. The provided time is written to the event's
// channel as the current time. The return value is the next time this event
// should fire, otherwise if it is zero then the event will be removed from
// the eventManager.
Fire(time.Time) time.Time
}
// event tracks details about an upcoming Timer or Ticker firing.
type event struct {
position int // The current index in the heap, needed for heap.Fix and heap.Remove.
when time.Time // A cache of the next time the event triggers to avoid locking issues if we were to get it from eh.
eh eventHandler
}
// eventManager tracks pending events created by Timer and Ticker. eventManager
// implements heap.Interface for efficient lookups of the next event.
type eventManager struct {
// clock is a real time clock for scheduling events with. When clock is nil,
// events only fire when AdvanceTo is called by the simulated clock that
// this eventManager belongs to. When clock is not nil, events may fire when
// timer triggers.
clock tstime.Clock
mu sync.Mutex
now time.Time
heap []*event
reverseLookup map[eventHandler]*event
// timer is an AfterFunc that triggers at heap[0].when.Sub(now) relative to
// the time represented by clock. In other words, if clock is real world
// time, then if an event is scheduled 1 second into the future in the
// simulated time, then the event will trigger after 1 second of actual test
// execution time (unless the test advances simulated time, in which case
// the timer is updated accordingly). This makes tests easier to write in
// situations where the simulated time only needs to be partially
// controlled, and the test writer wishes for simulated time to pass with an
// offset but still synchronized with the real world.
//
// In the future, this could be extended to allow simulated time to run at a
// multiple of real world time.
timer tstime.TimerController
}
func (em *eventManager) handleTimer() {
rt := em.clock.Now()
em.AdvanceTo(rt)
}
// Push implements heap.Interface.Push and must only be called by heap funcs
// with em.mu already held.
func (em *eventManager) Push(x any) {
e, ok := x.(*event)
if !ok {
panic("incorrect event type")
}
if e == nil {
panic("nil event")
}
mak.Set(&em.reverseLookup, e.eh, e)
e.position = len(em.heap)
em.heap = append(em.heap, e)
}
// Pop implements heap.Interface.Pop and must only be called by heap funcs with
// em.mu already held.
func (em *eventManager) Pop() any {
e := em.heap[len(em.heap)-1]
em.heap = em.heap[:len(em.heap)-1]
delete(em.reverseLookup, e.eh)
return e
}
// Len implements sort.Interface.Len and must only be called by heap funcs with
// em.mu already held.
func (em *eventManager) Len() int {
return len(em.heap)
}
// Less implements sort.Interface.Less and must only be called by heap funcs
// with em.mu already held.
func (em *eventManager) Less(i, j int) bool {
return em.heap[i].when.Before(em.heap[j].when)
}
// Swap implements sort.Interface.Swap and must only be called by heap funcs
// with em.mu already held.
func (em *eventManager) Swap(i, j int) {
em.heap[i], em.heap[j] = em.heap[j], em.heap[i]
em.heap[i].position = i
em.heap[j].position = j
}
// Reschedule adds/updates/deletes an event in the heap, whichever
// operation is applicable (use a zero time to delete).
func (em *eventManager) Reschedule(eh eventHandler, t time.Time) {
em.mu.Lock()
defer em.mu.Unlock()
defer em.updateTimerLocked()
e, ok := em.reverseLookup[eh]
if !ok {
if t.IsZero() {
// eh is not scheduled and also not active, so do nothing.
return
}
// eh is not scheduled but is active, so add it.
heap.Push(em, &event{
when: t,
eh: eh,
})
em.processEventsLocked(em.now) // This is always safe and required when !t.After(em.now).
return
}
if t.IsZero() {
// e is scheduled but not active, so remove it.
heap.Remove(em, e.position)
return
}
// e is scheduled and active, so update it.
e.when = t
heap.Fix(em, e.position)
em.processEventsLocked(em.now) // This is always safe and required when !t.After(em.now).
}
// AdvanceTo updates the current time to tm and fires all events scheduled
// before or equal to tm. When an event fires, it may request rescheduling and
// the rescheduled events will be combined with the other existing events that
// are waiting, and will be run in the unified ordering. A poorly behaved event
// may theoretically prevent this from ever completing, but both Timer and
// Ticker require positive steps into the future.
func (em *eventManager) AdvanceTo(tm time.Time) {
em.mu.Lock()
defer em.mu.Unlock()
defer em.updateTimerLocked()
em.processEventsLocked(tm)
em.now = tm
}
// Now returns the cached current time. It is intended for use by a Timer or
// Ticker that needs to convert a relative time to an absolute time.
func (em *eventManager) Now() time.Time {
em.mu.Lock()
defer em.mu.Unlock()
return em.now
} }
func (em *eventManager) processEventsLocked(tm time.Time) { // Reset rewinds the virtual clock to its start time.
for len(em.heap) > 0 && !em.heap[0].when.After(tm) { func (c *Clock) Reset() {
// Ideally some jitter would be added here but it's difficult to do so c.Lock()
// in a deterministic fashion. defer c.Unlock()
em.now = em.heap[0].when c.Present = c.Start
if nextFire := em.heap[0].eh.Fire(em.now); !nextFire.IsZero() {
em.heap[0].when = nextFire
heap.Fix(em, 0)
} else {
heap.Pop(em)
}
}
}
func (em *eventManager) updateTimerLocked() {
if em.clock == nil {
return
}
if len(em.heap) == 0 {
if em.timer != nil {
em.timer.Stop()
}
return
}
timeToEvent := em.heap[0].when.Sub(em.now)
if em.timer == nil {
em.timer = em.clock.AfterFunc(timeToEvent, em.handleTimer)
return
}
em.timer.Reset(timeToEvent)
}
// Ticker is a time.Ticker lookalike for use in tests that need to control when
// events fire. Ticker could be made standalone in future but for now is
// expected to be paired with a Clock and created by Clock.NewTicker.
type Ticker struct {
C <-chan time.Time // The channel on which ticks are delivered.
// em is the eventManager to be notified when nextTrigger changes.
// eventManager has its own mutex, and the pointer is immutable, therefore
// em can be accessed without holding mu.
em *eventManager
c chan<- time.Time // The writer side of C.
mu sync.Mutex
// nextTrigger is the time of the ticker's next scheduled activation. When
// Fire activates the ticker, nextTrigger is the timestamp written to the
// channel.
nextTrigger time.Time
// period is the duration that is added to nextTrigger when the ticker
// fires.
period time.Duration
}
func (t *Ticker) init(channelSize int) {
if channelSize <= 0 {
panic("ticker channel size must be non-negative")
}
c := make(chan time.Time, channelSize)
t.c = c
t.C = c
t.em.Reschedule(t, t.nextTrigger)
}
// Fire triggers the ticker. curTime is the timestamp to write to the channel.
// The next trigger time for the ticker is updated to the last computed trigger
// time + the ticker period (set at creation or using Reset). The next trigger
// time is computed this way to match standard time.Ticker behavior, which
// prevents accumulation of long term drift caused by delays in event execution.
func (t *Ticker) Fire(curTime time.Time) time.Time {
t.mu.Lock()
defer t.mu.Unlock()
if t.nextTrigger.IsZero() {
return time.Time{}
}
select {
case t.c <- curTime:
default:
}
t.nextTrigger = t.nextTrigger.Add(t.period)
return t.nextTrigger
}
// Reset adjusts the Ticker's period to d and reschedules the next fire time to
// the current simulated time + d.
func (t *Ticker) Reset(d time.Duration) {
if d <= 0 {
// The standard time.Ticker requires a positive period.
panic("non-positive period for Ticker.Reset")
}
now := t.em.Now()
t.mu.Lock()
t.resetLocked(now.Add(d), d)
t.mu.Unlock()
t.em.Reschedule(t, t.nextTrigger)
}
// ResetAbsolute adjusts the Ticker's period to d and reschedules the next fire
// time to nextTrigger.
func (t *Ticker) ResetAbsolute(nextTrigger time.Time, d time.Duration) {
if nextTrigger.IsZero() {
panic("zero nextTrigger time for ResetAbsolute")
}
if d <= 0 {
panic("non-positive period for ResetAbsolute")
}
t.mu.Lock()
t.resetLocked(nextTrigger, d)
t.mu.Unlock()
t.em.Reschedule(t, t.nextTrigger)
}
func (t *Ticker) resetLocked(nextTrigger time.Time, d time.Duration) {
t.nextTrigger = nextTrigger
t.period = d
}
// Stop deactivates the Ticker.
func (t *Ticker) Stop() {
t.mu.Lock()
t.nextTrigger = time.Time{}
t.mu.Unlock()
t.em.Reschedule(t, t.nextTrigger)
}
// Timer is a time.Timer lookalike for use in tests that need to control when
// events fire. Timer could be made standalone in future but for now must be
// paired with a Clock and created by Clock.NewTimer.
type Timer struct {
C <-chan time.Time // The channel on which ticks are delivered.
// em is the eventManager to be notified when nextTrigger changes.
// eventManager has its own mutex, and the pointer is immutable, therefore
// em can be accessed without holding mu.
em *eventManager
f func(time.Time) // The function to call when the timer expires.
mu sync.Mutex
// nextTrigger is the time of the ticker's next scheduled activation. When
// Fire activates the ticker, nextTrigger is the timestamp written to the
// channel.
nextTrigger time.Time
}
func (t *Timer) init(channelSize int, afterFunc func()) {
if channelSize <= 0 {
panic("ticker channel size must be non-negative")
}
c := make(chan time.Time, channelSize)
t.C = c
if afterFunc == nil {
t.f = func(curTime time.Time) {
select {
case c <- curTime:
default:
}
}
} else {
t.f = func(_ time.Time) { afterFunc() }
}
t.em.Reschedule(t, t.nextTrigger)
}
// Fire triggers the ticker. curTime is the timestamp to write to the channel.
// The next trigger time for the ticker is updated to the last computed trigger
// time + the ticker period (set at creation or using Reset). The next trigger
// time is computed this way to match standard time.Ticker behavior, which
// prevents accumulation of long term drift caused by delays in event execution.
func (t *Timer) Fire(curTime time.Time) time.Time {
t.mu.Lock()
defer t.mu.Unlock()
if t.nextTrigger.IsZero() {
return time.Time{}
}
t.nextTrigger = time.Time{}
t.f(curTime)
return time.Time{}
}
// Reset reschedules the next fire time to the current simulated time + d.
// Reset reports whether the timer was still active before the reset.
func (t *Timer) Reset(d time.Duration) bool {
if d <= 0 {
// The standard time.Timer requires a positive delay.
panic("non-positive delay for Timer.Reset")
}
return t.reset(t.em.Now().Add(d))
}
// ResetAbsolute reschedules the next fire time to nextTrigger.
// ResetAbsolute reports whether the timer was still active before the reset.
func (t *Timer) ResetAbsolute(nextTrigger time.Time) bool {
if nextTrigger.IsZero() {
panic("zero nextTrigger time for ResetAbsolute")
}
return t.reset(nextTrigger)
}
// Stop deactivates the Timer. Stop reports whether the timer was active before
// stopping.
func (t *Timer) Stop() bool {
return t.reset(time.Time{})
}
func (t *Timer) reset(nextTrigger time.Time) bool {
t.mu.Lock()
wasActive := !t.nextTrigger.IsZero()
t.nextTrigger = nextTrigger
t.mu.Unlock()
t.em.Reschedule(t, t.nextTrigger)
return wasActive
} }

File diff suppressed because it is too large Load Diff

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