Compare commits

..

1 Commits

Author SHA1 Message Date
Jonathan Nobels
82ca894ff5 wgengine: return explicit lo0 for loopback addrs on sandboxed macOS
fixes tailscale/tailscale#TODO

The source address link selection on sandboxed macOS doesn't deal
with loopback addresses correctly.  This adds an explicit check to ensure
we return the loopback interface for loopback addresses instead of the
default empty interface.

Specifcially, this allows the dns resolver to route queries to a loopback
IP which is a common tactic for local DNS proxies.
2025-03-24 15:35:18 -04:00
83 changed files with 489 additions and 1283 deletions

View File

@@ -1 +1 @@
3.19
3.18

View File

@@ -62,10 +62,8 @@ RUN GOARCH=$TARGETARCH go install -ldflags="\
-X tailscale.com/version.gitCommitStamp=$VERSION_GIT_HASH" \
-v ./cmd/tailscale ./cmd/tailscaled ./cmd/containerboot
FROM alpine:3.19
FROM alpine:3.18
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
RUN rm /sbin/iptables && ln -s /sbin/iptables-legacy /sbin/iptables
RUN rm /sbin/ip6tables && ln -s /sbin/ip6tables-legacy /sbin/ip6tables
COPY --from=build-env /go/bin/* /usr/local/bin/
# For compat with the previous run.sh, although ideally you should be

View File

@@ -1,12 +1,5 @@
# Copyright (c) Tailscale Inc & AUTHORS
# SPDX-License-Identifier: BSD-3-Clause
FROM alpine:3.19
RUN apk add --no-cache ca-certificates iptables iptables-legacy iproute2 ip6tables iputils
# Alpine 3.19 replaces legacy iptables with nftables based implementation. We
# can't be certain that all hosts that run Tailscale containers currently
# suppport nftables, so link back to legacy for backwards compatibility reasons.
# TODO(irbekrm): add some way how to determine if we still run on nodes that
# don't support nftables, so that we can eventually remove these symlinks.
RUN rm /sbin/iptables && ln -s /sbin/iptables-legacy /sbin/iptables
RUN rm /sbin/ip6tables && ln -s /sbin/ip6tables-legacy /sbin/ip6tables
FROM alpine:3.18
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables iputils

View File

@@ -16,7 +16,7 @@ eval "$(./build_dist.sh shellvars)"
DEFAULT_TARGET="client"
DEFAULT_TAGS="v${VERSION_SHORT},v${VERSION_MINOR}"
DEFAULT_BASE="tailscale/alpine-base:3.19"
DEFAULT_BASE="tailscale/alpine-base:3.18"
# Set a few pre-defined OCI annotations. The source annotation is used by tools such as Renovate that scan the linked
# Github repo to find release notes for any new image tags. Note that for official Tailscale images the default
# annotations defined here will be overriden by release scripts that call this script.

View File

@@ -60,9 +60,6 @@ func (cm *certManager) ensureCertLoops(ctx context.Context, sc *ipn.ServeConfig)
if _, exists := cm.certLoops[domain]; !exists {
cancelCtx, cancel := context.WithCancel(ctx)
mak.Set(&cm.certLoops, domain, cancel)
// Note that most of the issuance anyway happens
// serially because the cert client has a shared lock
// that's held during any issuance.
cm.tracker.Go(func() { cm.runCertLoop(cancelCtx, domain) })
}
}
@@ -119,13 +116,7 @@ func (cm *certManager) runCertLoop(ctx context.Context, domain string) {
// issuance endpoint that explicitly only triggers
// issuance and stores certs in the relevant store, but
// does not return certs to the caller?
// An issuance holds a shared lock, so we need to avoid
// a situation where other services cannot issue certs
// because a single one is holding the lock.
ctxT, cancel := context.WithTimeout(ctx, time.Second*300)
defer cancel()
_, _, err := cm.lc.CertPair(ctxT, domain)
_, _, err := cm.lc.CertPair(ctx, domain)
if err != nil {
log.Printf("error refreshing certificate for %s: %v", domain, err)
}

View File

@@ -155,7 +155,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
💣 tailscale.com/util/deephash from tailscale.com/util/syspolicy/setting
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/hostinfo+
tailscale.com/util/eventbus from tailscale.com/net/netmon
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
tailscale.com/util/httpm from tailscale.com/client/tailscale
tailscale.com/util/lineiter from tailscale.com/hostinfo+
@@ -309,7 +308,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
hash/fnv from google.golang.org/protobuf/internal/detrand
hash/maphash from go4.org/mem
html from net/http/pprof+
html/template from tailscale.com/cmd/derper+
html/template from tailscale.com/cmd/derper
internal/abi from crypto/x509/internal/macos+
internal/asan from internal/runtime/maps+
internal/bisect from internal/godebug

View File

@@ -82,10 +82,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
github.com/coder/websocket from tailscale.com/util/eventbus
github.com/coder/websocket/internal/errd from github.com/coder/websocket
github.com/coder/websocket/internal/util from github.com/coder/websocket
github.com/coder/websocket/internal/xsync from github.com/coder/websocket
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
💣 github.com/davecgh/go-spew/spew from k8s.io/apimachinery/pkg/util/dump
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com+
@@ -907,8 +903,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
tailscale.com/tstime from tailscale.com/cmd/k8s-operator+
tailscale.com/tstime/mono from tailscale.com/net/tstun+
tailscale.com/tstime/rate from tailscale.com/derp+
tailscale.com/tsweb from tailscale.com/util/eventbus
tailscale.com/tsweb/varz from tailscale.com/util/usermetric+
tailscale.com/tsweb/varz from tailscale.com/util/usermetric
tailscale.com/types/appctype from tailscale.com/ipn/ipnlocal
tailscale.com/types/bools from tailscale.com/tsnet
tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+
@@ -937,7 +932,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics+
tailscale.com/util/dnsname from tailscale.com/appc+
tailscale.com/util/eventbus from tailscale.com/tsd+
tailscale.com/util/execqueue from tailscale.com/appc+
tailscale.com/util/goroutines from tailscale.com/ipn/ipnlocal
tailscale.com/util/groupmember from tailscale.com/client/web+
@@ -1155,7 +1149,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
hash/fnv from google.golang.org/protobuf/internal/detrand
hash/maphash from go4.org/mem
html from html/template+
html/template from github.com/gorilla/csrf+
html/template from github.com/gorilla/csrf
internal/abi from crypto/x509/internal/macos+
internal/asan from internal/runtime/maps+
internal/bisect from internal/godebug

View File

@@ -103,7 +103,7 @@ spec:
pattern: ^tag:[a-zA-Z][a-zA-Z0-9-]*$
type:
description: |-
Type of the ProxyGroup proxies. Currently the only supported type is egress.
Type of the ProxyGroup proxies. Supported types are egress and ingress.
Type is immutable once a ProxyGroup is created.
type: string
enum:

View File

@@ -2876,7 +2876,7 @@ spec:
type: array
type:
description: |-
Type of the ProxyGroup proxies. Currently the only supported type is egress.
Type of the ProxyGroup proxies. Supported types are egress and ingress.
Type is immutable once a ProxyGroup is created.
enum:
- egress

View File

@@ -49,11 +49,10 @@ const (
// FinalizerNamePG is the finalizer used by the IngressPGReconciler
FinalizerNamePG = "tailscale.com/ingress-pg-finalizer"
indexIngressProxyGroup = ".metadata.annotations.ingress-proxy-group"
// annotationHTTPEndpoint can be used to configure the Ingress to expose an HTTP endpoint to tailnet (as
// well as the default HTTPS endpoint).
annotationHTTPEndpoint = "tailscale.com/http-endpoint"
labelDomain = "tailscale.com/domain"
)
var gaugePGIngressResources = clientmetric.NewGauge(kubetypes.MetricIngressPGResourceCount)
@@ -242,7 +241,7 @@ func (r *HAIngressReconciler) maybeProvision(ctx context.Context, hostname strin
return false, nil
}
// 3. Ensure that TLS Secret and RBAC exists
if err := r.ensureCertResources(ctx, pgName, dnsName, ing); err != nil {
if err := r.ensureCertResources(ctx, pgName, dnsName); err != nil {
return false, fmt.Errorf("error ensuring cert resources: %w", err)
}
@@ -339,11 +338,7 @@ func (r *HAIngressReconciler) maybeProvision(ctx context.Context, hostname strin
// 5. Update tailscaled's AdvertiseServices config, which should add the VIPService
// IPs to the ProxyGroup Pods' AllowedIPs in the next netmap update if approved.
mode := serviceAdvertisementHTTPS
if isHTTPEndpointEnabled(ing) {
mode = serviceAdvertisementHTTPAndHTTPS
}
if err = r.maybeUpdateAdvertiseServicesConfig(ctx, pg.Name, serviceName, mode, logger); err != nil {
if err = r.maybeUpdateAdvertiseServicesConfig(ctx, pg.Name, serviceName, true, logger); err != nil {
return false, fmt.Errorf("failed to update tailscaled config: %w", err)
}
@@ -359,17 +354,11 @@ func (r *HAIngressReconciler) maybeProvision(ctx context.Context, hostname strin
case 0:
ing.Status.LoadBalancer.Ingress = nil
default:
var ports []networkingv1.IngressPortStatus
hasCerts, err := r.hasCerts(ctx, serviceName)
if err != nil {
return false, fmt.Errorf("error checking TLS credentials provisioned for Ingress: %w", err)
}
// If TLS certs have not been issued (yet), do not set port 443.
if hasCerts {
ports = append(ports, networkingv1.IngressPortStatus{
ports := []networkingv1.IngressPortStatus{
{
Protocol: "TCP",
Port: 443,
})
},
}
if isHTTPEndpointEnabled(ing) {
ports = append(ports, networkingv1.IngressPortStatus{
@@ -377,14 +366,9 @@ func (r *HAIngressReconciler) maybeProvision(ctx context.Context, hostname strin
Port: 80,
})
}
// Set Ingress status hostname only if either port 443 or 80 is advertised.
var hostname string
if len(ports) != 0 {
hostname = dnsName
}
ing.Status.LoadBalancer.Ingress = []networkingv1.IngressLoadBalancerIngress{
{
Hostname: hostname,
Hostname: dnsName,
Ports: ports,
},
}
@@ -445,7 +429,7 @@ func (r *HAIngressReconciler) maybeCleanupProxyGroup(ctx context.Context, proxyG
}
// Make sure the VIPService is not advertised in tailscaled or serve config.
if err = r.maybeUpdateAdvertiseServicesConfig(ctx, proxyGroupName, vipServiceName, serviceAdvertisementOff, logger); err != nil {
if err = r.maybeUpdateAdvertiseServicesConfig(ctx, proxyGroupName, vipServiceName, false, logger); err != nil {
return false, fmt.Errorf("failed to update tailscaled config services: %w", err)
}
_, ok := cfg.Services[vipServiceName]
@@ -528,7 +512,7 @@ func (r *HAIngressReconciler) maybeCleanup(ctx context.Context, hostname string,
}
// 4. Unadvertise the VIPService in tailscaled config.
if err = r.maybeUpdateAdvertiseServicesConfig(ctx, pg, serviceName, serviceAdvertisementOff, logger); err != nil {
if err = r.maybeUpdateAdvertiseServicesConfig(ctx, pg, serviceName, false, logger); err != nil {
return false, fmt.Errorf("failed to update tailscaled config services: %w", err)
}
@@ -725,16 +709,8 @@ func isHTTPEndpointEnabled(ing *networkingv1.Ingress) bool {
return ing.Annotations[annotationHTTPEndpoint] == "enabled"
}
// serviceAdvertisementMode describes the desired state of a VIPService.
type serviceAdvertisementMode int
const (
serviceAdvertisementOff serviceAdvertisementMode = iota // Should not be advertised
serviceAdvertisementHTTPS // Port 443 should be advertised
serviceAdvertisementHTTPAndHTTPS // Both ports 80 and 443 should be advertised
)
func (a *HAIngressReconciler) maybeUpdateAdvertiseServicesConfig(ctx context.Context, pgName string, serviceName tailcfg.ServiceName, mode serviceAdvertisementMode, logger *zap.SugaredLogger) (err error) {
func (a *HAIngressReconciler) maybeUpdateAdvertiseServicesConfig(ctx context.Context, pgName string, serviceName tailcfg.ServiceName, shouldBeAdvertised bool, logger *zap.SugaredLogger) (err error) {
logger.Debugf("Updating ProxyGroup tailscaled configs to advertise service %q: %v", serviceName, shouldBeAdvertised)
// Get all config Secrets for this ProxyGroup.
secrets := &corev1.SecretList{}
@@ -742,21 +718,6 @@ func (a *HAIngressReconciler) maybeUpdateAdvertiseServicesConfig(ctx context.Con
return fmt.Errorf("failed to list config Secrets: %w", err)
}
// Verify that TLS cert for the VIPService has been successfully issued
// before attempting to advertise the service.
// This is so that in multi-cluster setups where some Ingresses succeed
// to issue certs and some do not (rate limits), clients are not pinned
// to a backend that is not able to serve HTTPS.
// The only exception is Ingresses with an HTTP endpoint enabled - if an
// Ingress has an HTTP endpoint enabled, it will be advertised even if the
// TLS cert is not yet provisioned.
hasCert, err := a.hasCerts(ctx, serviceName)
if err != nil {
return fmt.Errorf("error checking TLS credentials provisioned for service %q: %w", serviceName, err)
}
shouldBeAdvertised := (mode == serviceAdvertisementHTTPAndHTTPS) ||
(mode == serviceAdvertisementHTTPS && hasCert) // if we only expose port 443 and don't have certs (yet), do not advertise
for _, secret := range secrets.Items {
var updated bool
for fileName, confB := range secret.Data {
@@ -909,8 +870,8 @@ func ownersAreSetAndEqual(a, b *tailscale.VIPService) bool {
// (domain) is a valid Kubernetes resource name.
// https://github.com/tailscale/tailscale/blob/8b1e7f646ee4730ad06c9b70c13e7861b964949b/util/dnsname/dnsname.go#L99
// https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#dns-subdomain-names
func (r *HAIngressReconciler) ensureCertResources(ctx context.Context, pgName, domain string, ing *networkingv1.Ingress) error {
secret := certSecret(pgName, r.tsNamespace, domain, ing)
func (r *HAIngressReconciler) ensureCertResources(ctx context.Context, pgName, domain string) error {
secret := certSecret(pgName, r.tsNamespace, domain)
if _, err := createOrUpdate(ctx, r.Client, r.tsNamespace, secret, nil); err != nil {
return fmt.Errorf("failed to create or update Secret %s: %w", secret.Name, err)
}
@@ -1005,14 +966,9 @@ func certSecretRoleBinding(pgName, namespace, domain string) *rbacv1.RoleBinding
// certSecret creates a Secret that will store the TLS certificate and private
// key for the given domain. Domain must be a valid Kubernetes resource name.
func certSecret(pgName, namespace, domain string, ing *networkingv1.Ingress) *corev1.Secret {
func certSecret(pgName, namespace, domain string) *corev1.Secret {
labels := certResourceLabels(pgName, domain)
labels[kubetypes.LabelSecretType] = "certs"
// Labels that let us identify the Ingress resource lets us reconcile
// the Ingress when the TLS Secret is updated (for example, when TLS
// certs have been provisioned).
labels[LabelParentName] = ing.Name
labels[LabelParentNamespace] = ing.Namespace
return &corev1.Secret{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
@@ -1033,9 +989,9 @@ func certSecret(pgName, namespace, domain string, ing *networkingv1.Ingress) *co
func certResourceLabels(pgName, domain string) map[string]string {
return map[string]string{
kubetypes.LabelManaged: "true",
labelProxyGroup: pgName,
labelDomain: domain,
kubetypes.LabelManaged: "true",
"tailscale.com/proxy-group": pgName,
"tailscale.com/domain": domain,
}
}
@@ -1048,28 +1004,3 @@ func (r *HAIngressReconciler) dnsNameForService(ctx context.Context, svc tailcfg
}
return s + "." + tcd, nil
}
// hasCerts checks if the TLS Secret for the given service has non-zero cert and key data.
func (r *HAIngressReconciler) hasCerts(ctx context.Context, svc tailcfg.ServiceName) (bool, error) {
domain, err := r.dnsNameForService(ctx, svc)
if err != nil {
return false, fmt.Errorf("failed to get DNS name for service: %w", err)
}
secret := &corev1.Secret{}
err = r.Get(ctx, client.ObjectKey{
Namespace: r.tsNamespace,
Name: domain,
}, secret)
if err != nil {
if apierrors.IsNotFound(err) {
return false, nil
}
return false, fmt.Errorf("failed to get TLS Secret: %w", err)
}
cert := secret.Data[corev1.TLSCertKey]
key := secret.Data[corev1.TLSPrivateKeyKey]
return len(cert) > 0 && len(key) > 0, nil
}

View File

@@ -31,7 +31,6 @@ import (
"tailscale.com/ipn/ipnstate"
tsoperator "tailscale.com/k8s-operator"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/kube/kubetypes"
"tailscale.com/tailcfg"
"tailscale.com/types/ptr"
)
@@ -60,7 +59,7 @@ func TestIngressPGReconciler(t *testing.T) {
},
},
TLS: []networkingv1.IngressTLS{
{Hosts: []string{"my-svc"}},
{Hosts: []string{"my-svc.tailnetxyz.ts.net"}},
},
},
}
@@ -68,14 +67,12 @@ func TestIngressPGReconciler(t *testing.T) {
// Verify initial reconciliation
expectReconciled(t, ingPGR, "default", "test-ingress")
populateTLSSecret(context.Background(), fc, "test-pg", "my-svc.ts.net")
expectReconciled(t, ingPGR, "default", "test-ingress")
verifyServeConfig(t, fc, "svc:my-svc", false)
verifyVIPService(t, ft, "svc:my-svc", []string{"443"})
verifyTailscaledConfig(t, fc, []string{"svc:my-svc"})
// Verify that Role and RoleBinding have been created for the first Ingress.
// Do not verify the cert Secret as that was already verified implicitly above.
// Verify cert resources were created for the first Ingress
expectEqual(t, fc, certSecret("test-pg", "operator-ns", "my-svc.ts.net"))
expectEqual(t, fc, certSecretRole("test-pg", "operator-ns", "my-svc.ts.net"))
expectEqual(t, fc, certSecretRoleBinding("test-pg", "operator-ns", "my-svc.ts.net"))
@@ -130,13 +127,11 @@ func TestIngressPGReconciler(t *testing.T) {
// Verify second Ingress reconciliation
expectReconciled(t, ingPGR, "default", "my-other-ingress")
populateTLSSecret(context.Background(), fc, "test-pg", "my-other-svc.ts.net")
expectReconciled(t, ingPGR, "default", "my-other-ingress")
verifyServeConfig(t, fc, "svc:my-other-svc", false)
verifyVIPService(t, ft, "svc:my-other-svc", []string{"443"})
// Verify that Role and RoleBinding have been created for the first Ingress.
// Do not verify the cert Secret as that was already verified implicitly above.
// Verify cert resources were created for the second Ingress
expectEqual(t, fc, certSecret("test-pg", "operator-ns", "my-other-svc.ts.net"))
expectEqual(t, fc, certSecretRole("test-pg", "operator-ns", "my-other-svc.ts.net"))
expectEqual(t, fc, certSecretRoleBinding("test-pg", "operator-ns", "my-other-svc.ts.net"))
@@ -236,7 +231,7 @@ func TestIngressPGReconciler_UpdateIngressHostname(t *testing.T) {
},
},
TLS: []networkingv1.IngressTLS{
{Hosts: []string{"my-svc"}},
{Hosts: []string{"my-svc.tailnetxyz.ts.net"}},
},
},
}
@@ -244,19 +239,15 @@ func TestIngressPGReconciler_UpdateIngressHostname(t *testing.T) {
// Verify initial reconciliation
expectReconciled(t, ingPGR, "default", "test-ingress")
populateTLSSecret(context.Background(), fc, "test-pg", "my-svc.ts.net")
expectReconciled(t, ingPGR, "default", "test-ingress")
verifyServeConfig(t, fc, "svc:my-svc", false)
verifyVIPService(t, ft, "svc:my-svc", []string{"443"})
verifyTailscaledConfig(t, fc, []string{"svc:my-svc"})
// Update the Ingress hostname and make sure the original VIPService is deleted.
mustUpdate(t, fc, "default", "test-ingress", func(ing *networkingv1.Ingress) {
ing.Spec.TLS[0].Hosts[0] = "updated-svc"
ing.Spec.TLS[0].Hosts[0] = "updated-svc.tailnetxyz.ts.net"
})
expectReconciled(t, ingPGR, "default", "test-ingress")
populateTLSSecret(context.Background(), fc, "test-pg", "updated-svc.ts.net")
expectReconciled(t, ingPGR, "default", "test-ingress")
verifyServeConfig(t, fc, "svc:updated-svc", false)
verifyVIPService(t, ft, "svc:updated-svc", []string{"443"})
verifyTailscaledConfig(t, fc, []string{"svc:updated-svc"})
@@ -477,8 +468,6 @@ func TestIngressPGReconciler_HTTPEndpoint(t *testing.T) {
// Verify initial reconciliation with HTTP enabled
expectReconciled(t, ingPGR, "default", "test-ingress")
populateTLSSecret(context.Background(), fc, "test-pg", "my-svc.ts.net")
expectReconciled(t, ingPGR, "default", "test-ingress")
verifyVIPService(t, ft, "svc:my-svc", []string{"80", "443"})
verifyServeConfig(t, fc, "svc:my-svc", true)
@@ -622,7 +611,6 @@ func verifyServeConfig(t *testing.T, fc client.Client, serviceName string, wantH
}
func verifyTailscaledConfig(t *testing.T, fc client.Client, expectedServices []string) {
t.Helper()
var expected string
if expectedServices != nil {
expectedServicesJSON, err := json.Marshal(expectedServices)
@@ -816,28 +804,3 @@ func TestIngressPGReconciler_MultiCluster(t *testing.T) {
t.Errorf("incorrect owner refs after deletion\ngot: %+v\nwant: %+v", o.OwnerRefs, wantOwnerRefs)
}
}
func populateTLSSecret(ctx context.Context, c client.Client, pgName, domain string) error {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: domain,
Namespace: "operator-ns",
Labels: map[string]string{
kubetypes.LabelManaged: "true",
labelProxyGroup: pgName,
labelDomain: domain,
kubetypes.LabelSecretType: "certs",
},
},
Type: corev1.SecretTypeTLS,
Data: map[string][]byte{
corev1.TLSCertKey: []byte("fake-cert"),
corev1.TLSPrivateKeyKey: []byte("fake-key"),
},
}
_, err := createOrUpdate(ctx, c, "operator-ns", secret, func(s *corev1.Secret) {
s.Data = secret.Data
})
return err
}

View File

@@ -9,6 +9,7 @@ package main
import (
"context"
"fmt"
"net/http"
"os"
"regexp"
@@ -39,6 +40,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"tailscale.com/client/local"
"tailscale.com/client/tailscale"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
@@ -331,6 +333,40 @@ func runReconcilers(opts reconcilerOpts) {
if err != nil {
startlog.Fatalf("could not create ingress reconciler: %v", err)
}
lc, err := opts.tsServer.LocalClient()
if err != nil {
startlog.Fatalf("could not get local client: %v", err)
}
id, err := id(context.Background(), lc)
if err != nil {
startlog.Fatalf("error determining stable ID of the operator's Tailscale device: %v", err)
}
ingressProxyGroupFilter := handler.EnqueueRequestsFromMapFunc(ingressesFromIngressProxyGroup(mgr.GetClient(), opts.log))
err = builder.
ControllerManagedBy(mgr).
For(&networkingv1.Ingress{}).
Named("ingress-pg-reconciler").
Watches(&corev1.Service{}, handler.EnqueueRequestsFromMapFunc(serviceHandlerForIngressPG(mgr.GetClient(), startlog))).
Watches(&corev1.Secret{}, handler.EnqueueRequestsFromMapFunc(ingressesFromPGStateSecret(mgr.GetClient(), startlog))).
Watches(&tsapi.ProxyGroup{}, ingressProxyGroupFilter).
Complete(&HAIngressReconciler{
recorder: eventRecorder,
tsClient: opts.tsClient,
tsnetServer: opts.tsServer,
defaultTags: strings.Split(opts.proxyTags, ","),
Client: mgr.GetClient(),
logger: opts.log.Named("ingress-pg-reconciler"),
lc: lc,
operatorID: id,
tsNamespace: opts.tailscaleNamespace,
})
if err != nil {
startlog.Fatalf("could not create ingress-pg-reconciler: %v", err)
}
if err := mgr.GetFieldIndexer().IndexField(context.Background(), new(networkingv1.Ingress), indexIngressProxyGroup, indexPGIngresses); err != nil {
startlog.Fatalf("failed setting up indexer for HA Ingresses: %v", err)
}
connectorFilter := handler.EnqueueRequestsFromMapFunc(managedResourceHandlerForType("connector"))
// If a ProxyClassChanges, enqueue all Connectors that have
// .spec.proxyClass set to the name of this ProxyClass.
@@ -1003,6 +1039,45 @@ func reconcileRequestsForPG(pg string, cl client.Client, ns string) []reconcile.
return reqs
}
func ingressesFromPGStateSecret(cl client.Client, logger *zap.SugaredLogger) handler.MapFunc {
return func(ctx context.Context, o client.Object) []reconcile.Request {
secret, ok := o.(*corev1.Secret)
if !ok {
logger.Infof("[unexpected] ProxyGroup handler triggered for an object that is not a ProxyGroup")
return nil
}
if secret.ObjectMeta.Labels[kubetypes.LabelManaged] != "true" {
return nil
}
if secret.ObjectMeta.Labels[LabelParentType] != "proxygroup" {
return nil
}
if secret.ObjectMeta.Labels[kubetypes.LabelSecretType] != "state" {
return nil
}
pgName, ok := secret.ObjectMeta.Labels[LabelParentName]
if !ok {
return nil
}
ingList := &networkingv1.IngressList{}
if err := cl.List(ctx, ingList, client.MatchingFields{indexIngressProxyGroup: pgName}); err != nil {
logger.Infof("error listing Ingresses, skipping a reconcile for event on Secret %s: %v", secret.Name, err)
return nil
}
reqs := make([]reconcile.Request, 0)
for _, ing := range ingList.Items {
reqs = append(reqs, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: ing.Namespace,
Name: ing.Name,
},
})
}
return reqs
}
}
// egressSvcsFromEgressProxyGroup is an event handler for egress ProxyGroups. It returns reconcile requests for all
// user-created ExternalName Services that should be exposed on this ProxyGroup.
func egressSvcsFromEgressProxyGroup(cl client.Client, logger *zap.SugaredLogger) handler.MapFunc {
@@ -1033,6 +1108,36 @@ func egressSvcsFromEgressProxyGroup(cl client.Client, logger *zap.SugaredLogger)
}
}
// ingressesFromIngressProxyGroup is an event handler for ingress ProxyGroups. It returns reconcile requests for all
// user-created Ingresses that should be exposed on this ProxyGroup.
func ingressesFromIngressProxyGroup(cl client.Client, logger *zap.SugaredLogger) handler.MapFunc {
return func(ctx context.Context, o client.Object) []reconcile.Request {
pg, ok := o.(*tsapi.ProxyGroup)
if !ok {
logger.Infof("[unexpected] ProxyGroup handler triggered for an object that is not a ProxyGroup")
return nil
}
if pg.Spec.Type != tsapi.ProxyGroupTypeIngress {
return nil
}
ingList := &networkingv1.IngressList{}
if err := cl.List(ctx, ingList, client.MatchingFields{indexIngressProxyGroup: pg.Name}); err != nil {
logger.Infof("error listing Ingresses: %v, skipping a reconcile for event on ProxyGroup %s", err, pg.Name)
return nil
}
reqs := make([]reconcile.Request, 0)
for _, svc := range ingList.Items {
reqs = append(reqs, reconcile.Request{
NamespacedName: types.NamespacedName{
Namespace: svc.Namespace,
Name: svc.Name,
},
})
}
return reqs
}
}
// epsFromExternalNameService is an event handler for ExternalName Services that define a Tailscale egress service that
// should be exposed on a ProxyGroup. It returns reconcile requests for EndpointSlices created for this Service.
func epsFromExternalNameService(cl client.Client, logger *zap.SugaredLogger, ns string) handler.MapFunc {
@@ -1153,7 +1258,63 @@ func indexEgressServices(o client.Object) []string {
return []string{o.GetAnnotations()[AnnotationProxyGroup]}
}
// indexPGIngresses adds a local index to a cached Tailscale Ingresses meant to be exposed on a ProxyGroup. The index is
// used a list filter.
func indexPGIngresses(o client.Object) []string {
if !hasProxyGroupAnnotation(o) {
return nil
}
return []string{o.GetAnnotations()[AnnotationProxyGroup]}
}
// serviceHandlerForIngressPG returns a handler for Service events that ensures that if the Service
// associated with an event is a backend Service for a tailscale Ingress with ProxyGroup annotation,
// the associated Ingress gets reconciled.
func serviceHandlerForIngressPG(cl client.Client, logger *zap.SugaredLogger) handler.MapFunc {
return func(ctx context.Context, o client.Object) []reconcile.Request {
ingList := networkingv1.IngressList{}
if err := cl.List(ctx, &ingList, client.InNamespace(o.GetNamespace())); err != nil {
logger.Debugf("error listing Ingresses: %v", err)
return nil
}
reqs := make([]reconcile.Request, 0)
for _, ing := range ingList.Items {
if ing.Spec.IngressClassName == nil || *ing.Spec.IngressClassName != tailscaleIngressClassName {
continue
}
if !hasProxyGroupAnnotation(&ing) {
continue
}
if ing.Spec.DefaultBackend != nil && ing.Spec.DefaultBackend.Service != nil && ing.Spec.DefaultBackend.Service.Name == o.GetName() {
reqs = append(reqs, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&ing)})
}
for _, rule := range ing.Spec.Rules {
if rule.HTTP == nil {
continue
}
for _, path := range rule.HTTP.Paths {
if path.Backend.Service != nil && path.Backend.Service.Name == o.GetName() {
reqs = append(reqs, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&ing)})
}
}
}
}
return reqs
}
}
func hasProxyGroupAnnotation(obj client.Object) bool {
ing := obj.(*networkingv1.Ingress)
return ing.Annotations[AnnotationProxyGroup] != ""
}
func id(ctx context.Context, lc *local.Client) (string, error) {
st, err := lc.StatusWithoutPeers(ctx)
if err != nil {
return "", fmt.Errorf("error getting tailscale status: %w", err)
}
if st.Self == nil {
return "", fmt.Errorf("unexpected: device's status does not contain node's metadata")
}
return string(st.Self.ID), nil
}

View File

@@ -94,24 +94,18 @@ func main() {
}
ignoreDstTable.Insert(pfx, true)
}
var (
v4Prefixes []netip.Prefix
numV4DNSAddrs int
)
var v4Prefixes []netip.Prefix
for _, s := range strings.Split(*v4PfxStr, ",") {
p := netip.MustParsePrefix(strings.TrimSpace(s))
if p.Masked() != p {
log.Fatalf("v4 prefix %v is not a masked prefix", p)
}
v4Prefixes = append(v4Prefixes, p)
numIPs := 1 << (32 - p.Bits())
numV4DNSAddrs += numIPs
}
if len(v4Prefixes) == 0 {
log.Fatalf("no v4 prefixes specified")
}
dnsAddr := v4Prefixes[0].Addr()
numV4DNSAddrs -= 1 // Subtract the dnsAddr allocated above.
ts := &tsnet.Server{
Hostname: *hostname,
}
@@ -159,13 +153,12 @@ func main() {
}
c := &connector{
ts: ts,
lc: lc,
dnsAddr: dnsAddr,
v4Ranges: v4Prefixes,
numV4DNSAddrs: numV4DNSAddrs,
v6ULA: ula(uint16(*siteID)),
ignoreDsts: ignoreDstTable,
ts: ts,
lc: lc,
dnsAddr: dnsAddr,
v4Ranges: v4Prefixes,
v6ULA: ula(uint16(*siteID)),
ignoreDsts: ignoreDstTable,
}
c.run(ctx)
}
@@ -184,11 +177,6 @@ type connector struct {
// v4Ranges is the list of IPv4 ranges to advertise and assign addresses from.
// These are masked prefixes.
v4Ranges []netip.Prefix
// numV4DNSAddrs is the total size of the IPv4 ranges in addresses, minus the
// dnsAddr allocation.
numV4DNSAddrs int
// v6ULA is the ULA prefix used by the app connector to assign IPv6 addresses.
v6ULA netip.Prefix
@@ -514,7 +502,6 @@ type perPeerState struct {
mu sync.Mutex
domainToAddr map[string][]netip.Addr
addrToDomain *bart.Table[string]
numV4Allocs int
}
// domainForIP returns the domain name assigned to the given IP address and
@@ -560,25 +547,17 @@ func (ps *perPeerState) isIPUsedLocked(ip netip.Addr) bool {
// unusedIPv4Locked returns an unused IPv4 address from the available ranges.
func (ps *perPeerState) unusedIPv4Locked() netip.Addr {
// All addresses have been allocated.
if ps.numV4Allocs >= ps.c.numV4DNSAddrs {
return netip.Addr{}
}
// TODO: skip ranges that have been exhausted
// TODO: implement a much more efficient algorithm for finding unused IPs,
// this is fairly crazy.
for {
for _, r := range ps.c.v4Ranges {
ip := randV4(r)
if !r.Contains(ip) {
panic("error: randV4 returned invalid address")
}
for _, r := range ps.c.v4Ranges {
ip := randV4(r)
for r.Contains(ip) {
if !ps.isIPUsedLocked(ip) && ip != ps.c.dnsAddr {
return ip
}
ip = ip.Next()
}
}
return netip.Addr{}
}
// randV4 returns a random IPv4 address within the given prefix.
@@ -604,7 +583,6 @@ func (ps *perPeerState) assignAddrsLocked(domain string) []netip.Addr {
if !v4.IsValid() {
return nil
}
ps.numV4Allocs++
as16 := ps.c.v6ULA.Addr().As16()
as4 := v4.As4()
copy(as16[12:], as4[:])

View File

@@ -1,429 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"errors"
"fmt"
"net/netip"
"slices"
"testing"
"github.com/gaissmai/bart"
"github.com/google/go-cmp/cmp"
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/tailcfg"
)
func prefixEqual(a, b netip.Prefix) bool {
return a.Bits() == b.Bits() && a.Addr() == b.Addr()
}
func TestULA(t *testing.T) {
tests := []struct {
name string
siteID uint16
expected string
}{
{"zero", 0, "fd7a:115c:a1e0:a99c:0000::/80"},
{"one", 1, "fd7a:115c:a1e0:a99c:0001::/80"},
{"max", 65535, "fd7a:115c:a1e0:a99c:ffff::/80"},
{"random", 12345, "fd7a:115c:a1e0:a99c:3039::/80"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := ula(tc.siteID)
expected := netip.MustParsePrefix(tc.expected)
if !prefixEqual(got, expected) {
t.Errorf("ula(%d) = %s; want %s", tc.siteID, got, expected)
}
})
}
}
func TestRandV4(t *testing.T) {
pfx := netip.MustParsePrefix("100.64.1.0/24")
for i := 0; i < 512; i++ {
ip := randV4(pfx)
if !pfx.Contains(ip) {
t.Errorf("randV4(%s) = %s; not contained in prefix", pfx, ip)
}
}
}
func TestDNSResponse(t *testing.T) {
tests := []struct {
name string
questions []dnsmessage.Question
addrs []netip.Addr
wantEmpty bool
wantAnswers []struct {
name string
qType dnsmessage.Type
addr netip.Addr
}
}{
{
name: "empty_request",
questions: []dnsmessage.Question{},
addrs: []netip.Addr{},
wantEmpty: false,
wantAnswers: nil,
},
{
name: "a_record",
questions: []dnsmessage.Question{
{
Name: dnsmessage.MustNewName("example.com."),
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
},
},
addrs: []netip.Addr{netip.MustParseAddr("100.64.1.5")},
wantAnswers: []struct {
name string
qType dnsmessage.Type
addr netip.Addr
}{
{
name: "example.com.",
qType: dnsmessage.TypeA,
addr: netip.MustParseAddr("100.64.1.5"),
},
},
},
{
name: "aaaa_record",
questions: []dnsmessage.Question{
{
Name: dnsmessage.MustNewName("example.com."),
Type: dnsmessage.TypeAAAA,
Class: dnsmessage.ClassINET,
},
},
addrs: []netip.Addr{netip.MustParseAddr("fd7a:115c:a1e0:a99c:0001:0505:0505:0505")},
wantAnswers: []struct {
name string
qType dnsmessage.Type
addr netip.Addr
}{
{
name: "example.com.",
qType: dnsmessage.TypeAAAA,
addr: netip.MustParseAddr("fd7a:115c:a1e0:a99c:0001:0505:0505:0505"),
},
},
},
{
name: "soa_record",
questions: []dnsmessage.Question{
{
Name: dnsmessage.MustNewName("example.com."),
Type: dnsmessage.TypeSOA,
Class: dnsmessage.ClassINET,
},
},
addrs: []netip.Addr{},
wantAnswers: nil,
},
{
name: "ns_record",
questions: []dnsmessage.Question{
{
Name: dnsmessage.MustNewName("example.com."),
Type: dnsmessage.TypeNS,
Class: dnsmessage.ClassINET,
},
},
addrs: []netip.Addr{},
wantAnswers: nil,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
req := &dnsmessage.Message{
Header: dnsmessage.Header{
ID: 1234,
},
Questions: tc.questions,
}
resp, err := dnsResponse(req, tc.addrs)
if err != nil {
t.Fatalf("dnsResponse() error = %v", err)
}
if tc.wantEmpty && len(resp) != 0 {
t.Errorf("dnsResponse() returned non-empty response when expected empty")
}
if !tc.wantEmpty && len(resp) == 0 {
t.Errorf("dnsResponse() returned empty response when expected non-empty")
}
if len(resp) > 0 {
var msg dnsmessage.Message
err = msg.Unpack(resp)
if err != nil {
t.Fatalf("Failed to unpack response: %v", err)
}
if !msg.Header.Response {
t.Errorf("Response header is not set")
}
if msg.Header.ID != req.Header.ID {
t.Errorf("Response ID = %d, want %d", msg.Header.ID, req.Header.ID)
}
if len(tc.wantAnswers) > 0 {
if len(msg.Answers) != len(tc.wantAnswers) {
t.Errorf("got %d answers, want %d", len(msg.Answers), len(tc.wantAnswers))
} else {
for i, want := range tc.wantAnswers {
ans := msg.Answers[i]
gotName := ans.Header.Name.String()
if gotName != want.name {
t.Errorf("answer[%d] name = %s, want %s", i, gotName, want.name)
}
if ans.Header.Type != want.qType {
t.Errorf("answer[%d] type = %v, want %v", i, ans.Header.Type, want.qType)
}
var gotIP netip.Addr
switch want.qType {
case dnsmessage.TypeA:
if ans.Body.(*dnsmessage.AResource) == nil {
t.Errorf("answer[%d] not an A record", i)
continue
}
resource := ans.Body.(*dnsmessage.AResource)
gotIP = netip.AddrFrom4([4]byte(resource.A))
case dnsmessage.TypeAAAA:
if ans.Body.(*dnsmessage.AAAAResource) == nil {
t.Errorf("answer[%d] not an AAAA record", i)
continue
}
resource := ans.Body.(*dnsmessage.AAAAResource)
gotIP = netip.AddrFrom16([16]byte(resource.AAAA))
}
if gotIP != want.addr {
t.Errorf("answer[%d] IP = %s, want %s", i, gotIP, want.addr)
}
}
}
}
}
})
}
}
func TestPerPeerState(t *testing.T) {
c := &connector{
v4Ranges: []netip.Prefix{netip.MustParsePrefix("100.64.1.0/24")},
v6ULA: netip.MustParsePrefix("fd7a:115c:a1e0:a99c:0001::/80"),
dnsAddr: netip.MustParseAddr("100.64.1.0"),
numV4DNSAddrs: (1<<(32-24) - 1),
}
ps := &perPeerState{c: c}
addrs, err := ps.ipForDomain("example.com")
if err != nil {
t.Fatalf("ipForDomain() error = %v", err)
}
if len(addrs) != 2 {
t.Fatalf("ipForDomain() returned %d addresses, want 2", len(addrs))
}
v4 := addrs[0]
v6 := addrs[1]
if !v4.Is4() {
t.Errorf("First address is not IPv4: %s", v4)
}
if !v6.Is6() {
t.Errorf("Second address is not IPv6: %s", v6)
}
if !c.v4Ranges[0].Contains(v4) {
t.Errorf("IPv4 address %s not in range %s", v4, c.v4Ranges[0])
}
domain, ok := ps.domainForIP(v4)
if !ok {
t.Errorf("domainForIP(%s) not found", v4)
} else if domain != "example.com" {
t.Errorf("domainForIP(%s) = %s, want %s", v4, domain, "example.com")
}
domain, ok = ps.domainForIP(v6)
if !ok {
t.Errorf("domainForIP(%s) not found", v6)
} else if domain != "example.com" {
t.Errorf("domainForIP(%s) = %s, want %s", v6, domain, "example.com")
}
addrs2, err := ps.ipForDomain("example.com")
if err != nil {
t.Fatalf("ipForDomain() second call error = %v", err)
}
if !slices.Equal(addrs, addrs2) {
t.Errorf("ipForDomain() second call = %v, want %v", addrs2, addrs)
}
}
func TestIgnoreDestination(t *testing.T) {
ignoreDstTable := &bart.Table[bool]{}
ignoreDstTable.Insert(netip.MustParsePrefix("192.168.1.0/24"), true)
ignoreDstTable.Insert(netip.MustParsePrefix("10.0.0.0/8"), true)
c := &connector{
ignoreDsts: ignoreDstTable,
}
tests := []struct {
name string
addrs []netip.Addr
expected bool
}{
{
name: "no_match",
addrs: []netip.Addr{netip.MustParseAddr("8.8.8.8"), netip.MustParseAddr("1.1.1.1")},
expected: false,
},
{
name: "one_match",
addrs: []netip.Addr{netip.MustParseAddr("8.8.8.8"), netip.MustParseAddr("192.168.1.5")},
expected: true,
},
{
name: "all_match",
addrs: []netip.Addr{netip.MustParseAddr("10.0.0.1"), netip.MustParseAddr("192.168.1.5")},
expected: true,
},
{
name: "empty_addrs",
addrs: []netip.Addr{},
expected: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := c.ignoreDestination(tc.addrs)
if got != tc.expected {
t.Errorf("ignoreDestination(%v) = %v, want %v", tc.addrs, got, tc.expected)
}
})
}
}
func TestConnectorGenerateDNSResponse(t *testing.T) {
c := &connector{
v4Ranges: []netip.Prefix{netip.MustParsePrefix("100.64.1.0/24")},
v6ULA: netip.MustParsePrefix("fd7a:115c:a1e0:a99c:0001::/80"),
dnsAddr: netip.MustParseAddr("100.64.1.0"),
numV4DNSAddrs: (1<<(32-24) - 1),
}
req := &dnsmessage.Message{
Header: dnsmessage.Header{ID: 1234},
Questions: []dnsmessage.Question{
{
Name: dnsmessage.MustNewName("example.com."),
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
},
},
}
nodeID := tailcfg.NodeID(12345)
resp1, err := c.generateDNSResponse(req, nodeID)
if err != nil {
t.Fatalf("generateDNSResponse() error = %v", err)
}
if len(resp1) == 0 {
t.Fatalf("generateDNSResponse() returned empty response")
}
resp2, err := c.generateDNSResponse(req, nodeID)
if err != nil {
t.Fatalf("generateDNSResponse() second call error = %v", err)
}
if !cmp.Equal(resp1, resp2) {
t.Errorf("generateDNSResponse() responses differ between calls")
}
}
func TestIPPoolExhaustion(t *testing.T) {
smallPrefix := netip.MustParsePrefix("100.64.1.0/30") // Only 4 IPs: .0, .1, .2, .3
c := &connector{
v6ULA: netip.MustParsePrefix("fd7a:115c:a1e0:a99c:0001::/80"),
v4Ranges: []netip.Prefix{smallPrefix},
dnsAddr: netip.MustParseAddr("100.64.1.0"),
numV4DNSAddrs: 3,
}
ps := &perPeerState{c: c}
assignedIPs := make(map[netip.Addr]string)
domains := []string{"a.example.com", "b.example.com", "c.example.com", "d.example.com"}
var errs []error
for i := 0; i < 5; i++ {
for _, domain := range domains {
addrs, err := ps.ipForDomain(domain)
if err != nil {
errs = append(errs, fmt.Errorf("failed to get IP for domain %q: %w", domain, err))
continue
}
for _, addr := range addrs {
if d, ok := assignedIPs[addr]; ok {
if d != domain {
t.Errorf("IP %s reused for domain %q, previously assigned to %q", addr, domain, d)
}
} else {
assignedIPs[addr] = domain
}
}
}
}
for addr, domain := range assignedIPs {
if addr.Is4() && !smallPrefix.Contains(addr) {
t.Errorf("IP %s for domain %q not in expected range %s", addr, domain, smallPrefix)
}
if addr.Is6() && !c.v6ULA.Contains(addr) {
t.Errorf("IP %s for domain %q not in expected range %s", addr, domain, c.v6ULA)
}
if addr == c.dnsAddr {
t.Errorf("IP %s for domain %q is the reserved DNS address", addr, domain)
}
}
// expect one error for each iteration with the 4th domain
if len(errs) != 5 {
t.Errorf("Expected 5 errors, got %d: %v", len(errs), errs)
}
for _, err := range errs {
if !errors.Is(err, ErrNoIPsAvailable) {
t.Errorf("generateDNSResponse() error = %v, want ErrNoIPsAvailable", err)
}
}
}

View File

@@ -43,7 +43,6 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/util/eventbus"
"tailscale.com/util/must"
)
@@ -957,10 +956,7 @@ func runTS2021(ctx context.Context, args []string) error {
logf = log.Printf
}
bus := eventbus.New()
defer bus.Close()
netMon, err := netmon.New(bus, logger.WithPrefix(logf, "netmon: "))
netMon, err := netmon.New(logger.WithPrefix(logf, "netmon: "))
if err != nil {
return fmt.Errorf("creating netmon: %w", err)
}

View File

@@ -24,7 +24,6 @@ import (
"tailscale.com/net/tlsdial"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/util/eventbus"
)
var netcheckCmd = &ffcli.Command{
@@ -49,19 +48,14 @@ var netcheckArgs struct {
func runNetcheck(ctx context.Context, args []string) error {
logf := logger.WithPrefix(log.Printf, "portmap: ")
bus := eventbus.New()
defer bus.Close()
netMon, err := netmon.New(bus, logf)
netMon, err := netmon.New(logf)
if err != nil {
return err
}
// Ensure that we close the portmapper after running a netcheck; this
// will release any port mappings created.
pm := portmapper.NewClient(portmapper.Config{
Logf: logf,
NetMon: netMon,
})
pm := portmapper.NewClient(logf, netMon, nil, nil, nil)
defer pm.Close()
c := &netcheck.Client{

View File

@@ -5,10 +5,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/coder/websocket from tailscale.com/util/eventbus
github.com/coder/websocket/internal/errd from github.com/coder/websocket
github.com/coder/websocket/internal/util from github.com/coder/websocket
github.com/coder/websocket/internal/xsync from github.com/coder/websocket
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/pe+
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/winutil/authenticode
@@ -93,7 +89,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/drive from tailscale.com/client/local+
tailscale.com/envknob from tailscale.com/client/local+
tailscale.com/envknob/featureknob from tailscale.com/client/web
tailscale.com/feature from tailscale.com/tsweb
tailscale.com/feature/capture/dissector from tailscale.com/cmd/tailscale/cli
tailscale.com/health from tailscale.com/net/tlsdial+
tailscale.com/health/healthmsg from tailscale.com/cmd/tailscale/cli
@@ -136,8 +131,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/tstime from tailscale.com/control/controlhttp+
tailscale.com/tstime/mono from tailscale.com/tstime/rate
tailscale.com/tstime/rate from tailscale.com/cmd/tailscale/cli+
tailscale.com/tsweb from tailscale.com/util/eventbus
tailscale.com/tsweb/varz from tailscale.com/util/usermetric+
tailscale.com/tsweb/varz from tailscale.com/util/usermetric
tailscale.com/types/dnstype from tailscale.com/tailcfg+
tailscale.com/types/empty from tailscale.com/ipn
tailscale.com/types/ipproto from tailscale.com/ipn+
@@ -162,7 +156,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
💣 tailscale.com/util/deephash from tailscale.com/util/syspolicy/setting
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
tailscale.com/util/eventbus from tailscale.com/net/portmapper+
tailscale.com/util/groupmember from tailscale.com/client/web
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
tailscale.com/util/httpm from tailscale.com/client/tailscale+
@@ -173,7 +166,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/util/must from tailscale.com/clientupdate/distsign+
tailscale.com/util/nocasemaps from tailscale.com/types/ipproto
tailscale.com/util/quarantine from tailscale.com/cmd/tailscale/cli
tailscale.com/util/rands from tailscale.com/tsweb
tailscale.com/util/set from tailscale.com/derp+
tailscale.com/util/singleflight from tailscale.com/net/dnscache+
tailscale.com/util/slicesx from tailscale.com/net/dns/recursive+
@@ -336,7 +328,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
hash/crc32 from compress/gzip+
hash/maphash from go4.org/mem
html from html/template+
html/template from github.com/gorilla/csrf+
html/template from github.com/gorilla/csrf
image from github.com/skip2/go-qrcode+
image/color from github.com/skip2/go-qrcode+
image/png from github.com/skip2/go-qrcode
@@ -360,8 +352,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
internal/nettrace from net+
internal/oserror from io/fs+
internal/poll from net+
internal/profile from net/http/pprof
internal/profilerecord from runtime+
internal/profilerecord from runtime
internal/race from internal/poll+
internal/reflectlite from context+
internal/runtime/atomic from internal/runtime/exithook+
@@ -403,7 +394,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
net/http/httputil from tailscale.com/client/web+
net/http/internal from net/http+
net/http/internal/ascii from net/http+
net/http/pprof from tailscale.com/tsweb
net/netip from go4.org/netipx+
net/textproto from golang.org/x/net/http/httpguts+
net/url from crypto/x509+
@@ -418,8 +408,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
regexp/syntax from regexp
runtime from archive/tar+
runtime/debug from tailscale.com+
runtime/pprof from net/http/pprof
runtime/trace from net/http/pprof
slices from tailscale.com/client/web+
sort from compress/flate+
strconv from archive/tar+

View File

@@ -27,7 +27,6 @@ import (
"tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/util/eventbus"
)
var debugArgs struct {
@@ -73,14 +72,11 @@ func debugMode(args []string) error {
}
func runMonitor(ctx context.Context, loop bool) error {
b := eventbus.New()
defer b.Close()
dump := func(st *netmon.State) {
j, _ := json.MarshalIndent(st, "", " ")
os.Stderr.Write(j)
}
mon, err := netmon.New(b, log.Printf)
mon, err := netmon.New(log.Printf)
if err != nil {
return err
}

View File

@@ -81,10 +81,6 @@ 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
github.com/coder/websocket from tailscale.com/util/eventbus
github.com/coder/websocket/internal/errd from github.com/coder/websocket
github.com/coder/websocket/internal/util from github.com/coder/websocket
github.com/coder/websocket/internal/xsync from github.com/coder/websocket
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
LD 💣 github.com/creack/pty from tailscale.com/ssh/tailssh
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com+
@@ -357,7 +353,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/tstime from tailscale.com/control/controlclient+
tailscale.com/tstime/mono from tailscale.com/net/tstun+
tailscale.com/tstime/rate from tailscale.com/derp+
tailscale.com/tsweb from tailscale.com/util/eventbus
tailscale.com/tsweb/varz from tailscale.com/cmd/tailscaled+
tailscale.com/types/appctype from tailscale.com/ipn/ipnlocal
tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+
@@ -387,7 +382,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics+
tailscale.com/util/dnsname from tailscale.com/appc+
tailscale.com/util/eventbus from tailscale.com/tsd+
tailscale.com/util/execqueue from tailscale.com/control/controlclient+
tailscale.com/util/goroutines from tailscale.com/ipn/ipnlocal
tailscale.com/util/groupmember from tailscale.com/client/web+
@@ -593,7 +587,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
hash/crc32 from compress/gzip+
hash/maphash from go4.org/mem
html from html/template+
html/template from github.com/gorilla/csrf+
html/template from github.com/gorilla/csrf
internal/abi from crypto/x509/internal/macos+
internal/asan from internal/runtime/maps+
internal/bisect from internal/godebug

View File

@@ -339,9 +339,7 @@ var debugMux *http.ServeMux
func run() (err error) {
var logf logger.Logf = log.Printf
// Install an event bus as early as possible, so that it's
// available universally when setting up everything else.
sys := tsd.NewSystem()
sys := new(tsd.System)
// Parse config, if specified, to fail early if it's invalid.
var conf *conffile.Config
@@ -356,7 +354,9 @@ func run() (err error) {
var netMon *netmon.Monitor
isWinSvc := isWindowsService()
if !isWinSvc {
netMon, err = netmon.New(sys.Bus.Get(), logf)
netMon, err = netmon.New(func(format string, args ...any) {
logf(format, args...)
})
if err != nil {
return fmt.Errorf("netmon.New: %w", err)
}

View File

@@ -327,8 +327,8 @@ func beWindowsSubprocess() bool {
log.Printf("Error pre-loading \"%s\": %v", fqWintunPath, err)
}
sys := tsd.NewSystem()
netMon, err := netmon.New(sys.Bus.Get(), log.Printf)
sys := new(tsd.System)
netMon, err := netmon.New(log.Printf)
if err != nil {
log.Fatalf("Could not create netMon: %v", err)
}

View File

@@ -100,7 +100,7 @@ func newIPN(jsConfig js.Value) map[string]any {
logtail := logtail.NewLogger(c, log.Printf)
logf := logtail.Logf
sys := tsd.NewSystem()
sys := new(tsd.System)
sys.Set(store)
dialer := &tsdial.Dialer{Logf: logf}
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{

View File

@@ -96,9 +96,6 @@ func (a *Dialer) httpsFallbackDelay() time.Duration {
var _ = envknob.RegisterBool("TS_USE_CONTROL_DIAL_PLAN") // to record at init time whether it's in use
func (a *Dialer) dial(ctx context.Context) (*ClientConn, error) {
a.logPort80Failure.Store(true)
// If we don't have a dial plan, just fall back to dialing the single
// host we know about.
useDialPlan := envknob.BoolDefaultTrue("TS_USE_CONTROL_DIAL_PLAN")
@@ -281,9 +278,7 @@ func (d *Dialer) forceNoise443() bool {
// This heuristic works around networks where port 80 is MITMed and
// appears to work for a bit post-Upgrade but then gets closed,
// such as seen in https://github.com/tailscale/tailscale/issues/13597.
if d.logPort80Failure.CompareAndSwap(true, false) {
d.logf("controlhttp: forcing port 443 dial due to recent noise dial")
}
d.logf("controlhttp: forcing port 443 dial due to recent noise dial")
return true
}

View File

@@ -6,7 +6,6 @@ package controlhttp
import (
"net/http"
"net/url"
"sync/atomic"
"time"
"tailscale.com/health"
@@ -91,11 +90,6 @@ type Dialer struct {
proxyFunc func(*http.Request) (*url.URL, error) // or nil
// logPort80Failure is whether we should log about port 80 interceptions
// and forcing a port 443 dial. We do this only once per "dial" method
// which can result in many concurrent racing dialHost calls.
logPort80Failure atomic.Bool
// For tests only
drainFinished chan struct{}
omitCertErrorLogging bool

View File

@@ -32,6 +32,7 @@ import (
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/tstest/deptest"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/logger"
@@ -821,3 +822,14 @@ func (c *closeTrackConn) Close() error {
c.d.noteClose(c)
return c.Conn.Close()
}
func TestDeps(t *testing.T) {
deptest.DepChecker{
GOOS: "darwin",
GOARCH: "arm64",
BadDeps: map[string]string{
// Only the controlhttpserver needs WebSockets...
"github.com/coder/websocket": "controlhttp client shouldn't need websockets",
},
}.Check(t)
}

View File

@@ -17,7 +17,9 @@ import (
"tailscale.com/derp"
"tailscale.com/net/netmon"
"tailscale.com/tstest/deptest"
"tailscale.com/types/key"
"tailscale.com/util/set"
)
func TestSendRecv(t *testing.T) {
@@ -485,3 +487,23 @@ func TestProbe(t *testing.T) {
}
}
}
func TestDeps(t *testing.T) {
deptest.DepChecker{
GOOS: "darwin",
GOARCH: "arm64",
BadDeps: map[string]string{
"github.com/coder/websocket": "shouldn't link websockets except on js/wasm",
},
}.Check(t)
deptest.DepChecker{
GOOS: "darwin",
GOARCH: "arm64",
Tags: "ts_debug_websockets",
WantDeps: set.Of(
"github.com/coder/websocket",
),
}.Check(t)
}

View File

@@ -958,9 +958,7 @@ func (b *LocalBackend) linkChange(delta *netmon.ChangeDelta) {
if peerAPIListenAsync && b.netMap != nil && b.state == ipn.Running {
want := b.netMap.GetAddresses().Len()
have := len(b.peerAPIListeners)
b.logf("[v1] linkChange: have %d peerAPIListeners, want %d", have, want)
if have < want {
if len(b.peerAPIListeners) < want {
b.logf("linkChange: peerAPIListeners too low; trying again")
b.goTracker.Go(b.initPeerAPIListener)
}
@@ -2404,9 +2402,11 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
}
var auditLogShutdown func()
// Audit logging is only available if the client has set up a proper persistent
// store for the logs in sys.
store, ok := b.sys.AuditLogStore.GetOK()
if !ok {
// Use memory store by default if no explicit store is provided.
b.logf("auditlog: [unexpected] no persistent audit log storage configured. using memory store.")
store = auditlog.NewLogStore(&memstore.Store{})
}
@@ -4968,7 +4968,7 @@ func (b *LocalBackend) authReconfig() {
return
}
oneCGNATRoute := shouldUseOneCGNATRoute(b.logf, b.sys.NetMon.Get(), b.sys.ControlKnobs(), version.OS())
oneCGNATRoute := shouldUseOneCGNATRoute(b.logf, b.sys.ControlKnobs(), version.OS())
rcfg := b.routerConfig(cfg, prefs, oneCGNATRoute)
err = b.e.Reconfig(cfg, rcfg, dcfg)
@@ -4992,7 +4992,7 @@ func (b *LocalBackend) authReconfig() {
//
// The versionOS is a Tailscale-style version ("iOS", "macOS") and not
// a runtime.GOOS.
func shouldUseOneCGNATRoute(logf logger.Logf, mon *netmon.Monitor, controlKnobs *controlknobs.Knobs, versionOS string) bool {
func shouldUseOneCGNATRoute(logf logger.Logf, controlKnobs *controlknobs.Knobs, versionOS string) bool {
if controlKnobs != nil {
// Explicit enabling or disabling always take precedence.
if v, ok := controlKnobs.OneCGNAT.Load().Get(); ok {
@@ -5007,7 +5007,7 @@ func shouldUseOneCGNATRoute(logf logger.Logf, mon *netmon.Monitor, controlKnobs
// use fine-grained routes if another interfaces is also using the CGNAT
// IP range.
if versionOS == "macOS" {
hasCGNATInterface, err := mon.HasCGNATInterface()
hasCGNATInterface, err := netmon.HasCGNATInterface()
if err != nil {
logf("shouldUseOneCGNATRoute: Could not determine if any interfaces use CGNAT: %v", err)
return false
@@ -5369,7 +5369,6 @@ func (b *LocalBackend) initPeerAPIListener() {
ln, err = ps.listen(a.Addr(), b.prevIfState)
if err != nil {
if peerAPIListenAsync {
b.logf("possibly transient peerapi listen(%q) error, will try again on linkChange: %v", a.Addr(), err)
// Expected. But we fix it later in linkChange
// ("peerAPIListeners too low").
continue

View File

@@ -436,7 +436,7 @@ func (panicOnUseTransport) RoundTrip(*http.Request) (*http.Response, error) {
}
func newTestLocalBackend(t testing.TB) *LocalBackend {
return newTestLocalBackendWithSys(t, tsd.NewSystem())
return newTestLocalBackendWithSys(t, new(tsd.System))
}
// newTestLocalBackendWithSys creates a new LocalBackend with the given tsd.System.
@@ -448,7 +448,7 @@ func newTestLocalBackendWithSys(t testing.TB, sys *tsd.System) *LocalBackend {
sys.Set(new(mem.Store))
}
if _, ok := sys.Engine.GetOK(); !ok {
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry(), sys.Bus.Get())
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
if err != nil {
t.Fatalf("NewFakeUserspaceEngine: %v", err)
}
@@ -4411,10 +4411,10 @@ func newLocalBackendWithTestControl(t *testing.T, enableLogging bool, newControl
if enableLogging {
logf = tstest.WhileTestRunningLogger(t)
}
sys := tsd.NewSystem()
sys := new(tsd.System)
store := new(mem.Store)
sys.Set(store)
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry(), sys.Bus.Get())
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
if err != nil {
t.Fatalf("NewFakeUserspaceEngine: %v", err)
}
@@ -4859,8 +4859,9 @@ func TestConfigFileReload(t *testing.T) {
// Create backend with initial config
tc.initial.Path = path
tc.initial.Raw = initialJSON
sys := tsd.NewSystem()
sys.InitialConfig = tc.initial
sys := &tsd.System{
InitialConfig: tc.initial,
}
b := newTestLocalBackendWithSys(t, sys)
// Update config file

View File

@@ -47,10 +47,10 @@ func TestLocalLogLines(t *testing.T) {
idA := logid(0xaa)
// set up a LocalBackend, super bare bones. No functional data.
sys := tsd.NewSystem()
sys := new(tsd.System)
store := new(mem.Store)
sys.Set(store)
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry(), sys.Bus.Get())
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
if err != nil {
t.Fatal(err)
}

View File

@@ -481,7 +481,7 @@ func (h *peerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Re
fmt.Fprintf(w, "<h3>Could not get the default route: %s</h3>\n", html.EscapeString(err.Error()))
}
if hasCGNATInterface, err := h.ps.b.sys.NetMon.Get().HasCGNATInterface(); hasCGNATInterface {
if hasCGNATInterface, err := netmon.HasCGNATInterface(); hasCGNATInterface {
fmt.Fprintln(w, "<p>There is another interface using the CGNAT range.</p>")
} else if err != nil {
fmt.Fprintf(w, "<p>Could not check for CGNAT interfaces: %s</p>\n", html.EscapeString(err.Error()))

View File

@@ -34,7 +34,6 @@ import (
"tailscale.com/tstest"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
"tailscale.com/util/eventbus"
"tailscale.com/util/must"
"tailscale.com/util/usermetric"
"tailscale.com/wgengine"
@@ -644,12 +643,9 @@ func TestPeerAPIReplyToDNSQueries(t *testing.T) {
h.isSelf = false
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
bus := eventbus.New()
defer bus.Close()
ht := new(health.Tracker)
reg := new(usermetric.Registry)
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg, bus)
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg)
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
h.ps = &peerAPIServer{
b: &LocalBackend{
@@ -699,12 +695,9 @@ func TestPeerAPIPrettyReplyCNAME(t *testing.T) {
var h peerAPIHandler
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
bus := eventbus.New()
defer bus.Close()
ht := new(health.Tracker)
reg := new(usermetric.Registry)
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg, bus)
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg)
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
var a *appc.AppConnector
if shouldStore {
@@ -775,12 +768,10 @@ func TestPeerAPIReplyToDNSQueriesAreObserved(t *testing.T) {
var h peerAPIHandler
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
bus := eventbus.New()
defer bus.Close()
rc := &appctest.RouteCollector{}
ht := new(health.Tracker)
reg := new(usermetric.Registry)
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg, bus)
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg)
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
var a *appc.AppConnector
if shouldStore {
@@ -842,12 +833,10 @@ func TestPeerAPIReplyToDNSQueriesAreObservedWithCNAMEFlattening(t *testing.T) {
var h peerAPIHandler
h.remoteAddr = netip.MustParseAddrPort("100.150.151.152:12345")
bus := eventbus.New()
defer bus.Close()
ht := new(health.Tracker)
reg := new(usermetric.Registry)
rc := &appctest.RouteCollector{}
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg, bus)
eng, _ := wgengine.NewFakeUserspaceEngine(logger.Discard, 0, ht, reg)
pm := must.Get(newProfileManager(new(mem.Store), t.Logf, ht))
var a *appc.AppConnector
if shouldStore {

View File

@@ -877,12 +877,11 @@ func newTestBackend(t *testing.T) *LocalBackend {
logf = logger.WithPrefix(tstest.WhileTestRunningLogger(t), "... ")
}
sys := tsd.NewSystem()
sys := &tsd.System{}
e, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
SetSubsystem: sys.Set,
HealthTracker: sys.HealthTracker(),
Metrics: sys.UserMetricsRegistry(),
EventBus: sys.Bus.Get(),
})
if err != nil {
t.Fatal(err)

View File

@@ -295,10 +295,10 @@ func TestStateMachine(t *testing.T) {
c := qt.New(t)
logf := tstest.WhileTestRunningLogger(t)
sys := tsd.NewSystem()
sys := new(tsd.System)
store := new(testStateStorage)
sys.Set(store)
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry(), sys.Bus.Get())
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
if err != nil {
t.Fatalf("NewFakeUserspaceEngine: %v", err)
}
@@ -934,9 +934,9 @@ func TestStateMachine(t *testing.T) {
func TestEditPrefsHasNoKeys(t *testing.T) {
logf := tstest.WhileTestRunningLogger(t)
sys := tsd.NewSystem()
sys := new(tsd.System)
sys.Set(new(mem.Store))
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry(), sys.Bus.Get())
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
if err != nil {
t.Fatalf("NewFakeUserspaceEngine: %v", err)
}
@@ -1014,10 +1014,10 @@ func TestWGEngineStatusRace(t *testing.T) {
t.Skip("test fails")
c := qt.New(t)
logf := tstest.WhileTestRunningLogger(t)
sys := tsd.NewSystem()
sys := new(tsd.System)
sys.Set(new(mem.Store))
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.Bus.Get())
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set)
c.Assert(err, qt.IsNil)
t.Cleanup(eng.Close)
sys.Set(eng)

View File

@@ -517,12 +517,12 @@ type newControlClientFn func(tb testing.TB, opts controlclient.Options) controlc
func newLocalBackendWithTestControl(tb testing.TB, newControl newControlClientFn, enableLogging bool) *ipnlocal.LocalBackend {
tb.Helper()
sys := tsd.NewSystem()
sys := &tsd.System{}
store := &mem.Store{}
sys.Set(store)
logf := testLogger(tb, enableLogging)
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry(), sys.Bus.Get())
e, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
if err != nil {
tb.Fatalf("NewFakeUserspaceEngine: %v", err)
}

View File

@@ -56,7 +56,6 @@ import (
"tailscale.com/types/ptr"
"tailscale.com/types/tkatype"
"tailscale.com/util/clientmetric"
"tailscale.com/util/eventbus"
"tailscale.com/util/httphdr"
"tailscale.com/util/httpm"
"tailscale.com/util/mak"
@@ -819,31 +818,23 @@ func (h *Handler) serveDebugPortmap(w http.ResponseWriter, r *http.Request) {
done := make(chan bool, 1)
var c *portmapper.Client
c = portmapper.NewClient(portmapper.Config{
Logf: logger.WithPrefix(logf, "portmapper: "),
NetMon: h.b.NetMon(),
DebugKnobs: debugKnobs,
ControlKnobs: h.b.ControlKnobs(),
OnChange: func() {
logf("portmapping changed.")
logf("have mapping: %v", c.HaveMapping())
c = portmapper.NewClient(logger.WithPrefix(logf, "portmapper: "), h.b.NetMon(), debugKnobs, h.b.ControlKnobs(), func() {
logf("portmapping changed.")
logf("have mapping: %v", c.HaveMapping())
if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
logf("cb: mapping: %v", ext)
select {
case done <- true:
default:
}
return
if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
logf("cb: mapping: %v", ext)
select {
case done <- true:
default:
}
logf("cb: no mapping")
},
return
}
logf("cb: no mapping")
})
defer c.Close()
bus := eventbus.New()
defer bus.Close()
netMon, err := netmon.New(bus, logger.WithPrefix(logf, "monitor: "))
netMon, err := netmon.New(logger.WithPrefix(logf, "monitor: "))
if err != nil {
logf("error creating monitor: %v", err)
return

View File

@@ -336,10 +336,10 @@ func TestServeWatchIPNBus(t *testing.T) {
func newTestLocalBackend(t testing.TB) *ipnlocal.LocalBackend {
var logf logger.Logf = logger.Discard
sys := tsd.NewSystem()
sys := new(tsd.System)
store := new(mem.Store)
sys.Set(store)
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry(), sys.Bus.Get())
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
if err != nil {
t.Fatalf("NewFakeUserspaceEngine: %v", err)
}

View File

@@ -143,6 +143,15 @@ func (s *Store) WriteTLSCertAndKey(domain string, cert, key []byte) (err error)
if err := dnsname.ValidHostname(domain); err != nil {
return fmt.Errorf("invalid domain name %q: %w", domain, err)
}
defer func() {
// TODO(irbekrm): a read between these two separate writes would
// get a mismatched cert and key. Allow writing both cert and
// key to the memory store in a single, lock-protected operation.
if err == nil {
s.memory.WriteState(ipn.StateKey(domain+".crt"), cert)
s.memory.WriteState(ipn.StateKey(domain+".key"), key)
}
}()
secretName := s.secretName
data := map[string][]byte{
domain + ".crt": cert,
@@ -157,32 +166,19 @@ func (s *Store) WriteTLSCertAndKey(domain string, cert, key []byte) (err error)
keyTLSKey: key,
}
}
if err := s.updateSecret(data, secretName); err != nil {
return fmt.Errorf("error writing TLS cert and key to Secret: %w", err)
}
// TODO(irbekrm): certs for write replicas are currently not
// written to memory to avoid out of sync memory state after
// Ingress resources have been recreated. This means that TLS
// certs for write replicas are retrieved from the Secret on
// each HTTPS request. This is a temporary solution till we
// implement a Secret watch.
if s.certShareMode != "rw" {
s.memory.WriteState(ipn.StateKey(domain+".crt"), cert)
s.memory.WriteState(ipn.StateKey(domain+".key"), key)
}
return nil
return s.updateSecret(data, secretName)
}
// ReadTLSCertAndKey reads a TLS cert and key from memory or from a
// domain-specific Secret. It first checks the in-memory store, if not found in
// memory and running cert store in read-only mode, looks up a Secret.
// Note that write replicas of HA Ingress always retrieve TLS certs from Secrets.
func (s *Store) ReadTLSCertAndKey(domain string) (cert, key []byte, err error) {
if err := dnsname.ValidHostname(domain); err != nil {
return nil, nil, fmt.Errorf("invalid domain name %q: %w", domain, err)
}
certKey := domain + ".crt"
keyKey := domain + ".key"
cert, err = s.memory.ReadState(ipn.StateKey(certKey))
if err == nil {
key, err = s.memory.ReadState(ipn.StateKey(keyKey))
@@ -190,12 +186,16 @@ func (s *Store) ReadTLSCertAndKey(domain string) (cert, key []byte, err error) {
return cert, key, nil
}
}
if s.certShareMode == "" {
if s.certShareMode != "ro" {
return nil, nil, ipn.ErrStateNotExist
}
// If we are in cert share read only mode, it is possible that a write
// replica just issued the TLS cert for this DNS name and it has not
// been loaded to store yet, so check the Secret.
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
secret, err := s.client.GetSecret(ctx, domain)
if err != nil {
if kubeclient.IsNotFoundErr(err) {
@@ -212,18 +212,9 @@ func (s *Store) ReadTLSCertAndKey(domain string) (cert, key []byte, err error) {
}
// TODO(irbekrm): a read between these two separate writes would
// get a mismatched cert and key. Allow writing both cert and
// key to the memory store in a single, lock-protected operation.
//
// TODO(irbekrm): currently certs for write replicas of HA Ingress get
// retrieved from the cluster Secret on each HTTPS request to avoid a
// situation when after Ingress recreation stale certs are read from
// memory.
// Fix this by watching Secrets to ensure that memory store gets updated
// when Secrets are deleted.
if s.certShareMode == "ro" {
s.memory.WriteState(ipn.StateKey(certKey), cert)
s.memory.WriteState(ipn.StateKey(keyKey), key)
}
// key to the memory store in a single lock-protected operation.
s.memory.WriteState(ipn.StateKey(certKey), cert)
s.memory.WriteState(ipn.StateKey(keyKey), key)
return cert, key, nil
}

View File

@@ -201,6 +201,10 @@ func TestWriteTLSCertAndKey(t *testing.T) {
"tls.crt": []byte(testCert),
"tls.key": []byte(testKey),
},
wantMemoryStore: map[ipn.StateKey][]byte{
"my-app.tailnetxyz.ts.net.crt": []byte(testCert),
"my-app.tailnetxyz.ts.net.key": []byte(testKey),
},
},
{
name: "cert_share_mode_write_update_existing",
@@ -215,6 +219,10 @@ func TestWriteTLSCertAndKey(t *testing.T) {
"tls.crt": []byte(testCert),
"tls.key": []byte(testKey),
},
wantMemoryStore: map[ipn.StateKey][]byte{
"my-app.tailnetxyz.ts.net.crt": []byte(testCert),
"my-app.tailnetxyz.ts.net.key": []byte(testKey),
},
},
{
name: "update_existing",
@@ -359,7 +367,7 @@ func TestReadTLSCertAndKey(t *testing.T) {
wantMemoryStore map[ipn.StateKey][]byte
}{
{
name: "found_in_memory",
name: "found",
memoryStore: map[ipn.StateKey][]byte{
"my-app.tailnetxyz.ts.net.crt": []byte(testCert),
"my-app.tailnetxyz.ts.net.key": []byte(testKey),
@@ -373,7 +381,7 @@ func TestReadTLSCertAndKey(t *testing.T) {
},
},
{
name: "not_found_in_memory",
name: "not_found",
domain: testDomain,
wantErr: ipn.ErrStateNotExist,
},
@@ -392,17 +400,6 @@ func TestReadTLSCertAndKey(t *testing.T) {
"my-app.tailnetxyz.ts.net.key": []byte(testKey),
},
},
{
name: "cert_share_rw_mode_found_in_secret",
certShareMode: "rw",
domain: testDomain,
secretData: map[string][]byte{
"tls.crt": []byte(testCert),
"tls.key": []byte(testKey),
},
wantCert: []byte(testCert),
wantKey: []byte(testKey),
},
{
name: "cert_share_ro_mode_found_in_memory",
certShareMode: "ro",

View File

@@ -600,7 +600,7 @@ _Appears in:_
| Field | Description | Default | Validation |
| --- | --- | --- | --- |
| `type` _[ProxyGroupType](#proxygrouptype)_ | Type of the ProxyGroup proxies. Currently the only supported type is egress.<br />Type is immutable once a ProxyGroup is created. | | Enum: [egress ingress] <br />Type: string <br /> |
| `type` _[ProxyGroupType](#proxygrouptype)_ | Type of the ProxyGroup proxies. Supported types are egress and ingress.<br />Type is immutable once a ProxyGroup is created. | | Enum: [egress ingress] <br />Type: string <br /> |
| `tags` _[Tags](#tags)_ | Tags that the Tailscale devices will be tagged with. Defaults to [tag:k8s].<br />If you specify custom tags here, make sure you also make the operator<br />an owner of these tags.<br />See https://tailscale.com/kb/1236/kubernetes-operator/#setting-up-the-kubernetes-operator.<br />Tags cannot be changed once a ProxyGroup device has been created.<br />Tag values must be in form ^tag:[a-zA-Z][a-zA-Z0-9-]*$. | | Pattern: `^tag:[a-zA-Z][a-zA-Z0-9-]*$` <br />Type: string <br /> |
| `replicas` _integer_ | Replicas specifies how many replicas to create the StatefulSet with.<br />Defaults to 2. | | Minimum: 0 <br /> |
| `hostnamePrefix` _[HostnamePrefix](#hostnameprefix)_ | HostnamePrefix is the hostname prefix to use for tailnet devices created<br />by the ProxyGroup. Each device will have the integer number from its<br />StatefulSet pod appended to this prefix to form the full hostname.<br />HostnamePrefix can contain lower case letters, numbers and dashes, it<br />must not start with a dash and must be between 1 and 62 characters long. | | Pattern: `^[a-z0-9][a-z0-9-]{0,61}$` <br />Type: string <br /> |

View File

@@ -48,7 +48,7 @@ type ProxyGroupList struct {
}
type ProxyGroupSpec struct {
// Type of the ProxyGroup proxies. Currently the only supported type is egress.
// Type of the ProxyGroup proxies. Supported types are egress and ingress.
// Type is immutable once a ProxyGroup is created.
// +kubebuilder:validation:XValidation:rule="self == oldSelf",message="ProxyGroup type is immutable"
Type ProxyGroupType `json:"type"`

View File

@@ -29,7 +29,6 @@ import (
"tailscale.com/net/tsdial"
"tailscale.com/tstest"
"tailscale.com/types/dnstype"
"tailscale.com/util/eventbus"
)
func (rr resolverAndDelay) String() string {
@@ -455,9 +454,7 @@ func makeLargeResponse(tb testing.TB, domain string) (request, response []byte)
func runTestQuery(tb testing.TB, request []byte, modify func(*forwarder), ports ...uint16) ([]byte, error) {
logf := tstest.WhileTestRunningLogger(tb)
bus := eventbus.New()
defer bus.Close()
netMon, err := netmon.New(bus, logf)
netMon, err := netmon.New(logf)
if err != nil {
tb.Fatal(err)
}

View File

@@ -31,7 +31,6 @@ import (
"tailscale.com/types/dnstype"
"tailscale.com/types/logger"
"tailscale.com/util/dnsname"
"tailscale.com/util/eventbus"
)
var (
@@ -1060,10 +1059,7 @@ func TestForwardLinkSelection(t *testing.T) {
// routes differently.
specialIP := netaddr.IPv4(1, 2, 3, 4)
bus := eventbus.New()
defer bus.Close()
netMon, err := netmon.New(bus, logger.WithPrefix(t.Logf, ".... netmon: "))
netMon, err := netmon.New(logger.WithPrefix(t.Logf, ".... netmon: "))
if err != nil {
t.Fatal(err)
}

View File

@@ -15,7 +15,6 @@ import (
"tailscale.com/net/netmon"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/util/eventbus"
)
func TestGetDERPMap(t *testing.T) {
@@ -186,10 +185,7 @@ func TestLookup(t *testing.T) {
logf, closeLogf := logger.LogfCloser(t.Logf)
defer closeLogf()
bus := eventbus.New()
defer bus.Close()
netMon, err := netmon.New(bus, logf)
netMon, err := netmon.New(logf)
if err != nil {
t.Fatal(err)
}

View File

@@ -13,7 +13,7 @@ import (
)
func TestGetState(t *testing.T) {
st, err := getState("")
st, err := getState()
if err != nil {
t.Fatal(err)
}

View File

@@ -7,14 +7,10 @@ import (
"bytes"
"fmt"
"testing"
"tailscale.com/util/eventbus"
)
func TestLinkChangeLogLimiter(t *testing.T) {
bus := eventbus.New()
defer bus.Close()
mon, err := New(bus, t.Logf)
mon, err := New(t.Logf)
if err != nil {
t.Fatal(err)
}

View File

@@ -16,7 +16,6 @@ import (
"tailscale.com/types/logger"
"tailscale.com/util/clientmetric"
"tailscale.com/util/eventbus"
"tailscale.com/util/set"
)
@@ -51,10 +50,7 @@ type osMon interface {
// Monitor represents a monitoring instance.
type Monitor struct {
logf logger.Logf
b *eventbus.Client
changed *eventbus.Publisher[*ChangeDelta]
logf logger.Logf
om osMon // nil means not supported on this platform
change chan bool // send false to wake poller, true to also force ChangeDeltas be sent
stop chan struct{} // closed on Stop
@@ -118,23 +114,21 @@ type ChangeDelta struct {
// New instantiates and starts a monitoring instance.
// The returned monitor is inactive until it's started by the Start method.
// Use RegisterChangeCallback to get notified of network changes.
func New(bus *eventbus.Bus, logf logger.Logf) (*Monitor, error) {
func New(logf logger.Logf) (*Monitor, error) {
logf = logger.WithPrefix(logf, "monitor: ")
m := &Monitor{
logf: logf,
b: bus.Client("netmon"),
change: make(chan bool, 1),
stop: make(chan struct{}),
lastWall: wallTime(),
}
m.changed = eventbus.Publish[*ChangeDelta](m.b)
st, err := m.interfaceStateUncached()
if err != nil {
return nil, err
}
m.ifState = st
m.om, err = newOSMon(bus, logf, m)
m.om, err = newOSMon(logf, m)
if err != nil {
return nil, err
}
@@ -167,7 +161,7 @@ func (m *Monitor) InterfaceState() *State {
}
func (m *Monitor) interfaceStateUncached() (*State, error) {
return getState(m.tsIfName)
return getState()
}
// SetTailscaleInterfaceName sets the name of the Tailscale interface. For
@@ -471,7 +465,6 @@ func (m *Monitor) handlePotentialChange(newState *State, forceCallbacks bool) {
if delta.TimeJumped {
metricChangeTimeJump.Add(1)
}
m.changed.Publish(delta)
for _, cb := range m.cbs {
go cb(delta)
}

View File

@@ -13,7 +13,6 @@ import (
"golang.org/x/sys/unix"
"tailscale.com/net/netaddr"
"tailscale.com/types/logger"
"tailscale.com/util/eventbus"
)
const debugRouteMessages = false
@@ -25,7 +24,7 @@ type unspecifiedMessage struct{}
func (unspecifiedMessage) ignore() bool { return false }
func newOSMon(_ *eventbus.Bus, logf logger.Logf, _ *Monitor) (osMon, error) {
func newOSMon(logf logger.Logf, _ *Monitor) (osMon, error) {
fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, 0)
if err != nil {
return nil, err

View File

@@ -10,7 +10,6 @@ import (
"strings"
"tailscale.com/types/logger"
"tailscale.com/util/eventbus"
)
// unspecifiedMessage is a minimal message implementation that should not
@@ -25,7 +24,7 @@ type devdConn struct {
conn net.Conn
}
func newOSMon(_ *eventbus.Bus, logf logger.Logf, m *Monitor) (osMon, error) {
func newOSMon(logf logger.Logf, m *Monitor) (osMon, error) {
conn, err := net.Dial("unixpacket", "/var/run/devd.seqpacket.pipe")
if err != nil {
logf("devd dial error: %v, falling back to polling method", err)

View File

@@ -16,7 +16,6 @@ import (
"tailscale.com/envknob"
"tailscale.com/net/tsaddr"
"tailscale.com/types/logger"
"tailscale.com/util/eventbus"
)
var debugNetlinkMessages = envknob.RegisterBool("TS_DEBUG_NETLINK")
@@ -28,26 +27,15 @@ type unspecifiedMessage struct{}
func (unspecifiedMessage) ignore() bool { return false }
// RuleDeleted reports that one of Tailscale's policy routing rules
// was deleted.
type RuleDeleted struct {
// Table is the table number that the deleted rule referenced.
Table uint8
// Priority is the lookup priority of the deleted rule.
Priority uint32
}
// nlConn wraps a *netlink.Conn and returns a monitor.Message
// instead of a netlink.Message. Currently, messages are discarded,
// but down the line, when messages trigger different logic depending
// on the type of event, this provides the capability of handling
// each architecture-specific message in a generic fashion.
type nlConn struct {
busClient *eventbus.Client
rulesDeleted *eventbus.Publisher[RuleDeleted]
logf logger.Logf
conn *netlink.Conn
buffered []netlink.Message
logf logger.Logf
conn *netlink.Conn
buffered []netlink.Message
// addrCache maps interface indices to a set of addresses, and is
// used to suppress duplicate RTM_NEWADDR messages. It is populated
@@ -56,7 +44,7 @@ type nlConn struct {
addrCache map[uint32]map[netip.Addr]bool
}
func newOSMon(bus *eventbus.Bus, logf logger.Logf, m *Monitor) (osMon, error) {
func newOSMon(logf logger.Logf, m *Monitor) (osMon, error) {
conn, err := netlink.Dial(unix.NETLINK_ROUTE, &netlink.Config{
// Routes get us most of the events of interest, but we need
// address as well to cover things like DHCP deciding to give
@@ -71,22 +59,12 @@ func newOSMon(bus *eventbus.Bus, logf logger.Logf, m *Monitor) (osMon, error) {
logf("monitor_linux: AF_NETLINK RTMGRP failed, falling back to polling")
return newPollingMon(logf, m)
}
client := bus.Client("netmon-iprules")
return &nlConn{
busClient: client,
rulesDeleted: eventbus.Publish[RuleDeleted](client),
logf: logf,
conn: conn,
addrCache: make(map[uint32]map[netip.Addr]bool),
}, nil
return &nlConn{logf: logf, conn: conn, addrCache: make(map[uint32]map[netip.Addr]bool)}, nil
}
func (c *nlConn) IsInterestingInterface(iface string) bool { return true }
func (c *nlConn) Close() error {
c.busClient.Close()
return c.conn.Close()
}
func (c *nlConn) Close() error { return c.conn.Close() }
func (c *nlConn) Receive() (message, error) {
if len(c.buffered) == 0 {
@@ -241,10 +219,6 @@ func (c *nlConn) Receive() (message, error) {
// On `ip -4 rule del pref 5210 table main`, logs:
// monitor: ip rule deleted: {Family:2 DstLength:0 SrcLength:0 Tos:0 Table:254 Protocol:0 Scope:0 Type:1 Flags:0 Attributes:{Dst:<nil> Src:<nil> Gateway:<nil> OutIface:0 Priority:5210 Table:254 Mark:4294967295 Expires:<nil> Metrics:<nil> Multipath:[]}}
}
c.rulesDeleted.Publish(RuleDeleted{
Table: rmsg.Table,
Priority: rmsg.Attributes.Priority,
})
rdm := ipRuleDeletedMessage{
table: rmsg.Table,
priority: rmsg.Attributes.Priority,

View File

@@ -7,10 +7,9 @@ package netmon
import (
"tailscale.com/types/logger"
"tailscale.com/util/eventbus"
)
func newOSMon(_ *eventbus.Bus, logf logger.Logf, m *Monitor) (osMon, error) {
func newOSMon(logf logger.Logf, m *Monitor) (osMon, error) {
return newPollingMon(logf, m)
}

View File

@@ -11,15 +11,11 @@ import (
"testing"
"time"
"tailscale.com/util/eventbus"
"tailscale.com/util/mak"
)
func TestMonitorStartClose(t *testing.T) {
bus := eventbus.New()
defer bus.Close()
mon, err := New(bus, t.Logf)
mon, err := New(t.Logf)
if err != nil {
t.Fatal(err)
}
@@ -30,10 +26,7 @@ func TestMonitorStartClose(t *testing.T) {
}
func TestMonitorJustClose(t *testing.T) {
bus := eventbus.New()
defer bus.Close()
mon, err := New(bus, t.Logf)
mon, err := New(t.Logf)
if err != nil {
t.Fatal(err)
}
@@ -43,10 +36,7 @@ func TestMonitorJustClose(t *testing.T) {
}
func TestMonitorInjectEvent(t *testing.T) {
bus := eventbus.New()
defer bus.Close()
mon, err := New(bus, t.Logf)
mon, err := New(t.Logf)
if err != nil {
t.Fatal(err)
}
@@ -81,11 +71,7 @@ func TestMonitorMode(t *testing.T) {
default:
t.Skipf(`invalid --monitor value: must be "raw" or "callback"`)
}
bus := eventbus.New()
defer bus.Close()
mon, err := New(bus, t.Logf)
mon, err := New(t.Logf)
if err != nil {
t.Fatal(err)
}

View File

@@ -13,7 +13,6 @@ import (
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"tailscale.com/net/tsaddr"
"tailscale.com/types/logger"
"tailscale.com/util/eventbus"
)
var (
@@ -46,7 +45,7 @@ type winMon struct {
noDeadlockTicker *time.Ticker
}
func newOSMon(_ *eventbus.Bus, logf logger.Logf, pm *Monitor) (osMon, error) {
func newOSMon(logf logger.Logf, pm *Monitor) (osMon, error) {
m := &winMon{
logf: logf,
isActive: pm.isActive,

View File

@@ -461,22 +461,21 @@ func isTailscaleInterface(name string, ips []netip.Prefix) bool {
// getPAC, if non-nil, returns the current PAC file URL.
var getPAC func() string
// getState returns the state of all the current machine's network interfaces.
// GetState returns the state of all the current machine's network interfaces.
//
// It does not set the returned State.IsExpensive. The caller can populate that.
//
// optTSInterfaceName is the name of the Tailscale interface, if known.
func getState(optTSInterfaceName string) (*State, error) {
// Deprecated: use netmon.Monitor.InterfaceState instead.
func getState() (*State, error) {
s := &State{
InterfaceIPs: make(map[string][]netip.Prefix),
Interface: make(map[string]Interface),
}
if err := ForeachInterface(func(ni Interface, pfxs []netip.Prefix) {
isTSInterfaceName := optTSInterfaceName != "" && ni.Name == optTSInterfaceName
ifUp := ni.IsUp()
s.Interface[ni.Name] = ni
s.InterfaceIPs[ni.Name] = append(s.InterfaceIPs[ni.Name], pfxs...)
if !ifUp || isTSInterfaceName || isTailscaleInterface(ni.Name, pfxs) {
if !ifUp || isTailscaleInterface(ni.Name, pfxs) {
return
}
for _, pfx := range pfxs {
@@ -756,12 +755,11 @@ func DefaultRoute() (DefaultRouteDetails, error) {
// HasCGNATInterface reports whether there are any non-Tailscale interfaces that
// use a CGNAT IP range.
func (m *Monitor) HasCGNATInterface() (bool, error) {
func HasCGNATInterface() (bool, error) {
hasCGNATInterface := false
cgnatRange := tsaddr.CGNATRange()
err := ForeachInterface(func(i Interface, pfxs []netip.Prefix) {
isTSInterfaceName := m.tsIfName != "" && i.Name == m.tsIfName
if hasCGNATInterface || !i.IsUp() || isTSInterfaceName || isTailscaleInterface(i.Name, pfxs) {
if hasCGNATInterface || !i.IsUp() || isTailscaleInterface(i.Name, pfxs) {
return
}
for _, pfx := range pfxs {

View File

@@ -10,7 +10,6 @@ import (
"testing"
"tailscale.com/net/netmon"
"tailscale.com/util/eventbus"
)
type conn struct {
@@ -73,10 +72,7 @@ func TestCheckReversePathFiltering(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skipf("skipping on %s", runtime.GOOS)
}
bus := eventbus.New()
defer bus.Close()
netMon, err := netmon.New(bus, t.Logf)
netMon, err := netmon.New(t.Logf)
if err != nil {
t.Fatal(err)
}

View File

@@ -19,7 +19,6 @@ import (
"tailscale.com/net/netmon"
"tailscale.com/syncs"
"tailscale.com/types/logger"
"tailscale.com/util/eventbus"
)
// TestIGD is an IGD (Internet Gateway Device) for testing. It supports fake
@@ -259,25 +258,15 @@ func (d *TestIGD) handlePCPQuery(pkt []byte, src netip.AddrPort) {
}
}
// newTestClient configures a new test client connected to igd for mapping updates.
// If bus != nil, update events are published to it.
// A cleanup for the resulting client is added to t.
func newTestClient(t *testing.T, igd *TestIGD, bus *eventbus.Bus) *Client {
func newTestClient(t *testing.T, igd *TestIGD) *Client {
var c *Client
c = NewClient(Config{
Logf: t.Logf,
NetMon: netmon.NewStatic(),
ControlKnobs: new(controlknobs.Knobs),
EventBus: bus,
OnChange: func() {
t.Logf("port map changed")
t.Logf("have mapping: %v", c.HaveMapping())
},
c = NewClient(t.Logf, netmon.NewStatic(), nil, new(controlknobs.Knobs), func() {
t.Logf("port map changed")
t.Logf("have mapping: %v", c.HaveMapping())
})
c.testPxPPort = igd.TestPxPPort()
c.testUPnPPort = igd.TestUPnPPort()
c.netMon = netmon.NewStatic()
c.SetGatewayLookupFunc(testIPAndGateway)
t.Cleanup(func() { c.Close() })
return c
}

View File

@@ -31,7 +31,6 @@ import (
"tailscale.com/types/logger"
"tailscale.com/types/nettype"
"tailscale.com/util/clientmetric"
"tailscale.com/util/eventbus"
)
var disablePortMapperEnv = envknob.RegisterBool("TS_DISABLE_PORTMAPPER")
@@ -85,11 +84,6 @@ const trustServiceStillAvailableDuration = 10 * time.Minute
// Client is a port mapping client.
type Client struct {
// The following two fields must either both be nil, or both non-nil.
// Both are immutable after construction.
pubClient *eventbus.Client
updates *eventbus.Publisher[Mapping]
logf logger.Logf
netMon *netmon.Monitor // optional; nil means interfaces will be looked up on-demand
controlKnobs *controlknobs.Knobs
@@ -207,56 +201,32 @@ func (m *pmpMapping) Release(ctx context.Context) {
uc.WriteToUDPAddrPort(pkt, m.gw)
}
// Config carries the settings for a [Client].
type Config struct {
// EventBus, if non-nil, is used for event publication and subscription by
// portmapper clients created from this config.
//
// TODO(creachadair): As of 2025-03-19 this is optional, but is intended to
// become required non-nil.
EventBus *eventbus.Bus
// Logf is called to generate text logs for the client. If nil, logger.Discard is used.
Logf logger.Logf
// NetMon is the network monitor used by the client. It must be non-nil.
NetMon *netmon.Monitor
// DebugKnobs, if non-nil, configure the behaviour of the portmapper for
// debugging. If nil, a sensible set of defaults will be used.
DebugKnobs *DebugKnobs
// ControlKnobs, if non-nil, specifies knobs from the control plane that
// might disable port mapping.
ControlKnobs *controlknobs.Knobs
// OnChange is called to run in a new goroutine whenever the port mapping
// status has changed. If nil, no callback is issued.
OnChange func()
}
// NewClient constructs a new portmapping [Client] from c. It will panic if any
// required parameters are omitted.
func NewClient(c Config) *Client {
if c.NetMon == nil {
// NewClient returns a new portmapping client.
//
// The netMon parameter is required.
//
// The debug argument allows configuring the behaviour of the portmapper for
// debugging; if nil, a sensible set of defaults will be used.
//
// The controlKnobs, if non-nil, specifies the control knobs from the control
// plane that might disable portmapping.
//
// The optional onChange argument specifies a func to run in a new goroutine
// whenever the port mapping status has changed. If nil, it doesn't make a
// callback.
func NewClient(logf logger.Logf, netMon *netmon.Monitor, debug *DebugKnobs, controlKnobs *controlknobs.Knobs, onChange func()) *Client {
if netMon == nil {
panic("nil netMon")
}
ret := &Client{
logf: c.Logf,
netMon: c.NetMon,
logf: logf,
netMon: netMon,
ipAndGateway: netmon.LikelyHomeRouterIP, // TODO(bradfitz): move this to method on netMon
onChange: c.OnChange,
controlKnobs: c.ControlKnobs,
onChange: onChange,
controlKnobs: controlKnobs,
}
if c.EventBus != nil {
ret.pubClient = c.EventBus.Client("portmapper")
ret.updates = eventbus.Publish[Mapping](ret.pubClient)
}
if ret.logf == nil {
ret.logf = logger.Discard
}
if c.DebugKnobs != nil {
ret.debug = *c.DebugKnobs
if debug != nil {
ret.debug = *debug
}
return ret
}
@@ -286,10 +256,6 @@ func (c *Client) Close() error {
}
c.closed = true
c.invalidateMappingsLocked(true)
if c.updates != nil {
c.updates.Close()
c.pubClient.Close()
}
// TODO: close some future ever-listening UDP socket(s),
// waiting for multicast announcements from router.
return nil
@@ -501,32 +467,13 @@ func (c *Client) createMapping() {
c.runningCreate = false
}()
mapping, _, err := c.createOrGetMapping(ctx)
if err != nil {
if !IsNoMappingError(err) {
c.logf("createOrGetMapping: %v", err)
}
return
}
c.updates.Publish(Mapping{
External: mapping.External(),
Type: mapping.MappingType(),
GoodUntil: mapping.GoodUntil(),
})
if c.onChange != nil {
if _, err := c.createOrGetMapping(ctx); err == nil && c.onChange != nil {
go c.onChange()
} else if err != nil && !IsNoMappingError(err) {
c.logf("createOrGetMapping: %v", err)
}
}
// Mapping is an event recording the allocation of a port mapping.
type Mapping struct {
External netip.AddrPort
Type string
GoodUntil time.Time
// TODO(creachadair): Record whether we reused an existing mapping?
}
// wildcardIP is used when the previous external IP is not known for PCP port mapping.
var wildcardIP = netip.MustParseAddr("0.0.0.0")
@@ -535,19 +482,19 @@ var wildcardIP = netip.MustParseAddr("0.0.0.0")
//
// If no mapping is available, the error will be of type
// NoMappingError; see IsNoMappingError.
func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, external netip.AddrPort, err error) {
func (c *Client) createOrGetMapping(ctx context.Context) (external netip.AddrPort, err error) {
if c.debug.disableAll() {
return nil, netip.AddrPort{}, NoMappingError{ErrPortMappingDisabled}
return netip.AddrPort{}, NoMappingError{ErrPortMappingDisabled}
}
if c.debug.DisableUPnP && c.debug.DisablePCP && c.debug.DisablePMP {
return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
return netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
}
gw, myIP, ok := c.gatewayAndSelfIP()
if !ok {
return nil, netip.AddrPort{}, NoMappingError{ErrGatewayRange}
return netip.AddrPort{}, NoMappingError{ErrGatewayRange}
}
if gw.Is6() {
return nil, netip.AddrPort{}, NoMappingError{ErrGatewayIPv6}
return netip.AddrPort{}, NoMappingError{ErrGatewayIPv6}
}
now := time.Now()
@@ -576,17 +523,6 @@ func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, exter
return
}
// TODO(creachadair): This is more subtle than it should be. Ideally we
// would just return the mapping directly, but there are many different
// paths through the function with carefully-balanced locks, and not all
// the paths have a mapping to return. As a workaround, while we're here
// doing cleanup under the lock, grab the final mapping value and return
// it, so the caller does not need to grab the lock again and potentially
// race with a later update. The mapping itself is concurrency-safe.
//
// We should restructure this code so the locks are properly scoped.
mapping = c.mapping
// Print the internal details of each mapping if we're being verbose.
if c.debug.VerboseLogs {
c.logf("successfully obtained mapping: now=%d external=%v type=%s mapping=%s",
@@ -612,7 +548,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, exter
if now.Before(m.RenewAfter()) {
defer c.mu.Unlock()
reusedExisting = true
return nil, m.External(), nil
return m.External(), nil
}
// The mapping might still be valid, so just try to renew it.
prevPort = m.External().Port()
@@ -621,10 +557,10 @@ func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, exter
if c.debug.DisablePCP && c.debug.DisablePMP {
c.mu.Unlock()
if external, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok {
return nil, external, nil
return external, nil
}
c.vlogf("fallback to UPnP due to PCP and PMP being disabled failed")
return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
return netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
}
// If we just did a Probe (e.g. via netchecker) but didn't
@@ -651,16 +587,16 @@ func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, exter
c.mu.Unlock()
// fallback to UPnP portmapping
if external, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok {
return nil, external, nil
return external, nil
}
c.vlogf("fallback to UPnP due to no PCP and PMP failed")
return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
return netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
}
c.mu.Unlock()
uc, err := c.listenPacket(ctx, "udp4", ":0")
if err != nil {
return nil, netip.AddrPort{}, err
return netip.AddrPort{}, err
}
defer uc.Close()
@@ -680,7 +616,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, exter
if neterror.TreatAsLostUDP(err) {
err = NoMappingError{ErrNoPortMappingServices}
}
return nil, netip.AddrPort{}, err
return netip.AddrPort{}, err
}
} else {
// Ask for our external address if needed.
@@ -689,7 +625,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, exter
if neterror.TreatAsLostUDP(err) {
err = NoMappingError{ErrNoPortMappingServices}
}
return nil, netip.AddrPort{}, err
return netip.AddrPort{}, err
}
}
@@ -698,7 +634,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, exter
if neterror.TreatAsLostUDP(err) {
err = NoMappingError{ErrNoPortMappingServices}
}
return nil, netip.AddrPort{}, err
return netip.AddrPort{}, err
}
}
@@ -707,13 +643,13 @@ func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, exter
n, src, err := uc.ReadFromUDPAddrPort(res)
if err != nil {
if ctx.Err() == context.Canceled {
return nil, netip.AddrPort{}, err
return netip.AddrPort{}, err
}
// fallback to UPnP portmapping
if mapping, ok := c.getUPnPPortMapping(ctx, gw, internalAddr, prevPort); ok {
return nil, mapping, nil
return mapping, nil
}
return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
return netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
}
src = netaddr.Unmap(src)
if !src.IsValid() {
@@ -729,7 +665,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, exter
continue
}
if pres.ResultCode != 0 {
return nil, netip.AddrPort{}, NoMappingError{fmt.Errorf("PMP response Op=0x%x,Res=0x%x", pres.OpCode, pres.ResultCode)}
return netip.AddrPort{}, NoMappingError{fmt.Errorf("PMP response Op=0x%x,Res=0x%x", pres.OpCode, pres.ResultCode)}
}
if pres.OpCode == pmpOpReply|pmpOpMapPublicAddr {
m.external = netip.AddrPortFrom(pres.PublicAddr, m.external.Port())
@@ -747,7 +683,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, exter
if err != nil {
c.logf("failed to get PCP mapping: %v", err)
// PCP should only have a single packet response
return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
return netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
}
pcpMapping.c = c
pcpMapping.internal = m.internal
@@ -755,10 +691,10 @@ func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, exter
c.mu.Lock()
defer c.mu.Unlock()
c.mapping = pcpMapping
return pcpMapping, pcpMapping.external, nil
return pcpMapping.external, nil
default:
c.logf("unknown PMP/PCP version number: %d %v", version, res[:n])
return nil, netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
return netip.AddrPort{}, NoMappingError{ErrNoPortMappingServices}
}
}
@@ -766,7 +702,7 @@ func (c *Client) createOrGetMapping(ctx context.Context) (mapping mapping, exter
c.mu.Lock()
defer c.mu.Unlock()
c.mapping = m
return nil, m.external, nil
return m.external, nil
}
}
}

View File

@@ -12,21 +12,20 @@ import (
"time"
"tailscale.com/control/controlknobs"
"tailscale.com/util/eventbus"
)
func TestCreateOrGetMapping(t *testing.T) {
if v, _ := strconv.ParseBool(os.Getenv("HIT_NETWORK")); !v {
t.Skip("skipping test without HIT_NETWORK=1")
}
c := NewClient(Config{Logf: t.Logf, ControlKnobs: new(controlknobs.Knobs)})
c := NewClient(t.Logf, nil, nil, new(controlknobs.Knobs), nil)
defer c.Close()
c.SetLocalPort(1234)
for i := range 2 {
if i > 0 {
time.Sleep(100 * time.Millisecond)
}
_, ext, err := c.createOrGetMapping(context.Background())
ext, err := c.createOrGetMapping(context.Background())
t.Logf("Got: %v, %v", ext, err)
}
}
@@ -35,7 +34,7 @@ func TestClientProbe(t *testing.T) {
if v, _ := strconv.ParseBool(os.Getenv("HIT_NETWORK")); !v {
t.Skip("skipping test without HIT_NETWORK=1")
}
c := NewClient(Config{Logf: t.Logf, ControlKnobs: new(controlknobs.Knobs)})
c := NewClient(t.Logf, nil, nil, new(controlknobs.Knobs), nil)
defer c.Close()
for i := range 3 {
if i > 0 {
@@ -50,13 +49,13 @@ func TestClientProbeThenMap(t *testing.T) {
if v, _ := strconv.ParseBool(os.Getenv("HIT_NETWORK")); !v {
t.Skip("skipping test without HIT_NETWORK=1")
}
c := NewClient(Config{Logf: t.Logf, ControlKnobs: new(controlknobs.Knobs)})
c := NewClient(t.Logf, nil, nil, new(controlknobs.Knobs), nil)
defer c.Close()
c.debug.VerboseLogs = true
c.SetLocalPort(1234)
res, err := c.Probe(context.Background())
t.Logf("Probe: %+v, %v", res, err)
_, ext, err := c.createOrGetMapping(context.Background())
ext, err := c.createOrGetMapping(context.Background())
t.Logf("createOrGetMapping: %v, %v", ext, err)
}
@@ -67,8 +66,9 @@ func TestProbeIntegration(t *testing.T) {
}
defer igd.Close()
c := newTestClient(t, igd, nil)
c := newTestClient(t, igd)
t.Logf("Listening on pxp=%v, upnp=%v", c.testPxPPort, c.testUPnPPort)
defer c.Close()
res, err := c.Probe(context.Background())
if err != nil {
@@ -101,7 +101,8 @@ func TestPCPIntegration(t *testing.T) {
}
defer igd.Close()
c := newTestClient(t, igd, nil)
c := newTestClient(t, igd)
defer c.Close()
res, err := c.Probe(context.Background())
if err != nil {
t.Fatalf("probe failed: %v", err)
@@ -113,7 +114,7 @@ func TestPCPIntegration(t *testing.T) {
t.Fatalf("probe did not see pcp: %+v", res)
}
_, external, err := c.createOrGetMapping(context.Background())
external, err := c.createOrGetMapping(context.Background())
if err != nil {
t.Fatalf("failed to get mapping: %v", err)
}
@@ -135,29 +136,3 @@ func TestGetUPnPErrorsMetric(t *testing.T) {
getUPnPErrorsMetric(0)
getUPnPErrorsMetric(-100)
}
func TestUpdateEvent(t *testing.T) {
igd, err := NewTestIGD(t.Logf, TestIGDOptions{PCP: true})
if err != nil {
t.Fatalf("Create test gateway: %v", err)
}
bus := eventbus.New()
defer bus.Close()
sub := eventbus.Subscribe[Mapping](bus.Client("TestUpdateEvent"))
c := newTestClient(t, igd, bus)
if _, err := c.Probe(t.Context()); err != nil {
t.Fatalf("Probe failed: %v", err)
}
c.GetCachedMappingOrStartCreatingOne()
select {
case evt := <-sub.Events():
t.Logf("Received portmap update: %+v", evt)
case <-sub.Done():
t.Error("Subscriber closed prematurely")
case <-time.After(5 * time.Second):
t.Error("Timed out waiting for an update event")
}
}

View File

@@ -163,8 +163,9 @@ func TestSelectBestService(t *testing.T) {
Desc: rootDesc,
Control: tt.control,
})
c := newTestClient(t, igd, nil)
c := newTestClient(t, igd)
t.Logf("Listening on upnp=%v", c.testUPnPPort)
defer c.Close()
// Ensure that we're using the HTTP client that talks to our test IGD server
ctx := context.Background()

View File

@@ -586,8 +586,9 @@ func TestGetUPnPPortMapping(t *testing.T) {
},
})
c := newTestClient(t, igd, nil)
c := newTestClient(t, igd)
t.Logf("Listening on upnp=%v", c.testUPnPPort)
defer c.Close()
c.debug.VerboseLogs = true
@@ -688,9 +689,10 @@ func TestGetUPnPPortMapping_LeaseDuration(t *testing.T) {
})
ctx := context.Background()
c := newTestClient(t, igd, nil)
c := newTestClient(t, igd)
c.debug.VerboseLogs = true
t.Logf("Listening on upnp=%v", c.testUPnPPort)
defer c.Close()
// Actually test the UPnP port mapping.
mustProbeUPnP(t, ctx, c)
@@ -733,7 +735,8 @@ func TestGetUPnPPortMapping_NoValidServices(t *testing.T) {
Desc: noSupportedServicesRootDesc,
})
c := newTestClient(t, igd, nil)
c := newTestClient(t, igd)
defer c.Close()
c.debug.VerboseLogs = true
ctx := context.Background()
@@ -775,7 +778,8 @@ func TestGetUPnPPortMapping_Legacy(t *testing.T) {
},
})
c := newTestClient(t, igd, nil)
c := newTestClient(t, igd)
defer c.Close()
c.debug.VerboseLogs = true
ctx := context.Background()
@@ -802,8 +806,9 @@ func TestGetUPnPPortMappingNoResponses(t *testing.T) {
}
defer igd.Close()
c := newTestClient(t, igd, nil)
c := newTestClient(t, igd)
t.Logf("Listening on upnp=%v", c.testUPnPPort)
defer c.Close()
c.debug.VerboseLogs = true
@@ -934,7 +939,8 @@ func TestGetUPnPPortMapping_Invalid(t *testing.T) {
},
})
c := newTestClient(t, igd, nil)
c := newTestClient(t, igd)
defer c.Close()
c.debug.VerboseLogs = true
ctx := context.Background()

View File

@@ -596,23 +596,11 @@ func (d *derpProber) updateMap(ctx context.Context) error {
}
func (d *derpProber) ProbeUDP(ipaddr string, port int) ProbeClass {
initLabels := make(Labels)
ip := net.ParseIP(ipaddr)
if ip.To4() != nil {
initLabels["address_family"] = "ipv4"
} else if ip.To16() != nil { // Will return an IPv4 as 16 byte, so ensure the check for IPv4 precedes this
initLabels["address_family"] = "ipv6"
} else {
initLabels["address_family"] = "unknown"
}
return ProbeClass{
Probe: func(ctx context.Context) error {
return derpProbeUDP(ctx, ipaddr, port)
},
Class: "derp_udp",
Labels: initLabels,
Class: "derp_udp",
}
}

View File

@@ -404,14 +404,10 @@ func (p *Probe) recordEndLocked(err error) {
p.mSeconds.WithLabelValues("ok").Add(latency.Seconds())
p.latencyHist.Value = latency
p.latencyHist = p.latencyHist.Next()
p.mAttempts.WithLabelValues("fail").Add(0)
p.mSeconds.WithLabelValues("fail").Add(0)
} else {
p.latency = 0
p.mAttempts.WithLabelValues("fail").Inc()
p.mSeconds.WithLabelValues("fail").Add(latency.Seconds())
p.mAttempts.WithLabelValues("ok").Add(0)
p.mSeconds.WithLabelValues("ok").Add(0)
}
p.successHist.Value = p.succeeded
p.successHist = p.successHist.Next()

View File

@@ -1037,8 +1037,8 @@ func TestSSHAuthFlow(t *testing.T) {
func TestSSH(t *testing.T) {
var logf logger.Logf = t.Logf
sys := tsd.NewSystem()
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry(), sys.Bus.Get())
sys := &tsd.System{}
eng, err := wgengine.NewFakeUserspaceEngine(logf, sys.Set, sys.HealthTracker(), sys.UserMetricsRegistry())
if err != nil {
t.Fatal(err)
}

View File

@@ -34,7 +34,6 @@ import (
"tailscale.com/net/tstun"
"tailscale.com/proxymap"
"tailscale.com/types/netmap"
"tailscale.com/util/eventbus"
"tailscale.com/util/usermetric"
"tailscale.com/wgengine"
"tailscale.com/wgengine/magicsock"
@@ -42,12 +41,7 @@ import (
)
// System contains all the subsystems of a Tailscale node (tailscaled, etc.)
//
// A valid System value must always have a non-nil Bus populated. Callers must
// ensure this before using the value further. Call [NewSystem] to obtain a
// value ready to use.
type System struct {
Bus SubSystem[*eventbus.Bus]
Dialer SubSystem[*tsdial.Dialer]
DNSManager SubSystem[*dns.Manager] // can get its *resolver.Resolver from DNSManager.Resolver
Engine SubSystem[wgengine.Engine]
@@ -80,14 +74,6 @@ type System struct {
userMetricsRegistry usermetric.Registry
}
// NewSystem constructs a new otherwise-empty [System] with a
// freshly-constructed event bus populated.
func NewSystem() *System {
sys := new(System)
sys.Set(eventbus.New())
return sys
}
// NetstackImpl is the interface that *netstack.Impl implements.
// It's an interface for circular dependency reasons: netstack.Impl
// references LocalBackend, and LocalBackend has a tsd.System.
@@ -100,8 +86,6 @@ type NetstackImpl interface {
// has already been set.
func (s *System) Set(v any) {
switch v := v.(type) {
case *eventbus.Bus:
s.Bus.Set(v)
case *netmon.Monitor:
s.NetMon.Set(v)
case *dns.Manager:

View File

@@ -435,11 +435,8 @@ func (s *Server) Close() error {
for _, ln := range s.listeners {
ln.closeLocked()
}
wg.Wait()
if bus := s.sys.Bus.Get(); bus != nil {
bus.Close()
}
wg.Wait()
s.closed = true
return nil
}
@@ -508,11 +505,6 @@ func (s *Server) start() (reterr error) {
// directory and hostname when they're not supplied. But we can fall
// back to "tsnet" as well.
exe = "tsnet"
case "ios":
// When compiled as a framework (via TailscaleKit in libtailscale),
// os.Executable() returns an error, so fall back to "tsnet" there
// too.
exe = "tsnet"
default:
return err
}
@@ -561,13 +553,13 @@ func (s *Server) start() (reterr error) {
s.Logf(format, a...)
}
sys := tsd.NewSystem()
sys := new(tsd.System)
s.sys = sys
if err := s.startLogger(&closePool, sys.HealthTracker(), tsLogf); err != nil {
return err
}
s.netMon, err = netmon.New(sys.Bus.Get(), tsLogf)
s.netMon, err = netmon.New(tsLogf)
if err != nil {
return err
}
@@ -575,7 +567,6 @@ func (s *Server) start() (reterr error) {
s.dialer = &tsdial.Dialer{Logf: tsLogf} // mutated below (before used)
eng, err := wgengine.NewUserspaceEngine(tsLogf, wgengine.Config{
EventBus: sys.Bus.Get(),
ListenPort: s.Port,
NetMon: s.netMon,
Dialer: s.dialer,

View File

@@ -48,7 +48,6 @@ import (
_ "tailscale.com/types/logger"
_ "tailscale.com/types/logid"
_ "tailscale.com/util/clientmetric"
_ "tailscale.com/util/eventbus"
_ "tailscale.com/util/multierr"
_ "tailscale.com/util/osshare"
_ "tailscale.com/version"

View File

@@ -48,7 +48,6 @@ import (
_ "tailscale.com/types/logger"
_ "tailscale.com/types/logid"
_ "tailscale.com/util/clientmetric"
_ "tailscale.com/util/eventbus"
_ "tailscale.com/util/multierr"
_ "tailscale.com/util/osshare"
_ "tailscale.com/version"

View File

@@ -48,7 +48,6 @@ import (
_ "tailscale.com/types/logger"
_ "tailscale.com/types/logid"
_ "tailscale.com/util/clientmetric"
_ "tailscale.com/util/eventbus"
_ "tailscale.com/util/multierr"
_ "tailscale.com/util/osshare"
_ "tailscale.com/version"

View File

@@ -48,7 +48,6 @@ import (
_ "tailscale.com/types/logger"
_ "tailscale.com/types/logid"
_ "tailscale.com/util/clientmetric"
_ "tailscale.com/util/eventbus"
_ "tailscale.com/util/multierr"
_ "tailscale.com/util/osshare"
_ "tailscale.com/version"

View File

@@ -56,7 +56,6 @@ import (
_ "tailscale.com/types/logger"
_ "tailscale.com/types/logid"
_ "tailscale.com/util/clientmetric"
_ "tailscale.com/util/eventbus"
_ "tailscale.com/util/multierr"
_ "tailscale.com/util/osdiag"
_ "tailscale.com/util/osshare"

View File

@@ -839,17 +839,15 @@ func (s *Server) serveMap(w http.ResponseWriter, r *http.Request, mkey key.Machi
w.WriteHeader(200)
for {
// Only send raw map responses to the streaming poll, to avoid a
// non-streaming map request beating the streaming poll in a race and
// potentially dropping the map response.
if streaming {
if resBytes, ok := s.takeRawMapMessage(req.NodeKey); ok {
if err := s.sendMapMsg(w, compress, resBytes); err != nil {
s.logf("sendMapMsg of raw message: %v", err)
return
}
if resBytes, ok := s.takeRawMapMessage(req.NodeKey); ok {
if err := s.sendMapMsg(w, compress, resBytes); err != nil {
s.logf("sendMapMsg of raw message: %v", err)
return
}
if streaming {
continue
}
return
}
if s.canGenerateAutomaticMapResponseFor(req.NodeKey) {

View File

@@ -9,6 +9,7 @@ import (
"html"
"io"
"net/http"
"net/http/pprof"
"net/url"
"os"
"runtime"
@@ -63,7 +64,16 @@ func Debugger(mux *http.ServeMux) *DebugHandler {
ret.Handle("varz", "Metrics (Prometheus)", http.HandlerFunc(varz.Handler))
}
addProfilingHandlers(ret)
// pprof.Index serves everything that runtime/pprof.Lookup finds:
// goroutine, threadcreate, heap, allocs, block, mutex
ret.Handle("pprof/", "pprof (index)", http.HandlerFunc(pprof.Index))
// But register the other ones from net/http/pprof directly:
ret.HandleSilent("pprof/cmdline", http.HandlerFunc(pprof.Cmdline))
ret.HandleSilent("pprof/profile", http.HandlerFunc(pprof.Profile))
ret.HandleSilent("pprof/symbol", http.HandlerFunc(pprof.Symbol))
ret.HandleSilent("pprof/trace", http.HandlerFunc(pprof.Trace))
ret.URL("/debug/pprof/goroutine?debug=1", "Goroutines (collapsed)")
ret.URL("/debug/pprof/goroutine?debug=2", "Goroutines (full)")
ret.Handle("gc", "force GC", http.HandlerFunc(gcHandler))
hostname, err := os.Hostname()
if err == nil {

View File

@@ -1,24 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !js && !wasm
package tsweb
import (
"net/http"
"net/http/pprof"
)
func addProfilingHandlers(d *DebugHandler) {
// pprof.Index serves everything that runtime/pprof.Lookup finds:
// goroutine, threadcreate, heap, allocs, block, mutex
d.Handle("pprof/", "pprof (index)", http.HandlerFunc(pprof.Index))
// But register the other ones from net/http/pprof directly:
d.HandleSilent("pprof/cmdline", http.HandlerFunc(pprof.Cmdline))
d.HandleSilent("pprof/profile", http.HandlerFunc(pprof.Profile))
d.HandleSilent("pprof/symbol", http.HandlerFunc(pprof.Symbol))
d.HandleSilent("pprof/trace", http.HandlerFunc(pprof.Trace))
d.URL("/debug/pprof/goroutine?debug=1", "Goroutines (collapsed)")
d.URL("/debug/pprof/goroutine?debug=2", "Goroutines (full)")
}

View File

@@ -1,10 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build js && wasm
package tsweb
func addProfilingHandlers(d *DebugHandler) {
// No pprof in js builds, pprof doesn't work and bloats the build.
}

View File

@@ -15,6 +15,7 @@ import (
"io"
"net"
"net/http"
_ "net/http/pprof"
"net/netip"
"net/url"
"os"

View File

@@ -46,7 +46,7 @@ func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netip.
logf: logger.WithPrefix(logf, "tun1: "),
traf: traf,
}
s1 := tsd.NewSystem()
s1 := new(tsd.System)
e1, err := wgengine.NewUserspaceEngine(l1, wgengine.Config{
Router: router.NewFake(l1),
NetMon: nil,
@@ -73,7 +73,7 @@ func setupWGTest(b *testing.B, logf logger.Logf, traf *TrafficGen, a1, a2 netip.
logf: logger.WithPrefix(logf, "tun2: "),
traf: traf,
}
s2 := tsd.NewSystem()
s2 := new(tsd.System)
e2, err := wgengine.NewUserspaceEngine(l2, wgengine.Config{
Router: router.NewFake(l2),
NetMon: nil,

View File

@@ -56,7 +56,6 @@ import (
"tailscale.com/types/nettype"
"tailscale.com/types/views"
"tailscale.com/util/clientmetric"
"tailscale.com/util/eventbus"
"tailscale.com/util/mak"
"tailscale.com/util/ringbuffer"
"tailscale.com/util/set"
@@ -137,8 +136,6 @@ type Conn struct {
// This block mirrors the contents and field order of the Options
// struct. Initialized once at construction, then constant.
eventBus *eventbus.Bus
eventClient *eventbus.Client
logf logger.Logf
epFunc func([]tailcfg.Endpoint)
derpActiveFunc func()
@@ -404,15 +401,8 @@ func (c *Conn) dlogf(format string, a ...any) {
// Options contains options for Listen.
type Options struct {
// EventBus, if non-nil, is used for event publication and subscription by
// each Conn created from these Options.
//
// TODO(creachadair): As of 2025-03-19 this is optional, but is intended to
// become required non-nil.
EventBus *eventbus.Bus
// Logf provides a log function to use. It must not be nil.
// Use [logger.Discard] to disrcard logs.
// Logf optionally provides a log function to use.
// Must not be nil.
Logf logger.Logf
// Port is the port to listen on.
@@ -539,7 +529,6 @@ func NewConn(opts Options) (*Conn, error) {
}
c := newConn(opts.logf())
c.eventBus = opts.EventBus
c.port.Store(uint32(opts.Port))
c.controlKnobs = opts.ControlKnobs
c.epFunc = opts.endpointsFunc()
@@ -548,31 +537,6 @@ func NewConn(opts Options) (*Conn, error) {
c.testOnlyPacketListener = opts.TestOnlyPacketListener
c.noteRecvActivity = opts.NoteRecvActivity
// If an event bus is enabled, subscribe to portmapping changes; otherwise
// use the callback mechanism of portmapper.Client.
//
// TODO(creachadair): Remove the switch once the event bus is mandatory.
onPortMapChanged := c.onPortMapChanged
if c.eventBus != nil {
c.eventClient = c.eventBus.Client("magicsock.Conn")
pmSub := eventbus.Subscribe[portmapper.Mapping](c.eventClient)
go func() {
defer pmSub.Close()
for {
select {
case <-pmSub.Events():
c.onPortMapChanged()
case <-pmSub.Done():
return
}
}
}()
// Disable the explicit callback from the portmapper, the subscriber handles it.
onPortMapChanged = nil
}
// Don't log the same log messages possibly every few seconds in our
// portmapper.
portmapperLogf := logger.WithPrefix(c.logf, "portmapper: ")
@@ -580,14 +544,7 @@ func NewConn(opts Options) (*Conn, error) {
portMapOpts := &portmapper.DebugKnobs{
DisableAll: func() bool { return opts.DisablePortMapper || c.onlyTCP443.Load() },
}
c.portMapper = portmapper.NewClient(portmapper.Config{
EventBus: c.eventBus,
Logf: portmapperLogf,
NetMon: opts.NetMon,
DebugKnobs: portMapOpts,
ControlKnobs: opts.ControlKnobs,
OnChange: onPortMapChanged,
})
c.portMapper = portmapper.NewClient(portmapperLogf, opts.NetMon, portMapOpts, opts.ControlKnobs, c.onPortMapChanged)
c.portMapper.SetGatewayLookupFunc(opts.NetMon.GatewayAndSelfIP)
c.netMon = opts.NetMon
c.health = opts.HealthTracker
@@ -2504,9 +2461,6 @@ func (c *connBind) Close() error {
if c.closeDisco6 != nil {
c.closeDisco6.Close()
}
if c.eventClient != nil {
c.eventClient.Close()
}
// Send an empty read result to unblock receiveDERP,
// which will then check connBind.Closed.
// connBind.Closed takes c.mu, but c.derpRecvCh is buffered.

View File

@@ -62,7 +62,6 @@ import (
"tailscale.com/types/nettype"
"tailscale.com/types/ptr"
"tailscale.com/util/cibuild"
"tailscale.com/util/eventbus"
"tailscale.com/util/must"
"tailscale.com/util/racebuild"
"tailscale.com/util/set"
@@ -174,10 +173,7 @@ func newMagicStack(t testing.TB, logf logger.Logf, l nettype.PacketListener, der
func newMagicStackWithKey(t testing.TB, logf logger.Logf, l nettype.PacketListener, derpMap *tailcfg.DERPMap, privateKey key.NodePrivate) *magicStack {
t.Helper()
bus := eventbus.New()
defer bus.Close()
netMon, err := netmon.New(bus, logf)
netMon, err := netmon.New(logf)
if err != nil {
t.Fatalf("netmon.New: %v", err)
}
@@ -394,10 +390,7 @@ func TestNewConn(t *testing.T) {
}
}
bus := eventbus.New()
defer bus.Close()
netMon, err := netmon.New(bus, logger.WithPrefix(t.Logf, "... netmon: "))
netMon, err := netmon.New(logger.WithPrefix(t.Logf, "... netmon: "))
if err != nil {
t.Fatalf("netmon.New: %v", err)
}
@@ -530,10 +523,7 @@ func TestDeviceStartStop(t *testing.T) {
tstest.PanicOnLog()
tstest.ResourceCheck(t)
bus := eventbus.New()
defer bus.Close()
netMon, err := netmon.New(bus, logger.WithPrefix(t.Logf, "... netmon: "))
netMon, err := netmon.New(logger.WithPrefix(t.Logf, "... netmon: "))
if err != nil {
t.Fatalf("netmon.New: %v", err)
}
@@ -1372,10 +1362,7 @@ func newTestConn(t testing.TB) *Conn {
t.Helper()
port := pickPort(t)
bus := eventbus.New()
defer bus.Close()
netMon, err := netmon.New(bus, logger.WithPrefix(t.Logf, "... netmon: "))
netMon, err := netmon.New(logger.WithPrefix(t.Logf, "... netmon: "))
if err != nil {
t.Fatalf("netmon.New: %v", err)
}
@@ -3130,10 +3117,7 @@ func TestMaybeRebindOnError(t *testing.T) {
}
func TestNetworkDownSendErrors(t *testing.T) {
bus := eventbus.New()
defer bus.Close()
netMon := must.Get(netmon.New(bus, t.Logf))
netMon := must.Get(netmon.New(t.Logf))
defer netMon.Close()
reg := new(usermetric.Registry)

View File

@@ -44,14 +44,13 @@ func TestInjectInboundLeak(t *testing.T) {
t.Logf(format, args...)
}
}
sys := tsd.NewSystem()
sys := new(tsd.System)
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
Tun: tunDev,
Dialer: dialer,
SetSubsystem: sys.Set,
HealthTracker: sys.HealthTracker(),
Metrics: sys.UserMetricsRegistry(),
EventBus: sys.Bus.Get(),
})
if err != nil {
t.Fatal(err)
@@ -101,7 +100,7 @@ func getMemStats() (ms runtime.MemStats) {
func makeNetstack(tb testing.TB, config func(*Impl)) *Impl {
tunDev := tstun.NewFake()
sys := tsd.NewSystem()
sys := &tsd.System{}
sys.Set(new(mem.Store))
dialer := new(tsdial.Dialer)
logf := tstest.WhileTestRunningLogger(tb)
@@ -111,7 +110,6 @@ func makeNetstack(tb testing.TB, config func(*Impl)) *Impl {
SetSubsystem: sys.Set,
HealthTracker: sys.HealthTracker(),
Metrics: sys.UserMetricsRegistry(),
EventBus: sys.Bus.Get(),
})
if err != nil {
tb.Fatal(err)

View File

@@ -27,7 +27,6 @@ import (
"tailscale.com/net/tsaddr"
"tailscale.com/tstest"
"tailscale.com/types/logger"
"tailscale.com/util/eventbus"
"tailscale.com/util/linuxfw"
"tailscale.com/version/distro"
)
@@ -364,9 +363,7 @@ ip route add throw 192.168.0.0/24 table 52` + basic,
},
}
bus := eventbus.New()
defer bus.Close()
mon, err := netmon.New(bus, logger.Discard)
mon, err := netmon.New(logger.Discard)
if err != nil {
t.Fatal(err)
}
@@ -976,10 +973,7 @@ func newLinuxRootTest(t *testing.T) *linuxTest {
logf := lt.logOutput.Logf
bus := eventbus.New()
defer bus.Close()
mon, err := netmon.New(bus, logger.Discard)
mon, err := netmon.New(logger.Discard)
if err != nil {
lt.Close()
t.Fatal(err)

View File

@@ -46,7 +46,6 @@ import (
"tailscale.com/types/views"
"tailscale.com/util/clientmetric"
"tailscale.com/util/deephash"
"tailscale.com/util/eventbus"
"tailscale.com/util/mak"
"tailscale.com/util/set"
"tailscale.com/util/testenv"
@@ -90,12 +89,8 @@ const statusPollInterval = 1 * time.Minute
const networkLoggerUploadTimeout = 5 * time.Second
type userspaceEngine struct {
// eventBus will eventually become required, but for now may be nil.
// TODO(creachadair): Enforce that this is non-nil at construction.
eventBus *eventbus.Bus
logf logger.Logf
wgLogger *wglog.Logger // a wireguard-go logging wrapper
wgLogger *wglog.Logger //a wireguard-go logging wrapper
reqCh chan struct{}
waitCh chan struct{} // chan is closed when first Close call completes; contrast with closing bool
timeNow func() mono.Time
@@ -232,13 +227,6 @@ type Config struct {
// DriveForLocal, if populated, will cause the engine to expose a Taildrive
// listener at 100.100.100.100:8080.
DriveForLocal drive.FileSystemForLocal
// EventBus, if non-nil, is used for event publication and subscription by
// the Engine and its subsystems.
//
// TODO(creachadair): As of 2025-03-19 this is optional, but is intended to
// become required non-nil.
EventBus *eventbus.Bus
}
// NewFakeUserspaceEngine returns a new userspace engine for testing.
@@ -267,8 +255,6 @@ func NewFakeUserspaceEngine(logf logger.Logf, opts ...any) (Engine, error) {
conf.HealthTracker = v
case *usermetric.Registry:
conf.Metrics = v
case *eventbus.Bus:
conf.EventBus = v
default:
return nil, fmt.Errorf("unknown option type %T", v)
}
@@ -337,7 +323,6 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
}
e := &userspaceEngine{
eventBus: conf.EventBus,
timeNow: mono.Now,
logf: logf,
reqCh: make(chan struct{}, 1),
@@ -363,7 +348,7 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
if conf.NetMon != nil {
e.netMon = conf.NetMon
} else {
mon, err := netmon.New(conf.EventBus, logf)
mon, err := netmon.New(logf)
if err != nil {
return nil, err
}
@@ -404,7 +389,6 @@ func NewUserspaceEngine(logf logger.Logf, conf Config) (_ Engine, reterr error)
}
}
magicsockOpts := magicsock.Options{
EventBus: e.eventBus,
Logf: logf,
Port: conf.ListenPort,
EndpointsFunc: endpointsFn,
@@ -1596,6 +1580,11 @@ type fwdDNSLinkSelector struct {
}
func (ls fwdDNSLinkSelector) PickLink(ip netip.Addr) (linkName string) {
// sandboxed macOS needs some extra hand-holding for loopback addresses.
if ip.IsLoopback() && version.IsSandboxedMacOS() {
return "lo0"
}
if ls.ue.isDNSIPOverTailscale.Load()(ip) {
return ls.tunName
}

View File

@@ -16,14 +16,13 @@ import (
)
func TestIsNetstack(t *testing.T) {
sys := tsd.NewSystem()
sys := new(tsd.System)
e, err := wgengine.NewUserspaceEngine(
tstest.WhileTestRunningLogger(t),
wgengine.Config{
SetSubsystem: sys.Set,
HealthTracker: sys.HealthTracker(),
Metrics: sys.UserMetricsRegistry(),
EventBus: sys.Bus.Get(),
},
)
if err != nil {
@@ -67,7 +66,7 @@ func TestIsNetstackRouter(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sys := tsd.NewSystem()
sys := &tsd.System{}
if tt.setNetstackRouter {
sys.NetstackRouter.Set(true)
}
@@ -75,7 +74,6 @@ func TestIsNetstackRouter(t *testing.T) {
conf.SetSubsystem = sys.Set
conf.HealthTracker = sys.HealthTracker()
conf.Metrics = sys.UserMetricsRegistry()
conf.EventBus = sys.Bus.Get()
e, err := wgengine.NewUserspaceEngine(logger.Discard, conf)
if err != nil {
t.Fatal(err)

View File

@@ -25,7 +25,6 @@ import (
"tailscale.com/types/key"
"tailscale.com/types/netmap"
"tailscale.com/types/opt"
"tailscale.com/util/eventbus"
"tailscale.com/util/usermetric"
"tailscale.com/wgengine/router"
"tailscale.com/wgengine/wgcfg"
@@ -101,12 +100,9 @@ func nodeViews(v []*tailcfg.Node) []tailcfg.NodeView {
}
func TestUserspaceEngineReconfig(t *testing.T) {
bus := eventbus.New()
defer bus.Close()
ht := new(health.Tracker)
reg := new(usermetric.Registry)
e, err := NewFakeUserspaceEngine(t.Logf, 0, ht, reg, bus)
e, err := NewFakeUserspaceEngine(t.Logf, 0, ht, reg)
if err != nil {
t.Fatal(err)
}
@@ -170,16 +166,13 @@ func TestUserspaceEnginePortReconfig(t *testing.T) {
var knobs controlknobs.Knobs
bus := eventbus.New()
defer bus.Close()
// Keep making a wgengine until we find an unused port
var ue *userspaceEngine
ht := new(health.Tracker)
reg := new(usermetric.Registry)
for i := range 100 {
attempt := uint16(defaultPort + i)
e, err := NewFakeUserspaceEngine(t.Logf, attempt, &knobs, ht, reg, bus)
e, err := NewFakeUserspaceEngine(t.Logf, attempt, &knobs, ht, reg)
if err != nil {
t.Fatal(err)
}
@@ -258,11 +251,9 @@ func TestUserspaceEnginePeerMTUReconfig(t *testing.T) {
var knobs controlknobs.Knobs
bus := eventbus.New()
defer bus.Close()
ht := new(health.Tracker)
reg := new(usermetric.Registry)
e, err := NewFakeUserspaceEngine(t.Logf, 0, &knobs, ht, reg, bus)
e, err := NewFakeUserspaceEngine(t.Logf, 0, &knobs, ht, reg)
if err != nil {
t.Fatal(err)
}

View File

@@ -9,7 +9,6 @@ import (
"time"
"tailscale.com/health"
"tailscale.com/util/eventbus"
"tailscale.com/util/usermetric"
)
@@ -25,11 +24,9 @@ func TestWatchdog(t *testing.T) {
t.Run("default watchdog does not fire", func(t *testing.T) {
t.Parallel()
bus := eventbus.New()
defer bus.Close()
ht := new(health.Tracker)
reg := new(usermetric.Registry)
e, err := NewFakeUserspaceEngine(t.Logf, 0, ht, reg, bus)
e, err := NewFakeUserspaceEngine(t.Logf, 0, ht, reg)
if err != nil {
t.Fatal(err)
}