Compare commits
3 Commits
main
...
maisem/egr
Author | SHA1 | Date |
---|---|---|
![]() |
2637f93cff | |
![]() |
731fecf3af | |
![]() |
2e60c3c684 |
|
@ -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 .
|
|
@ -32,7 +32,7 @@ jobs:
|
|||
|
||||
- name: golangci-lint
|
||||
# Note: this is the 'v3' tag as of 2023-04-17
|
||||
uses: golangci/golangci-lint-action@639cd343e1d3b897ff35927a75193d57cfcba299
|
||||
uses: golangci/golangci-lint-action@08e2f20817b15149a52b5b3ebe7de50aff2ba8c5
|
||||
with:
|
||||
version: v1.52.2
|
||||
|
|
@ -90,11 +90,11 @@ jobs:
|
|||
- name: build test wrapper
|
||||
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
|
||||
- name: test all
|
||||
run: PATH=$PWD/tool:$PATH /tmp/testwrapper ./... ${{matrix.buildflags}}
|
||||
run: ./tool/go test ${{matrix.buildflags}} -exec=/tmp/testwrapper
|
||||
env:
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
- 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:
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
- name: check that no tracked files changed
|
|
@ -47,7 +47,8 @@ RUN go install \
|
|||
golang.org/x/crypto/ssh \
|
||||
golang.org/x/crypto/acme \
|
||||
nhooyr.io/websocket \
|
||||
github.com/mdlayher/netlink
|
||||
github.com/mdlayher/netlink \
|
||||
golang.zx2c4.com/wireguard/device
|
||||
|
||||
COPY . .
|
||||
|
||||
|
@ -72,4 +73,4 @@ RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
|
|||
COPY --from=build-env /go/bin/* /usr/local/bin/
|
||||
# For compat with the previous run.sh, although ideally you should be
|
||||
# using build_docker.sh which sets an entrypoint for the image.
|
||||
RUN mkdir /tailscale && ln -s /usr/local/bin/containerboot /tailscale/run.sh
|
||||
RUN ln -s /usr/local/bin/containerboot /tailscale/run.sh
|
||||
|
|
|
@ -2,4 +2,4 @@
|
|||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
FROM alpine:3.16
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables iputils
|
||||
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
|
||||
|
|
|
@ -1 +1 @@
|
|||
1.45.0
|
||||
1.43.0
|
||||
|
|
21
api.md
21
api.md
|
@ -101,8 +101,8 @@ You can also [list all devices in the tailnet](#list-tailnet-devices) to get the
|
|||
``` jsonc
|
||||
{
|
||||
// addresses (array of strings) is a list of Tailscale IP
|
||||
// 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.
|
||||
// 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.
|
||||
"addresses": [
|
||||
"100.87.74.78",
|
||||
"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": 86400
|
||||
|
||||
// description (string) is an optional short phrase that describes what
|
||||
// this key is used for. It can be a maximum of 50 alphanumeric characters.
|
||||
// Hyphens and underscores are also allowed.
|
||||
"description": "short description of key purpose"
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -1313,9 +1308,6 @@ Note the following about required vs. optional values:
|
|||
Specifies the duration in seconds until the key should expire.
|
||||
Defaults to 90 days if not supplied.
|
||||
|
||||
- **`description`:** Optional in `POST` body.
|
||||
A short string specifying the purpose of the key. Can be a maximum of 50 alphanumeric characters. Hyphens and spaces are also allowed.
|
||||
|
||||
### Request example
|
||||
|
||||
``` jsonc
|
||||
|
@ -1333,8 +1325,7 @@ curl "https://api.tailscale.com/api/v2/tailnet/example.com/keys" \
|
|||
}
|
||||
}
|
||||
},
|
||||
"expirySeconds": 86400,
|
||||
"description": "dev access"
|
||||
"expirySeconds": 86400
|
||||
}'
|
||||
```
|
||||
|
||||
|
@ -1360,8 +1351,7 @@ It holds the capabilities specified in the request and can no longer be retrieve
|
|||
"tags": [ "tag:example" ]
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "dev access"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -1413,8 +1403,7 @@ The response is a JSON object with information about the key supplied.
|
|||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"description": "dev access"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
@ -946,21 +946,6 @@ func (lc *LocalClient) NetworkLockForceLocalDisable(ctx context.Context) error {
|
|||
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.
|
||||
// If config is nil, settings are cleared and serving is disabled.
|
||||
func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
// - TS_ROUTES: subnet routes to advertise.
|
||||
// - TS_DEST_IP: proxy all incoming Tailscale traffic to the given
|
||||
// destination.
|
||||
// - TS_EGRESS_IP: proxy all incoming non-Tailscale traffic to the given
|
||||
// destination.
|
||||
// - TS_TAILSCALED_EXTRA_ARGS: extra arguments to 'tailscaled'.
|
||||
// - TS_EXTRA_ARGS: extra arguments to 'tailscale up'.
|
||||
// - TS_USERSPACE: run with userspace networking (the default)
|
||||
|
@ -76,7 +78,8 @@ func main() {
|
|||
AuthKey: defaultEnvs([]string{"TS_AUTHKEY", "TS_AUTH_KEY"}, ""),
|
||||
Hostname: defaultEnv("TS_HOSTNAME", ""),
|
||||
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", ""),
|
||||
ExtraArgs: defaultEnv("TS_EXTRA_ARGS", ""),
|
||||
InKubernetes: os.Getenv("KUBERNETES_SERVICE_HOST") != "",
|
||||
|
@ -91,7 +94,7 @@ func main() {
|
|||
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")
|
||||
}
|
||||
|
||||
|
@ -99,8 +102,8 @@ func main() {
|
|||
if err := ensureTunFile(cfg.Root); err != nil {
|
||||
log.Fatalf("Unable to create tuntap device file: %v", err)
|
||||
}
|
||||
if cfg.ProxyTo != "" || cfg.Routes != "" {
|
||||
if err := ensureIPForwarding(cfg.Root, cfg.ProxyTo, cfg.Routes); err != nil {
|
||||
if cfg.IngressProxyTo != "" || cfg.Routes != "" {
|
||||
if err := ensureIPForwarding(cfg.Root, cfg.IngressProxyTo, cfg.Routes); err != nil {
|
||||
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.")
|
||||
if cfg.InKubernetes {
|
||||
|
@ -240,11 +243,13 @@ authLoop:
|
|||
}
|
||||
|
||||
var (
|
||||
wantProxy = cfg.ProxyTo != ""
|
||||
wantProxy = cfg.IngressProxyTo != "" || cfg.EgressProxyTo != ""
|
||||
wantDeviceInfo = cfg.InKubernetes && cfg.KubeSecret != "" && cfg.KubernetesCanPatch
|
||||
startupTasksDone = false
|
||||
currentIPs deephash.Sum // tailscale IPs assigned to device
|
||||
currentDeviceInfo deephash.Sum // device ID and fqdn
|
||||
|
||||
installedEgressProxy = false
|
||||
)
|
||||
for {
|
||||
n, err := w.Next()
|
||||
|
@ -261,11 +266,17 @@ authLoop:
|
|||
log.Fatalf("tailscaled left running state (now in state %q), exiting", *n.State)
|
||||
}
|
||||
if n.NetMap != nil {
|
||||
if cfg.ProxyTo != "" && len(n.NetMap.Addresses) > 0 && deephash.Update(¤tIPs, &n.NetMap.Addresses) {
|
||||
if err := installIPTablesRule(ctx, cfg.ProxyTo, n.NetMap.Addresses); err != nil {
|
||||
if cfg.IngressProxyTo != "" && len(n.NetMap.Addresses) > 0 && deephash.Update(¤tIPs, &n.NetMap.Addresses) {
|
||||
if err := installIngressForwardingRule(ctx, cfg.IngressProxyTo, n.NetMap.Addresses); err != nil {
|
||||
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}
|
||||
if cfg.InKubernetes && cfg.KubernetesCanPatch && cfg.KubeSecret != "" && deephash.Update(¤tDeviceInfo, &deviceInfo) {
|
||||
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
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -529,10 +561,20 @@ func installIPTablesRule(ctx context.Context, dstStr string, tsIPs []netip.Prefi
|
|||
|
||||
// settings is all the configuration for containerboot.
|
||||
type settings struct {
|
||||
AuthKey string
|
||||
Hostname string
|
||||
Routes string
|
||||
ProxyTo string
|
||||
AuthKey string
|
||||
Hostname string
|
||||
Routes 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
|
||||
ExtraArgs string
|
||||
InKubernetes bool
|
||||
|
|
|
@ -72,7 +72,7 @@ func NewManualCertManager(certdir, hostname string) (certProvider, error) {
|
|||
return nil, fmt.Errorf("can not load cert: %w", err)
|
||||
}
|
||||
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
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ func (m *manualCertManager) TLSConfig() *tls.Config {
|
|||
|
||||
func (m *manualCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
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
|
||||
|
|
|
@ -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
|
||||
github.com/beorn7/perks/quantile 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/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
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
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
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
|
||||
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/nltest from github.com/google/nftables
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
|
||||
💣 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/internal/fs 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
|
||||
💣 go4.org/mem from tailscale.com/client/tailscale+
|
||||
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/types/descriptorpb from google.golang.org/protobuf/reflect/protodesc
|
||||
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/internal/errd 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/sockstats from tailscale.com/derp/derphttp
|
||||
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/tsaddr from tailscale.com/ipn+
|
||||
💣 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/httpm from tailscale.com/client/tailscale
|
||||
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/multierr from tailscale.com/health+
|
||||
tailscale.com/util/multierr from tailscale.com/health
|
||||
tailscale.com/util/set from tailscale.com/health+
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
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/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
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+
|
||||
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
|
||||
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+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from internal/profile+
|
||||
L compress/zlib from debug/elf
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdsa+
|
||||
|
@ -232,8 +203,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
|||
crypto/tls from golang.org/x/crypto/acme+
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509+
|
||||
L debug/dwarf from debug/elf
|
||||
L debug/elf from golang.org/x/sys/unix
|
||||
embed from crypto/internal/nistec+
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
|
@ -249,7 +218,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
|||
fmt from compress/flate+
|
||||
go/token from google.golang.org/protobuf/internal/strs
|
||||
hash from crypto+
|
||||
L hash/adler32 from compress/zlib
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from google.golang.org/protobuf/internal/detrand
|
||||
hash/maphash from go4.org/mem
|
||||
|
@ -258,7 +226,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
|||
io/fs from crypto/x509+
|
||||
io/ioutil from github.com/mitchellh/go-ps+
|
||||
log from expvar+
|
||||
log/internal from log
|
||||
math from compress/flate+
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
|
|
|
@ -274,6 +274,8 @@ const (
|
|||
AnnotationExpose = "tailscale.com/expose"
|
||||
AnnotationTags = "tailscale.com/tags"
|
||||
AnnotationHostname = "tailscale.com/hostname"
|
||||
|
||||
AnnotationTargetIP = "tailscale.com/target-ip"
|
||||
)
|
||||
|
||||
// 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 {
|
||||
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")
|
||||
return reconcile.Result{}, a.maybeCleanup(ctx, logger, svc)
|
||||
}
|
||||
|
@ -402,6 +404,7 @@ func (a *ServiceReconciler) maybeCleanup(ctx context.Context, logger *zap.Sugare
|
|||
return nil
|
||||
}
|
||||
|
||||
// TODO(maisem): XXXXXXXXXXXXXXXXXXX update docs
|
||||
// maybeProvision ensures that svc is exposed over tailscale, taking any actions
|
||||
// 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)
|
||||
}
|
||||
|
||||
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) {
|
||||
logger.Debugf("service is not a LoadBalancer, so not updating ingress")
|
||||
return nil
|
||||
|
@ -482,7 +498,7 @@ func (a *ServiceReconciler) shouldExpose(svc *corev1.Service) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
return a.hasLoadBalancerClass(svc) || a.hasAnnotation(svc)
|
||||
return a.hasLoadBalancerClass(svc) || a.hasExposeAnnotation(svc)
|
||||
}
|
||||
|
||||
func (a *ServiceReconciler) hasLoadBalancerClass(svc *corev1.Service) bool {
|
||||
|
@ -492,9 +508,12 @@ func (a *ServiceReconciler) hasLoadBalancerClass(svc *corev1.Service) bool {
|
|||
*svc.Spec.LoadBalancerClass == "tailscale"
|
||||
}
|
||||
|
||||
func (a *ServiceReconciler) hasAnnotation(svc *corev1.Service) bool {
|
||||
return svc != nil &&
|
||||
svc.Annotations[AnnotationExpose] == "true"
|
||||
func (a *ServiceReconciler) hasExposeAnnotation(svc *corev1.Service) bool {
|
||||
return svc != nil && 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) {
|
||||
|
@ -612,11 +631,22 @@ func (a *ServiceReconciler) reconcileSTS(ctx context.Context, logger *zap.Sugare
|
|||
}
|
||||
container := &ss.Spec.Template.Spec.Containers[0]
|
||||
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,
|
||||
corev1.EnvVar{
|
||||
Name: "TS_DEST_IP",
|
||||
Value: parentSvc.Spec.ClusterIP,
|
||||
},
|
||||
)
|
||||
}
|
||||
container.Env = append(container.Env,
|
||||
corev1.EnvVar{
|
||||
Name: "TS_DEST_IP",
|
||||
Value: parentSvc.Spec.ClusterIP,
|
||||
},
|
||||
corev1.EnvVar{
|
||||
Name: "TS_KUBE_SECRET",
|
||||
Value: authKeySecret,
|
||||
|
|
|
@ -26,7 +26,6 @@ import (
|
|||
|
||||
var (
|
||||
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")
|
||||
)
|
||||
|
||||
|
@ -41,7 +40,6 @@ func main() {
|
|||
hostinfo.SetApp("sniproxy")
|
||||
|
||||
var s server
|
||||
s.ts.Port = uint16(*wgPort)
|
||||
defer s.ts.Close()
|
||||
|
||||
lc, err := s.ts.LocalClient()
|
||||
|
|
|
@ -30,10 +30,10 @@ func newFunnelCommand(e *serveEnv) *ffcli.Command {
|
|||
return &ffcli.Command{
|
||||
Name: "funnel",
|
||||
ShortHelp: "Turn on/off Funnel service",
|
||||
ShortUsage: strings.Join([]string{
|
||||
"funnel <serve-port> {on|off}",
|
||||
"funnel status [--json]",
|
||||
}, "\n "),
|
||||
ShortUsage: strings.TrimSpace(`
|
||||
funnel <serve-port> {on|off}
|
||||
funnel status [--json]
|
||||
`),
|
||||
LongHelp: strings.Join([]string{
|
||||
"Funnel allows you to publish a 'tailscale serve'",
|
||||
"server publicly, open to the entire internet.",
|
||||
|
|
|
@ -465,16 +465,7 @@ func runNetworkLockSign(ctx context.Context, args []string) error {
|
|||
}
|
||||
}
|
||||
|
||||
err := 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
|
||||
return localClient.NetworkLockSign(ctx, nodeKey, []byte(rotationKey.Verifier()))
|
||||
}
|
||||
|
||||
var nlDisableCmd = &ffcli.Command{
|
||||
|
|
|
@ -51,7 +51,7 @@ relay node.
|
|||
fs.BoolVar(&pingArgs.tsmp, "tsmp", false, "do a TSMP-level ping (through WireGuard, but not either host OS stack)")
|
||||
fs.BoolVar(&pingArgs.icmp, "icmp", false, "do a ICMP-level ping (through WireGuard, but not the local host OS stack)")
|
||||
fs.BoolVar(&pingArgs.peerAPI, "peerapi", false, "try hitting the peer's peerapi HTTP server")
|
||||
fs.IntVar(&pingArgs.num, "c", 10, "max number of pings to send. 0 for infinity.")
|
||||
fs.IntVar(&pingArgs.num, "c", 10, "max number of pings to send")
|
||||
fs.DurationVar(&pingArgs.timeout, "timeout", 5*time.Second, "timeout before giving up on a ping")
|
||||
return fs
|
||||
})(),
|
||||
|
|
|
@ -35,14 +35,13 @@ func newServeCommand(e *serveEnv) *ffcli.Command {
|
|||
return &ffcli.Command{
|
||||
Name: "serve",
|
||||
ShortHelp: "Serve content and local servers",
|
||||
ShortUsage: strings.Join([]string{
|
||||
"serve http:<port> <mount-point> <source> [off]",
|
||||
"serve https:<port> <mount-point> <source> [off]",
|
||||
"serve tcp:<port> tcp://localhost:<local-port> [off]",
|
||||
"serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]",
|
||||
"serve status [--json]",
|
||||
"serve reset",
|
||||
}, "\n "),
|
||||
ShortUsage: strings.TrimSpace(`
|
||||
serve https:<port> <mount-point> <source> [off]
|
||||
serve tcp:<port> tcp://localhost:<local-port> [off]
|
||||
serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]
|
||||
serve status [--json]
|
||||
serve reset
|
||||
`),
|
||||
LongHelp: strings.TrimSpace(`
|
||||
*** BETA; all of this is subject to change ***
|
||||
|
||||
|
@ -59,8 +58,8 @@ EXAMPLES
|
|||
- To proxy requests to a web server at 127.0.0.1:3000:
|
||||
$ tailscale serve https:443 / http://127.0.0.1:3000
|
||||
|
||||
Or, using the default port (443):
|
||||
$ tailscale serve https / http://127.0.0.1:3000
|
||||
Or, using the default port:
|
||||
$ tailscale serve https / http://127.0.0.1:3000
|
||||
|
||||
- To serve a single file or a directory of files:
|
||||
$ tailscale serve https / /home/alice/blog/index.html
|
||||
|
@ -69,12 +68,6 @@ EXAMPLES
|
|||
- To serve simple static text:
|
||||
$ tailscale serve https:8080 / text:"Hello, world!"
|
||||
|
||||
- To serve over HTTP (tailnet only):
|
||||
$ tailscale serve http:80 / http://127.0.0.1:3000
|
||||
|
||||
Or, using the default port (80):
|
||||
$ tailscale serve http / http://127.0.0.1:3000
|
||||
|
||||
- To forward incoming TCP connections on port 2222 to a local TCP server on
|
||||
port 22 (e.g. to run OpenSSH in parallel with Tailscale SSH):
|
||||
$ tailscale serve tcp:2222 tcp://localhost:22
|
||||
|
@ -182,7 +175,6 @@ func (e *serveEnv) getLocalClientStatus(ctx context.Context) (*ipnstate.Status,
|
|||
// serve config types like proxy, path, and text.
|
||||
//
|
||||
// Examples:
|
||||
// - tailscale serve http / http://localhost:3000
|
||||
// - tailscale serve https / http://localhost:3000
|
||||
// - tailscale serve https /images/ /var/www/images/
|
||||
// - tailscale serve https:10000 /motd.txt text:"Hello, world!"
|
||||
|
@ -207,14 +199,19 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
|||
return e.lc.SetServeConfig(ctx, sc)
|
||||
}
|
||||
|
||||
parsePort := func(portStr string) (uint16, error) {
|
||||
port64, err := strconv.ParseUint(portStr, 10, 16)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint16(port64), nil
|
||||
}
|
||||
|
||||
srcType, srcPortStr, found := strings.Cut(args[0], ":")
|
||||
if !found {
|
||||
if srcType == "https" && srcPortStr == "" {
|
||||
// Default https port to 443.
|
||||
srcPortStr = "443"
|
||||
} else if srcType == "http" && srcPortStr == "" {
|
||||
// Default http port to 80.
|
||||
srcPortStr = "80"
|
||||
} else {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
@ -222,18 +219,18 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
|||
|
||||
turnOff := "off" == args[len(args)-1]
|
||||
|
||||
if len(args) < 2 || ((srcType == "https" || srcType == "http") && !turnOff && len(args) < 3) {
|
||||
if len(args) < 2 || (srcType == "https" && !turnOff && len(args) < 3) {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid number of arguments\n\n")
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
srcPort, err := parseServePort(srcPortStr)
|
||||
srcPort, err := parsePort(srcPortStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid port %q: %w", srcPortStr, err)
|
||||
return err
|
||||
}
|
||||
|
||||
switch srcType {
|
||||
case "https", "http":
|
||||
case "https":
|
||||
mount, err := cleanMountPoint(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -241,8 +238,7 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
|||
if turnOff {
|
||||
return e.handleWebServeRemove(ctx, srcPort, mount)
|
||||
}
|
||||
useTLS := srcType == "https"
|
||||
return e.handleWebServe(ctx, srcPort, useTLS, mount, args[2])
|
||||
return e.handleWebServe(ctx, srcPort, mount, args[2])
|
||||
case "tcp", "tls-terminated-tcp":
|
||||
if turnOff {
|
||||
return e.handleTCPServeRemove(ctx, srcPort)
|
||||
|
@ -250,20 +246,20 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
|||
return e.handleTCPServe(ctx, srcType, srcPort, args[1])
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "error: invalid serve type %q\n", srcType)
|
||||
fmt.Fprint(os.Stderr, "must be one of: http:<port>, https:<port>, tcp:<port> or tls-terminated-tcp:<port>\n\n", srcType)
|
||||
fmt.Fprint(os.Stderr, "must be one of: https:<port>, tcp:<port> or tls-terminated-tcp:<port>\n\n", srcType)
|
||||
return flag.ErrHelp
|
||||
}
|
||||
}
|
||||
|
||||
// handleWebServe handles the "tailscale serve (http/https):..." subcommand. It
|
||||
// configures the serve config to forward HTTPS connections to the given source.
|
||||
// handleWebServe handles the "tailscale serve https:..." subcommand.
|
||||
// It configures the serve config to forward HTTPS connections to the
|
||||
// given source.
|
||||
//
|
||||
// Examples:
|
||||
// - tailscale serve http / http://localhost:3000
|
||||
// - tailscale serve https / http://localhost:3000
|
||||
// - tailscale serve https:8443 /files/ /home/alice/shared-files/
|
||||
// - tailscale serve https:10000 /motd.txt text:"Hello, world!"
|
||||
func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, useTLS bool, mount, source string) error {
|
||||
func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, mount, source string) error {
|
||||
h := new(ipn.HTTPHandler)
|
||||
|
||||
ts, _, _ := strings.Cut(source, ":")
|
||||
|
@ -322,7 +318,7 @@ func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, useTLS bo
|
|||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
mak.Set(&sc.TCP, srvPort, &ipn.TCPPortHandler{HTTPS: useTLS, HTTP: !useTLS})
|
||||
mak.Set(&sc.TCP, srvPort, &ipn.TCPPortHandler{HTTPS: true})
|
||||
|
||||
if _, ok := sc.Web[hp]; !ok {
|
||||
mak.Set(&sc.Web, hp, new(ipn.WebServerConfig))
|
||||
|
@ -630,10 +626,7 @@ func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
|
|||
printf("\n")
|
||||
}
|
||||
for hp := range sc.Web {
|
||||
err := e.printWebStatusTree(sc, hp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printWebStatusTree(sc, hp)
|
||||
printf("\n")
|
||||
}
|
||||
printFunnelWarning(sc)
|
||||
|
@ -672,37 +665,20 @@ func printTCPStatusTree(ctx context.Context, sc *ipn.ServeConfig, st *ipnstate.S
|
|||
return nil
|
||||
}
|
||||
|
||||
func (e *serveEnv) printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) error {
|
||||
// No-op if no serve config
|
||||
func printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) {
|
||||
if sc == nil {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
fStatus := "tailnet only"
|
||||
if sc.AllowFunnel[hp] {
|
||||
fStatus = "Funnel on"
|
||||
}
|
||||
host, portStr, _ := net.SplitHostPort(string(hp))
|
||||
|
||||
port, err := parseServePort(portStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid port %q: %w", portStr, err)
|
||||
if portStr == "443" {
|
||||
printf("https://%s (%s)\n", host, fStatus)
|
||||
} else {
|
||||
printf("https://%s:%s (%s)\n", host, portStr, fStatus)
|
||||
}
|
||||
|
||||
scheme := "https"
|
||||
if sc.IsServingHTTP(port) {
|
||||
scheme = "http"
|
||||
}
|
||||
|
||||
portPart := ":" + portStr
|
||||
if scheme == "http" && portStr == "80" ||
|
||||
scheme == "https" && portStr == "443" {
|
||||
portPart = ""
|
||||
}
|
||||
if scheme == "http" {
|
||||
hostname, _, _ := strings.Cut("host", ".")
|
||||
printf("%s://%s%s (%s)\n", scheme, hostname, portPart, fStatus)
|
||||
}
|
||||
printf("%s://%s%s (%s)\n", scheme, host, portPart, fStatus)
|
||||
srvTypeAndDesc := func(h *ipn.HTTPHandler) (string, string) {
|
||||
switch {
|
||||
case h.Path != "":
|
||||
|
@ -729,8 +705,6 @@ func (e *serveEnv) printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) erro
|
|||
t, d := srvTypeAndDesc(h)
|
||||
printf("%s %s%s %-5s %s\n", "|--", m, strings.Repeat(" ", maxLen-len(m)), t, d)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func elipticallyTruncate(s string, max int) string {
|
||||
|
@ -751,16 +725,3 @@ func (e *serveEnv) runServeReset(ctx context.Context, args []string) error {
|
|||
sc := new(ipn.ServeConfig)
|
||||
return e.lc.SetServeConfig(ctx, sc)
|
||||
}
|
||||
|
||||
// parseServePort parses a port number from a string and returns it as a
|
||||
// uint16. It returns an error if the port number is invalid or zero.
|
||||
func parseServePort(s string) (uint16, error) {
|
||||
p, err := strconv.ParseUint(s, 10, 16)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if p == 0 {
|
||||
return 0, errors.New("port number must be non-zero")
|
||||
}
|
||||
return uint16(p), nil
|
||||
}
|
||||
|
|
|
@ -89,59 +89,6 @@ func TestServeConfigMutations(t *testing.T) {
|
|||
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||
})
|
||||
|
||||
// https
|
||||
add(step{reset: true})
|
||||
add(step{ // allow omitting port (default to 80)
|
||||
command: cmd("http / http://localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{ // support non Funnel port
|
||||
command: cmd("http:9999 /abc http://localhost:3001"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}, 9999: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:9999": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("http:9999 /abc off"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("http:8080 /abc http://127.0.0.1:3001"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}, 8080: {HTTP: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:8080": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// https
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
|
|
|
@ -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/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||
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/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/hdevalence/ed25519consensus from tailscale.com/tka
|
||||
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+
|
||||
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/nltest from github.com/google/nftables
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
|
||||
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/soap 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/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
|
||||
💣 go4.org/mem from tailscale.com/derp+
|
||||
go4.org/netipx from tailscale.com/wgengine/filter
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
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
|
||||
nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||
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/sockstats from tailscale.com/control/controlhttp+
|
||||
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/tsaddr from tailscale.com/net/interfaces+
|
||||
💣 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/httpm from tailscale.com/client/tailscale
|
||||
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/multierr from tailscale.com/control/controlhttp+
|
||||
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/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
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/net/bpf from github.com/mdlayher/netlink+
|
||||
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+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from net/http
|
||||
compress/zlib from image/png+
|
||||
compress/zlib from image/png
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
crypto from crypto/ecdsa+
|
||||
|
@ -230,8 +202,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509+
|
||||
database/sql/driver from github.com/google/uuid
|
||||
L debug/dwarf from debug/elf
|
||||
L debug/elf from golang.org/x/sys/unix
|
||||
embed from tailscale.com/cmd/tailscale/cli+
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
|
@ -259,7 +229,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||
io/fs from crypto/x509+
|
||||
io/ioutil from golang.org/x/sys/cpu+
|
||||
log from expvar+
|
||||
log/internal from log
|
||||
math from compress/flate+
|
||||
math/big from crypto/dsa+
|
||||
math/bits from compress/flate+
|
||||
|
|
|
@ -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/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/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
|
||||
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com
|
||||
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+
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
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
|
||||
L 💣 github.com/illarion/gonotify from tailscale.com/net/dns
|
||||
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/netlink 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/socket from github.com/mdlayher/netlink
|
||||
💣 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+
|
||||
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+
|
||||
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/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/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+
|
||||
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/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/refs from gvisor.dev/gvisor/pkg/bufferv2+
|
||||
💣 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/sockstats from tailscale.com/control/controlclient+
|
||||
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/tsaddr from tailscale.com/ipn+
|
||||
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/httpm from tailscale.com/client/tailscale+
|
||||
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/multierr from tailscale.com/control/controlclient+
|
||||
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+
|
||||
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/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/net/bpf from github.com/mdlayher/genetlink+
|
||||
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+
|
||||
compress/flate from compress/gzip+
|
||||
compress/gzip from golang.org/x/net/http2+
|
||||
L compress/zlib from debug/elf
|
||||
container/heap from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
|
||||
container/list from crypto/tls+
|
||||
context from crypto/tls+
|
||||
|
@ -437,8 +422,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||
crypto/tls from github.com/tcnksm/go-httpstat+
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509+
|
||||
L debug/dwarf from debug/elf
|
||||
L debug/elf from golang.org/x/sys/unix
|
||||
embed from tailscale.com+
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
|
@ -454,7 +437,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||
flag from net/http/httptest+
|
||||
fmt from compress/flate+
|
||||
hash from crypto+
|
||||
hash/adler32 from tailscale.com/ipn/ipnlocal+
|
||||
hash/adler32 from tailscale.com/ipn/ipnlocal
|
||||
hash/crc32 from compress/gzip+
|
||||
hash/fnv from tailscale.com/wgengine/magicsock+
|
||||
hash/maphash from go4.org/mem
|
||||
|
@ -463,7 +446,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||
io/fs from crypto/x509+
|
||||
io/ioutil from github.com/godbus/dbus/v5+
|
||||
log from expvar+
|
||||
log/internal from log
|
||||
LD log/syslog from tailscale.com/ssh/tailssh
|
||||
math from compress/flate+
|
||||
math/big from crypto/dsa+
|
||||
|
|
|
@ -7,20 +7,16 @@
|
|||
package flakytest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// FlakyTestLogMessage is a sentinel value that is printed to stderr when a
|
||||
// flaky test is marked. This is used by cmd/testwrapper to detect flaky tests
|
||||
// and retry them.
|
||||
const FlakyTestLogMessage = "flakytest: this is a known flaky test"
|
||||
|
||||
// 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"
|
||||
// InTestWrapper returns whether or not this binary is running under our test
|
||||
// wrapper.
|
||||
func InTestWrapper() bool {
|
||||
return os.Getenv("TS_IN_TESTWRAPPER") != ""
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
fmt.Fprintln(os.Stderr, FlakyTestLogMessage) // sentinel value for testwrapper
|
||||
t.Logf("flakytest: issue tracking this flaky test: %s", issue)
|
||||
if !InTestWrapper() {
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -3,10 +3,7 @@
|
|||
|
||||
package flakytest
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
import "testing"
|
||||
|
||||
func TestIssueFormat(t *testing.T) {
|
||||
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.")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,278 +1,62 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// testwrapper is a wrapper for retrying flaky tests. It is an alternative to
|
||||
// `go test` and re-runs failed marked flaky tests (using the flakytest pkg). It
|
||||
// takes different arguments than go test and requires the first positional
|
||||
// argument to be the pattern to test.
|
||||
// testwrapper is a wrapper for retrying flaky tests, using the -exec flag of
|
||||
// 'go test'. Tests that are flaky can use the 'flakytest' subpackage to mark
|
||||
// themselves as flaky and be retried on failure.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/exp/maps"
|
||||
"tailscale.com/cmd/testwrapper/flakytest"
|
||||
)
|
||||
|
||||
const maxAttempts = 3
|
||||
|
||||
type testAttempt struct {
|
||||
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
|
||||
}
|
||||
const (
|
||||
retryStatus = 123
|
||||
maxIterations = 3
|
||||
)
|
||||
|
||||
func main() {
|
||||
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
|
||||
// for a test. We don't need to parse any other flags, so we just use the
|
||||
// flag package to parse the -v flag and then pass the rest of the args
|
||||
// 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
|
||||
log.SetPrefix("testwrapper: ")
|
||||
if !debug {
|
||||
log.SetFlags(0)
|
||||
}
|
||||
|
||||
toRun := []*nextRun{
|
||||
{
|
||||
tests: []*packageTests{{pattern: pattern}},
|
||||
attempt: 1,
|
||||
},
|
||||
}
|
||||
printPkgOutcome := func(pkg, outcome string, attempt int) {
|
||||
if outcome == "skip" {
|
||||
fmt.Printf("?\t%s [skipped/no tests] \n", pkg)
|
||||
for i := 1; i <= maxIterations; i++ {
|
||||
if i > 1 {
|
||||
log.Printf("retrying flaky tests (%d of %d)", i, maxIterations)
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, os.Args[1], os.Args[2:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = append(os.Environ(), "TS_IN_TESTWRAPPER=1")
|
||||
err := cmd.Run()
|
||||
if err == nil {
|
||||
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 thisRun *nextRun
|
||||
thisRun, toRun = toRun[0], toRun[1:]
|
||||
|
||||
if thisRun.attempt >= maxAttempts {
|
||||
fmt.Println("max attempts reached")
|
||||
os.Exit(1)
|
||||
}
|
||||
if thisRun.attempt > 1 {
|
||||
fmt.Printf("\n\nAttempt #%d: Retrying flaky tests:\n\n", thisRun.attempt)
|
||||
}
|
||||
|
||||
failed := false
|
||||
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
|
||||
}
|
||||
var exitErr *exec.ExitError
|
||||
if !errors.As(err, &exitErr) {
|
||||
if debug {
|
||||
log.Printf("error isn't an ExitError")
|
||||
}
|
||||
}
|
||||
if failed {
|
||||
fmt.Println("\n\nNot retrying flaky tests because non-flaky tests failed.")
|
||||
os.Exit(1)
|
||||
}
|
||||
if len(toRetry) == 0 {
|
||||
continue
|
||||
|
||||
if code := exitErr.ExitCode(); code != retryStatus {
|
||||
if debug {
|
||||
log.Printf("code (%d) != retryStatus (%d)", code, retryStatus)
|
||||
}
|
||||
os.Exit(code)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
log.Printf("test did not pass in %d iterations", maxIterations)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ func dumpGoroutinesToURL(c *http.Client, targetURL string) {
|
|||
|
||||
zbuf := new(bytes.Buffer)
|
||||
zw := gzip.NewWriter(zbuf)
|
||||
zw.Write(goroutines.ScrubbedGoroutineDump(true))
|
||||
zw.Write(goroutines.ScrubbedGoroutineDump())
|
||||
zw.Close()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", targetURL, zbuf)
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
// 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) {
|
||||
nc.mu.Lock()
|
||||
if last := nc.last; last != nil && last.canTakeNewRequest() {
|
||||
|
@ -314,35 +295,11 @@ func (nc *NoiseClient) getConn(ctx context.Context) (*noiseConn, error) {
|
|||
}
|
||||
nc.mu.Unlock()
|
||||
|
||||
for {
|
||||
// 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 ctx.Err() != nil {
|
||||
return nil, contextErr{ctx.Err()}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return c, 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()
|
||||
conn, err, _ := nc.sfDial.Do(struct{}{}, nc.dial)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
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
|
||||
// if not cached.
|
||||
func (nc *NoiseClient) dial(ctx context.Context) (*noiseConn, error) {
|
||||
func (nc *NoiseClient) dial() (*noiseConn, error) {
|
||||
nc.mu.Lock()
|
||||
connID := nc.nextID
|
||||
nc.nextID++
|
||||
|
@ -435,7 +392,7 @@ func (nc *NoiseClient) dial(ctx context.Context) (*noiseConn, error) {
|
|||
}
|
||||
|
||||
timeout := time.Duration(timeoutSec * float64(time.Second))
|
||||
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
clientConn, err := (&controlhttp.Dialer{
|
||||
|
|
|
@ -583,20 +583,19 @@ func TestDialPlan(t *testing.T) {
|
|||
}},
|
||||
want: goodAddr,
|
||||
},
|
||||
// TODO(#8442): fix this test
|
||||
// {
|
||||
// name: "multiple-priority-fast-path",
|
||||
// plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
|
||||
// // Dials some good IPs and our bad one (which
|
||||
// // hangs forever), which then hits the fast
|
||||
// // path where we bail without waiting.
|
||||
// {IP: brokenAddr, Priority: 1, DialTimeoutSec: 10},
|
||||
// {IP: goodAddr, Priority: 1, DialTimeoutSec: 10},
|
||||
// {IP: other2Addr, Priority: 1, DialTimeoutSec: 10},
|
||||
// {IP: otherAddr, Priority: 2, DialTimeoutSec: 10},
|
||||
// }},
|
||||
// want: otherAddr,
|
||||
// },
|
||||
{
|
||||
name: "multiple-priority-fast-path",
|
||||
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
|
||||
// Dials some good IPs and our bad one (which
|
||||
// hangs forever), which then hits the fast
|
||||
// path where we bail without waiting.
|
||||
{IP: brokenAddr, Priority: 1, DialTimeoutSec: 10},
|
||||
{IP: goodAddr, Priority: 1, DialTimeoutSec: 10},
|
||||
{IP: other2Addr, Priority: 1, DialTimeoutSec: 10},
|
||||
{IP: otherAddr, Priority: 2, DialTimeoutSec: 10},
|
||||
}},
|
||||
want: otherAddr,
|
||||
},
|
||||
{
|
||||
name: "multiple-priority-slow-path",
|
||||
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
|
||||
|
|
|
@ -9,18 +9,19 @@ import (
|
|||
"net"
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/tcpinfo"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func (c *sclient) statsLoop(ctx context.Context) error {
|
||||
// Get the RTT initially to verify it's supported.
|
||||
conn := c.tcpConn()
|
||||
if conn == nil {
|
||||
// If we can't get a TCP socket, then we can't send stats.
|
||||
tcpConn := c.tcpConn()
|
||||
if tcpConn == nil {
|
||||
c.s.tcpRtt.Add("non-tcp", 1)
|
||||
return nil
|
||||
}
|
||||
if _, err := tcpinfo.RTT(conn); err != nil {
|
||||
c.logf("error fetching initial RTT: %v", err)
|
||||
rawConn, err := tcpConn.SyscallConn()
|
||||
if err != nil {
|
||||
c.logf("error getting SyscallConn: %v", err)
|
||||
c.s.tcpRtt.Add("error", 1)
|
||||
return nil
|
||||
}
|
||||
|
@ -30,16 +31,23 @@ func (c *sclient) statsLoop(ctx context.Context) error {
|
|||
ticker := time.NewTicker(statsInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
var (
|
||||
tcpInfo *unix.TCPInfo
|
||||
sysErr error
|
||||
)
|
||||
statsLoop:
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
rtt, err := tcpinfo.RTT(conn)
|
||||
if err != nil {
|
||||
err = rawConn.Control(func(fd uintptr) {
|
||||
tcpInfo, sysErr = unix.GetsockoptTCPInfo(int(fd), unix.IPPROTO_TCP, unix.TCP_INFO)
|
||||
})
|
||||
if err != nil || sysErr != nil {
|
||||
continue statsLoop
|
||||
}
|
||||
|
||||
// TODO(andrew): more metrics?
|
||||
rtt := time.Duration(tcpInfo.Rtt) * time.Microsecond
|
||||
c.s.tcpRtt.Add(durationToLabel(rtt), 1)
|
||||
|
||||
case <-ctx.Done():
|
||||
|
|
|
@ -6,20 +6,22 @@ SA_NAME ?= tailscale
|
|||
TS_KUBE_SECRET ?= tailscale
|
||||
|
||||
rbac:
|
||||
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" role.yaml
|
||||
@echo "---"
|
||||
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" rolebinding.yaml
|
||||
@echo "---"
|
||||
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" sa.yaml
|
||||
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" role.yaml | kubectl apply -f -
|
||||
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" rolebinding.yaml | kubectl apply -f -
|
||||
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" sa.yaml | kubectl apply -f -
|
||||
|
||||
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:
|
||||
@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:
|
||||
@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:
|
||||
@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-
|
||||
|
|
|
@ -26,7 +26,7 @@ There are quite a few ways of running Tailscale inside a Kubernetes Cluster, som
|
|||
```bash
|
||||
export SA_NAME=tailscale
|
||||
export TS_KUBE_SECRET=tailscale-auth
|
||||
make rbac | kubectl apply -f-
|
||||
make rbac
|
||||
```
|
||||
|
||||
### 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
|
||||
|
||||
```bash
|
||||
make sidecar | kubectl apply -f-
|
||||
make sidecar
|
||||
# If not using an auth key, authenticate by grabbing the Login URL here:
|
||||
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
|
||||
|
||||
```bash
|
||||
make userspace-sidecar | kubectl apply -f-
|
||||
make userspace-sidecar
|
||||
# If not using an auth key, authenticate by grabbing the Login URL here:
|
||||
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
|
||||
|
||||
```bash
|
||||
make proxy | kubectl apply -f-
|
||||
make proxy
|
||||
# If not using an auth key, authenticate by grabbing the Login URL here:
|
||||
kubectl logs proxy
|
||||
```
|
||||
|
@ -133,7 +133,7 @@ the entire Kubernetes cluster network (assuming NetworkPolicies allow) over Tail
|
|||
1. Deploy the subnet-router pod.
|
||||
|
||||
```bash
|
||||
make subnet-router | kubectl apply -f-
|
||||
make subnet-router
|
||||
# If not using an auth key, authenticate by grabbing the Login URL here:
|
||||
kubectl logs subnet-router
|
||||
```
|
||||
|
|
|
@ -115,4 +115,4 @@
|
|||
in
|
||||
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
|
||||
}
|
||||
# nix-direnv cache busting line: sha256-fgCrmtJs1svFz0Xn7iwLNrbBNlcO6V0yqGPMY0+V1VQ=
|
||||
# nix-direnv cache busting line: sha256-l2uIma2oEdSN0zVo9BOFJF2gC3S60vXwTLVadv8yQPo=
|
||||
|
|
4
go.mod
4
go.mod
|
@ -48,7 +48,7 @@ require (
|
|||
github.com/mdlayher/genetlink v1.3.2
|
||||
github.com/mdlayher/netlink v1.7.2
|
||||
github.com/mdlayher/sdnotify v1.0.0
|
||||
github.com/miekg/dns v1.1.55
|
||||
github.com/miekg/dns v1.1.54
|
||||
github.com/mitchellh/go-ps v1.0.0
|
||||
github.com/peterbourgon/ff/v3 v3.3.0
|
||||
github.com/pkg/errors v0.9.1
|
||||
|
@ -79,7 +79,7 @@ require (
|
|||
golang.org/x/net v0.10.0
|
||||
golang.org/x/oauth2 v0.7.0
|
||||
golang.org/x/sync v0.2.0
|
||||
golang.org/x/sys v0.8.1-0.20230609144347-5059a07aa46a
|
||||
golang.org/x/sys v0.8.0
|
||||
golang.org/x/term v0.8.0
|
||||
golang.org/x/time v0.3.0
|
||||
golang.org/x/tools v0.9.1
|
||||
|
|
|
@ -1 +1 @@
|
|||
sha256-fgCrmtJs1svFz0Xn7iwLNrbBNlcO6V0yqGPMY0+V1VQ=
|
||||
sha256-l2uIma2oEdSN0zVo9BOFJF2gC3S60vXwTLVadv8yQPo=
|
||||
|
|
8
go.sum
8
go.sum
|
@ -767,8 +767,8 @@ github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8Ku
|
|||
github.com/mgechev/revive v1.3.1 h1:OlQkcH40IB2cGuprTPcjB0iIUddgVZgGmDX3IAMR8D4=
|
||||
github.com/mgechev/revive v1.3.1/go.mod h1:YlD6TTWl2B8A103R9KWJSPVI9DrEf+oqr15q21Ld+5I=
|
||||
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
|
||||
github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo=
|
||||
github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
||||
github.com/miekg/dns v1.1.54 h1:5jon9mWcb0sFJGpnI99tOMhCPyJ+RPVz5b63MQG0VWI=
|
||||
github.com/miekg/dns v1.1.54/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY=
|
||||
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
|
||||
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
|
@ -1432,8 +1432,8 @@ golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.1-0.20230609144347-5059a07aa46a h1:qMsju+PNttu/NMbq8bQ9waDdxgJMu9QNoUDuhnBaYt0=
|
||||
golang.org/x/sys v0.8.1-0.20230609144347-5059a07aa46a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
|
|
@ -1 +1 @@
|
|||
tailscale.go1.21
|
||||
tailscale.go1.20
|
||||
|
|
|
@ -1 +1 @@
|
|||
492f6d9d792fa6e4caa388e4d7bab46b48d07ad5
|
||||
40dc4d834a5fde9872bcf470be50069f56c3e3b3
|
||||
|
|
|
@ -7,10 +7,8 @@ package hostinfo
|
|||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
@ -283,7 +281,7 @@ func inContainer() opt.Bool {
|
|||
return nil
|
||||
})
|
||||
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)
|
||||
return io.EOF
|
||||
}
|
||||
|
@ -436,12 +434,3 @@ func etcAptSourceFileIsDisabled(r io.Reader) bool {
|
|||
}
|
||||
return disabled
|
||||
}
|
||||
|
||||
// IsSELinuxEnforcing reports whether SELinux is in "Enforcing" mode.
|
||||
func IsSELinuxEnforcing() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
return false
|
||||
}
|
||||
out, _ := exec.Command("getenforce").Output()
|
||||
return string(bytes.TrimSpace(out)) == "Enforcing"
|
||||
}
|
||||
|
|
|
@ -103,7 +103,6 @@ func (src *TCPPortHandler) Clone() *TCPPortHandler {
|
|||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _TCPPortHandlerCloneNeedsRegeneration = TCPPortHandler(struct {
|
||||
HTTPS bool
|
||||
HTTP bool
|
||||
TCPForward string
|
||||
TerminateTLS string
|
||||
}{})
|
||||
|
|
|
@ -228,14 +228,12 @@ func (v *TCPPortHandlerView) UnmarshalJSON(b []byte) error {
|
|||
}
|
||||
|
||||
func (v TCPPortHandlerView) HTTPS() bool { return v.ж.HTTPS }
|
||||
func (v TCPPortHandlerView) HTTP() bool { return v.ж.HTTP }
|
||||
func (v TCPPortHandlerView) TCPForward() string { return v.ж.TCPForward }
|
||||
func (v TCPPortHandlerView) TerminateTLS() string { return v.ж.TerminateTLS }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _TCPPortHandlerViewNeedsRegeneration = TCPPortHandler(struct {
|
||||
HTTPS bool
|
||||
HTTP bool
|
||||
TCPForward string
|
||||
TerminateTLS string
|
||||
}{})
|
||||
|
|
|
@ -49,7 +49,7 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
case "/debug/goroutines":
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write(goroutines.ScrubbedGoroutineDump(true))
|
||||
w.Write(goroutines.ScrubbedGoroutineDump())
|
||||
case "/debug/prefs":
|
||||
writeJSON(b.Prefs())
|
||||
case "/debug/metrics":
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package ipnlocal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
|
@ -17,6 +18,7 @@ import (
|
|||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
@ -30,7 +32,6 @@ import (
|
|||
"go4.org/mem"
|
||||
"go4.org/netipx"
|
||||
"golang.org/x/exp/slices"
|
||||
"gvisor.dev/gvisor/pkg/tcpip"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/doctor"
|
||||
|
@ -742,6 +743,7 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
|
|||
HostName: p.Hostinfo.Hostname(),
|
||||
DNSName: p.Name,
|
||||
OS: p.Hostinfo.OS(),
|
||||
KeepAlive: p.KeepAlive,
|
||||
LastSeen: lastSeen,
|
||||
Online: p.Online != nil && *p.Online,
|
||||
ShareeNode: p.Hostinfo.ShareeNode(),
|
||||
|
@ -2580,7 +2582,7 @@ func (b *LocalBackend) checkSSHPrefsLocked(p *ipn.Prefs) error {
|
|||
if distro.Get() == distro.QNAP && !envknob.UseWIPCode() {
|
||||
return errors.New("The Tailscale SSH server does not run on QNAP.")
|
||||
}
|
||||
b.updateSELinuxHealthWarning()
|
||||
checkSELinux()
|
||||
// otherwise okay
|
||||
case "darwin":
|
||||
// okay only in tailscaled mode for now.
|
||||
|
@ -2826,14 +2828,14 @@ func (b *LocalBackend) GetPeerAPIPort(ip netip.Addr) (port uint16, ok bool) {
|
|||
return 0, false
|
||||
}
|
||||
|
||||
// handlePeerAPIConn serves an already-accepted connection c.
|
||||
// ServePeerAPIConnection serves an already-accepted connection c.
|
||||
//
|
||||
// The remote parameter is the remote address.
|
||||
// The local parameter is the local address (either a Tailscale IPv4
|
||||
// or IPv6 IP and the peerapi port for that address).
|
||||
//
|
||||
// The connection will be closed by handlePeerAPIConn.
|
||||
func (b *LocalBackend) handlePeerAPIConn(remote, local netip.AddrPort, c net.Conn) {
|
||||
// The connection will be closed by ServePeerAPIConnection.
|
||||
func (b *LocalBackend) ServePeerAPIConnection(remote, local netip.AddrPort, c net.Conn) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
|
@ -2847,48 +2849,6 @@ func (b *LocalBackend) handlePeerAPIConn(remote, local netip.AddrPort, c net.Con
|
|||
return
|
||||
}
|
||||
|
||||
func (b *LocalBackend) isLocalIP(ip netip.Addr) bool {
|
||||
nm := b.NetMap()
|
||||
return nm != nil && slices.Contains(nm.Addresses, netip.PrefixFrom(ip, ip.BitLen()))
|
||||
}
|
||||
|
||||
var (
|
||||
magicDNSIP = tsaddr.TailscaleServiceIP()
|
||||
magicDNSIPv6 = tsaddr.TailscaleServiceIPv6()
|
||||
)
|
||||
|
||||
// TCPHandlerForDst returns a TCP handler for connections to dst, or nil if
|
||||
// no handler is needed. It also returns a list of TCP socket options to
|
||||
// apply to the socket before calling the handler.
|
||||
func (b *LocalBackend) TCPHandlerForDst(src, dst netip.AddrPort) (handler func(c net.Conn) error, opts []tcpip.SettableSocketOption) {
|
||||
if dst.Port() == 80 && (dst.Addr() == magicDNSIP || dst.Addr() == magicDNSIPv6) {
|
||||
return b.HandleQuad100Port80Conn, opts
|
||||
}
|
||||
if !b.isLocalIP(dst.Addr()) {
|
||||
return nil, nil
|
||||
}
|
||||
if dst.Port() == 22 && b.ShouldRunSSH() {
|
||||
// Use a higher keepalive idle time for SSH connections, as they are
|
||||
// typically long lived and idle connections are more likely to be
|
||||
// intentional. Ideally we would turn this off entirely, but we can't
|
||||
// tell the difference between a long lived connection that is idle
|
||||
// vs a connection that is dead because the peer has gone away.
|
||||
// We pick 72h as that is typically sufficient for a long weekend.
|
||||
opts = append(opts, ptr.To(tcpip.KeepaliveIdleOption(72*time.Hour)))
|
||||
return b.handleSSHConn, opts
|
||||
}
|
||||
if port, ok := b.GetPeerAPIPort(dst.Addr()); ok && dst.Port() == port {
|
||||
return func(c net.Conn) error {
|
||||
b.handlePeerAPIConn(src, dst, c)
|
||||
return nil
|
||||
}, opts
|
||||
}
|
||||
if handler := b.tcpHandlerForServe(dst.Port(), src); handler != nil {
|
||||
return handler, opts
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) peerAPIServicesLocked() (ret []tailcfg.Service) {
|
||||
for _, pln := range b.peerAPIListeners {
|
||||
proto := tailcfg.PeerAPI4
|
||||
|
@ -4128,10 +4088,6 @@ func (b *LocalBackend) setServeProxyHandlersLocked() {
|
|||
b.serveConfig.Web().Range(func(_ ipn.HostPort, conf ipn.WebServerConfigView) (cont bool) {
|
||||
conf.Handlers().Range(func(_ string, h ipn.HTTPHandlerView) (cont bool) {
|
||||
backend := h.Proxy()
|
||||
if backend == "" {
|
||||
// Only create proxy handlers for servers with a proxy backend.
|
||||
return true
|
||||
}
|
||||
mak.Set(&backends, backend, true)
|
||||
if _, ok := b.serveProxyHandlers.Load(backend); ok {
|
||||
return true
|
||||
|
@ -4706,29 +4662,33 @@ func (b *LocalBackend) sshServerOrInit() (_ SSHServer, err error) {
|
|||
|
||||
var warnSSHSELinux = health.NewWarnable()
|
||||
|
||||
func (b *LocalBackend) updateSELinuxHealthWarning() {
|
||||
if hostinfo.IsSELinuxEnforcing() {
|
||||
func checkSELinux() {
|
||||
if runtime.GOOS != "linux" {
|
||||
return
|
||||
}
|
||||
out, _ := exec.Command("getenforce").Output()
|
||||
if string(bytes.TrimSpace(out)) == "Enforcing" {
|
||||
warnSSHSELinux.Set(errors.New("SELinux is enabled; Tailscale SSH may not work. See https://tailscale.com/s/ssh-selinux"))
|
||||
} else {
|
||||
warnSSHSELinux.Set(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) handleSSHConn(c net.Conn) (err error) {
|
||||
func (b *LocalBackend) HandleSSHConn(c net.Conn) (err error) {
|
||||
s, err := b.sshServerOrInit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.updateSELinuxHealthWarning()
|
||||
checkSELinux()
|
||||
return s.HandleSSHConn(c)
|
||||
}
|
||||
|
||||
// HandleQuad100Port80Conn serves http://100.100.100.100/ on port 80 (and
|
||||
// the equivalent tsaddr.TailscaleServiceIPv6 address).
|
||||
func (b *LocalBackend) HandleQuad100Port80Conn(c net.Conn) error {
|
||||
func (b *LocalBackend) HandleQuad100Port80Conn(c net.Conn) {
|
||||
var s http.Server
|
||||
s.Handler = http.HandlerFunc(b.handleQuad100Port80Conn)
|
||||
return s.Serve(netutil.NewOneConnListener(c, nil))
|
||||
s.Serve(netutil.NewOneConnListener(c, nil))
|
||||
}
|
||||
|
||||
func validQuad100Host(h string) bool {
|
||||
|
|
|
@ -158,9 +158,7 @@ func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap, prefs ipn.PrefsVie
|
|||
return nil
|
||||
}
|
||||
|
||||
if b.tka != nil || nm.TKAEnabled {
|
||||
b.logf("tkaSyncIfNeeded: enabled=%v, head=%v", nm.TKAEnabled, nm.TKAHead)
|
||||
}
|
||||
b.logf("tkaSyncIfNeeded: enabled=%v, head=%v", nm.TKAEnabled, nm.TKAHead)
|
||||
|
||||
ourNodeKey := prefs.Persist().PublicNodeKey()
|
||||
|
||||
|
@ -199,7 +197,7 @@ func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap, prefs ipn.PrefsVie
|
|||
health.SetTKAHealth(nil)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("[bug] unreachable invariant of wantEnabled w/ isEnabled")
|
||||
return fmt.Errorf("[bug] unreachable invariant of wantEnabled /w isEnabled")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -889,18 +887,6 @@ func (b *LocalBackend) NetworkLockWrapPreauthKey(preauthKey string, tkaKey key.N
|
|||
return fmt.Sprintf("%s--TL%s-%s", preauthKey, tkaSuffixEncoder.EncodeToString(sig.Serialize()), tkaSuffixEncoder.EncodeToString(priv)), nil
|
||||
}
|
||||
|
||||
// NetworkLockVerifySigningDeeplink asks the authority to verify the given deeplink
|
||||
// URL. See the comment for ValidateDeeplink for details.
|
||||
func (b *LocalBackend) NetworkLockVerifySigningDeeplink(url string) tka.DeeplinkValidationResult {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.tka == nil {
|
||||
return tka.DeeplinkValidationResult{IsValid: false, Error: errNetworkLockNotActive.Error()}
|
||||
}
|
||||
|
||||
return b.tka.authority.ValidateDeeplink(url)
|
||||
}
|
||||
|
||||
func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeKeySignature, error) {
|
||||
p, err := nodeInfo.NodePublic.MarshalBinary()
|
||||
if err != nil {
|
||||
|
|
|
@ -780,7 +780,7 @@ func (h *peerAPIHandler) handleServeIngress(w http.ResponseWriter, r *http.Reque
|
|||
return
|
||||
}
|
||||
|
||||
getConnOrReset := func() (net.Conn, bool) {
|
||||
getConn := func() (net.Conn, bool) {
|
||||
conn, _, err := w.(http.Hijacker).Hijack()
|
||||
if err != nil {
|
||||
h.logf("ingress: failed hijacking conn")
|
||||
|
@ -798,7 +798,7 @@ func (h *peerAPIHandler) handleServeIngress(w http.ResponseWriter, r *http.Reque
|
|||
http.Error(w, "denied", http.StatusForbidden)
|
||||
}
|
||||
|
||||
h.ps.b.HandleIngressTCPConn(h.peerNode, target, srcAddr, getConnOrReset, sendRST)
|
||||
h.ps.b.HandleIngressTCPConn(h.peerNode, target, srcAddr, getConn, sendRST)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -162,13 +162,12 @@ func (s *serveListener) handleServeListenersAccept(ln net.Listener) error {
|
|||
return err
|
||||
}
|
||||
srcAddr := conn.RemoteAddr().(*net.TCPAddr).AddrPort()
|
||||
handler := s.b.tcpHandlerForServe(s.ap.Port(), srcAddr)
|
||||
if handler == nil {
|
||||
getConn := func() (net.Conn, bool) { return conn, true }
|
||||
sendRST := func() {
|
||||
s.b.logf("serve RST for %v", srcAddr)
|
||||
conn.Close()
|
||||
continue
|
||||
}
|
||||
go handler(conn)
|
||||
go s.b.HandleInterceptedTCPConn(s.ap.Port(), srcAddr, getConn, sendRST)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -257,7 +256,7 @@ func (b *LocalBackend) ServeConfig() ipn.ServeConfigView {
|
|||
return b.serveConfig
|
||||
}
|
||||
|
||||
func (b *LocalBackend) HandleIngressTCPConn(ingressPeer *tailcfg.Node, target ipn.HostPort, srcAddr netip.AddrPort, getConnOrReset func() (net.Conn, bool), sendRST func()) {
|
||||
func (b *LocalBackend) HandleIngressTCPConn(ingressPeer *tailcfg.Node, target ipn.HostPort, srcAddr netip.AddrPort, getConn func() (net.Conn, bool), sendRST func()) {
|
||||
b.mu.Lock()
|
||||
sc := b.serveConfig
|
||||
b.mu.Unlock()
|
||||
|
@ -290,7 +289,7 @@ func (b *LocalBackend) HandleIngressTCPConn(ingressPeer *tailcfg.Node, target ip
|
|||
if b.getTCPHandlerForFunnelFlow != nil {
|
||||
handler := b.getTCPHandlerForFunnelFlow(srcAddr, dport)
|
||||
if handler != nil {
|
||||
c, ok := getConnOrReset()
|
||||
c, ok := getConn()
|
||||
if !ok {
|
||||
b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport)
|
||||
return
|
||||
|
@ -299,41 +298,39 @@ func (b *LocalBackend) HandleIngressTCPConn(ingressPeer *tailcfg.Node, target ip
|
|||
return
|
||||
}
|
||||
}
|
||||
// TODO(bradfitz): pass ingressPeer etc in context to tcpHandlerForServe,
|
||||
// TODO(bradfitz): pass ingressPeer etc in context to HandleInterceptedTCPConn,
|
||||
// extend serveHTTPContext or similar.
|
||||
handler := b.tcpHandlerForServe(dport, srcAddr)
|
||||
if handler == nil {
|
||||
sendRST()
|
||||
return
|
||||
}
|
||||
c, ok := getConnOrReset()
|
||||
if !ok {
|
||||
b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport)
|
||||
return
|
||||
}
|
||||
handler(c)
|
||||
b.HandleInterceptedTCPConn(dport, srcAddr, getConn, sendRST)
|
||||
}
|
||||
|
||||
// tcpHandlerForServe returns a handler for a TCP connection to be served via
|
||||
// the ipn.ServeConfig.
|
||||
func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort) (handler func(net.Conn) error) {
|
||||
func (b *LocalBackend) HandleInterceptedTCPConn(dport uint16, srcAddr netip.AddrPort, getConn func() (net.Conn, bool), sendRST func()) {
|
||||
b.mu.Lock()
|
||||
sc := b.serveConfig
|
||||
b.mu.Unlock()
|
||||
|
||||
if !sc.Valid() {
|
||||
b.logf("[unexpected] localbackend: got TCP conn w/o serveConfig; from %v to port %v", srcAddr, dport)
|
||||
return nil
|
||||
sendRST()
|
||||
return
|
||||
}
|
||||
|
||||
tcph, ok := sc.TCP().GetOk(dport)
|
||||
if !ok {
|
||||
b.logf("[unexpected] localbackend: got TCP conn without TCP config for port %v; from %v", dport, srcAddr)
|
||||
return nil
|
||||
sendRST()
|
||||
return
|
||||
}
|
||||
|
||||
if tcph.HTTPS() || tcph.HTTP() {
|
||||
if tcph.HTTPS() {
|
||||
conn, ok := getConn()
|
||||
if !ok {
|
||||
b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport)
|
||||
return
|
||||
}
|
||||
hs := &http.Server{
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: b.getTLSServeCertForPort(dport),
|
||||
},
|
||||
Handler: http.HandlerFunc(b.serveWebHandler),
|
||||
BaseContext: func(_ net.Listener) context.Context {
|
||||
return context.WithValue(context.Background(), serveHTTPContextKey{}, &serveHTTPContext{
|
||||
|
@ -342,92 +339,79 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort)
|
|||
})
|
||||
},
|
||||
}
|
||||
if tcph.HTTPS() {
|
||||
hs.TLSConfig = &tls.Config{
|
||||
GetCertificate: b.getTLSServeCertForPort(dport),
|
||||
}
|
||||
return func(c net.Conn) error {
|
||||
return hs.ServeTLS(netutil.NewOneConnListener(c, nil), "", "")
|
||||
}
|
||||
}
|
||||
|
||||
return func(c net.Conn) error {
|
||||
return hs.Serve(netutil.NewOneConnListener(c, nil))
|
||||
}
|
||||
hs.ServeTLS(netutil.NewOneConnListener(conn, nil), "", "")
|
||||
return
|
||||
}
|
||||
|
||||
if backDst := tcph.TCPForward(); backDst != "" {
|
||||
return func(conn net.Conn) error {
|
||||
defer conn.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
backConn, err := b.dialer.SystemDial(ctx, "tcp", backDst)
|
||||
cancel()
|
||||
if err != nil {
|
||||
b.logf("localbackend: failed to TCP proxy port %v (from %v) to %s: %v", dport, srcAddr, backDst, err)
|
||||
return nil
|
||||
}
|
||||
defer backConn.Close()
|
||||
if sni := tcph.TerminateTLS(); sni != "" {
|
||||
conn = tls.Server(conn, &tls.Config{
|
||||
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
pair, err := b.GetCertPEM(ctx, sni)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert, err := tls.X509KeyPair(pair.CertPEM, pair.KeyPEM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cert, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// TODO(bradfitz): do the RegisterIPPortIdentity and
|
||||
// UnregisterIPPortIdentity stuff that netstack does
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := io.Copy(backConn, conn)
|
||||
errc <- err
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(conn, backConn)
|
||||
errc <- err
|
||||
}()
|
||||
return <-errc
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
backConn, err := b.dialer.SystemDial(ctx, "tcp", backDst)
|
||||
cancel()
|
||||
if err != nil {
|
||||
b.logf("localbackend: failed to TCP proxy port %v (from %v) to %s: %v", dport, srcAddr, backDst, err)
|
||||
sendRST()
|
||||
return
|
||||
}
|
||||
conn, ok := getConn()
|
||||
if !ok {
|
||||
b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport)
|
||||
backConn.Close()
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
defer backConn.Close()
|
||||
|
||||
if sni := tcph.TerminateTLS(); sni != "" {
|
||||
conn = tls.Server(conn, &tls.Config{
|
||||
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
pair, err := b.GetCertPEM(ctx, sni)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cert, err := tls.X509KeyPair(pair.CertPEM, pair.KeyPEM)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cert, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// TODO(bradfitz): do the RegisterIPPortIdentity and
|
||||
// UnregisterIPPortIdentity stuff that netstack does
|
||||
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := io.Copy(backConn, conn)
|
||||
errc <- err
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(conn, backConn)
|
||||
errc <- err
|
||||
}()
|
||||
<-errc
|
||||
return
|
||||
}
|
||||
|
||||
b.logf("closing TCP conn to port %v (from %v) with actionless TCPPortHandler", dport, srcAddr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getServeHTTPContext(r *http.Request) (c *serveHTTPContext, ok bool) {
|
||||
c, ok = r.Context().Value(serveHTTPContextKey{}).(*serveHTTPContext)
|
||||
return c, ok
|
||||
sendRST()
|
||||
}
|
||||
|
||||
func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView, at string, ok bool) {
|
||||
var z ipn.HTTPHandlerView // zero value
|
||||
|
||||
hostname := r.Host
|
||||
if r.TLS == nil {
|
||||
tcd := "." + b.Status().CurrentTailnet.MagicDNSSuffix
|
||||
if !strings.HasSuffix(hostname, tcd) {
|
||||
hostname += tcd
|
||||
}
|
||||
} else {
|
||||
hostname = r.TLS.ServerName
|
||||
return z, "", false
|
||||
}
|
||||
|
||||
sctx, ok := getServeHTTPContext(r)
|
||||
sctx, ok := r.Context().Value(serveHTTPContextKey{}).(*serveHTTPContext)
|
||||
if !ok {
|
||||
b.logf("[unexpected] localbackend: no serveHTTPContext in request")
|
||||
return z, "", false
|
||||
}
|
||||
wsc, ok := b.webServerConfig(hostname, sctx.DestPort)
|
||||
wsc, ok := b.webServerConfig(r.TLS.ServerName, sctx.DestPort)
|
||||
if !ok {
|
||||
return z, "", false
|
||||
}
|
||||
|
@ -463,8 +447,11 @@ func (b *LocalBackend) proxyHandlerForBackend(backend string) (*httputil.Reverse
|
|||
Rewrite: func(r *httputil.ProxyRequest) {
|
||||
r.SetURL(u)
|
||||
r.Out.Host = r.In.Host
|
||||
addProxyForwardedHeaders(r)
|
||||
b.addTailscaleIdentityHeaders(r)
|
||||
r.Out.Header.Set("X-Forwarded-Host", r.In.Host)
|
||||
r.Out.Header.Set("X-Forwarded-Proto", "https")
|
||||
if c, ok := r.Out.Context().Value(serveHTTPContextKey{}).(*serveHTTPContext); ok {
|
||||
r.Out.Header.Set("X-Forwarded-For", c.SrcAddr.Addr().String())
|
||||
}
|
||||
},
|
||||
Transport: &http.Transport{
|
||||
DialContext: b.dialer.SystemDial,
|
||||
|
@ -482,40 +469,6 @@ func (b *LocalBackend) proxyHandlerForBackend(backend string) (*httputil.Reverse
|
|||
return rp, nil
|
||||
}
|
||||
|
||||
func addProxyForwardedHeaders(r *httputil.ProxyRequest) {
|
||||
r.Out.Header.Set("X-Forwarded-Host", r.In.Host)
|
||||
if r.In.TLS != nil {
|
||||
r.Out.Header.Set("X-Forwarded-Proto", "https")
|
||||
}
|
||||
if c, ok := getServeHTTPContext(r.Out); ok {
|
||||
r.Out.Header.Set("X-Forwarded-For", c.SrcAddr.Addr().String())
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) addTailscaleIdentityHeaders(r *httputil.ProxyRequest) {
|
||||
// Clear any incoming values squatting in the headers.
|
||||
r.Out.Header.Del("Tailscale-User-Login")
|
||||
r.Out.Header.Del("Tailscale-User-Name")
|
||||
r.Out.Header.Del("Tailscale-Headers-Info")
|
||||
|
||||
c, ok := getServeHTTPContext(r.Out)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
node, user, ok := b.WhoIs(c.SrcAddr)
|
||||
if !ok {
|
||||
return // traffic from outside of Tailnet (funneled)
|
||||
}
|
||||
if node.IsTagged() {
|
||||
// 2023-06-14: Not setting identity headers for tagged nodes.
|
||||
// Only currently set for nodes with user identities.
|
||||
return
|
||||
}
|
||||
r.Out.Header.Set("Tailscale-User-Login", user.LoginName)
|
||||
r.Out.Header.Set("Tailscale-User-Name", user.DisplayName)
|
||||
r.Out.Header.Set("Tailscale-Headers-Info", "https://tailscale.com/s/serve-headers")
|
||||
}
|
||||
|
||||
func (b *LocalBackend) serveWebHandler(w http.ResponseWriter, r *http.Request) {
|
||||
h, mountPoint, ok := b.getServeHandler(r)
|
||||
if !ok {
|
||||
|
@ -648,8 +601,8 @@ func allNumeric(s string) bool {
|
|||
return s != ""
|
||||
}
|
||||
|
||||
func (b *LocalBackend) webServerConfig(hostname string, port uint16) (c ipn.WebServerConfigView, ok bool) {
|
||||
key := ipn.HostPort(fmt.Sprintf("%s:%v", hostname, port))
|
||||
func (b *LocalBackend) webServerConfig(sniName string, port uint16) (c ipn.WebServerConfigView, ok bool) {
|
||||
key := ipn.HostPort(fmt.Sprintf("%s:%v", sniName, port))
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
|
|
@ -10,22 +10,13 @@ import (
|
|||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tsd"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/util/cmpx"
|
||||
"tailscale.com/util/must"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
|
||||
func TestExpandProxyArg(t *testing.T) {
|
||||
|
@ -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) {
|
||||
td := t.TempDir()
|
||||
writeFile := func(suffix, contents string) {
|
||||
|
|
|
@ -223,8 +223,9 @@ type PeerStatus struct {
|
|||
LastSeen time.Time // last seen to tailcontrol; only present if offline
|
||||
LastHandshake time.Time // with local wireguard
|
||||
Online bool // whether node is connected to the control plane
|
||||
ExitNode bool // true if this is the currently selected exit node.
|
||||
ExitNodeOption bool // true if this node can be an exit node (offered && approved)
|
||||
KeepAlive bool
|
||||
ExitNode bool // true if this is the currently selected exit node.
|
||||
ExitNodeOption bool // true if this node can be an exit node (offered && approved)
|
||||
|
||||
// Active is whether the node was recently active. The
|
||||
// definition is somewhat undefined but has historically and
|
||||
|
@ -436,6 +437,9 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
|
|||
if st.InEngine {
|
||||
e.InEngine = true
|
||||
}
|
||||
if st.KeepAlive {
|
||||
e.KeepAlive = true
|
||||
}
|
||||
if st.ExitNode {
|
||||
e.ExitNode = true
|
||||
}
|
||||
|
|
|
@ -104,7 +104,6 @@ var handler = map[string]localAPIHandler{
|
|||
"tka/force-local-disable": (*Handler).serveTKALocalDisable,
|
||||
"tka/affected-sigs": (*Handler).serveTKAAffectedSigs,
|
||||
"tka/wrap-preauth-key": (*Handler).serveTKAWrapPreauthKey,
|
||||
"tka/verify-deeplink": (*Handler).serveTKAVerifySigningDeeplink,
|
||||
"upload-client-metrics": (*Handler).serveUploadClientMetrics,
|
||||
"watch-ipn-bus": (*Handler).serveWatchIPNBus,
|
||||
"whois": (*Handler).serveWhoIs,
|
||||
|
@ -1331,7 +1330,7 @@ func (h *Handler) servePing(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
pingTypeStr := r.FormValue("type")
|
||||
if pingTypeStr == "" {
|
||||
if ipStr == "" {
|
||||
http.Error(w, "missing 'type' parameter", 400)
|
||||
return
|
||||
}
|
||||
|
@ -1611,35 +1610,6 @@ func (h *Handler) serveTKAWrapPreauthKey(w http.ResponseWriter, r *http.Request)
|
|||
w.Write([]byte(wrappedKey))
|
||||
}
|
||||
|
||||
func (h *Handler) serveTKAVerifySigningDeeplink(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitRead {
|
||||
http.Error(w, "signing deeplink verification access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != httpm.POST {
|
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
type verifyRequest struct {
|
||||
URL string
|
||||
}
|
||||
var req verifyRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON for verifyRequest body", 400)
|
||||
return
|
||||
}
|
||||
|
||||
res := h.b.NetworkLockVerifySigningDeeplink(req.URL)
|
||||
j, err := json.MarshalIndent(res, "", "\t")
|
||||
if err != nil {
|
||||
http.Error(w, "JSON encoding error", 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(j)
|
||||
}
|
||||
|
||||
func (h *Handler) serveTKADisable(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
|
||||
|
|
48
ipn/serve.go
48
ipn/serve.go
|
@ -76,12 +76,6 @@ type TCPPortHandler struct {
|
|||
// It is mutually exclusive with TCPForward.
|
||||
HTTPS bool `json:",omitempty"`
|
||||
|
||||
// HTTP, if true, means that tailscaled should handle this connection as an
|
||||
// HTTP request as configured by ServeConfig.Web.
|
||||
//
|
||||
// It is mutually exclusive with TCPForward.
|
||||
HTTP bool `json:",omitempty"`
|
||||
|
||||
// TCPForward is the IP:port to forward TCP connections to.
|
||||
// Whether or not TLS is terminated by tailscaled depends on
|
||||
// TerminateTLS.
|
||||
|
@ -109,7 +103,7 @@ type HTTPHandler struct {
|
|||
// temporary ones? Error codes? Redirects?
|
||||
}
|
||||
|
||||
// WebHandlerExists reports whether if the ServeConfig Web handler exists for
|
||||
// WebHandlerExists checks if the ServeConfig Web handler exists for
|
||||
// the given host:port and mount point.
|
||||
func (sc *ServeConfig) WebHandlerExists(hp HostPort, mount string) bool {
|
||||
h := sc.GetWebHandler(hp, mount)
|
||||
|
@ -134,8 +128,9 @@ func (sc *ServeConfig) GetTCPPortHandler(port uint16) *TCPPortHandler {
|
|||
return sc.TCP[port]
|
||||
}
|
||||
|
||||
// IsTCPForwardingAny reports whether ServeConfig is currently forwarding in
|
||||
// TCPForward mode on any port. This is exclusive of Web/HTTPS serving.
|
||||
// IsTCPForwardingAny checks if ServeConfig is currently forwarding
|
||||
// in TCPForward mode on any port.
|
||||
// This is exclusive of Web/HTTPS serving.
|
||||
func (sc *ServeConfig) IsTCPForwardingAny() bool {
|
||||
if sc == nil || len(sc.TCP) == 0 {
|
||||
return false
|
||||
|
@ -148,47 +143,34 @@ func (sc *ServeConfig) IsTCPForwardingAny() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// IsTCPForwardingOnPort reports whether if ServeConfig is currently forwarding
|
||||
// in TCPForward mode on the given port. This is exclusive of Web/HTTPS serving.
|
||||
// IsTCPForwardingOnPort checks if ServeConfig is currently forwarding
|
||||
// in TCPForward mode on the given port.
|
||||
// This is exclusive of Web/HTTPS serving.
|
||||
func (sc *ServeConfig) IsTCPForwardingOnPort(port uint16) bool {
|
||||
if sc == nil || sc.TCP[port] == nil {
|
||||
return false
|
||||
}
|
||||
return !sc.IsServingWeb(port)
|
||||
return !sc.TCP[port].HTTPS
|
||||
}
|
||||
|
||||
// IsServingWeb reports whether if ServeConfig is currently serving Web
|
||||
// (HTTP/HTTPS) on the given port. This is exclusive of TCPForwarding.
|
||||
// IsServingWeb checks if ServeConfig is currently serving
|
||||
// Web/HTTPS on the given port.
|
||||
// This is exclusive of TCPForwarding.
|
||||
func (sc *ServeConfig) IsServingWeb(port uint16) bool {
|
||||
return sc.IsServingHTTP(port) || sc.IsServingHTTPS(port)
|
||||
}
|
||||
|
||||
// IsServingHTTPS reports whether if ServeConfig is currently serving HTTPS on
|
||||
// the given port. This is exclusive of HTTP and TCPForwarding.
|
||||
func (sc *ServeConfig) IsServingHTTPS(port uint16) bool {
|
||||
if sc == nil || sc.TCP[port] == nil {
|
||||
return false
|
||||
}
|
||||
return sc.TCP[port].HTTPS
|
||||
}
|
||||
|
||||
// IsServingHTTP reports whether if ServeConfig is currently serving HTTP on the
|
||||
// given port. This is exclusive of HTTPS and TCPForwarding.
|
||||
func (sc *ServeConfig) IsServingHTTP(port uint16) bool {
|
||||
if sc == nil || sc.TCP[port] == nil {
|
||||
return false
|
||||
}
|
||||
return sc.TCP[port].HTTP
|
||||
}
|
||||
|
||||
// IsFunnelOn reports whether if ServeConfig is currently allowing funnel
|
||||
// traffic for any host:port.
|
||||
// IsFunnelOn checks if ServeConfig is currently allowing
|
||||
// funnel traffic for any host:port.
|
||||
//
|
||||
// View version of ServeConfig.IsFunnelOn.
|
||||
func (v ServeConfigView) IsFunnelOn() bool { return v.ж.IsFunnelOn() }
|
||||
|
||||
// IsFunnelOn reports whether if ServeConfig is currently allowing funnel
|
||||
// traffic for any host:port.
|
||||
// IsFunnelOn checks if ServeConfig is currently allowing
|
||||
// funnel traffic for any host:port.
|
||||
func (sc *ServeConfig) IsFunnelOn() bool {
|
||||
if sc == nil {
|
||||
return false
|
||||
|
|
|
@ -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/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))
|
||||
- [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/shiny](https://pkg.go.dev/golang.org/x/exp/shiny) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/334a2380:shiny/LICENSE))
|
||||
- [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.7.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.10.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.9.0:LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/5059a07a:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.8.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.8.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.7.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.9.0:LICENSE))
|
||||
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/7b0a1988a28f/LICENSE))
|
||||
|
|
|
@ -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/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/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/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))
|
||||
|
@ -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))
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/f1b76eb4bb35/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.10.0:LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.8.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47ecfdc1:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://github.com/tailscale/golang-x-net/blob/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/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.9.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.9.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.10.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.8.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.8.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.9.0:LICENSE))
|
||||
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/7b0a1988a28f/LICENSE))
|
||||
- [inet.af/peercred](https://pkg.go.dev/inet.af/peercred) ([BSD-3-Clause](https://github.com/inetaf/peercred/blob/0893ea02156a/LICENSE))
|
||||
|
|
|
@ -84,7 +84,7 @@ Some packages may only be included on certain architectures or operating systems
|
|||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.10.0:LICENSE))
|
||||
- [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](https://cs.opensource.google/go/x/oauth2/+/v0.7.0:LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.2.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/5059a07a:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.8.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.8.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.9.0:LICENSE))
|
||||
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE))
|
||||
|
|
|
@ -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/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/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/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/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/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))
|
||||
|
@ -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/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/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/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/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))
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/f1b76eb4bb35/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.10.0:LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.8.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47ecfdc1:LICENSE))
|
||||
- [golang.org/x/image/bmp](https://pkg.go.dev/golang.org/x/image/bmp) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.7.0:LICENSE))
|
||||
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.10.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://github.com/tailscale/golang-x-net/blob/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/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.9.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.9.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.10.0:LICENSE))
|
||||
- [golang.org/x/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/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.8.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.8.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.9.0:LICENSE))
|
||||
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))
|
||||
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))
|
||||
- [gopkg.in/Knetic/govaluate.v3](https://pkg.go.dev/gopkg.in/Knetic/govaluate.v3) ([MIT](https://github.com/Knetic/govaluate/blob/v3.0.0/LICENSE))
|
||||
- [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))
|
||||
|
||||
## Additional Dependencies
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !windows && !wasm
|
||||
//go:build !windows && !js
|
||||
|
||||
package filch
|
||||
|
||||
|
|
|
@ -45,14 +45,6 @@ func (m *LabelMap) Get(key string) *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
|
||||
// if necessary.
|
||||
func (m *LabelMap) GetFloat(key string) *expvar.Float {
|
||||
|
|
|
@ -11,18 +11,6 @@ import (
|
|||
"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) {
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skipf("skipping on %v", runtime.GOOS)
|
||||
|
|
|
@ -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{}
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
||||
// 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()))
|
||||
return out.Addr(), true
|
||||
}
|
||||
|
|
|
@ -18,9 +18,9 @@ import (
|
|||
)
|
||||
|
||||
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),
|
||||
})
|
||||
}
|
||||
mc := &MessageCache{Clock: clock.Now}
|
||||
mc.SetMaxCacheSize(2)
|
||||
clock.Advance(time.Second)
|
||||
|
|
|
@ -22,10 +22,6 @@ type Listener struct {
|
|||
ch chan Conn
|
||||
closeOnce sync.Once
|
||||
closed chan struct{}
|
||||
|
||||
// NewConn, if non-nil, is called to create a new pair of connections
|
||||
// when dialing. If nil, NewConn is used.
|
||||
NewConn func(network, addr string, maxBuf int) (Conn, Conn)
|
||||
}
|
||||
|
||||
// Listen returns a new Listener for the provided address.
|
||||
|
@ -74,14 +70,7 @@ func (l *Listener) Dial(ctx context.Context, network, addr string) (_ net.Conn,
|
|||
Addr: addr,
|
||||
}
|
||||
}
|
||||
|
||||
newConn := l.NewConn
|
||||
if newConn == nil {
|
||||
newConn = func(network, addr string, maxBuf int) (Conn, Conn) {
|
||||
return NewConn(addr, maxBuf)
|
||||
}
|
||||
}
|
||||
c, s := newConn(network, addr, bufferSize)
|
||||
c, s := NewConn(addr, bufferSize)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
c.Close()
|
||||
|
|
|
@ -100,7 +100,7 @@ func (c *nlConn) Receive() (message, error) {
|
|||
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
|
||||
if label == "" {
|
||||
itf, err := net.InterfaceByIndex(int(rmsg.Index))
|
||||
|
|
|
@ -17,9 +17,16 @@ import (
|
|||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netmon"
|
||||
"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.
|
||||
var socketMarkWorksOnce struct {
|
||||
sync.Once
|
||||
|
@ -112,7 +119,7 @@ func controlC(network, address string, c syscall.RawConn) 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 nil
|
||||
|
|
|
@ -4,9 +4,51 @@
|
|||
package netns
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"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) {
|
||||
_ = socketMarkWorks()
|
||||
// we cannot actually assert whether the test runner has SO_MARK available
|
||||
|
|
|
@ -212,16 +212,9 @@ func ipForwardingEnabledLinux(p protocol, iface string) (bool, error) {
|
|||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
val, err := strconv.ParseInt(string(bytes.TrimSpace(bs)), 10, 32)
|
||||
on, err := strconv.ParseBool(string(bytes.TrimSpace(bs)))
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !wasm
|
||||
//go:build !js
|
||||
|
||||
// Package tun creates a tuntap device, working around OS-specific
|
||||
// quirks if necessary.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !windows && !js && !wasip1
|
||||
//go:build !windows && !js
|
||||
|
||||
package paths
|
||||
|
||||
|
|
|
@ -188,9 +188,6 @@ func (d *derpProber) updateMap(ctx context.Context) error {
|
|||
if existing, ok := d.nodes[n.HostName]; ok {
|
||||
return fmt.Errorf("derpmap has duplicate nodes: %+v and %+v", existing, n)
|
||||
}
|
||||
// Allow the prober to monitor nodes marked as
|
||||
// STUN only in the default map
|
||||
n.STUNOnly = false
|
||||
d.nodes[n.HostName] = n
|
||||
}
|
||||
}
|
||||
|
|
|
@ -184,17 +184,6 @@ func (b *Build) TmpDir() string {
|
|||
// binary. Builds are cached by path and env, so each build only happens once
|
||||
// per process execution.
|
||||
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 {
|
||||
log.Printf("Initializing Go 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
|
||||
}
|
||||
|
||||
buildKey := []any{"go-build", path, env, tags}
|
||||
buildKey := []any{"go-build", path, env}
|
||||
return b.goBuilds.Do(buildKey, func() (string, error) {
|
||||
b.goBuildLimit <- struct{}{}
|
||||
defer func() { <-b.goBuildLimit }()
|
||||
|
@ -218,17 +207,9 @@ func (b *Build) BuildGoBinaryWithTags(path string, env map[string]string, tags [
|
|||
envStrs = append(envStrs, k+"="+v)
|
||||
}
|
||||
sort.Strings(envStrs)
|
||||
log.Printf("Building %s (with env %s)", path, strings.Join(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, " "))
|
||||
}
|
||||
args = append(args, path)
|
||||
cmd := b.Command(b.Repo, b.Go, args...)
|
||||
cmd := b.Command(b.Repo, b.Go, "build", "-v", "-o", buildDir, path)
|
||||
for k, v := range env {
|
||||
cmd.Cmd.Env = append(cmd.Cmd.Env, k+"="+v)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
".url": {
|
||||
"SYNO.SDS.Tailscale": {
|
||||
"type": "url",
|
||||
"version": "1.8.3",
|
||||
"title": "Tailscale",
|
||||
"icon": "PACKAGE_ICON_256.PNG",
|
||||
"url": "webman/3rdparty/Tailscale/",
|
||||
|
|
|
@ -176,7 +176,7 @@ func (m *synologyBuilds) buildInnerPackage(b *dist.Build, dsmVersion int, goenv
|
|||
static(fmt.Sprintf("logrotate-dsm%d", dsmVersion), "conf/logrotate.conf", 0644),
|
||||
dir("ui"),
|
||||
static("PACKAGE_ICON_256.PNG", "ui/PACKAGE_ICON_256.PNG", 0644),
|
||||
static("config", "ui/config", 0644),
|
||||
static("config", "ui/config", 0644), // TODO: this has "1.8.3" hard-coded in it; why? what is it? bug?
|
||||
static("index.cgi", "ui/index.cgi", 0755))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -16,4 +16,4 @@
|
|||
) {
|
||||
src = ./.;
|
||||
}).shellNix
|
||||
# nix-direnv cache busting line: sha256-fgCrmtJs1svFz0Xn7iwLNrbBNlcO6V0yqGPMY0+V1VQ=
|
||||
# nix-direnv cache busting line: sha256-l2uIma2oEdSN0zVo9BOFJF2gC3S60vXwTLVadv8yQPo=
|
||||
|
|
|
@ -34,7 +34,6 @@ import (
|
|||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/cmd/tailscaled/childproc"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/tempfork/gliderlabs/ssh"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version/distro"
|
||||
|
@ -121,18 +120,10 @@ func (ss *sshSession) newIncubatorCommand() (cmd *exec.Cmd) {
|
|||
if isShell {
|
||||
incubatorArgs = append(incubatorArgs, "--shell")
|
||||
}
|
||||
// Only the macOS version of the login command supports executing a
|
||||
// command, all other versions only support launching a shell
|
||||
// without taking any arguments.
|
||||
shouldUseLoginCmd := isShell || runtime.GOOS == "darwin"
|
||||
if hostinfo.IsSELinuxEnforcing() {
|
||||
// If we're running on a SELinux-enabled system, the login
|
||||
// command will be unable to set the correct context for the
|
||||
// shell. Fall back to using the incubator to launch the shell.
|
||||
// See http://github.com/tailscale/tailscale/issues/4908.
|
||||
shouldUseLoginCmd = false
|
||||
}
|
||||
if shouldUseLoginCmd {
|
||||
if isShell || runtime.GOOS == "darwin" {
|
||||
// Only the macOS version of the login command supports executing a
|
||||
// command, all other versions only support launching a shell
|
||||
// without taking any arguments.
|
||||
if lp, err := exec.LookPath("login"); err == nil {
|
||||
incubatorArgs = append(incubatorArgs, "--login-cmd="+lp)
|
||||
}
|
||||
|
@ -476,10 +467,10 @@ func (ss *sshSession) launchProcess() error {
|
|||
}
|
||||
go resizeWindow(ptyDup /* arbitrary fd */, winCh)
|
||||
|
||||
ss.wrStdin = pty
|
||||
ss.rdStdout = os.NewFile(uintptr(ptyDup), pty.Name())
|
||||
ss.rdStderr = nil // not available for pty
|
||||
ss.childPipes = []io.Closer{tty}
|
||||
ss.tty = tty
|
||||
ss.stdin = pty
|
||||
ss.stdout = os.NewFile(uintptr(ptyDup), pty.Name())
|
||||
ss.stderr = nil // not available for pty
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -658,29 +649,40 @@ func (ss *sshSession) startWithPTY() (ptyFile, tty *os.File, err error) {
|
|||
|
||||
// startWithStdPipes starts cmd with os.Pipe for Stdin, Stdout and Stderr.
|
||||
func (ss *sshSession) startWithStdPipes() (err error) {
|
||||
var rdStdin, wrStdout, wrStderr io.ReadWriteCloser
|
||||
var stdin io.WriteCloser
|
||||
var stdout, stderr io.ReadCloser
|
||||
defer func() {
|
||||
if err != nil {
|
||||
closeAll(rdStdin, ss.wrStdin, ss.rdStdout, wrStdout, ss.rdStderr, wrStderr)
|
||||
for _, c := range []io.Closer{stdin, stdout, stderr} {
|
||||
if c != nil {
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
if ss.cmd == nil {
|
||||
cmd := ss.cmd
|
||||
if cmd == nil {
|
||||
return errors.New("nil cmd")
|
||||
}
|
||||
if rdStdin, ss.wrStdin, err = os.Pipe(); err != nil {
|
||||
stdin, err = cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ss.rdStdout, wrStdout, err = os.Pipe(); err != nil {
|
||||
stdout, err = cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ss.rdStderr, wrStderr, err = os.Pipe(); err != nil {
|
||||
stderr, err = cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ss.cmd.Stdin = rdStdin
|
||||
ss.cmd.Stdout = wrStdout
|
||||
ss.cmd.Stderr = wrStderr
|
||||
ss.childPipes = []io.Closer{rdStdin, wrStdout, wrStderr}
|
||||
return ss.cmd.Start()
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
ss.stdin = stdin
|
||||
ss.stdout = stdout
|
||||
ss.stderr = stderr
|
||||
return nil
|
||||
}
|
||||
|
||||
func envForUser(u *userMeta) []string {
|
||||
|
|
|
@ -422,7 +422,6 @@ func (srv *server) newConn() (*conn, error) {
|
|||
c := &conn{srv: srv}
|
||||
now := srv.now()
|
||||
c.connID = fmt.Sprintf("ssh-conn-%s-%02x", now.UTC().Format("20060102T150405"), randBytes(5))
|
||||
fwdHandler := &ssh.ForwardedTCPHandler{}
|
||||
c.Server = &ssh.Server{
|
||||
Version: "Tailscale",
|
||||
ServerConfigCallback: c.ServerConfig,
|
||||
|
@ -431,9 +430,8 @@ func (srv *server) newConn() (*conn, error) {
|
|||
PublicKeyHandler: c.PublicKeyHandler,
|
||||
PasswordHandler: c.fakePasswordHandler,
|
||||
|
||||
Handler: c.handleSessionPostSSHAuth,
|
||||
LocalPortForwardingCallback: c.mayForwardLocalPortTo,
|
||||
ReversePortForwardingCallback: c.mayReversePortForwardTo,
|
||||
Handler: c.handleSessionPostSSHAuth,
|
||||
LocalPortForwardingCallback: c.mayForwardLocalPortTo,
|
||||
SubsystemHandlers: map[string]ssh.SubsystemHandler{
|
||||
"sftp": c.handleSessionPostSSHAuth,
|
||||
},
|
||||
|
@ -443,10 +441,7 @@ func (srv *server) newConn() (*conn, error) {
|
|||
ChannelHandlers: map[string]ssh.ChannelHandler{
|
||||
"direct-tcpip": ssh.DirectTCPIPHandler,
|
||||
},
|
||||
RequestHandlers: map[string]ssh.RequestHandler{
|
||||
"tcpip-forward": fwdHandler.HandleSSHRequest,
|
||||
"cancel-tcpip-forward": fwdHandler.HandleSSHRequest,
|
||||
},
|
||||
RequestHandlers: map[string]ssh.RequestHandler{},
|
||||
}
|
||||
ss := c.Server
|
||||
for k, v := range ssh.DefaultRequestHandlers {
|
||||
|
@ -468,17 +463,6 @@ func (srv *server) newConn() (*conn, error) {
|
|||
return c, nil
|
||||
}
|
||||
|
||||
// mayReversePortPortForwardTo reports whether the ctx should be allowed to port forward
|
||||
// to the specified host and port.
|
||||
// TODO(bradfitz/maisem): should we have more checks on host/port?
|
||||
func (c *conn) mayReversePortForwardTo(ctx ssh.Context, destinationHost string, destinationPort uint32) bool {
|
||||
if c.finalAction != nil && c.finalAction.AllowRemotePortForwarding {
|
||||
metricRemotePortForward.Add(1)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// mayForwardLocalPortTo reports whether the ctx should be allowed to port forward
|
||||
// to the specified host and port.
|
||||
// TODO(bradfitz/maisem): should we have more checks on host/port?
|
||||
|
@ -823,16 +807,12 @@ type sshSession struct {
|
|||
agentListener net.Listener // non-nil if agent-forwarding requested+allowed
|
||||
|
||||
// initialized by launchProcess:
|
||||
cmd *exec.Cmd
|
||||
wrStdin io.WriteCloser
|
||||
rdStdout io.ReadCloser
|
||||
rdStderr io.ReadCloser // rdStderr is nil for pty sessions
|
||||
ptyReq *ssh.Pty // non-nil for pty sessions
|
||||
|
||||
// childPipes is a list of pipes that need to be closed when the process exits.
|
||||
// For pty sessions, this is the tty fd.
|
||||
// For non-pty sessions, this is the stdin, stdout, stderr fds.
|
||||
childPipes []io.Closer
|
||||
cmd *exec.Cmd
|
||||
stdin io.WriteCloser
|
||||
stdout io.ReadCloser
|
||||
stderr io.Reader // nil for pty sessions
|
||||
ptyReq *ssh.Pty // non-nil for pty sessions
|
||||
tty *os.File // non-nil for pty sessions, must be closed after process exits
|
||||
|
||||
// We use this sync.Once to ensure that we only terminate the process once,
|
||||
// either it exits itself or is terminated
|
||||
|
@ -1111,22 +1091,21 @@ func (ss *sshSession) run() {
|
|||
|
||||
var processDone atomic.Bool
|
||||
go func() {
|
||||
defer ss.wrStdin.Close()
|
||||
if _, err := io.Copy(rec.writer("i", ss.wrStdin), ss); err != nil {
|
||||
defer ss.stdin.Close()
|
||||
if _, err := io.Copy(rec.writer("i", ss.stdin), ss); err != nil {
|
||||
logf("stdin copy: %v", err)
|
||||
ss.cancelCtx(err)
|
||||
}
|
||||
}()
|
||||
outputDone := make(chan struct{})
|
||||
var openOutputStreams atomic.Int32
|
||||
if ss.rdStderr != nil {
|
||||
if ss.stderr != nil {
|
||||
openOutputStreams.Store(2)
|
||||
} else {
|
||||
openOutputStreams.Store(1)
|
||||
}
|
||||
go func() {
|
||||
defer ss.rdStdout.Close()
|
||||
_, err := io.Copy(rec.writer("o", ss), ss.rdStdout)
|
||||
defer ss.stdout.Close()
|
||||
_, err := io.Copy(rec.writer("o", ss), ss.stdout)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
isErrBecauseProcessExited := processDone.Load() && errors.Is(err, syscall.EIO)
|
||||
if !isErrBecauseProcessExited {
|
||||
|
@ -1136,41 +1115,32 @@ func (ss *sshSession) run() {
|
|||
}
|
||||
if openOutputStreams.Add(-1) == 0 {
|
||||
ss.CloseWrite()
|
||||
close(outputDone)
|
||||
}
|
||||
}()
|
||||
// rdStderr is nil for ptys.
|
||||
if ss.rdStderr != nil {
|
||||
// stderr is nil for ptys.
|
||||
if ss.stderr != nil {
|
||||
go func() {
|
||||
defer ss.rdStderr.Close()
|
||||
_, err := io.Copy(ss.Stderr(), ss.rdStderr)
|
||||
_, err := io.Copy(ss.Stderr(), ss.stderr)
|
||||
if err != nil {
|
||||
logf("stderr copy: %v", err)
|
||||
}
|
||||
if openOutputStreams.Add(-1) == 0 {
|
||||
ss.CloseWrite()
|
||||
close(outputDone)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if ss.tty != nil {
|
||||
// If running a tty session, close the tty when the session is done.
|
||||
defer ss.tty.Close()
|
||||
}
|
||||
err = ss.cmd.Wait()
|
||||
processDone.Store(true)
|
||||
|
||||
// This will either make the SSH Termination goroutine be a no-op,
|
||||
// or itself will be a no-op because the process was killed by the
|
||||
// aforementioned goroutine.
|
||||
ss.exitOnce.Do(func() {})
|
||||
|
||||
// Close the process-side of all pipes to signal the asynchronous
|
||||
// io.Copy routines reading/writing from the pipes to terminate.
|
||||
// Block for the io.Copy to finish before calling ss.Exit below.
|
||||
closeAll(ss.childPipes...)
|
||||
select {
|
||||
case <-outputDone:
|
||||
case <-ss.ctx.Done():
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
ss.logf("Session complete")
|
||||
ss.Exit(0)
|
||||
|
@ -1890,7 +1860,6 @@ var (
|
|||
metricPolicyChangeKick = clientmetric.NewCounter("ssh_policy_change_kick")
|
||||
metricSFTP = clientmetric.NewCounter("ssh_sftp_requests")
|
||||
metricLocalPortForward = clientmetric.NewCounter("ssh_local_port_forward_requests")
|
||||
metricRemotePortForward = clientmetric.NewCounter("ssh_remote_port_forward_requests")
|
||||
)
|
||||
|
||||
// userVisibleError is a wrapper around an error that implements
|
||||
|
@ -1908,11 +1877,3 @@ type SSHTerminationError interface {
|
|||
error
|
||||
SSHTerminationMessage() string
|
||||
}
|
||||
|
||||
func closeAll(cs ...io.Closer) {
|
||||
for _, c := range cs {
|
||||
if c != nil {
|
||||
c.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@ import (
|
|||
"os/user"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
@ -948,19 +947,6 @@ func TestSSH(t *testing.T) {
|
|||
// "foo\n" and "bar\n", not "\n" and "bar\n".
|
||||
})
|
||||
|
||||
t.Run("large_file", func(t *testing.T) {
|
||||
const wantSize = 1e6
|
||||
var outBuf bytes.Buffer
|
||||
cmd := execSSH("head", "-c", strconv.Itoa(wantSize), "/dev/zero")
|
||||
cmd.Stdout = &outBuf
|
||||
if err := cmd.Run(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if gotSize := outBuf.Len(); gotSize != wantSize {
|
||||
t.Fatalf("got %d, want %d", gotSize, int(wantSize))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("stdin", func(t *testing.T) {
|
||||
if cibuild.On() {
|
||||
t.Skip("Skipping for now; see https://github.com/tailscale/tailscale/issues/4051")
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
package tailcfg
|
||||
|
||||
//go:generate go run tailscale.com/cmd/viewer --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location --clonefunc
|
||||
//go:generate go run tailscale.com/cmd/viewer --type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan --clonefunc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
@ -99,8 +99,7 @@ type CapabilityVersion int
|
|||
// - 60: 2023-04-06: Client understands IsWireGuardOnly
|
||||
// - 61: 2023-04-18: Client understand SSHAction.SSHRecorderFailureAction
|
||||
// - 62: 2023-05-05: Client can notify control over noise for SSHEventNotificationRequest recording failure events
|
||||
// - 63: 2023-06-08: Client understands SSHAction.AllowRemotePortForwarding.
|
||||
const CurrentCapabilityVersion CapabilityVersion = 63
|
||||
const CurrentCapabilityVersion CapabilityVersion = 62
|
||||
|
||||
type StableID string
|
||||
|
||||
|
@ -242,6 +241,8 @@ type Node struct {
|
|||
// current node doesn't have permission to know.
|
||||
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
|
||||
|
||||
// Capabilities are capabilities that the node has.
|
||||
|
@ -529,31 +530,6 @@ type Service struct {
|
|||
// TODO(apenwarr): add "tags" here for each service?
|
||||
}
|
||||
|
||||
// Location represents geographical location data about a
|
||||
// Tailscale host. Location is optional and only set if
|
||||
// explicitly declared by a node.
|
||||
type Location struct {
|
||||
Country string `json:",omitempty"` // User friendly country name, with proper capitalization ("Canada")
|
||||
CountryCode string `json:",omitempty"` // ISO 3166-1 alpha-2 in upper case ("CA")
|
||||
City string `json:",omitempty"` // User friendly city name, with proper capitalization ("Squamish")
|
||||
|
||||
// CityCode 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.
|
||||
//
|
||||
// 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
|
||||
UserspaceRouter opt.Bool `json:",omitempty"` // if the client's subnet router is running in userspace (netstack) mode
|
||||
|
||||
// Location represents geographical location data about a
|
||||
// Tailscale host. Location is optional and only set if
|
||||
// explicitly declared by a node.
|
||||
Location *Location `json:",omitempty"`
|
||||
|
||||
// NOTE: any new fields containing pointers in this type
|
||||
// require changes to Hostinfo.Equal.
|
||||
}
|
||||
|
@ -1282,7 +1253,7 @@ type DNSConfig struct {
|
|||
// match.
|
||||
//
|
||||
// Matches are case insensitive.
|
||||
ExitNodeFilteredSet []string `json:",omitempty"`
|
||||
ExitNodeFilteredSet []string
|
||||
}
|
||||
|
||||
// DNSRecord is an extra DNS record to add to MagicDNS.
|
||||
|
@ -2077,10 +2048,6 @@ type SSHAction struct {
|
|||
// to use local port forwarding if requested.
|
||||
AllowLocalPortForwarding bool `json:"allowLocalPortForwarding,omitempty"`
|
||||
|
||||
// AllowRemotePortForwarding, if true, allows accepted connections
|
||||
// to use remote port forwarding if requested.
|
||||
AllowRemotePortForwarding bool `json:"allowRemotePortForwarding,omitempty"`
|
||||
|
||||
// Recorders defines the destinations of the SSH session recorders.
|
||||
// The recording will be uploaded to http://addr:port/record.
|
||||
Recorders []netip.AddrPort `json:"recorders,omitempty"`
|
||||
|
|
|
@ -93,6 +93,7 @@ var _NodeCloneNeedsRegeneration = Node(struct {
|
|||
PrimaryRoutes []netip.Prefix
|
||||
LastSeen *time.Time
|
||||
Online *bool
|
||||
KeepAlive bool
|
||||
MachineAuthorized bool
|
||||
Capabilities []string
|
||||
UnsignedPeerAPIOnly bool
|
||||
|
@ -118,10 +119,6 @@ func (src *Hostinfo) Clone() *Hostinfo {
|
|||
dst.Services = append(src.Services[:0:0], src.Services...)
|
||||
dst.NetInfo = src.NetInfo.Clone()
|
||||
dst.SSH_HostKeys = append(src.SSH_HostKeys[:0:0], src.SSH_HostKeys...)
|
||||
if dst.Location != nil {
|
||||
dst.Location = new(Location)
|
||||
*dst.Location = *src.Location
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
|
@ -160,7 +157,6 @@ var _HostinfoCloneNeedsRegeneration = Hostinfo(struct {
|
|||
Cloud string
|
||||
Userspace opt.Bool
|
||||
UserspaceRouter opt.Bool
|
||||
Location *Location
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of NetInfo.
|
||||
|
@ -412,16 +408,15 @@ func (src *SSHAction) Clone() *SSHAction {
|
|||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _SSHActionCloneNeedsRegeneration = SSHAction(struct {
|
||||
Message string
|
||||
Reject bool
|
||||
Accept bool
|
||||
SessionDuration time.Duration
|
||||
AllowAgentForwarding bool
|
||||
HoldAndDelegate string
|
||||
AllowLocalPortForwarding bool
|
||||
AllowRemotePortForwarding bool
|
||||
Recorders []netip.AddrPort
|
||||
OnRecordingFailure *SSHRecorderFailureAction
|
||||
Message string
|
||||
Reject bool
|
||||
Accept bool
|
||||
SessionDuration time.Duration
|
||||
AllowAgentForwarding bool
|
||||
HoldAndDelegate string
|
||||
AllowLocalPortForwarding bool
|
||||
Recorders []netip.AddrPort
|
||||
OnRecordingFailure *SSHRecorderFailureAction
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of SSHPrincipal.
|
||||
|
@ -462,29 +457,9 @@ var _ControlDialPlanCloneNeedsRegeneration = ControlDialPlan(struct {
|
|||
Candidates []ControlIPCandidate
|
||||
}{})
|
||||
|
||||
// Clone makes a deep copy of Location.
|
||||
// The result aliases no memory with the original.
|
||||
func (src *Location) Clone() *Location {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
dst := new(Location)
|
||||
*dst = *src
|
||||
return dst
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _LocationCloneNeedsRegeneration = Location(struct {
|
||||
Country string
|
||||
CountryCode string
|
||||
City string
|
||||
CityCode string
|
||||
Priority int
|
||||
}{})
|
||||
|
||||
// Clone duplicates src into dst and reports whether it succeeded.
|
||||
// To succeed, <src, dst> must be of types <*T, *T> or <*T, **T>,
|
||||
// where T is one of User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location.
|
||||
// where T is one of User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan.
|
||||
func Clone(dst, src any) bool {
|
||||
switch src := src.(type) {
|
||||
case *User:
|
||||
|
@ -613,15 +588,6 @@ func Clone(dst, src any) bool {
|
|||
*dst = src.Clone()
|
||||
return true
|
||||
}
|
||||
case *Location:
|
||||
switch dst := dst.(type) {
|
||||
case *Location:
|
||||
*dst = *src.Clone()
|
||||
return true
|
||||
case **Location:
|
||||
*dst = src.Clone()
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -65,7 +65,6 @@ func TestHostinfoEqual(t *testing.T) {
|
|||
"Cloud",
|
||||
"Userspace",
|
||||
"UserspaceRouter",
|
||||
"Location",
|
||||
}
|
||||
if have := fieldsOf(reflect.TypeOf(Hostinfo{})); !reflect.DeepEqual(have, hiHandles) {
|
||||
t.Errorf("Hostinfo.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||
|
@ -347,7 +346,7 @@ func TestNodeEqual(t *testing.T) {
|
|||
"Key", "KeyExpiry", "KeySignature", "Machine", "DiscoKey",
|
||||
"Addresses", "AllowedIPs", "Endpoints", "DERP", "Hostinfo",
|
||||
"Created", "Cap", "Tags", "PrimaryRoutes",
|
||||
"LastSeen", "Online", "MachineAuthorized",
|
||||
"LastSeen", "Online", "KeepAlive", "MachineAuthorized",
|
||||
"Capabilities",
|
||||
"UnsignedPeerAPIOnly",
|
||||
"ComputedName", "computedHostIfDifferent", "ComputedNameWithHost",
|
||||
|
|
|
@ -20,7 +20,7 @@ import (
|
|||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan,Location
|
||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type=User,Node,Hostinfo,NetInfo,Login,DNSConfig,RegisterResponse,DERPRegion,DERPMap,DERPNode,SSHRule,SSHAction,SSHPrincipal,ControlDialPlan
|
||||
|
||||
// View returns a readonly view of User.
|
||||
func (p *User) View() UserView {
|
||||
|
@ -168,6 +168,7 @@ func (v NodeView) Online() *bool {
|
|||
return &x
|
||||
}
|
||||
|
||||
func (v NodeView) KeepAlive() bool { return v.ж.KeepAlive }
|
||||
func (v NodeView) MachineAuthorized() bool { return v.ж.MachineAuthorized }
|
||||
func (v NodeView) Capabilities() views.Slice[string] { return views.SliceOf(v.ж.Capabilities) }
|
||||
func (v NodeView) UnsignedPeerAPIOnly() bool { return v.ж.UnsignedPeerAPIOnly }
|
||||
|
@ -209,6 +210,7 @@ var _NodeViewNeedsRegeneration = Node(struct {
|
|||
PrimaryRoutes []netip.Prefix
|
||||
LastSeen *time.Time
|
||||
Online *bool
|
||||
KeepAlive bool
|
||||
MachineAuthorized bool
|
||||
Capabilities []string
|
||||
UnsignedPeerAPIOnly bool
|
||||
|
@ -301,15 +303,7 @@ func (v HostinfoView) SSH_HostKeys() views.Slice[string] { return views.SliceOf(
|
|||
func (v HostinfoView) Cloud() string { return v.ж.Cloud }
|
||||
func (v HostinfoView) Userspace() opt.Bool { return v.ж.Userspace }
|
||||
func (v HostinfoView) UserspaceRouter() opt.Bool { return v.ж.UserspaceRouter }
|
||||
func (v HostinfoView) Location() *Location {
|
||||
if v.ж.Location == nil {
|
||||
return nil
|
||||
}
|
||||
x := *v.ж.Location
|
||||
return &x
|
||||
}
|
||||
|
||||
func (v HostinfoView) Equal(v2 HostinfoView) bool { return v.ж.Equal(v2.ж) }
|
||||
func (v HostinfoView) Equal(v2 HostinfoView) bool { return v.ж.Equal(v2.ж) }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _HostinfoViewNeedsRegeneration = Hostinfo(struct {
|
||||
|
@ -346,7 +340,6 @@ var _HostinfoViewNeedsRegeneration = Hostinfo(struct {
|
|||
Cloud string
|
||||
Userspace opt.Bool
|
||||
UserspaceRouter opt.Bool
|
||||
Location *Location
|
||||
}{})
|
||||
|
||||
// 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) HoldAndDelegate() string { return v.ж.HoldAndDelegate }
|
||||
func (v SSHActionView) AllowLocalPortForwarding() bool { return v.ж.AllowLocalPortForwarding }
|
||||
func (v SSHActionView) AllowRemotePortForwarding() bool { return v.ж.AllowRemotePortForwarding }
|
||||
func (v SSHActionView) Recorders() views.Slice[netip.AddrPort] { return views.SliceOf(v.ж.Recorders) }
|
||||
func (v SSHActionView) OnRecordingFailure() *SSHRecorderFailureAction {
|
||||
if v.ж.OnRecordingFailure == nil {
|
||||
|
@ -959,16 +951,15 @@ func (v SSHActionView) OnRecordingFailure() *SSHRecorderFailureAction {
|
|||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _SSHActionViewNeedsRegeneration = SSHAction(struct {
|
||||
Message string
|
||||
Reject bool
|
||||
Accept bool
|
||||
SessionDuration time.Duration
|
||||
AllowAgentForwarding bool
|
||||
HoldAndDelegate string
|
||||
AllowLocalPortForwarding bool
|
||||
AllowRemotePortForwarding bool
|
||||
Recorders []netip.AddrPort
|
||||
OnRecordingFailure *SSHRecorderFailureAction
|
||||
Message string
|
||||
Reject bool
|
||||
Accept bool
|
||||
SessionDuration time.Duration
|
||||
AllowAgentForwarding bool
|
||||
HoldAndDelegate string
|
||||
AllowLocalPortForwarding bool
|
||||
Recorders []netip.AddrPort
|
||||
OnRecordingFailure *SSHRecorderFailureAction
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of SSHPrincipal.
|
||||
|
@ -1084,63 +1075,3 @@ func (v ControlDialPlanView) Candidates() views.Slice[ControlIPCandidate] {
|
|||
var _ControlDialPlanViewNeedsRegeneration = ControlDialPlan(struct {
|
||||
Candidates []ControlIPCandidate
|
||||
}{})
|
||||
|
||||
// View returns a readonly view of Location.
|
||||
func (p *Location) View() LocationView {
|
||||
return LocationView{ж: p}
|
||||
}
|
||||
|
||||
// LocationView provides a read-only view over Location.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type LocationView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *Location
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v LocationView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v LocationView) AsStruct() *Location {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v LocationView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *LocationView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x Location
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v LocationView) Country() string { return v.ж.Country }
|
||||
func (v LocationView) CountryCode() string { return v.ж.CountryCode }
|
||||
func (v LocationView) City() string { return v.ж.City }
|
||||
func (v LocationView) CityCode() string { return v.ж.CityCode }
|
||||
func (v LocationView) Priority() int { return v.ж.Priority }
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _LocationViewNeedsRegeneration = Location(struct {
|
||||
Country string
|
||||
CountryCode string
|
||||
City string
|
||||
CityCode string
|
||||
Priority int
|
||||
}{})
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
221
tka/deeplink.go
221
tka/deeplink.go
|
@ -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,
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -111,11 +111,7 @@ func autoflagsForTest(argv []string, env *Environment, goroot, nativeGOOS, nativ
|
|||
// e.g. -mmacosx-version-min=11.3, -miphoneos-version-min=15.0
|
||||
switch {
|
||||
case env.IsSet("IPHONEOS_DEPLOYMENT_TARGET"):
|
||||
if env.Get("TARGET_DEVICE_PLATFORM_NAME", "") == "iphonesimulator" {
|
||||
xcodeFlags = append(xcodeFlags, "-miphonesimulator-version-min="+env.Get("IPHONEOS_DEPLOYMENT_TARGET", ""))
|
||||
} else {
|
||||
xcodeFlags = append(xcodeFlags, "-miphoneos-version-min="+env.Get("IPHONEOS_DEPLOYMENT_TARGET", ""))
|
||||
}
|
||||
xcodeFlags = append(xcodeFlags, "-miphoneos-version-min="+env.Get("IPHONEOS_DEPLOYMENT_TARGET", ""))
|
||||
case env.IsSet("MACOSX_DEPLOYMENT_TARGET"):
|
||||
xcodeFlags = append(xcodeFlags, "-mmacosx-version-min="+env.Get("MACOSX_DEPLOYMENT_TARGET", ""))
|
||||
case env.IsSet("TVOS_DEPLOYMENT_TARGET"):
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/usr/bin/env bash
|
||||
#!/usr/bin/env sh
|
||||
# Copyright (c) Tailscale Inc & AUTHORS
|
||||
# SPDX-License-Identifier: BSD-3-Clause
|
||||
#
|
||||
|
@ -6,9 +6,9 @@
|
|||
# transparently builds gocross using a "bootstrap" Go toolchain, and
|
||||
# then invokes gocross.
|
||||
|
||||
set -euo pipefail
|
||||
set -eu
|
||||
|
||||
if [[ "${CI:-}" == "true" ]]; then
|
||||
if [ "${CI:-}" = "true" ]; then
|
||||
set -x
|
||||
fi
|
||||
|
||||
|
@ -17,7 +17,7 @@ fi
|
|||
# accidentally mutate the input environment that will get passed to gocross at
|
||||
# the bottom of this script.
|
||||
(
|
||||
repo_root="${BASH_SOURCE%/*}/../.."
|
||||
repo_root="$(dirname $0)/../.."
|
||||
|
||||
# Figuring out if gocross needs a rebuild, as well as the rebuild itself, need
|
||||
# to happen with CWD inside this repo. Since we're in a subshell entirely
|
||||
|
@ -28,21 +28,16 @@ cd "$repo_root"
|
|||
|
||||
toolchain="$HOME/.cache/tailscale-go"
|
||||
|
||||
if [[ -d "$toolchain" ]]; then
|
||||
if [ -d "$toolchain" ]; then
|
||||
# A toolchain exists, but is it recent enough to compile gocross? If not,
|
||||
# wipe it out so that the next if block fetches a usable one.
|
||||
want_go_minor=$(grep -E '^go ' "go.mod" | cut -f2 -d'.')
|
||||
have_go_minor=$(head -1 "$toolchain/VERSION" | cut -f2 -d'.')
|
||||
# Shortly before stable releases, we run release candidate
|
||||
# toolchains, which have a non-numeric suffix on the version
|
||||
# number. Remove the rc qualifier, we just care about the minor
|
||||
# version.
|
||||
have_go_minor="${have_go_minor%rc*}"
|
||||
if [[ -z "$have_go_minor" || "$have_go_minor" -lt "$want_go_minor" ]]; then
|
||||
have_go_minor=$(cut -f2 -d'.' <$toolchain/VERSION)
|
||||
if [ -z "$have_go_minor" -o "$have_go_minor" -lt "$want_go_minor" ]; then
|
||||
rm -rf "$toolchain" "$toolchain.extracted"
|
||||
fi
|
||||
fi
|
||||
if [[ ! -d "$toolchain" ]]; then
|
||||
if [ ! -d "$toolchain" ]; then
|
||||
mkdir -p "$HOME/.cache"
|
||||
|
||||
# We need any Go toolchain to build gocross, but the toolchain also has to
|
||||
|
@ -61,10 +56,10 @@ if [[ ! -d "$toolchain" ]]; then
|
|||
# (we do not build tailscale-go for other targets).
|
||||
HOST_OS=$(uname -s | tr A-Z a-z)
|
||||
HOST_ARCH="$(uname -m)"
|
||||
if [[ "$HOST_ARCH" == "aarch64" ]]; then
|
||||
if [ "$HOST_ARCH" = "aarch64" ]; then
|
||||
# Go uses the name "arm64".
|
||||
HOST_ARCH="arm64"
|
||||
elif [[ "$HOST_ARCH" == "x86_64" ]]; then
|
||||
elif [ "$HOST_ARCH" = "x86_64" ]; then
|
||||
# Go uses the name "amd64".
|
||||
HOST_ARCH="amd64"
|
||||
fi
|
||||
|
@ -88,13 +83,13 @@ fi
|
|||
gocross_path="gocross"
|
||||
gocross_ok=0
|
||||
wantver="$(git rev-parse HEAD)"
|
||||
if [[ -x "$gocross_path" ]]; then
|
||||
if [ -x "$gocross_path" ]; then
|
||||
gotver="$($gocross_path gocross-version 2>/dev/null || echo '')"
|
||||
if [[ "$gotver" == "$wantver" ]]; then
|
||||
if [ "$gotver" = "$wantver" ]; then
|
||||
gocross_ok=1
|
||||
fi
|
||||
fi
|
||||
if [[ "$gocross_ok" == "0" ]]; then
|
||||
if [ "$gocross_ok" = "0" ]; then
|
||||
unset GOOS
|
||||
unset GOARCH
|
||||
unset GO111MODULE
|
||||
|
@ -104,4 +99,4 @@ if [[ "$gocross_ok" == "0" ]]; then
|
|||
fi
|
||||
) # End of the subshell execution.
|
||||
|
||||
exec "${BASH_SOURCE%/*}/../../gocross" "$@"
|
||||
exec "$(dirname $0)/../../gocross" "$@"
|
||||
|
|
|
@ -22,7 +22,6 @@ import (
|
|||
"net/netip"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
@ -61,7 +60,7 @@ func inTest() bool { return flag.Lookup("test.v") != nil }
|
|||
|
||||
// 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 {
|
||||
// Dir specifies the name of the directory to use for
|
||||
// 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`.
|
||||
// See tailscale.com/ipn/store for supported stores.
|
||||
//
|
||||
// Logs will automatically be uploaded to log.tailscale.io,
|
||||
// Logs will automatically be uploaded to uploaded to log.tailscale.io,
|
||||
// where the configuration file for logging will be saved at
|
||||
// `Dir/tailscaled.log.conf`.
|
||||
Store ipn.StateStore
|
||||
|
@ -108,11 +107,6 @@ type Server struct {
|
|||
// If empty, the Tailscale default is used.
|
||||
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)
|
||||
|
||||
initOnce sync.Once
|
||||
|
@ -447,16 +441,7 @@ func (s *Server) start() (reterr error) {
|
|||
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
switch runtime.GOOS {
|
||||
case "js", "wasip1":
|
||||
// These platforms don't implement os.Executable (at least as of Go
|
||||
// 1.21), but we don't really care much: it's only used as a default
|
||||
// directory and hostname when they're not supplied. But we can fall
|
||||
// back to "tsnet" as well.
|
||||
exe = "tsnet"
|
||||
default:
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
prog := strings.TrimSuffix(strings.ToLower(filepath.Base(exe)), ".exe")
|
||||
|
||||
|
@ -507,7 +492,7 @@ func (s *Server) start() (reterr error) {
|
|||
sys := new(tsd.System)
|
||||
s.dialer = &tsdial.Dialer{Logf: logf} // mutated below (before used)
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
ListenPort: s.Port,
|
||||
ListenPort: 0,
|
||||
NetMon: s.netMon,
|
||||
Dialer: s.dialer,
|
||||
SetSubsystem: sys.Set,
|
||||
|
|
|
@ -30,7 +30,6 @@ import (
|
|||
"time"
|
||||
|
||||
"golang.org/x/net/proxy"
|
||||
"tailscale.com/cmd/testwrapper/flakytest"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/net/netns"
|
||||
|
@ -283,7 +282,6 @@ func TestConn(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestLoopbackLocalAPI(t *testing.T) {
|
||||
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/8557")
|
||||
tstest.ResourceCheck(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
@ -358,7 +356,6 @@ func TestLoopbackLocalAPI(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)
|
||||
defer cancel()
|
||||
|
||||
|
|
699
tstest/clock.go
699
tstest/clock.go
|
@ -4,686 +4,57 @@
|
|||
package tstest
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"sync"
|
||||
"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
|
||||
// called, beginning at its start time. If no start time is specified using
|
||||
// ClockBuilder, an arbitrary start time will be selected when the Clock is
|
||||
// created and can be retrieved by calling Clock.Start().
|
||||
// called, beginning at 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 {
|
||||
// start is the first value returned by Now. It must not be modified after
|
||||
// init is called.
|
||||
start time.Time
|
||||
// Start is the first value returned by Now.
|
||||
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
|
||||
// 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)
|
||||
})
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// Now returns the virtual clock's current time, and advances it
|
||||
// according to its step configuration.
|
||||
func (c *Clock) Now() time.Time {
|
||||
c.init()
|
||||
rt := c.maybeGetRealTime()
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.initLocked()
|
||||
step := c.Step
|
||||
ret := c.Present
|
||||
c.Present = c.Present.Add(step)
|
||||
return ret
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
func (c *Clock) Advance(d time.Duration) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.initLocked()
|
||||
c.Present = c.Present.Add(d)
|
||||
}
|
||||
|
||||
step := c.step
|
||||
if c.skipStep {
|
||||
step = 0
|
||||
c.skipStep = false
|
||||
func (c *Clock) initLocked() {
|
||||
if c.Start.IsZero() {
|
||||
c.Start = time.Now()
|
||||
}
|
||||
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) {
|
||||
if !now.IsZero() {
|
||||
add += now.Sub(c.realTime)
|
||||
c.realTime = now
|
||||
}
|
||||
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,
|
||||
// PeekNow returns the same value as GetStart.
|
||||
func (c *Clock) PeekNow() time.Time {
|
||||
c.init()
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
return c.present
|
||||
}
|
||||
|
||||
// Advance moves simulated time forward or backwards by a relative amount. Any
|
||||
// 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) {
|
||||
for len(em.heap) > 0 && !em.heap[0].when.After(tm) {
|
||||
// Ideally some jitter would be added here but it's difficult to do so
|
||||
// in a deterministic fashion.
|
||||
em.now = em.heap[0].when
|
||||
|
||||
if nextFire := em.heap[0].eh.Fire(em.now); !nextFire.IsZero() {
|
||||
em.heap[0].when = nextFire
|
||||
heap.Fix(em, 0)
|
||||
} else {
|
||||
heap.Pop(em)
|
||||
}
|
||||
if c.Present.Before(c.Start) {
|
||||
c.Present = c.Start
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
// Reset rewinds the virtual clock to its start time.
|
||||
func (c *Clock) Reset() {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
c.Present = c.Start
|
||||
}
|
||||
|
|
2439
tstest/clock_test.go
2439
tstest/clock_test.go
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
Loading…
Reference in New Issue