Compare commits

..

15 Commits

Author SHA1 Message Date
9418d7190b 更新 cmd/derper/cert.go
Some checks failed
checklocks / checklocks (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
Dockerfile build / deploy (push) Has been cancelled
CI / race-root-integration (1/4) (push) Has been cancelled
CI / race-root-integration (2/4) (push) Has been cancelled
CI / race-root-integration (3/4) (push) Has been cancelled
CI / race-root-integration (4/4) (push) Has been cancelled
CI / test (-race, amd64, 1/3) (push) Has been cancelled
CI / test (-race, amd64, 2/3) (push) Has been cancelled
CI / test (-race, amd64, 3/3) (push) Has been cancelled
CI / test (386) (push) Has been cancelled
CI / test (amd64) (push) Has been cancelled
CI / windows (push) Has been cancelled
CI / privileged (push) Has been cancelled
CI / vm (push) Has been cancelled
CI / race-build (push) Has been cancelled
CI / cross (386, linux) (push) Has been cancelled
CI / cross (amd64, darwin) (push) Has been cancelled
CI / cross (amd64, freebsd) (push) Has been cancelled
CI / cross (amd64, openbsd) (push) Has been cancelled
CI / cross (amd64, windows) (push) Has been cancelled
CI / cross (arm, 5, linux) (push) Has been cancelled
CI / cross (arm, 7, linux) (push) Has been cancelled
CI / cross (arm64, darwin) (push) Has been cancelled
CI / cross (arm64, linux) (push) Has been cancelled
CI / cross (arm64, windows) (push) Has been cancelled
CI / cross (loong64, linux) (push) Has been cancelled
CI / ios (push) Has been cancelled
CI / crossmin (amd64, illumos) (push) Has been cancelled
CI / crossmin (amd64, plan9) (push) Has been cancelled
CI / crossmin (amd64, solaris) (push) Has been cancelled
CI / crossmin (ppc64, aix) (push) Has been cancelled
CI / android (push) Has been cancelled
CI / wasm (push) Has been cancelled
CI / tailscale_go (push) Has been cancelled
CI / fuzz (push) Has been cancelled
CI / depaware (push) Has been cancelled
CI / go_generate (push) Has been cancelled
CI / go_mod_tidy (push) Has been cancelled
CI / licenses (push) Has been cancelled
CI / staticcheck (386, windows) (push) Has been cancelled
CI / staticcheck (amd64, darwin) (push) Has been cancelled
CI / staticcheck (amd64, linux) (push) Has been cancelled
CI / staticcheck (amd64, windows) (push) Has been cancelled
CI / notify_slack (push) Has been cancelled
CI / check_mergeability (push) Has been cancelled
govulncheck / source-scan (push) Has been cancelled
将与域名验证相关的内容删除或注释
2025-03-27 06:00:44 +00:00
kari-ts
1ec1a60c10 VERSION.txt: this is v1.83.0 (#15443)
Signed-off-by: kari-ts <kari@tailscale.com>
2025-03-26 14:22:21 -07:00
Irbe Krumina
fea74a60d5 cmd/k8s-operator,k8s-operator: disable HA Ingress before stable release (#15433)
Temporarily make sure that the HA Ingress reconciler does not run,
as we do not want to release this to stable just yet.

Updates tailscale/corp#24795

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2025-03-26 13:29:38 +00:00
Irbe Krumina
e3c04c5d6c build_docker.sh: bump default base image (#15432)
We now have a tailscale/alpine-base:3.19 use that as the default base image.

Updates tailscale/tailscale#15328

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2025-03-26 11:58:26 +00:00
James Tucker
d0e7af3830 cmd/natc: add test and fix for ip exhaustion
This is a very dumb fix as it has an unbounded worst case runtime. IP
allocation needs to be done in a more sane way in a follow-up.

Updates #15367

Signed-off-by: James Tucker <james@tailscale.com>
2025-03-25 19:16:02 -07:00
Irbe Krumina
2685484f26 Bump Alpine, link iptables back to legacy (#15428)
Bumps Alpine 3.18 -> 3.19.

Alpine 3.19 links iptables to nftables-based
implementation that can break hosts that don't
support nftables.
Link iptables back to the legacy implementation
till we have some certainty that changing to
nftables based implementation will not break existing
setups.

Updates tailscale/tailscale#15328

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2025-03-26 01:48:01 +00:00
Irbe Krumina
a622debe9b cmd/{k8s-operator,containerboot}: check TLS cert before advertising VIPService (#15427)
cmd/{k8s-operator,containerboot}: check TLS cert before advertising VIPService

- Ensures that Ingress status does not advertise port 443 before
TLS cert has been issued
- Ensure that Ingress backends do not advertise a VIPService
before TLS cert has been issued, unless the service also
exposes port 80

Updates tailscale/corp#24795

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2025-03-26 01:32:13 +00:00
Irbe Krumina
4777cc2cda ipn/store/kubestore: skip cache for the write replica in cert share mode (#15417)
ipn/store/kubestore: skip cache for the write replica in cert share mode

This is to avoid issues where stale cache after Ingress recreation
causes the certs not to be re-issued.

Updates tailscale/corp#24795

Signed-off-by: Irbe Krumina <irbe@tailscale.com>
2025-03-25 23:25:29 +00:00
James Nugent
75373896c7 tsnet: Default executable name on iOS
When compiled into TailscaleKit.framework (via the libtailscale
repository), os.Executable() returns an error instead of the name of the
executable. This commit adds another branch to the switch statement that
enumerates platforms which behave in this manner, and defaults to
"tsnet" in the same manner as those other platforms.

Fixes #15410.

Signed-off-by: James Nugent <james@jen20.com>
2025-03-25 15:28:35 -07:00
Brad Fitzpatrick
5aa1c27aad control/controlhttp: quiet "forcing port 443" log spam
Minimal mitigation that doesn't do the full refactor that's probably
warranted.

Updates #15402

Change-Id: I79fd91de0e0661d25398f7d95563982ed1d11561
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2025-03-25 14:26:24 -07:00
Jonathan Nobels
725c8d298a ipn/ipnlocal: remove misleading [unexpected] log for auditlog (#15421)
fixes tailscale/tailscale#15394

In the current iteration, usage of the memstore for the audit
logger is expected on some platforms.

Signed-off-by: Jonathan Nobels <jonathan@tailscale.com>
2025-03-25 15:05:50 -04:00
Mike O'Driscoll
08c8ccb48e prober: add address family label for udp metrics (#15413)
Add a label which differentiates the address family
for STUN checks.

Also initialize the derpprobe_attempts_total and
derpprobe_seconds_total metrics by adding 0 for
the alternate fail/ok case.

Updates tailscale/corp#27249

Signed-off-by: Mike O'Driscoll <mikeo@tailscale.com>
2025-03-25 12:49:54 -04:00
Percy Wegmann
e78055eb01 ipn/ipnlocal: add more logging for initializing peerAPIListeners
On Windows and Android, peerAPIListeners may be initialized after a link change.
This commit adds log statements to make it easier to trace this flow.

Updates #14393

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2025-03-25 06:56:50 -05:00
James Sanderson
ea79dc161d tstest/integration/testcontrol: fix AddRawMapResponse race condition
Only send a stored raw map message in reply to a streaming map response.
Otherwise a non-streaming map response might pick it up first, and
potentially drop it. This guarantees that a map response sent via
AddRawMapResponse will be picked up by the main map response loop in the
client.

Fixes #15362

Signed-off-by: James Sanderson <jsanderson@tailscale.com>
2025-03-25 10:39:54 +00:00
James Tucker
b3455fa99a cmd/natc: add some initial unit test coverage
These tests aren't perfect, nor is this complete coverage, but this is a
set of coverage that is at least stable.

Updates #15367

Signed-off-by: James Tucker <james@tailscale.com>
2025-03-24 15:08:28 -07:00
58 changed files with 860 additions and 1407 deletions

View File

@@ -1 +1 @@
3.18
3.19

View File

@@ -62,8 +62,10 @@ RUN GOARCH=$TARGETARCH go install -ldflags="\
-X tailscale.com/version.gitCommitStamp=$VERSION_GIT_HASH" \
-v ./cmd/tailscale ./cmd/tailscaled ./cmd/containerboot
FROM alpine:3.18
FROM alpine:3.19
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,5 +1,12 @@
# Copyright (c) Tailscale Inc & AUTHORS
# SPDX-License-Identifier: BSD-3-Clause
FROM alpine:3.18
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables iputils
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

View File

@@ -110,15 +110,6 @@ publishdevnameserver: ## Build and publish k8s-nameserver image to location spec
@test "${REPO}" != "ghcr.io/tailscale/k8s-nameserver" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-nameserver" && exit 1)
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=k8s-nameserver ./build_docker.sh
plan93:
GOOS=plan9 GOARCH=386 ${HOME}/hack/go/bin/go build -o ${HOME}/hack/rsc-plan9/386/bin/tailscaled ./cmd/tailscaled
GOOS=plan9 GOARCH=386 ${HOME}/hack/go/bin/go build -o ${HOME}/hack/rsc-plan9/386/bin/tailscale ./cmd/tailscale
plan9a:
GOOS=plan9 GOARCH=amd64 ${HOME}/hack/go/bin/go build -o ${HOME}/hack/rsc-plan9/amd64/bin/tailscaled ./cmd/tailscaled
GOOS=plan9 GOARCH=amd64 ${HOME}/hack/go/bin/go build -o ${HOME}/hack/rsc-plan9/amd64/bin/tailscale ./cmd/tailscale
.PHONY: sshintegrationtest
sshintegrationtest: ## Run the SSH integration tests in various Docker containers
@GOOS=linux GOARCH=amd64 ./tool/go test -tags integrationtest -c ./ssh/tailssh -o ssh/tailssh/testcontainers/tailssh.test && \

View File

@@ -1 +1 @@
1.81.0
1.83.0

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.18"
DEFAULT_BASE="tailscale/alpine-base:3.19"
# 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,6 +60,9 @@ 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) })
}
}
@@ -116,7 +119,13 @@ 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?
_, _, err := cm.lc.CertPair(ctx, domain)
// 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)
if err != nil {
log.Printf("error refreshing certificate for %s: %v", domain, err)
}

View File

@@ -128,16 +128,17 @@ func (m *manualCertManager) TLSConfig() *tls.Config {
}
func (m *manualCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
if hi.ServerName != m.hostname && !m.noHostname {
return nil, fmt.Errorf("cert mismatch with hostname: %q", hi.ServerName)
}
// if hi.ServerName != m.hostname && !m.noHostname {
// return nil, fmt.Errorf("cert mismatch with hostname: %q", hi.ServerName)
// }
// Return a shallow copy of the cert so the caller can append to its
// Certificate field.
certCopy := new(tls.Certificate)
*certCopy = *m.cert
certCopy.Certificate = certCopy.Certificate[:len(certCopy.Certificate):len(certCopy.Certificate)]
return certCopy, nil
// certCopy := new(tls.Certificate)
// *certCopy = *m.cert
// certCopy.Certificate = certCopy.Certificate[:len(certCopy.Certificate):len(certCopy.Certificate)]
// return certCopy, nil
return m.cert, nil
}
func (m *manualCertManager) HTTPHandler(fallback http.Handler) http.Handler {

View File

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

View File

@@ -49,10 +49,11 @@ 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)
@@ -241,7 +242,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); err != nil {
if err := r.ensureCertResources(ctx, pgName, dnsName, ing); err != nil {
return false, fmt.Errorf("error ensuring cert resources: %w", err)
}
@@ -338,7 +339,11 @@ 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.
if err = r.maybeUpdateAdvertiseServicesConfig(ctx, pg.Name, serviceName, true, logger); err != nil {
mode := serviceAdvertisementHTTPS
if isHTTPEndpointEnabled(ing) {
mode = serviceAdvertisementHTTPAndHTTPS
}
if err = r.maybeUpdateAdvertiseServicesConfig(ctx, pg.Name, serviceName, mode, logger); err != nil {
return false, fmt.Errorf("failed to update tailscaled config: %w", err)
}
@@ -354,11 +359,17 @@ func (r *HAIngressReconciler) maybeProvision(ctx context.Context, hostname strin
case 0:
ing.Status.LoadBalancer.Ingress = nil
default:
ports := []networkingv1.IngressPortStatus{
{
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{
Protocol: "TCP",
Port: 443,
},
})
}
if isHTTPEndpointEnabled(ing) {
ports = append(ports, networkingv1.IngressPortStatus{
@@ -366,9 +377,14 @@ 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: dnsName,
Hostname: hostname,
Ports: ports,
},
}
@@ -429,7 +445,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, false, logger); err != nil {
if err = r.maybeUpdateAdvertiseServicesConfig(ctx, proxyGroupName, vipServiceName, serviceAdvertisementOff, logger); err != nil {
return false, fmt.Errorf("failed to update tailscaled config services: %w", err)
}
_, ok := cfg.Services[vipServiceName]
@@ -512,7 +528,7 @@ func (r *HAIngressReconciler) maybeCleanup(ctx context.Context, hostname string,
}
// 4. Unadvertise the VIPService in tailscaled config.
if err = r.maybeUpdateAdvertiseServicesConfig(ctx, pg, serviceName, false, logger); err != nil {
if err = r.maybeUpdateAdvertiseServicesConfig(ctx, pg, serviceName, serviceAdvertisementOff, logger); err != nil {
return false, fmt.Errorf("failed to update tailscaled config services: %w", err)
}
@@ -709,8 +725,16 @@ func isHTTPEndpointEnabled(ing *networkingv1.Ingress) bool {
return ing.Annotations[annotationHTTPEndpoint] == "enabled"
}
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)
// 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) {
// Get all config Secrets for this ProxyGroup.
secrets := &corev1.SecretList{}
@@ -718,6 +742,21 @@ 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 {
@@ -870,8 +909,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) error {
secret := certSecret(pgName, r.tsNamespace, domain)
func (r *HAIngressReconciler) ensureCertResources(ctx context.Context, pgName, domain string, ing *networkingv1.Ingress) error {
secret := certSecret(pgName, r.tsNamespace, domain, ing)
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)
}
@@ -966,9 +1005,14 @@ 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) *corev1.Secret {
func certSecret(pgName, namespace, domain string, ing *networkingv1.Ingress) *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",
@@ -989,9 +1033,9 @@ func certSecret(pgName, namespace, domain string) *corev1.Secret {
func certResourceLabels(pgName, domain string) map[string]string {
return map[string]string{
kubetypes.LabelManaged: "true",
"tailscale.com/proxy-group": pgName,
"tailscale.com/domain": domain,
kubetypes.LabelManaged: "true",
labelProxyGroup: pgName,
labelDomain: domain,
}
}
@@ -1004,3 +1048,28 @@ 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,6 +31,7 @@ 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"
)
@@ -59,7 +60,7 @@ func TestIngressPGReconciler(t *testing.T) {
},
},
TLS: []networkingv1.IngressTLS{
{Hosts: []string{"my-svc.tailnetxyz.ts.net"}},
{Hosts: []string{"my-svc"}},
},
},
}
@@ -67,12 +68,14 @@ 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 cert resources were created for the first Ingress
expectEqual(t, fc, certSecret("test-pg", "operator-ns", "my-svc.ts.net"))
// 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.
expectEqual(t, fc, certSecretRole("test-pg", "operator-ns", "my-svc.ts.net"))
expectEqual(t, fc, certSecretRoleBinding("test-pg", "operator-ns", "my-svc.ts.net"))
@@ -127,11 +130,13 @@ 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 cert resources were created for the second Ingress
expectEqual(t, fc, certSecret("test-pg", "operator-ns", "my-other-svc.ts.net"))
// 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.
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"))
@@ -231,7 +236,7 @@ func TestIngressPGReconciler_UpdateIngressHostname(t *testing.T) {
},
},
TLS: []networkingv1.IngressTLS{
{Hosts: []string{"my-svc.tailnetxyz.ts.net"}},
{Hosts: []string{"my-svc"}},
},
},
}
@@ -239,15 +244,19 @@ 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.tailnetxyz.ts.net"
ing.Spec.TLS[0].Hosts[0] = "updated-svc"
})
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"})
@@ -468,6 +477,8 @@ 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)
@@ -611,6 +622,7 @@ 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)
@@ -804,3 +816,28 @@ 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,7 +9,6 @@ package main
import (
"context"
"fmt"
"net/http"
"os"
"regexp"
@@ -40,7 +39,6 @@ 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"
@@ -333,40 +331,6 @@ 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.
@@ -1039,45 +1003,6 @@ 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 {
@@ -1108,36 +1033,6 @@ 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 {
@@ -1258,63 +1153,7 @@ 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,18 +94,24 @@ func main() {
}
ignoreDstTable.Insert(pfx, true)
}
var v4Prefixes []netip.Prefix
var (
v4Prefixes []netip.Prefix
numV4DNSAddrs int
)
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,
}
@@ -153,12 +159,13 @@ func main() {
}
c := &connector{
ts: ts,
lc: lc,
dnsAddr: dnsAddr,
v4Ranges: v4Prefixes,
v6ULA: ula(uint16(*siteID)),
ignoreDsts: ignoreDstTable,
ts: ts,
lc: lc,
dnsAddr: dnsAddr,
v4Ranges: v4Prefixes,
numV4DNSAddrs: numV4DNSAddrs,
v6ULA: ula(uint16(*siteID)),
ignoreDsts: ignoreDstTable,
}
c.run(ctx)
}
@@ -177,6 +184,11 @@ 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
@@ -502,6 +514,7 @@ 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
@@ -547,17 +560,25 @@ 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
for _, r := range ps.c.v4Ranges {
ip := randV4(r)
for r.Contains(ip) {
// 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")
}
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.
@@ -583,6 +604,7 @@ 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[:])

