Compare commits
15 Commits
bradfitz/p
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 9418d7190b | |||
|
|
1ec1a60c10 | ||
|
|
fea74a60d5 | ||
|
|
e3c04c5d6c | ||
|
|
d0e7af3830 | ||
|
|
2685484f26 | ||
|
|
a622debe9b | ||
|
|
4777cc2cda | ||
|
|
75373896c7 | ||
|
|
5aa1c27aad | ||
|
|
725c8d298a | ||
|
|
08c8ccb48e | ||
|
|
e78055eb01 | ||
|
|
ea79dc161d | ||
|
|
b3455fa99a |
@@ -1 +1 @@
|
||||
3.18
|
||||
3.19
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
9
Makefile
9
Makefile
@@ -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 && \
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.81.0
|
||||
1.83.0
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
429
cmd/natc/natc_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
6
go.mod
@@ -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
20
go.sum
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 /> |
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user