429
cmd/natc/natc_test.go Normal file
View File

@@ -0,0 +1,429 @@
// 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

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build (linux || darwin || freebsd || openbsd || plan9) && !ts_omit_ssh
//go:build (linux || darwin || freebsd || openbsd) && !ts_omit_ssh
package main

View File

@@ -82,9 +82,7 @@ func defaultTunName() string {
// "utun" is recognized by wireguard-go/tun/tun_darwin.go
// as a magic value that uses/creates any free number.
return "utun"
case "plan9":
return "auto"
case "aix", "solaris", "illumos":
case "plan9", "aix", "solaris", "illumos":
return "userspace-networking"
case "linux":
switch distro.Get() {
@@ -182,10 +180,6 @@ func main() {
return
}
if runtime.GOOS == "plan9" && os.Getenv("_NETSHELL_CHILD_") != "" {
os.Args = []string{"tailscaled", "be-child", "plan9-netshell"}
}
if len(os.Args) > 1 {
sub := os.Args[1]
if fp, ok := subCommands[sub]; ok {
@@ -236,18 +230,7 @@ func main() {
// Only apply a default statepath when neither have been provided, so that a
// user may specify only --statedir if they wish.
if args.statepath == "" && args.statedir == "" {
if runtime.GOOS == "plan9" {
home, err := os.UserHomeDir()
if err != nil {
log.Fatalf("failed to get home directory: %v", err)
}
args.statedir = filepath.Join(home, "tailscale-state")
if err := os.MkdirAll(args.statedir, 0700); err != nil {
log.Fatalf("failed to create state directory: %v", err)
}
} else {
args.statepath = paths.DefaultTailscaledStateFile()
}
args.statepath = paths.DefaultTailscaledStateFile()
}
if args.disableLogs {
@@ -748,12 +731,6 @@ func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack boo
return false, err
}
if runtime.GOOS == "plan9" {
// TODO(bradfitz): why don't we do this on all platforms?
// We should. Doing it just on plan9 for now conservatively.
sys.NetMon.Get().SetTailscaleInterfaceName(devName)
}
r, err := router.New(logf, dev, sys.NetMon.Get(), sys.HealthTracker())
if err != nil {
dev.Close()

View File

@@ -96,6 +96,9 @@ 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")
@@ -246,11 +249,6 @@ func (a *Dialer) dial(ctx context.Context) (*ClientConn, error) {
results[i].conn = nil // so we don't close it in the defer
return conn, nil
}
if ctx.Err() != nil {
a.logf("controlhttp: context aborted dialing")
return nil, ctx.Err()
}
merr := multierr.New(errs...)
// If we get here, then we didn't get anywhere with our dial plan; fall back to just using DNS.
@@ -283,7 +281,9 @@ 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.
d.logf("controlhttp: forcing port 443 dial due to recent noise dial")
if d.logPort80Failure.CompareAndSwap(true, false) {
d.logf("controlhttp: forcing port 443 dial due to recent noise dial")
}
return true
}

View File

@@ -6,6 +6,7 @@ package controlhttp
import (
"net/http"
"net/url"
"sync/atomic"
"time"
"tailscale.com/health"
@@ -90,6 +91,11 @@ 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

@@ -40,7 +40,7 @@ func CanRunTailscaleSSH() error {
if version.IsSandboxedMacOS() {
return errors.New("The Tailscale SSH server does not run in sandboxed Tailscale GUI builds.")
}
case "freebsd", "openbsd", "plan9":
case "freebsd", "openbsd":
default:
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
}

6
go.mod
View File

@@ -3,7 +3,6 @@ module tailscale.com
go 1.24.0
require (
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f
filippo.io/mkcert v1.4.4
fyne.io/systray v1.11.0
github.com/akutz/memconn v0.1.0
@@ -37,7 +36,6 @@ require (
github.com/go-json-experiment/json v0.0.0-20250223041408-d3c622f1b874
github.com/go-logr/zapr v1.3.0
github.com/go-ole/go-ole v1.3.0
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737
github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
github.com/golang/snappy v0.0.4
@@ -86,12 +84,12 @@ require (
github.com/tailscale/setec v0.0.0-20250205144240-8898a29c3fbb
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6
github.com/tailscale/wireguard-go v0.0.0-20250304000100-91a0587fb251
github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e
github.com/tc-hib/winres v0.2.1
github.com/tcnksm/go-httpstat v0.2.0
github.com/toqueteos/webbrowser v1.2.0
github.com/u-root/u-root v0.14.0
github.com/u-root/u-root v0.12.0
github.com/vishvananda/netns v0.0.4
go.uber.org/zap v1.27.0
go4.org/mem v0.0.0-20240501181205-ae6ca9944745

20
go.sum
View File

@@ -2,8 +2,6 @@
4d63.com/gocheckcompilerdirectives v1.2.1/go.mod h1:yjDJSxmDTtIHHCqX0ufRYZDL6vQtMG7tJdKVeWwsqvs=
4d63.com/gochecknoglobals v0.2.1 h1:1eiorGsgHOFOuoOiJDy2psSrQbRdIHrlge0IJIkUgDc=
4d63.com/gochecknoglobals v0.2.1/go.mod h1:KRE8wtJB3CXCsb1xy421JfTHIIbmT3U5ruxw2Qu8fSU=
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f h1:1C7nZuxUMNz7eiQALRfiqNOm04+m3edWlRff/BYHf0Q=
9fans.net/go v0.0.8-0.20250307142834-96bdba94b63f/go.mod h1:hHyrZRryGqVdqrknjq5OWDLGCTJ2NeEvtrpR96mjraM=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -391,8 +389,6 @@ github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsM
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/go-xmlfmt/xmlfmt v1.1.2 h1:Nea7b4icn8s57fTx1M5AI4qQT5HEM3rVUO8MuE6g80U=
github.com/go-xmlfmt/xmlfmt v1.1.2/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737 h1:cf60tHxREO3g1nroKr2osU3JWZsJzkfi7rEg+oAB0Lo=
github.com/go4org/plan9netshell v0.0.0-20250324183649-788daa080737/go.mod h1:MIS0jDzbU/vuM9MC4YnBITCv+RYuTRq8dJzmCrFsK9g=
github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4=
github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
@@ -549,8 +545,8 @@ github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSo
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=
github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/hugelgupf/vmtest v0.0.0-20240216064925-0561770280a1 h1:jWoR2Yqg8tzM0v6LAiP7i1bikZJu3gxpgvu3g1Lw+a0=
github.com/hugelgupf/vmtest v0.0.0-20240216064925-0561770280a1/go.mod h1:B63hDJMhTupLWCHwopAyEo7wRFowx9kOc8m8j1sfOqE=
github.com/hugelgupf/vmtest v0.0.0-20240102225328-693afabdd27f h1:ov45/OzrJG8EKbGjn7jJZQJTN7Z1t73sFYNIRd64YlI=
github.com/hugelgupf/vmtest v0.0.0-20240102225328-693afabdd27f/go.mod h1:JoDrYMZpDPYo6uH9/f6Peqms3zNNWT2XiGgioMOIGuI=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/illarion/gonotify/v3 v3.0.2 h1:O7S6vcopHexutmpObkeWsnzMJt/r1hONIEogeVNmJMk=
github.com/illarion/gonotify/v3 v3.0.2/go.mod h1:HWGPdPe817GfvY3w7cx6zkbzNZfi3QjcBm/wgVvEL1U=
@@ -926,8 +922,8 @@ github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:U
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M=
github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y=
github.com/tailscale/wireguard-go v0.0.0-20250304000100-91a0587fb251 h1:h/41LFTrwMxB9Xvvug0kRdQCU5TlV1+pAMQw0ZtDE3U=
github.com/tailscale/wireguard-go v0.0.0-20250304000100-91a0587fb251/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19 h1:BcEJP2ewTIK2ZCsqgl6YGpuO6+oKqqag5HHb7ehljKw=
github.com/tailscale/wireguard-go v0.0.0-20250107165329-0b8b35511f19/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek=
github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg=
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
@@ -954,10 +950,10 @@ github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+
github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
github.com/u-root/gobusybox/src v0.0.0-20240225013946-a274a8d5d83a h1:eg5FkNoQp76ZsswyGZ+TjYqA/rhKefxK8BW7XOlQsxo=
github.com/u-root/gobusybox/src v0.0.0-20240225013946-a274a8d5d83a/go.mod h1:e/8TmrdreH0sZOw2DFKBaUV7bvDWRq6SeM9PzkuVM68=
github.com/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg=
github.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1WMluqE=
github.com/u-root/gobusybox/src v0.0.0-20231228173702-b69f654846aa h1:unMPGGK/CRzfg923allsikmvk2l7beBeFPUNC4RVX/8=
github.com/u-root/gobusybox/src v0.0.0-20231228173702-b69f654846aa/go.mod h1:Zj4Tt22fJVn/nz/y6Ergm1SahR9dio1Zm/D2/S0TmXM=
github.com/u-root/u-root v0.12.0 h1:K0AuBFriwr0w/PGS3HawiAw89e3+MU7ks80GpghAsNs=
github.com/u-root/u-root v0.12.0/go.mod h1:FYjTOh4IkIZHhjsd17lb8nYW6udgXdJhG1c0r6u0arI=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8=

View File

@@ -958,7 +958,9 @@ func (b *LocalBackend) linkChange(delta *netmon.ChangeDelta) {
if peerAPIListenAsync && b.netMap != nil && b.state == ipn.Running {
want := b.netMap.GetAddresses().Len()
if len(b.peerAPIListeners) < want {
have := len(b.peerAPIListeners)
b.logf("[v1] linkChange: have %d peerAPIListeners, want %d", have, want)
if have < want {
b.logf("linkChange: peerAPIListeners too low; trying again")
b.goTracker.Go(b.initPeerAPIListener)
}
@@ -2402,11 +2404,9 @@ 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 {
b.logf("auditlog: [unexpected] no persistent audit log storage configured. using memory store.")
// Use memory store by default if no explicit store is provided.
store = auditlog.NewLogStore(&memstore.Store{})
}
@@ -5001,11 +5001,6 @@ func shouldUseOneCGNATRoute(logf logger.Logf, mon *netmon.Monitor, controlKnobs
}
}
if versionOS == "plan9" {
// Just temporarily during plan9 bringup to have fewer routes to debug.
return true
}
// Also prefer to do this on the Mac, so that we don't need to constantly
// update the network extension configuration (which is disruptive to
// Chrome, see https://github.com/tailscale/tailscale/issues/3102). Only
@@ -5374,6 +5369,7 @@ 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

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux || (darwin && !ios) || freebsd || openbsd || plan9
//go:build linux || (darwin && !ios) || freebsd || openbsd
package ipnlocal

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build ios || (!linux && !darwin && !freebsd && !openbsd && !plan9)
//go:build ios || (!linux && !darwin && !freebsd && !openbsd)
package ipnlocal

View File

@@ -331,7 +331,7 @@ func (a *actor) Permissions(operatorUID string) (read, write bool) {
// checks here. Note that this permission model is being changed in
// tailscale/corp#18342.
return true, true
case "js", "plan9":
case "js":
return true, true
}
if a.ci.IsUnixSock() {

View File

@@ -143,15 +143,6 @@ 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,
@@ -166,19 +157,32 @@ func (s *Store) WriteTLSCertAndKey(domain string, cert, key []byte) (err error)
keyTLSKey: key,
}
}
return s.updateSecret(data, secretName)
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
}
// 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))
@@ -186,16 +190,12 @@ func (s *Store) ReadTLSCertAndKey(domain string) (cert, key []byte, err error) {
return cert, key, nil
}
}
if s.certShareMode != "ro" {
if s.certShareMode == "" {
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,9 +212,18 @@ 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.
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.
//
// 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)
}
return cert, key, nil
}

View File

@@ -201,10 +201,6 @@ 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",
@@ -219,10 +215,6 @@ 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",
@@ -367,7 +359,7 @@ func TestReadTLSCertAndKey(t *testing.T) {
wantMemoryStore map[ipn.StateKey][]byte
}{
{
name: "found",
name: "found_in_memory",
memoryStore: map[ipn.StateKey][]byte{
"my-app.tailnetxyz.ts.net.crt": []byte(testCert),
"my-app.tailnetxyz.ts.net.key": []byte(testKey),
@@ -381,7 +373,7 @@ func TestReadTLSCertAndKey(t *testing.T) {
},
},
{
name: "not_found",
name: "not_found_in_memory",
domain: testDomain,
wantErr: ipn.ErrStateNotExist,
},
@@ -400,6 +392,17 @@ 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. Supported types are egress and ingress.<br />Type is immutable once a ProxyGroup is created. | | Enum: [egress ingress] <br />Type: string <br /> |
| `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 /> |
| `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. Supported types are egress and ingress.
// Type of the ProxyGroup proxies. Currently the only supported type is egress.
// 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

@@ -627,7 +627,7 @@ func (opts Options) New() *Policy {
conf.IncludeProcSequence = true
}
if envknob.NoLogsNoSupport() || testenv.InTest() || runtime.GOOS == "plan9" {
if envknob.NoLogsNoSupport() || testenv.InTest() {
opts.Logf("You have disabled logging. Tailscale will not be able to provide support.")
conf.HTTPC = &http.Client{Transport: noopPretendSuccessTransport{}}
} else {

View File

@@ -284,7 +284,7 @@ func (m *Manager) compileConfig(cfg Config) (rcfg resolver.Config, ocfg OSConfig
// Deal with trivial configs first.
switch {
case !cfg.needsOSResolver() || runtime.GOOS == "plan9":
case !cfg.needsOSResolver():
// Set search domains, but nothing else. This also covers the
// case where cfg is entirely zero, in which case these
// configs clear all Tailscale DNS settings.

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !linux && !freebsd && !openbsd && !windows && !darwin && !illumos && !solaris && !plan9
//go:build !linux && !freebsd && !openbsd && !windows && !darwin && !illumos && !solaris
package dns

View File

@@ -1,181 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// TODO: man 6 ndb | grep -e 'suffix.*same line'
// to detect Russ's https://9fans.topicbox.com/groups/9fans/T9c9d81b5801a0820/ndb-suffix-specific-dns-changes
package dns
import (
"bufio"
"bytes"
"fmt"
"io"
"net/netip"
"os"
"regexp"
"strings"
"unicode"
"tailscale.com/control/controlknobs"
"tailscale.com/health"
"tailscale.com/types/logger"
"tailscale.com/util/set"
)
func NewOSConfigurator(logf logger.Logf, ht *health.Tracker, knobs *controlknobs.Knobs, interfaceName string) (OSConfigurator, error) {
return &plan9DNSManager{
logf: logf,
ht: ht,
knobs: knobs,
}, nil
}
type plan9DNSManager struct {
logf logger.Logf
ht *health.Tracker
knobs *controlknobs.Knobs
}
// netNDBBytesWithoutTailscale returns raw (the contents of /net/ndb) with any
// Tailscale bits removed.
func netNDBBytesWithoutTailscale(raw []byte) ([]byte, error) {
var ret bytes.Buffer
bs := bufio.NewScanner(bytes.NewReader(raw))
removeLine := set.Set[string]{}
for bs.Scan() {
t := bs.Text()
if rest, ok := strings.CutPrefix(t, "#tailscaled-added-line:"); ok {
removeLine.Add(strings.TrimSpace(rest))
continue
}
trimmed := strings.TrimSpace(t)
if removeLine.Contains(trimmed) {
removeLine.Delete(trimmed)
continue
}
// Also remove any DNS line referencing *.ts.net. This is
// Tailscale-specific (and won't work with, say, Headscale), but
// the Headscale case will be covered by the #tailscaled-added-line
// logic above, assuming the user didn't delete those comments.
if (strings.HasPrefix(trimmed, "dns=") || strings.Contains(trimmed, "dnsdomain=")) &&
strings.HasSuffix(trimmed, ".ts.net") {
continue
}
ret.WriteString(t)
ret.WriteByte('\n')
}
return ret.Bytes(), bs.Err()
}
// setNDBSuffix adds lines to tsFree (the contents of /net/ndb already cleaned
// of Tailscale-added lines) to add the optional DNS search domain (e.g.
// "foo.ts.net") and DNS server to it.
func setNDBSuffix(tsFree []byte, suffix string) []byte {
suffix = strings.TrimSuffix(suffix, ".")
if suffix == "" {
return tsFree
}
var buf bytes.Buffer
bs := bufio.NewScanner(bytes.NewReader(tsFree))
var added []string
addLine := func(s string) {
added = append(added, strings.TrimSpace(s))
buf.WriteString(s)
}
for bs.Scan() {
buf.Write(bs.Bytes())
buf.WriteByte('\n')
t := bs.Text()
if suffix != "" && len(added) == 0 && strings.HasPrefix(t, "\tdns=") {
addLine(fmt.Sprintf("\tdns=100.100.100.100 suffix=%s\n", suffix))
addLine(fmt.Sprintf("\tdnsdomain=%s\n", suffix))
}
}
bufTrim := bytes.TrimLeftFunc(buf.Bytes(), unicode.IsSpace)
if len(added) == 0 {
return bufTrim
}
var ret bytes.Buffer
for _, s := range added {
ret.WriteString("#tailscaled-added-line: ")
ret.WriteString(s)
ret.WriteString("\n")
}
ret.WriteString("\n")
ret.Write(bufTrim)
return ret.Bytes()
}
func (m *plan9DNSManager) SetDNS(c OSConfig) error {
ndbOnDisk, err := os.ReadFile("/net/ndb")
if err != nil {
return err
}
tsFree, err := netNDBBytesWithoutTailscale(ndbOnDisk)
if err != nil {
return err
}
var suffix string
if len(c.SearchDomains) > 0 {
suffix = string(c.SearchDomains[0])
}
newBuf := setNDBSuffix(tsFree, suffix)
if !bytes.Equal(newBuf, ndbOnDisk) {
if err := os.WriteFile("/net/ndb", newBuf, 0644); err != nil {
return fmt.Errorf("writing /net/ndb: %w", err)
}
if f, err := os.OpenFile("/net/dns", os.O_RDWR, 0); err == nil {
if _, err := io.WriteString(f, "refresh\n"); err != nil {
f.Close()
return fmt.Errorf("/net/dns refresh write: %w", err)
}
if err := f.Close(); err != nil {
return fmt.Errorf("/net/dns refresh close: %w", err)
}
}
}
return nil
}
func (m *plan9DNSManager) SupportsSplitDNS() bool { return false }
func (m *plan9DNSManager) Close() error {
// TODO(bradfitz): remove the Tailscale bits from /net/ndb ideally
return nil
}
var dnsRegex = regexp.MustCompile(`\bdns=(\d+\.\d+\.\d+\.\d+)\b`)
func (m *plan9DNSManager) GetBaseConfig() (OSConfig, error) {
var oc OSConfig
f, err := os.Open("/net/ndb")
if err != nil {
return oc, err
}
defer f.Close()
bs := bufio.NewScanner(f)
for bs.Scan() {
m := dnsRegex.FindSubmatch(bs.Bytes())
if m == nil {
continue
}
addr, err := netip.ParseAddr(string(m[1]))
if err != nil {
continue
}
oc.Nameservers = append(oc.Nameservers, addr)
}
if err := bs.Err(); err != nil {
return oc, err
}
return oc, nil
}

View File

@@ -1,86 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build plan9
package dns
import "testing"
func TestNetNDBBytesWithoutTailscale(t *testing.T) {
tests := []struct {
name string
raw string
want string
}{
{
name: "empty",
raw: "",
want: "",
},
{
name: "no-tailscale",
raw: "# This is a comment\nip=10.0.2.15 ipmask=255.255.255.0 ipgw=10.0.2.2\n\tsys=gnot\n",
want: "# This is a comment\nip=10.0.2.15 ipmask=255.255.255.0 ipgw=10.0.2.2\n\tsys=gnot\n",
},
{
name: "remove-by-comments",
raw: "# This is a comment\n#tailscaled-added-line: dns=100.100.100.100\nip=10.0.2.15 ipmask=255.255.255.0 ipgw=10.0.2.2\n\tdns=100.100.100.100\n\tsys=gnot\n",
want: "# This is a comment\nip=10.0.2.15 ipmask=255.255.255.0 ipgw=10.0.2.2\n\tsys=gnot\n",
},
{
name: "remove-by-ts.net",
raw: "Some line\n\tdns=100.100.100.100 suffix=foo.ts.net\n\tfoo=bar\n",
want: "Some line\n\tfoo=bar\n",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := netNDBBytesWithoutTailscale([]byte(tt.raw))
if err != nil {
t.Fatal(err)
}
if string(got) != tt.want {
t.Errorf("GOT:\n%s\n\nWANT:\n%s\n", string(got), tt.want)
}
})
}
}
func TestSetNDBSuffix(t *testing.T) {
tests := []struct {
name string
raw string
want string
}{
{
name: "empty",
raw: "",
want: "",
},
{
name: "set",
raw: "ip=10.0.2.15 ipmask=255.255.255.0 ipgw=10.0.2.2\n\tsys=gnot\n\tdns=100.100.100.100\n\n# foo\n",
want: `#tailscaled-added-line: dns=100.100.100.100 suffix=foo.ts.net
#tailscaled-added-line: dnsdomain=foo.ts.net
ip=10.0.2.15 ipmask=255.255.255.0 ipgw=10.0.2.2
sys=gnot
dns=100.100.100.100
dns=100.100.100.100 suffix=foo.ts.net
dnsdomain=foo.ts.net
# foo
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := setNDBSuffix([]byte(tt.raw), "foo.ts.net")
if string(got) != tt.want {
t.Errorf("wrong value\n GOT %q:\n%s\n\nWANT %q:\n%s\n", got, got, tt.want, tt.want)
}
})
}
}

View File

@@ -1188,10 +1188,6 @@ func (c *Client) measureAllICMPLatency(ctx context.Context, rs *reportState, nee
if len(need) == 0 {
return nil
}
if runtime.GOOS == "plan9" {
// ICMP isn't implemented.
return nil
}
ctx, done := context.WithTimeout(ctx, icmpProbeTimeout)
defer done()

View File

@@ -596,7 +596,7 @@ func (m *Monitor) pollWallTime() {
//
// We don't do this on mobile platforms for battery reasons, and because these
// platforms don't really sleep in the same way.
const shouldMonitorTimeJump = runtime.GOOS != "android" && runtime.GOOS != "ios" && runtime.GOOS != "plan9"
const shouldMonitorTimeJump = runtime.GOOS != "android" && runtime.GOOS != "ios"
// checkWallTimeAdvanceLocked reports whether wall time jumped more than 150% of
// pollWallTimeInterval, indicating we probably just came out of sleep. Once a

View File

@@ -242,7 +242,7 @@ func changeAffectsConn(delta *netmon.ChangeDelta, conn net.Conn) bool {
// In a few cases, we don't have a new DefaultRouteInterface (e.g. on
// Android; see tailscale/corp#19124); if so, pessimistically assume
// that all connections are affected.
if delta.New.DefaultRouteInterface == "" && runtime.GOOS != "plan9" {
if delta.New.DefaultRouteInterface == "" {
return true
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build aix || solaris || illumos
//go:build plan9 || aix || solaris || illumos
package tstun

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !wasm && !tamago && !aix && !solaris && !illumos
//go:build !wasm && !plan9 && !tamago && !aix && !solaris && !illumos
// Package tun creates a tuntap device, working around OS-specific
// quirks if necessary.
@@ -9,9 +9,6 @@ package tstun
import (
"errors"
"fmt"
"log"
"os"
"runtime"
"strings"
"time"
@@ -48,9 +45,6 @@ func New(logf logger.Logf, tunName string) (tun.Device, string, error) {
}
dev, err = CreateTAP.Get()(logf, tapName, bridgeName)
} else {
if runtime.GOOS == "plan9" {
cleanUpPlan9Interfaces()
}
dev, err = tun.CreateTUN(tunName, int(DefaultTUNMTU()))
}
if err != nil {
@@ -71,35 +65,6 @@ func New(logf logger.Logf, tunName string) (tun.Device, string, error) {
return dev, name, nil
}
func cleanUpPlan9Interfaces() {
maybeUnbind := func(n int) {
b, err := os.ReadFile(fmt.Sprintf("/net/ipifc/%d/status", n))
if err != nil {
return
}
status := string(b)
if !(strings.HasPrefix(status, "device maxtu ") ||
strings.Contains(status, "fd7a:115c:a1e0:")) {
return
}
f, err := os.OpenFile(fmt.Sprintf("/net/ipifc/%d/ctl", n), os.O_RDWR, 0)
if err != nil {
return
}
defer f.Close()
if _, err := fmt.Fprintf(f, "unbind\n"); err != nil {
log.Printf("unbind interface %v: %v", n, err)
return
}
log.Printf("tun: unbound stale interface %v", n)
}
// A common case: after unclean shutdown, the /net/ipifc/clone file
for n := 2; n < 5; n++ {
maybeUnbind(n)
}
}
// tunDiagnoseFailure, if non-nil, does OS-specific diagnostics of why
// TUN failed to work.
var tunDiagnoseFailure func(tunName string, logf logger.Logf, err error)

View File

@@ -928,10 +928,8 @@ func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
// packet from OS read and sent to WG
res, ok := <-t.vectorOutbound
if !ok {
t.logf("XXX Wrapper.vectorInbound done")
return 0, io.EOF
}
// t.logf("XXX Wrapper.vec in: err=%v, len(data)=%d, offset=%d", res.err, len(res.data), offset)
if res.err != nil && len(res.data) == 0 {
return 0, res.err
}
@@ -949,7 +947,6 @@ func (t *Wrapper) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
var buffsGRO *gro.GRO
for _, data := range res.data {
p.Decode(data[res.dataOffset:])
// t.logf("XXX Wrapper.Read decode (off=%d): %v", res.dataOffset, p.String())
if m := t.destIPActivity.Load(); m != nil {
if fn := m[p.Dst.Addr()]; fn != nil {

View File

@@ -1,122 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package portlist
import (
"bufio"
"bytes"
"os"
"strconv"
"strings"
"time"
)
func init() {
newOSImpl = newPlan9Impl
pollInterval = 5 * time.Second
}
type plan9Impl struct {
known map[protoPort]*portMeta // inode string => metadata
br *bufio.Reader // reused
portsBuf []Port
includeLocalhost bool
}
type protoPort struct {
proto string
port uint16
}
type portMeta struct {
port Port
keep bool
}
func newPlan9Impl(includeLocalhost bool) osImpl {
return &plan9Impl{
known: map[protoPort]*portMeta{},
br: bufio.NewReader(bytes.NewReader(nil)),
includeLocalhost: includeLocalhost,
}
}
func (*plan9Impl) Close() error { return nil }
func (im *plan9Impl) AppendListeningPorts(base []Port) ([]Port, error) {
ret := base
des, err := os.ReadDir("/proc")
if err != nil {
return nil, err
}
for _, de := range des {
if !de.IsDir() {
continue
}
pidStr := de.Name()
pid, err := strconv.Atoi(pidStr)
if err != nil {
continue
}
st, _ := os.ReadFile("/proc/" + pidStr + "/fd")
if !bytes.Contains(st, []byte("/net/tcp/clone")) {
continue
}
args, _ := os.ReadFile("/proc/" + pidStr + "/args")
procName := string(bytes.TrimSpace(args))
// term% cat /proc/417/fd
// /usr/glenda
// 0 r M 35 (0000000000000001 0 00) 16384 260 /dev/cons
// 1 w c 0 (000000000000000a 0 00) 0 471 /dev/null
// 2 w M 35 (0000000000000001 0 00) 16384 108 /dev/cons
// 3 rw I 0 (000000000000002c 0 00) 0 14 /net/tcp/clone
for line := range bytes.Lines(st) {
if !bytes.Contains(line, []byte("/net/tcp/clone")) {
continue
}
f := strings.Fields(string(line))
if len(f) < 10 {
continue
}
if f[9] != "/net/tcp/clone" {
continue
}
qid, err := strconv.ParseUint(strings.TrimPrefix(f[4], "("), 16, 64)
if err != nil {
continue
}
tcpN := (qid >> 5) & (1<<12 - 1)
tcpNStr := strconv.FormatUint(tcpN, 10)
st, _ := os.ReadFile("/net/tcp/" + tcpNStr + "/status")
if !bytes.Contains(st, []byte("Listen ")) {
// Unexpected. Or a race.
continue
}
bl, _ := os.ReadFile("/net/tcp/" + tcpNStr + "/local")
i := bytes.LastIndexByte(bl, '!')
if i == -1 {
continue
}
if bytes.HasPrefix(bl, []byte("127.0.0.1!")) && !im.includeLocalhost {
continue
}
portStr := strings.TrimSpace(string(bl[i+1:]))
port, _ := strconv.Atoi(portStr)
if port == 0 {
continue
}
ret = append(ret, Port{
Proto: "tcp",
Port: uint16(port),
Process: procName,
Pid: pid,
})
}
}
return ret, nil
}

View File

@@ -596,11 +596,23 @@ 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",
Class: "derp_udp",
Labels: initLabels,
}
}

View File

@@ -404,10 +404,14 @@ 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

@@ -7,13 +7,119 @@ package safesocket
import (
"context"
"fmt"
"net"
"os"
"syscall"
"time"
"golang.org/x/sys/plan9"
)
func connect(_ context.Context, path string) (net.Conn, error) {
return net.Dial("tcp", "localhost:5252")
// Plan 9's devsrv srv(3) is a server registry and
// it is conventionally bound to "/srv" in the default
// namespace. It is "a one level directory for holding
// already open channels to services". Post one end of
// a pipe to "/srv/tailscale.sock" and use the other
// end for communication with a requestor. Plan 9 pipes
// are bidirectional.
type plan9SrvAddr string
func (sl plan9SrvAddr) Network() string {
return "/srv"
}
func listen(path string) (net.Listener, error) {
return net.Listen("tcp", "localhost:5252")
func (sl plan9SrvAddr) String() string {
return string(sl)
}
// There is no net.FileListener for Plan 9 at this time
type plan9SrvListener struct {
name string
srvf *os.File
file *os.File
}
func (sl *plan9SrvListener) Accept() (net.Conn, error) {
// sl.file is the server end of the pipe that's
// connected to /srv/tailscale.sock
return plan9FileConn{name: sl.name, file: sl.file}, nil
}
func (sl *plan9SrvListener) Close() error {
sl.file.Close()
return sl.srvf.Close()
}
func (sl *plan9SrvListener) Addr() net.Addr {
return plan9SrvAddr(sl.name)
}
type plan9FileConn struct {
name string
file *os.File
}
func (fc plan9FileConn) Read(b []byte) (n int, err error) {
return fc.file.Read(b)
}
func (fc plan9FileConn) Write(b []byte) (n int, err error) {
return fc.file.Write(b)
}
func (fc plan9FileConn) Close() error {
return fc.file.Close()
}
func (fc plan9FileConn) LocalAddr() net.Addr {
return plan9SrvAddr(fc.name)
}
func (fc plan9FileConn) RemoteAddr() net.Addr {
return plan9SrvAddr(fc.name)
}
func (fc plan9FileConn) SetDeadline(t time.Time) error {
return syscall.EPLAN9
}
func (fc plan9FileConn) SetReadDeadline(t time.Time) error {
return syscall.EPLAN9
}
func (fc plan9FileConn) SetWriteDeadline(t time.Time) error {
return syscall.EPLAN9
}
func connect(_ context.Context, path string) (net.Conn, error) {
f, err := os.OpenFile(path, os.O_RDWR, 0666)
if err != nil {
return nil, err
}
return plan9FileConn{name: path, file: f}, nil
}
// Create an entry in /srv, open a pipe, write the
// client end to the entry and return the server
// end of the pipe to the caller. When the server
// end of the pipe is closed, /srv name associated
// with it will be removed (controlled by ORCLOSE flag)
func listen(path string) (net.Listener, error) {
const O_RCLOSE = 64 // remove on close; should be in plan9 package
var pip [2]int
err := plan9.Pipe(pip[:])
if err != nil {
return nil, err
}
defer plan9.Close(pip[1])
srvfd, err := plan9.Create(path, plan9.O_WRONLY|plan9.O_CLOEXEC|O_RCLOSE, 0600)
if err != nil {
return nil, err
}
srv := os.NewFile(uintptr(srvfd), path)
_, err = fmt.Fprintf(srv, "%d", pip[1])
if err != nil {
return nil, err
}
return &plan9SrvListener{name: path, srvf: srv, file: os.NewFile(uintptr(pip[0]), path)}, nil
}

View File

@@ -1,421 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// This file contains the plan9-specific version of the incubator. Tailscaled
// launches the incubator as the same user as it was launched as. The
// incubator then registers a new session with the OS, sets its UID
// and groups to the specified `--uid`, `--gid` and `--groups`, and
// then launches the requested `--cmd`.
package tailssh
import (
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"os/exec"
"runtime"
"strconv"
"strings"
"sync/atomic"
"github.com/go4org/plan9netshell"
"github.com/pkg/sftp"
"tailscale.com/cmd/tailscaled/childproc"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
)
func init() {
childproc.Add("ssh", beIncubator)
childproc.Add("sftp", beSFTP)
childproc.Add("plan9-netshell", beNetshell)
}
// newIncubatorCommand returns a new exec.Cmd configured with
// `tailscaled be-child ssh` as the entrypoint.
//
// If ss.srv.tailscaledPath is empty, this method is equivalent to
// exec.CommandContext.
//
// The returned Cmd.Env is guaranteed to be nil; the caller populates it.
func (ss *sshSession) newIncubatorCommand(logf logger.Logf) (cmd *exec.Cmd, err error) {
defer func() {
if cmd.Env != nil {
panic("internal error")
}
}()
var isSFTP, isShell bool
switch ss.Subsystem() {
case "sftp":
isSFTP = true
case "":
isShell = ss.RawCommand() == ""
default:
panic(fmt.Sprintf("unexpected subsystem: %v", ss.Subsystem()))
}
if ss.conn.srv.tailscaledPath == "" {
if isSFTP {
// SFTP relies on the embedded Go-based SFTP server in tailscaled,
// so without tailscaled, we can't serve SFTP.
return nil, errors.New("no tailscaled found on path, can't serve SFTP")
}
loginShell := ss.conn.localUser.LoginShell()
logf("directly running /bin/rc -c %q", ss.RawCommand())
return exec.CommandContext(ss.ctx, loginShell, "-c", ss.RawCommand()), nil
}
lu := ss.conn.localUser
ci := ss.conn.info
remoteUser := ci.uprof.LoginName
if ci.node.IsTagged() {
remoteUser = strings.Join(ci.node.Tags().AsSlice(), ",")
}
incubatorArgs := []string{
"be-child",
"ssh",
// TODO: "--uid=" + lu.Uid,
// TODO: "--gid=" + lu.Gid,
"--local-user=" + lu.Username,
"--home-dir=" + lu.HomeDir,
"--remote-user=" + remoteUser,
"--remote-ip=" + ci.src.Addr().String(),
"--has-tty=false", // updated in-place by startWithPTY
"--tty-name=", // updated in-place by startWithPTY
}
nm := ss.conn.srv.lb.NetMap()
forceV1Behavior := nm.HasCap(tailcfg.NodeAttrSSHBehaviorV1) && !nm.HasCap(tailcfg.NodeAttrSSHBehaviorV2)
if forceV1Behavior {
incubatorArgs = append(incubatorArgs, "--force-v1-behavior")
}
if debugTest.Load() {
incubatorArgs = append(incubatorArgs, "--debug-test")
}
switch {
case isSFTP:
// Note that we include both the `--sftp` flag and a command to launch
// tailscaled as `be-child sftp`. If login or su is available, and
// we're not running with tailcfg.NodeAttrSSHBehaviorV1, this will
// result in serving SFTP within a login shell, with full PAM
// integration. Otherwise, we'll serve SFTP in the incubator process
// with no PAM integration.
incubatorArgs = append(incubatorArgs, "--sftp", fmt.Sprintf("--cmd=%s be-child sftp", ss.conn.srv.tailscaledPath))
case isShell:
incubatorArgs = append(incubatorArgs, "--shell")
default:
incubatorArgs = append(incubatorArgs, "--cmd="+ss.RawCommand())
}
allowSendEnv := nm.HasCap(tailcfg.NodeAttrSSHEnvironmentVariables)
if allowSendEnv {
env, err := filterEnv(ss.conn.acceptEnv, ss.Session.Environ())
if err != nil {
return nil, err
}
if len(env) > 0 {
encoded, err := json.Marshal(env)
if err != nil {
return nil, fmt.Errorf("failed to encode environment: %w", err)
}
incubatorArgs = append(incubatorArgs, fmt.Sprintf("--encoded-env=%q", encoded))
}
}
return exec.CommandContext(ss.ctx, ss.conn.srv.tailscaledPath, incubatorArgs...), nil
}
var debugTest atomic.Bool
type stdRWC struct{}
func (stdRWC) Read(p []byte) (n int, err error) {
return os.Stdin.Read(p)
}
func (stdRWC) Write(b []byte) (n int, err error) {
return os.Stdout.Write(b)
}
func (stdRWC) Close() error {
os.Exit(0)
return nil
}
type incubatorArgs struct {
localUser string
homeDir string
remoteUser string
remoteIP string
ttyName string
hasTTY bool
cmd string
isSFTP bool
isShell bool
forceV1Behavior bool
debugTest bool
isSELinuxEnforcing bool
encodedEnv string
}
func parseIncubatorArgs(args []string) (incubatorArgs, error) {
var ia incubatorArgs
flags := flag.NewFlagSet("", flag.ExitOnError)
flags.StringVar(&ia.localUser, "local-user", "", "the user to run as")
flags.StringVar(&ia.homeDir, "home-dir", "/", "the user's home directory")
flags.StringVar(&ia.remoteUser, "remote-user", "", "the remote user/tags")
flags.StringVar(&ia.remoteIP, "remote-ip", "", "the remote Tailscale IP")
flags.StringVar(&ia.ttyName, "tty-name", "", "the tty name (pts/3)")
flags.BoolVar(&ia.hasTTY, "has-tty", false, "is the output attached to a tty")
flags.StringVar(&ia.cmd, "cmd", "", "the cmd to launch, including all arguments (ignored in sftp mode)")
flags.BoolVar(&ia.isShell, "shell", false, "is launching a shell (with no cmds)")
flags.BoolVar(&ia.isSFTP, "sftp", false, "run sftp server (cmd is ignored)")
flags.BoolVar(&ia.forceV1Behavior, "force-v1-behavior", false, "allow falling back to the su command if login is unavailable")
flags.BoolVar(&ia.debugTest, "debug-test", false, "should debug in test mode")
flags.BoolVar(&ia.isSELinuxEnforcing, "is-selinux-enforcing", false, "whether SELinux is in enforcing mode")
flags.StringVar(&ia.encodedEnv, "encoded-env", "", "JSON encoded array of environment variables in '['key=value']' format")
flags.Parse(args)
return ia, nil
}
func (ia incubatorArgs) forwardedEnviron() ([]string, string, error) {
environ := os.Environ()
// pass through SSH_AUTH_SOCK environment variable to support ssh agent forwarding
allowListKeys := "SSH_AUTH_SOCK"
if ia.encodedEnv != "" {
unquoted, err := strconv.Unquote(ia.encodedEnv)
if err != nil {
return nil, "", fmt.Errorf("unable to parse encodedEnv %q: %w", ia.encodedEnv, err)
}
var extraEnviron []string
err = json.Unmarshal([]byte(unquoted), &extraEnviron)
if err != nil {
return nil, "", fmt.Errorf("unable to parse encodedEnv %q: %w", ia.encodedEnv, err)
}
environ = append(environ, extraEnviron...)
for _, v := range extraEnviron {
allowListKeys = fmt.Sprintf("%s,%s", allowListKeys, strings.Split(v, "=")[0])
}
}
return environ, allowListKeys, nil
}
func beNetshell(args []string) error {
plan9netshell.Main()
return nil
}
// beIncubator is the entrypoint to the `tailscaled be-child ssh` subcommand.
// It is responsible for informing the system of a new login session for the
// user. This is sometimes necessary for mounting home directories and
// decrypting file systems.
//
// Tailscaled launches the incubator as the same user as it was launched as.
func beIncubator(args []string) error {
// To defend against issues like https://golang.org/issue/1435,
// defensively lock our current goroutine's thread to the current
// system thread before we start making any UID/GID/group changes.
//
// This shouldn't matter on Linux because syscall.AllThreadsSyscall is
// used to invoke syscalls on all OS threads, but (as of 2023-03-23)
// that function is not implemented on all platforms.
runtime.LockOSThread()
defer runtime.UnlockOSThread()
ia, err := parseIncubatorArgs(args)
if err != nil {
return err
}
if ia.isSFTP && ia.isShell {
return fmt.Errorf("--sftp and --shell are mutually exclusive")
}
if ia.isShell {
plan9netshell.Main()
return nil
}
dlogf := logger.Discard
if ia.debugTest {
// In testing, we don't always have syslog, so log to a temp file.
if logFile, err := os.OpenFile("/tmp/tailscalessh.log", os.O_APPEND|os.O_WRONLY, 0666); err == nil {
lf := log.New(logFile, "", 0)
dlogf = func(msg string, args ...any) {
lf.Printf(msg, args...)
logFile.Sync()
}
defer logFile.Close()
}
}
return handleInProcess(dlogf, ia)
}
func handleInProcess(dlogf logger.Logf, ia incubatorArgs) error {
if ia.isSFTP {
return handleSFTPInProcess(dlogf, ia)
}
return handleSSHInProcess(dlogf, ia)
}
func handleSFTPInProcess(dlogf logger.Logf, ia incubatorArgs) error {
dlogf("handling sftp")
return serveSFTP()
}
// beSFTP serves SFTP in-process.
func beSFTP(args []string) error {
return serveSFTP()
}
func serveSFTP() error {
server, err := sftp.NewServer(stdRWC{})
if err != nil {
return err
}
// TODO(https://github.com/pkg/sftp/pull/554): Revert the check for io.EOF,
// when sftp is patched to report clean termination.
if err := server.Serve(); err != nil && err != io.EOF {
return err
}
return nil
}
// handleSSHInProcess is a last resort if we couldn't use login or su. It
// registers a new session with the OS, sets its UID, GID and groups to the
// specified values, and then launches the requested `--cmd` in the user's
// login shell.
func handleSSHInProcess(dlogf logger.Logf, ia incubatorArgs) error {
environ, _, err := ia.forwardedEnviron()
if err != nil {
return err
}
dlogf("running /bin/rc -c %q", ia.cmd)
cmd := newCommand("/bin/rc", environ, []string{"-c", ia.cmd})
err = cmd.Run()
if ee, ok := err.(*exec.ExitError); ok {
ps := ee.ProcessState
code := ps.ExitCode()
if code < 0 {
// TODO(bradfitz): do we need to also check the syscall.WaitStatus
// and make our process look like it also died by signal/same signal
// as our child process? For now we just do the exit code.
fmt.Fprintf(os.Stderr, "[tailscale-ssh: process died: %v]\n", ps.String())
code = 1 // for now. so we don't exit with negative
}
os.Exit(code)
}
return err
}
func newCommand(cmdPath string, cmdEnviron []string, cmdArgs []string) *exec.Cmd {
cmd := exec.Command(cmdPath, cmdArgs...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = cmdEnviron
return cmd
}
// launchProcess launches an incubator process for the provided session.
// It is responsible for configuring the process execution environment.
// The caller can wait for the process to exit by calling cmd.Wait().
//
// It sets ss.cmd, stdin, stdout, and stderr.
func (ss *sshSession) launchProcess() error {
var err error
ss.cmd, err = ss.newIncubatorCommand(ss.logf)
if err != nil {
return err
}
cmd := ss.cmd
cmd.Dir = "/"
cmd.Env = append(os.Environ(), envForUser(ss.conn.localUser)...)
for _, kv := range ss.Environ() {
if acceptEnvPair(kv) {
cmd.Env = append(cmd.Env, kv)
}
}
ci := ss.conn.info
cmd.Env = append(cmd.Env,
fmt.Sprintf("SSH_CLIENT=%s %d %d", ci.src.Addr(), ci.src.Port(), ci.dst.Port()),
fmt.Sprintf("SSH_CONNECTION=%s %d %s %d", ci.src.Addr(), ci.src.Port(), ci.dst.Addr(), ci.dst.Port()),
)
if ss.agentListener != nil {
cmd.Env = append(cmd.Env, fmt.Sprintf("SSH_AUTH_SOCK=%s", ss.agentListener.Addr()))
}
return ss.startWithStdPipes()
}
// startWithStdPipes starts cmd with os.Pipe for Stdin, Stdout and Stderr.
func (ss *sshSession) startWithStdPipes() (err error) {
var rdStdin, wrStdout, wrStderr io.ReadWriteCloser
defer func() {
if err != nil {
closeAll(rdStdin, ss.wrStdin, ss.rdStdout, wrStdout, ss.rdStderr, wrStderr)
}
}()
if ss.cmd == nil {
return errors.New("nil cmd")
}
if rdStdin, ss.wrStdin, err = os.Pipe(); err != nil {
return err
}
if ss.rdStdout, wrStdout, err = os.Pipe(); err != nil {
return err
}
if ss.rdStderr, wrStderr, err = os.Pipe(); err != nil {
return err
}
ss.cmd.Stdin = rdStdin
ss.cmd.Stdout = wrStdout
ss.cmd.Stderr = wrStderr
ss.childPipes = []io.Closer{rdStdin, wrStdout, wrStderr}
return ss.cmd.Start()
}
func envForUser(u *userMeta) []string {
return []string{
fmt.Sprintf("user=%s", u.Username),
fmt.Sprintf("home=%s", u.HomeDir),
fmt.Sprintf("path=%s", defaultPathForUser(&u.User)),
}
}
// acceptEnvPair reports whether the environment variable key=value pair
// should be accepted from the client. It uses the same default as OpenSSH
// AcceptEnv.
func acceptEnvPair(kv string) bool {
k, _, ok := strings.Cut(kv, "=")
if !ok {
return false
}
_ = k
return true // permit anything on plan9 during bringup, for debugging at least
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux || (darwin && !ios) || freebsd || openbsd || plan9
//go:build linux || (darwin && !ios) || freebsd || openbsd
// Package tailssh is an SSH server integrated into Tailscale.
package tailssh
@@ -672,6 +672,7 @@ type sshSession struct {
wrStdin io.WriteCloser
rdStdout io.ReadCloser
rdStderr io.ReadCloser // rdStderr is nil for pty sessions
ptyReq *ssh.Pty // non-nil for pty sessions
// childPipes is a list of pipes that need to be closed when the process exits.
// For pty sessions, this is the tty fd.
@@ -902,7 +903,7 @@ func (ss *sshSession) run() {
defer t.Stop()
}
if euid := os.Geteuid(); euid != 0 && runtime.GOOS != "plan9" {
if euid := os.Geteuid(); euid != 0 {
if lu.Uid != fmt.Sprint(euid) {
ss.logf("can't switch to user %q from process euid %v", lu.Username, euid)
fmt.Fprintf(ss, "can't switch user\r\n")

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux || (darwin && !ios) || freebsd || openbsd || plan9
//go:build linux || (darwin && !ios) || freebsd || openbsd
package tailssh
@@ -48,9 +48,6 @@ func userLookup(username string) (*userMeta, error) {
}
func (u *userMeta) LoginShell() string {
if runtime.GOOS == "plan9" {
return "/bin/rc"
}
if u.loginShellCached != "" {
// This field should be populated on Linux, at least, because
// func userLookup on Linux uses "getent" to look up the user
@@ -88,9 +85,6 @@ func defaultPathForUser(u *user.User) string {
if s := defaultPathTmpl(); s != "" {
return expandDefaultPathTmpl(s, u)
}
if runtime.GOOS == "plan9" {
return "/bin"
}
isRoot := u.Uid == "0"
switch distro.Get() {
case distro.Debian:

View File

@@ -505,6 +505,11 @@ 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
}

View File

@@ -839,15 +839,17 @@ func (s *Server) serveMap(w http.ResponseWriter, r *http.Request, mkey key.Machi
w.WriteHeader(200)
for {
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 {
// 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
}
continue
}
return
}
if s.canGenerateAutomaticMapResponseFor(req.NodeKey) {

View File

@@ -14,7 +14,6 @@ import (
"fmt"
"io"
"log"
"runtime"
"strings"
"sync"
"time"
@@ -163,10 +162,6 @@ func RateLimitedFnWithClock(logf Logf, f time.Duration, burst int, maxCache int,
if envknob.String("TS_DEBUG_LOG_RATE") == "all" {
return logf
}
if runtime.GOOS == "plan9" {
// To ease bring-up.
return logf
}
var (
mu sync.Mutex
msgLim = make(map[string]*limitData) // keyed by logf format

View File

@@ -19,10 +19,6 @@ import (
// an error. It will first try to use the 'id' command to get the group IDs,
// and if that fails, it will fall back to the user.GroupIds method.
func GetGroupIds(user *user.User) ([]string, error) {
if runtime.GOOS == "plan9" {
return nil, nil
}
if runtime.GOOS != "linux" {
return user.GroupIds()
}

View File

@@ -54,18 +54,9 @@ func lookup(usernameOrUID string, std lookupStd, wantShell bool) (*user.User, st
// Skip getent entirely on Non-Unix platforms that won't ever have it.
// (Using HasPrefix for "wasip1", anticipating that WASI support will
// move beyond "preview 1" some day.)
if runtime.GOOS == "windows" || runtime.GOOS == "js" || runtime.GOARCH == "wasm" || runtime.GOOS == "plan9" {
var shell string
if wantShell && runtime.GOOS == "plan9" {
shell = "/bin/rc"
}
if runtime.GOOS == "plan9" {
if u, err := user.Current(); err == nil {
return u, shell, nil
}
}
if runtime.GOOS == "windows" || runtime.GOOS == "js" || runtime.GOARCH == "wasm" {
u, err := std(usernameOrUID)
return u, shell, err
return u, "", err
}
// No getent on Gokrazy. So hard-code the login shell.
@@ -87,16 +78,6 @@ func lookup(usernameOrUID string, std lookupStd, wantShell bool) (*user.User, st
return u, shell, nil
}
if runtime.GOOS == "plan9" {
return &user.User{
Uid: "0",
Gid: "0",
Username: "glenda",
Name: "Glenda",
HomeDir: "/",
}, "/bin/rc", nil
}
// Start with getent if caller wants to get the user shell.
if wantShell {
return userLookupGetent(usernameOrUID, std)

View File

@@ -3018,10 +3018,6 @@ func (c *Conn) DebugForcePreferDERP(n int) {
// portableTrySetSocketBuffer sets SO_SNDBUF and SO_RECVBUF on pconn to socketBufferSize,
// logging an error if it occurs.
func portableTrySetSocketBuffer(pconn nettype.PacketConn, logf logger.Logf) {
if runtime.GOOS == "plan9" {
// Not supported. Don't try. Avoid logspam.
return
}
if c, ok := pconn.(*net.UDPConn); ok {
// Attempt to increase the buffer size, and allow failures.
if err := c.SetReadBuffer(socketBufferSize); err != nil {

View File

@@ -7,11 +7,9 @@ import (
"errors"
"net"
"net/netip"
"runtime"
"sync"
"sync/atomic"
"syscall"
"time"
"golang.org/x/net/ipv6"
"tailscale.com/net/netaddr"
@@ -152,12 +150,6 @@ func (c *RebindingUDPConn) closeLocked() error {
return errNilPConn
}
c.port = 0
if runtime.GOOS == "plan9" {
// Work around Go bug https://github.com/golang/go/issues/72770.
// This does https://go-review.googlesource.com/c/go/+/656395
// manually until the upstream Go bug is fixed + released.
c.pconn.SetReadDeadline(time.Now().Add(-time.Hour))
}
return c.pconn.Close()
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !windows && !linux && !darwin && !openbsd && !freebsd && !plan9
//go:build !windows && !linux && !darwin && !openbsd && !freebsd
package router

View File

@@ -1,158 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package router
import (
"bufio"
"bytes"
"fmt"
"net/netip"
"os"
"strings"
"github.com/tailscale/wireguard-go/tun"
"tailscale.com/health"
"tailscale.com/net/netmon"
"tailscale.com/types/logger"
)
func newUserspaceRouter(logf logger.Logf, tundev tun.Device, netMon *netmon.Monitor, health *health.Tracker) (Router, error) {
r := &plan9Router{
logf: logf,
tundev: tundev,
netMon: netMon,
}
cleanAllTailscaleRoutes(logf)
return r, nil
}
type plan9Router struct {
logf logger.Logf
tundev tun.Device
netMon *netmon.Monitor
health *health.Tracker
}
func (r *plan9Router) Up() error {
return nil
}
func (r *plan9Router) Set(cfg *Config) error {
if cfg == nil {
cleanAllTailscaleRoutes(r.logf)
return nil
}
var self4, self6 netip.Addr
for _, addr := range cfg.LocalAddrs {
ctl := r.tundev.File()
maskBits := addr.Bits()
if addr.Addr().Is4() {
// The mask sizes in Plan9 are in IPv6 bits, even for IPv4.
maskBits += (128 - 32)
self4 = addr.Addr()
}
if addr.Addr().Is6() {
self6 = addr.Addr()
}
_, err := fmt.Fprintf(ctl, "add %s /%d\n", addr.Addr().String(), maskBits)
r.logf("route/plan9: add %s /%d = %v", addr.Addr().String(), maskBits, err)
}
ipr, err := os.OpenFile("/net/iproute", os.O_RDWR, 0)
if err != nil {
return fmt.Errorf("open /net/iproute: %w", err)
}
defer ipr.Close()
// TODO(bradfitz): read existing routes, delete ones tagged "tail"
// that aren't in cfg.LocalRoutes.
if _, err := fmt.Fprintf(ipr, "tag tail\n"); err != nil {
return fmt.Errorf("tag tail: %w", err)
}
for _, route := range cfg.Routes {
maskBits := route.Bits()
if route.Addr().Is4() {
// The mask sizes in Plan9 are in IPv6 bits, even for IPv4.
maskBits += (128 - 32)
}
var nextHop netip.Addr
if route.Addr().Is4() {
nextHop = self4
} else if route.Addr().Is6() {
nextHop = self6
}
if !nextHop.IsValid() {
r.logf("route/plan9: skipping route %s: no next hop (no self addr)", route.String())
continue
}
r.logf("route/plan9: plan9.router: add %s /%d %s", route.Addr(), maskBits, nextHop)
if _, err := fmt.Fprintf(ipr, "add %s /%d %s\n", route.Addr(), maskBits, nextHop); err != nil {
return fmt.Errorf("add %s: %w", route.String(), err)
}
}
if len(cfg.LocalRoutes) > 0 {
r.logf("route/plan9: TODO: Set LocalRoutes %v", cfg.LocalRoutes)
}
if len(cfg.SubnetRoutes) > 0 {
r.logf("route/plan9: TODO: Set SubnetRoutes %v", cfg.SubnetRoutes)
}
return nil
}
// UpdateMagicsockPort implements the Router interface. This implementation
// does nothing and returns nil because this router does not currently need
// to know what the magicsock UDP port is.
func (r *plan9Router) UpdateMagicsockPort(_ uint16, _ string) error {
return nil
}
func (r *plan9Router) Close() error {
// TODO(bradfitz): unbind
return nil
}
func cleanUp(logf logger.Logf, _ string) {
cleanAllTailscaleRoutes(logf)
}
func cleanAllTailscaleRoutes(logf logger.Logf) {
routes, err := os.OpenFile("/net/iproute", os.O_RDWR, 0)
if err != nil {
logf("cleaning routes: %v", err)
return
}
defer routes.Close()
// Using io.ReadAll or os.ReadFile on /net/iproute fails; it results in a
// 511 byte result when the actual /net/iproute contents are over 1k.
// So do it in one big read instead. Who knows.
routeBuf := make([]byte, 1<<20)
n, err := routes.Read(routeBuf)
if err != nil {
logf("cleaning routes: %v", err)
return
}
routeBuf = routeBuf[:n]
//logf("cleaning routes: %d bytes: %q", len(routeBuf), routeBuf)
bs := bufio.NewScanner(bytes.NewReader(routeBuf))
for bs.Scan() {
f := strings.Fields(bs.Text())
if len(f) < 6 {
continue
}
tag := f[4]
if tag != "tail" {
continue
}
_, err := fmt.Fprintf(routes, "remove %s %s\n", f[0], f[1])
logf("router: cleaning route %s %s: %v", f[0], f[1], err)
}
}

View File

@@ -569,18 +569,6 @@ func (e *userspaceEngine) handleLocalPackets(p *packet.Parsed, t *tstun.Wrapper)
return filter.Drop
}
}
if runtime.GOOS == "plan9" {
isLocalAddr, ok := e.isLocalAddr.LoadOk()
if ok {
if isLocalAddr(p.Dst.Addr()) {
e.logf("XXX plan9 inject inbound")
// On Plan9's "tun" equivalent, everything goes back in and out
// the tun, even when the kernel's replying to itself.
t.InjectInboundCopy(p.Buffer())
return filter.Drop
}
}
}
return filter.Accept
}