Compare commits

..

1 Commits

Author SHA1 Message Date
Will Norris
1b35416b37 ipn/ipnlocal: allow running webclient on mobile 2024-01-10 09:47:31 -08:00
57 changed files with 179 additions and 1847 deletions

View File

@@ -3,8 +3,6 @@ SYNO_ARCH ?= "amd64"
SYNO_DSM ?= "7"
TAGS ?= "latest"
PLATFORM ?= "flyio" ## flyio==linux/amd64. Set to "" to build all platforms.
vet: ## Run go vet
./tool/go vet ./...
@@ -90,7 +88,7 @@ publishdevimage: ## Build and publish tailscale image to location specified by $
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
@test "${REPO}" != "tailscale/k8s-operator" || (echo "REPO=... must not be tailscale/k8s-operator" && exit 1)
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=client ./build_docker.sh
TAGS="${TAGS}" REPOS=${REPO} PUSH=true TARGET=client ./build_docker.sh
publishdevoperator: ## Build and publish k8s-operator image to location specified by ${REPO}
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
@@ -98,7 +96,7 @@ publishdevoperator: ## Build and publish k8s-operator image to location specifie
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
@test "${REPO}" != "tailscale/k8s-operator" || (echo "REPO=... must not be tailscale/k8s-operator" && exit 1)
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=operator ./build_docker.sh
TAGS="${TAGS}" REPOS=${REPO} PUSH=true TARGET=operator ./build_docker.sh
help: ## Show this help
@echo "\nSpecify a command. The choices are:\n"

View File

@@ -32,7 +32,6 @@ PUSH="${PUSH:-false}"
TARGET="${TARGET:-${DEFAULT_TARGET}}"
TAGS="${TAGS:-${DEFAULT_TAGS}}"
BASE="${BASE:-${DEFAULT_BASE}}"
PLATFORM="${PLATFORM:-}" # default to all platforms
case "$TARGET" in
client)
@@ -51,7 +50,6 @@ case "$TARGET" in
--tags="${TAGS}" \
--repos="${REPOS}" \
--push="${PUSH}" \
--target="${PLATFORM}" \
/usr/local/bin/containerboot
;;
operator)
@@ -67,7 +65,6 @@ case "$TARGET" in
--tags="${TAGS}" \
--repos="${REPOS}" \
--push="${PUSH}" \
--target="${PLATFORM}" \
/usr/local/bin/operator
;;
*)

View File

@@ -8,7 +8,6 @@ import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"net/http"
"net/url"
"strings"
@@ -233,55 +232,3 @@ func (s *Server) newSessionID() (string, error) {
}
return "", errors.New("too many collisions generating new session; please refresh page")
}
type peerCapabilities map[capFeature]bool // value is true if the peer can edit the given feature
// canEdit is true if the peerCapabilities grant edit access
// to the given feature.
func (p peerCapabilities) canEdit(feature capFeature) bool {
if p == nil {
return false
}
if p[capFeatureAll] {
return true
}
return p[feature]
}
type capFeature string
const (
// The following values should not be edited.
// New caps can be added, but existing ones should not be changed,
// as these exact values are used by users in tailnet policy files.
capFeatureAll capFeature = "*" // grants peer management of all features
capFeatureFunnel capFeature = "funnel" // grants peer serve/funnel management
capFeatureSSH capFeature = "ssh" // grants peer SSH server management
capFeatureSubnet capFeature = "subnet" // grants peer subnet routes management
capFeatureExitNode capFeature = "exitnode" // grants peer ability to advertise-as and use exit nodes
capFeatureAccount capFeature = "account" // grants peer ability to turn on auto updates and log out of node
)
type capRule struct {
CanEdit []string `json:"canEdit,omitempty"` // list of features peer is allowed to edit
}
// toPeerCapabilities parses out the web ui capabilities from the
// given whois response.
func toPeerCapabilities(whois *apitype.WhoIsResponse) (peerCapabilities, error) {
caps := peerCapabilities{}
if whois == nil {
return caps, nil
}
rules, err := tailcfg.UnmarshalCapJSON[capRule](whois.CapMap, tailcfg.PeerCapabilityWebUI)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal capability: %v", err)
}
for _, c := range rules {
for _, f := range c.CanEdit {
caps[capFeature(strings.ToLower(f))] = true
}
}
return caps, nil
}

View File

@@ -95,16 +95,9 @@ function LoginPopoverContent({
const [canConnectOverTS, setCanConnectOverTS] = useState<boolean>(false)
const [isRunningCheck, setIsRunningCheck] = useState<boolean>(false)
// Whether the current page is loaded over HTTPS.
// If it is, then the connectivity check to the management client
// will fail with a mixed-content error.
const isHTTPS = window.location.protocol === "https:"
const checkTSConnection = useCallback(() => {
if (auth.viewerIdentity || isHTTPS) {
// Skip the connectivity check if we either already know we're connected over Tailscale,
// or know the connectivity check will fail because the current page is loaded over HTTPS.
setCanConnectOverTS(true)
if (auth.viewerIdentity) {
setCanConnectOverTS(true) // already connected over ts
return
}
// Otherwise, test connection to the ts IP.
@@ -118,7 +111,7 @@ function LoginPopoverContent({
setIsRunningCheck(false)
})
.catch(() => setIsRunningCheck(false))
}, [auth.viewerIdentity, isRunningCheck, node.IPv4, isHTTPS])
}, [auth.viewerIdentity, isRunningCheck, node.IPv4])
/**
* Checking connection for first time on page load.
@@ -200,14 +193,6 @@ function LoginPopoverContent({
You can see most of this device's details. To make changes,
you need to sign in.
</p>
{isHTTPS && (
// we don't know if the user can connect over TS, so
// provide extra tips in case they have trouble.
<p className="text-gray-500 text-xs font-semibold pt-2">
Make sure you are connected to your tailnet, and that your
policy file allows access.
</p>
)}
<SignInButton auth={auth} onClick={handleSignInClick} />
</>
)}

View File

@@ -450,11 +450,10 @@ type authResponse struct {
// viewerIdentity is the Tailscale identity of the source node
// connected to this web client.
type viewerIdentity struct {
LoginName string `json:"loginName"`
NodeName string `json:"nodeName"`
NodeIP string `json:"nodeIP"`
ProfilePicURL string `json:"profilePicUrl,omitempty"`
Capabilities peerCapabilities `json:"capabilities"` // features peer is allowed to edit
LoginName string `json:"loginName"`
NodeName string `json:"nodeName"`
NodeIP string `json:"nodeIP"`
ProfilePicURL string `json:"profilePicUrl,omitempty"`
}
// serverAPIAuth handles requests to the /api/auth endpoint
@@ -465,16 +464,10 @@ func (s *Server) serveAPIAuth(w http.ResponseWriter, r *http.Request) {
session, whois, status, sErr := s.getSession(r)
if whois != nil {
caps, err := toPeerCapabilities(whois)
if err != nil {
http.Error(w, sErr.Error(), http.StatusInternalServerError)
return
}
resp.ViewerIdentity = &viewerIdentity{
LoginName: whois.UserProfile.LoginName,
NodeName: whois.Node.Name,
ProfilePicURL: whois.UserProfile.ProfilePicURL,
Capabilities: caps,
}
if addrs := whois.Node.Addresses; len(addrs) > 0 {
resp.ViewerIdentity.NodeIP = addrs[0].Addr().String()

View File

@@ -450,7 +450,6 @@ func TestServeAuth(t *testing.T) {
NodeName: remoteNode.Node.Name,
NodeIP: remoteIP,
ProfilePicURL: user.ProfilePicURL,
Capabilities: peerCapabilities{},
}
testControlURL := &defaultControlURL
@@ -1098,163 +1097,6 @@ func TestRequireTailscaleIP(t *testing.T) {
}
}
func TestPeerCapabilities(t *testing.T) {
// Testing web.toPeerCapabilities
toPeerCapsTests := []struct {
name string
whois *apitype.WhoIsResponse
wantCaps peerCapabilities
}{
{
name: "empty-whois",
whois: nil,
wantCaps: peerCapabilities{},
},
{
name: "no-webui-caps",
whois: &apitype.WhoIsResponse{
CapMap: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityDebugPeer: []tailcfg.RawMessage{},
},
},
wantCaps: peerCapabilities{},
},
{
name: "one-webui-cap",
whois: &apitype.WhoIsResponse{
CapMap: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
"{\"canEdit\":[\"ssh\",\"subnet\"]}",
},
},
},
wantCaps: peerCapabilities{
capFeatureSSH: true,
capFeatureSubnet: true,
},
},
{
name: "multiple-webui-cap",
whois: &apitype.WhoIsResponse{
CapMap: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
"{\"canEdit\":[\"ssh\",\"subnet\"]}",
"{\"canEdit\":[\"subnet\",\"exitnode\",\"*\"]}",
},
},
},
wantCaps: peerCapabilities{
capFeatureSSH: true,
capFeatureSubnet: true,
capFeatureExitNode: true,
capFeatureAll: true,
},
},
{
name: "case=insensitive-caps",
whois: &apitype.WhoIsResponse{
CapMap: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
"{\"canEdit\":[\"SSH\",\"sUBnet\"]}",
},
},
},
wantCaps: peerCapabilities{
capFeatureSSH: true,
capFeatureSubnet: true,
},
},
{
name: "random-canEdit-contents-dont-error",
whois: &apitype.WhoIsResponse{
CapMap: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
"{\"canEdit\":[\"unknown-feature\"]}",
},
},
},
wantCaps: peerCapabilities{
"unknown-feature": true,
},
},
{
name: "no-canEdit-section",
whois: &apitype.WhoIsResponse{
CapMap: tailcfg.PeerCapMap{
tailcfg.PeerCapabilityWebUI: []tailcfg.RawMessage{
"{\"canDoSomething\":[\"*\"]}",
},
},
},
wantCaps: peerCapabilities{},
},
}
for _, tt := range toPeerCapsTests {
t.Run("toPeerCapabilities-"+tt.name, func(t *testing.T) {
got, err := toPeerCapabilities(tt.whois)
if err != nil {
t.Fatalf("unexpected: %v", err)
}
if diff := cmp.Diff(got, tt.wantCaps); diff != "" {
t.Errorf("wrong caps; (-got+want):%v", diff)
}
})
}
// Testing web.peerCapabilities.canEdit
canEditTests := []struct {
name string
caps peerCapabilities
wantCanEdit map[capFeature]bool
}{
{
name: "empty-caps",
caps: nil,
wantCanEdit: map[capFeature]bool{
capFeatureAll: false,
capFeatureFunnel: false,
capFeatureSSH: false,
capFeatureSubnet: false,
capFeatureExitNode: false,
capFeatureAccount: false,
},
},
{
name: "some-caps",
caps: peerCapabilities{capFeatureSSH: true, capFeatureAccount: true},
wantCanEdit: map[capFeature]bool{
capFeatureAll: false,
capFeatureFunnel: false,
capFeatureSSH: true,
capFeatureSubnet: false,
capFeatureExitNode: false,
capFeatureAccount: true,
},
},
{
name: "wildcard-in-caps",
caps: peerCapabilities{capFeatureAll: true, capFeatureAccount: true},
wantCanEdit: map[capFeature]bool{
capFeatureAll: true,
capFeatureFunnel: true,
capFeatureSSH: true,
capFeatureSubnet: true,
capFeatureExitNode: true,
capFeatureAccount: true,
},
},
}
for _, tt := range canEditTests {
t.Run("canEdit-"+tt.name, func(t *testing.T) {
for f, want := range tt.wantCanEdit {
if got := tt.caps.canEdit(f); got != want {
t.Errorf("wrong canEdit(%s); got=%v, want=%v", f, got, want)
}
}
})
}
}
var (
defaultControlURL = "https://controlplane.tailscale.com"
testAuthPath = "/a/12345"

View File

@@ -142,7 +142,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/util/cloudenv from tailscale.com/hostinfo+
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
tailscale.com/util/cmpx from tailscale.com/cmd/derper+
tailscale.com/util/ctxkey from tailscale.com/tsweb+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/hostinfo+
tailscale.com/util/httpm from tailscale.com/client/tailscale

View File

@@ -59,6 +59,8 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: ENABLE_CONNECTOR
value: "{{ .Values.enableConnector }}"
- name: CLIENT_ID_FILE
value: /oauth/client_id
- name: CLIENT_SECRET_FILE

View File

@@ -1,8 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: tailscale # class name currently can not be changed
annotations: {} # we do not support default IngressClass annotation https://kubernetes.io/docs/concepts/services-networking/ingress/#default-ingress-class
spec:
controller: tailscale.com/ts-ingress # controller name currently can not be changed
# parameters: {} # currently no parameters are supported

View File

@@ -18,11 +18,8 @@ rules:
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses", "ingresses/status"]
verbs: ["*"]
- apiGroups: ["networking.k8s.io"]
resources: ["ingressclasses"]
verbs: ["get", "list", "watch"]
- apiGroups: ["tailscale.com"]
resources: ["connectors", "connectors/status", "proxyclasses", "proxyclasses/status"]
resources: ["connectors", "connectors/status"]
verbs: ["get", "list", "watch", "update"]
---
apiVersion: rbac.authorization.k8s.io/v1

View File

@@ -8,6 +8,10 @@ oauth: {}
# clientId: ""
# clientSecret: ""
# enableConnector determines whether the operator should reconcile
# connector.tailscale.com custom resources.
enableConnector: "false"
# installCRDs determines whether tailscale.com CRDs should be installed as part
# of chart installation. We do not use Helm's CRD installation mechanism as that
# does not allow for upgrading CRDs.

View File

@@ -1,465 +0,0 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.13.0
name: proxyclasses.tailscale.com
spec:
group: tailscale.com
names:
kind: ProxyClass
listKind: ProxyClassList
plural: proxyclasses
shortNames:
- pc
singular: proxyclass
scope: Cluster
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
type: object
required:
- spec
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
type: object
properties:
service:
description: Configuration for the headless Service, not actually used in this prototype, but is here to better illustrate the API structure
type: object
properties:
labels:
type: object
additionalProperties:
type: string
statefulSet:
type: object
properties:
annotations:
type: object
additionalProperties:
type: string
labels:
type: object
additionalProperties:
type: string
pod:
type: object
properties:
annotations:
type: object
additionalProperties:
type: string
imagePullSecrets:
type: array
items:
description: LocalObjectReference contains enough information to let you locate the referenced object inside the same namespace.
type: object
properties:
name:
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names TODO: Add other useful fields. apiVersion, kind, uid?'
type: string
x-kubernetes-map-type: atomic
labels:
description: Or should we just sync statefulset.labels, statefulset.annotations?
type: object
additionalProperties:
type: string
nodeName:
type: string
nodeSelector:
type: object
additionalProperties:
type: string
patches:
type: array
items:
description: RFC 6902 JSON patch
type: object
required:
- op
- path
properties:
op:
type: string
path:
type: string
value:
type: string
podSecurityContext:
description: PodSecurityContext holds pod-level security attributes and common container settings. Some fields are also present in container.securityContext. Field values of container.securityContext take precedence over field values of PodSecurityContext.
type: object
properties:
fsGroup:
description: "A special supplemental group that applies to all containers in a pod. Some volume types allow the Kubelet to change the ownership of that volume to be owned by the pod: \n 1. The owning GID will be the FSGroup 2. The setgid bit is set (new files created in the volume will be owned by FSGroup) 3. The permission bits are OR'd with rw-rw---- \n If unset, the Kubelet will not modify the ownership and permissions of any volume. Note that this field cannot be set when spec.os.name is windows."
type: integer
format: int64
fsGroupChangePolicy:
description: 'fsGroupChangePolicy defines behavior of changing ownership and permission of the volume before being exposed inside Pod. This field will only apply to volume types which support fsGroup based ownership(and permissions). It will have no effect on ephemeral volume types such as: secret, configmaps and emptydir. Valid values are "OnRootMismatch" and "Always". If not specified, "Always" is used. Note that this field cannot be set when spec.os.name is windows.'
type: string
runAsGroup:
description: The GID to run the entrypoint of the container process. Uses runtime default if unset. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence for that container. Note that this field cannot be set when spec.os.name is windows.
type: integer
format: int64
runAsNonRoot:
description: Indicates that the container must run as a non-root user. If true, the Kubelet will validate the image at runtime to ensure that it does not run as UID 0 (root) and fail to start the container if it does. If unset or false, no such validation will be performed. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
type: boolean
runAsUser:
description: The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence for that container. Note that this field cannot be set when spec.os.name is windows.
type: integer
format: int64
seLinuxOptions:
description: The SELinux context to be applied to all containers. If unspecified, the container runtime will allocate a random SELinux context for each container. May also be set in SecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence for that container. Note that this field cannot be set when spec.os.name is windows.
type: object
properties:
level:
description: Level is SELinux level label that applies to the container.
type: string
role:
description: Role is a SELinux role label that applies to the container.
type: string
type:
description: Type is a SELinux type label that applies to the container.
type: string
user:
description: User is a SELinux user label that applies to the container.
type: string
seccompProfile:
description: The seccomp options to use by the containers in this pod. Note that this field cannot be set when spec.os.name is windows.
type: object
required:
- type
properties:
localhostProfile:
description: localhostProfile indicates a profile defined in a file on the node should be used. The profile must be preconfigured on the node to work. Must be a descending path, relative to the kubelet's configured seccomp profile location. Must be set if type is "Localhost". Must NOT be set for any other type.
type: string
type:
description: "type indicates which kind of seccomp profile will be applied. Valid options are: \n Localhost - a profile defined in a file on the node should be used. RuntimeDefault - the container runtime default profile should be used. Unconfined - no profile should be applied."
type: string
supplementalGroups:
description: A list of groups applied to the first process run in each container, in addition to the container's primary GID, the fsGroup (if specified), and group memberships defined in the container image for the uid of the container process. If unspecified, no additional groups are added to any container. Note that group memberships defined in the container image for the uid of the container process are still effective, even if they are not included in this list. Note that this field cannot be set when spec.os.name is windows.
type: array
items:
type: integer
format: int64
sysctls:
description: Sysctls hold a list of namespaced sysctls used for the pod. Pods with unsupported sysctls (by the container runtime) might fail to launch. Note that this field cannot be set when spec.os.name is windows.
type: array
items:
description: Sysctl defines a kernel parameter to be set
type: object
required:
- name
- value
properties:
name:
description: Name of a property to set
type: string
value:
description: Value of a property to set
type: string
windowsOptions:
description: The Windows specific settings applied to all containers. If unspecified, the options within a container's SecurityContext will be used. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is linux.
type: object
properties:
gmsaCredentialSpec:
description: GMSACredentialSpec is where the GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the GMSA credential spec named by the GMSACredentialSpecName field.
type: string
gmsaCredentialSpecName:
description: GMSACredentialSpecName is the name of the GMSA credential spec to use.
type: string
hostProcess:
description: HostProcess determines if a container should be run as a 'Host Process' container. All of a Pod's containers must have the same effective HostProcess value (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). In addition, if HostProcess is true then HostNetwork must also be set to true.
type: boolean
runAsUserName:
description: The UserName in Windows to run the entrypoint of the container process. Defaults to the user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
type: string
tailscaleContainer:
type: object
properties:
resources:
description: ResourceRequirements describes the compute resource requirements.
type: object
properties:
claims:
description: "Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. \n This is an alpha field and requires enabling the DynamicResourceAllocation feature gate. \n This field is immutable. It can only be set for containers."
type: array
items:
description: ResourceClaim references one entry in PodSpec.ResourceClaims.
type: object
required:
- name
properties:
name:
description: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.
type: string
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
limits:
description: 'Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
type: object
additionalProperties:
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
anyOf:
- type: integer
- type: string
x-kubernetes-int-or-string: true
requests:
description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
type: object
additionalProperties:
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
anyOf:
- type: integer
- type: string
x-kubernetes-int-or-string: true
securityContext:
description: SecurityContext holds security configuration that will be applied to a container. Some fields are present in both SecurityContext and PodSecurityContext. When both are set, the values in SecurityContext take precedence.
type: object
properties:
allowPrivilegeEscalation:
description: 'AllowPrivilegeEscalation controls whether a process can gain more privileges than its parent process. This bool directly controls if the no_new_privs flag will be set on the container process. AllowPrivilegeEscalation is true always when the container is: 1) run as Privileged 2) has CAP_SYS_ADMIN Note that this field cannot be set when spec.os.name is windows.'
type: boolean
capabilities:
description: The capabilities to add/drop when running containers. Defaults to the default set of capabilities granted by the container runtime. Note that this field cannot be set when spec.os.name is windows.
type: object
properties:
add:
description: Added capabilities
type: array
items:
description: Capability represent POSIX capabilities type
type: string
drop:
description: Removed capabilities
type: array
items:
description: Capability represent POSIX capabilities type
type: string
privileged:
description: Run container in privileged mode. Processes in privileged containers are essentially equivalent to root on the host. Defaults to false. Note that this field cannot be set when spec.os.name is windows.
type: boolean
procMount:
description: procMount denotes the type of proc mount to use for the containers. The default is DefaultProcMount which uses the container runtime defaults for readonly paths and masked paths. This requires the ProcMountType feature flag to be enabled. Note that this field cannot be set when spec.os.name is windows.
type: string
readOnlyRootFilesystem:
description: Whether this container has a read-only root filesystem. Default is false. Note that this field cannot be set when spec.os.name is windows.
type: boolean
runAsGroup:
description: The GID to run the entrypoint of the container process. Uses runtime default if unset. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.
type: integer
format: int64
runAsNonRoot:
description: Indicates that the container must run as a non-root user. If true, the Kubelet will validate the image at runtime to ensure that it does not run as UID 0 (root) and fail to start the container if it does. If unset or false, no such validation will be performed. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
type: boolean
runAsUser:
description: The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.
type: integer
format: int64
seLinuxOptions:
description: The SELinux context to be applied to the container. If unspecified, the container runtime will allocate a random SELinux context for each container. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.
type: object
properties:
level:
description: Level is SELinux level label that applies to the container.
type: string
role:
description: Role is a SELinux role label that applies to the container.
type: string
type:
description: Type is a SELinux type label that applies to the container.
type: string
user:
description: User is a SELinux user label that applies to the container.
type: string
seccompProfile:
description: The seccomp options to use by this container. If seccomp options are provided at both the pod & container level, the container options override the pod options. Note that this field cannot be set when spec.os.name is windows.
type: object
required:
- type
properties:
localhostProfile:
description: localhostProfile indicates a profile defined in a file on the node should be used. The profile must be preconfigured on the node to work. Must be a descending path, relative to the kubelet's configured seccomp profile location. Must be set if type is "Localhost". Must NOT be set for any other type.
type: string
type:
description: "type indicates which kind of seccomp profile will be applied. Valid options are: \n Localhost - a profile defined in a file on the node should be used. RuntimeDefault - the container runtime default profile should be used. Unconfined - no profile should be applied."
type: string
windowsOptions:
description: The Windows specific settings applied to all containers. If unspecified, the options from the PodSecurityContext will be used. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is linux.
type: object
properties:
gmsaCredentialSpec:
description: GMSACredentialSpec is where the GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the GMSA credential spec named by the GMSACredentialSpecName field.
type: string
gmsaCredentialSpecName:
description: GMSACredentialSpecName is the name of the GMSA credential spec to use.
type: string
hostProcess:
description: HostProcess determines if a container should be run as a 'Host Process' container. All of a Pod's containers must have the same effective HostProcess value (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). In addition, if HostProcess is true then HostNetwork must also be set to true.
type: boolean
runAsUserName:
description: The UserName in Windows to run the entrypoint of the container process. Defaults to the user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
type: string
tailscaleInitContainer:
type: object
properties:
resources:
description: ResourceRequirements describes the compute resource requirements.
type: object
properties:
claims:
description: "Claims lists the names of resources, defined in spec.resourceClaims, that are used by this container. \n This is an alpha field and requires enabling the DynamicResourceAllocation feature gate. \n This field is immutable. It can only be set for containers."
type: array
items:
description: ResourceClaim references one entry in PodSpec.ResourceClaims.
type: object
required:
- name
properties:
name:
description: Name must match the name of one entry in pod.spec.resourceClaims of the Pod where this field is used. It makes that resource available inside a container.
type: string
x-kubernetes-list-map-keys:
- name
x-kubernetes-list-type: map
limits:
description: 'Limits describes the maximum amount of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
type: object
additionalProperties:
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
anyOf:
- type: integer
- type: string
x-kubernetes-int-or-string: true
requests:
description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, otherwise to an implementation-defined value. Requests cannot exceed Limits. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/'
type: object
additionalProperties:
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
anyOf:
- type: integer
- type: string
x-kubernetes-int-or-string: true
securityContext:
description: SecurityContext holds security configuration that will be applied to a container. Some fields are present in both SecurityContext and PodSecurityContext. When both are set, the values in SecurityContext take precedence.
type: object
properties:
allowPrivilegeEscalation:
description: 'AllowPrivilegeEscalation controls whether a process can gain more privileges than its parent process. This bool directly controls if the no_new_privs flag will be set on the container process. AllowPrivilegeEscalation is true always when the container is: 1) run as Privileged 2) has CAP_SYS_ADMIN Note that this field cannot be set when spec.os.name is windows.'
type: boolean
capabilities:
description: The capabilities to add/drop when running containers. Defaults to the default set of capabilities granted by the container runtime. Note that this field cannot be set when spec.os.name is windows.
type: object
properties:
add:
description: Added capabilities
type: array
items:
description: Capability represent POSIX capabilities type
type: string
drop:
description: Removed capabilities
type: array
items:
description: Capability represent POSIX capabilities type
type: string
privileged:
description: Run container in privileged mode. Processes in privileged containers are essentially equivalent to root on the host. Defaults to false. Note that this field cannot be set when spec.os.name is windows.
type: boolean
procMount:
description: procMount denotes the type of proc mount to use for the containers. The default is DefaultProcMount which uses the container runtime defaults for readonly paths and masked paths. This requires the ProcMountType feature flag to be enabled. Note that this field cannot be set when spec.os.name is windows.
type: string
readOnlyRootFilesystem:
description: Whether this container has a read-only root filesystem. Default is false. Note that this field cannot be set when spec.os.name is windows.
type: boolean
runAsGroup:
description: The GID to run the entrypoint of the container process. Uses runtime default if unset. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.
type: integer
format: int64
runAsNonRoot:
description: Indicates that the container must run as a non-root user. If true, the Kubelet will validate the image at runtime to ensure that it does not run as UID 0 (root) and fail to start the container if it does. If unset or false, no such validation will be performed. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
type: boolean
runAsUser:
description: The UID to run the entrypoint of the container process. Defaults to user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.
type: integer
format: int64
seLinuxOptions:
description: The SELinux context to be applied to the container. If unspecified, the container runtime will allocate a random SELinux context for each container. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is windows.
type: object
properties:
level:
description: Level is SELinux level label that applies to the container.
type: string
role:
description: Role is a SELinux role label that applies to the container.
type: string
type:
description: Type is a SELinux type label that applies to the container.
type: string
user:
description: User is a SELinux user label that applies to the container.
type: string
seccompProfile:
description: The seccomp options to use by this container. If seccomp options are provided at both the pod & container level, the container options override the pod options. Note that this field cannot be set when spec.os.name is windows.
type: object
required:
- type
properties:
localhostProfile:
description: localhostProfile indicates a profile defined in a file on the node should be used. The profile must be preconfigured on the node to work. Must be a descending path, relative to the kubelet's configured seccomp profile location. Must be set if type is "Localhost". Must NOT be set for any other type.
type: string
type:
description: "type indicates which kind of seccomp profile will be applied. Valid options are: \n Localhost - a profile defined in a file on the node should be used. RuntimeDefault - the container runtime default profile should be used. Unconfined - no profile should be applied."
type: string
windowsOptions:
description: The Windows specific settings applied to all containers. If unspecified, the options from the PodSecurityContext will be used. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence. Note that this field cannot be set when spec.os.name is linux.
type: object
properties:
gmsaCredentialSpec:
description: GMSACredentialSpec is where the GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) inlines the contents of the GMSA credential spec named by the GMSACredentialSpecName field.
type: string
gmsaCredentialSpecName:
description: GMSACredentialSpecName is the name of the GMSA credential spec to use.
type: string
hostProcess:
description: HostProcess determines if a container should be run as a 'Host Process' container. All of a Pod's containers must have the same effective HostProcess value (it is not allowed to have a mix of HostProcess containers and non-HostProcess containers). In addition, if HostProcess is true then HostNetwork must also be set to true.
type: boolean
runAsUserName:
description: The UserName in Windows to run the entrypoint of the container process. Defaults to the user specified in image metadata if unspecified. May also be set in PodSecurityContext. If set in both SecurityContext and PodSecurityContext, the value specified in SecurityContext takes precedence.
type: string
tolerations:
type: array
items:
description: The pod this Toleration is attached to tolerates any taint that matches the triple <key,value,effect> using the matching operator <operator>.
type: object
properties:
effect:
description: Effect indicates the taint effect to match. Empty means match all taint effects. When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
type: string
key:
description: Key is the taint key that the toleration applies to. Empty means match all taint keys. If the key is empty, operator must be Exists; this combination means to match all values and all keys.
type: string
operator:
description: Operator represents a key's relationship to the value. Valid operators are Exists and Equal. Defaults to Equal. Exists is equivalent to wildcard for value, so that a pod can tolerate all taints of a particular category.
type: string
tolerationSeconds:
description: TolerationSeconds represents the period of time the toleration (which must be of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default, it is not set, which means tolerate the taint forever (do not evict). Zero and negative values will be treated as 0 (evict immediately) by the system.
type: integer
format: int64
value:
description: Value is the taint value the toleration matches to. If the operator is Exists, the value should be empty, otherwise just a regular string.
type: string
served: true
storage: true
subresources:
status: {}

View File

@@ -1,11 +0,0 @@
apiVersion: tailscale.com/v1alpha1
kind: ProxyClass
metadata:
name: prod
spec:
statefulSet:
pod:
nodeSelector:
beta.kubernetes.io/os: "linux"
imagePullSecrets:
- name: "foo"

View File

@@ -1,10 +0,0 @@
apiVersion: tailscale.com/v1alpha1
kind: ProxyClass
metadata:
name: removeinit
spec:
statefulSet:
pod:
patches:
- op: remove
path: "/spec/initContainers/0"

View File

@@ -173,21 +173,11 @@ rules:
- ingresses/status
verbs:
- '*'
- apiGroups:
- networking.k8s.io
resources:
- ingressclasses
verbs:
- get
- list
- watch
- apiGroups:
- tailscale.com
resources:
- connectors
- connectors/status
- proxyclasses
- proxyclasses/status
verbs:
- get
- list
@@ -296,6 +286,8 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: ENABLE_CONNECTOR
value: "false"
- name: CLIENT_ID_FILE
value: /oauth/client_id
- name: CLIENT_SECRET_FILE
@@ -322,11 +314,3 @@ spec:
- name: oauth
secret:
secretName: operator-oauth
---
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
annotations: {}
name: tailscale
spec:
controller: tailscale.com/ts-ingress

View File

@@ -12,12 +12,10 @@ import (
"strings"
"sync"
"github.com/pkg/errors"
"go.uber.org/zap"
corev1 "k8s.io/api/core/v1"
networkingv1 "k8s.io/api/networking/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -28,12 +26,6 @@ import (
"tailscale.com/util/set"
)
const (
tailscaleIngressClassName = "tailscale" // ingressClass.metadata.name for tailscale IngressClass resource
tailscaleIngressControllerName = "tailscale.com/ts-ingress" // ingressClass.spec.controllerName for tailscale IngressClass resource
ingressClassDefaultAnnotation = "ingressclass.kubernetes.io/is-default-class" // we do not support this https://kubernetes.io/docs/concepts/services-networking/ingress/#default-ingress-class
)
type IngressReconciler struct {
client.Client
@@ -117,10 +109,6 @@ func (a *IngressReconciler) maybeCleanup(ctx context.Context, logger *zap.Sugare
// This function adds a finalizer to ing, ensuring that we can handle orderly
// deprovisioning later.
func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.SugaredLogger, ing *networkingv1.Ingress) error {
if err := a.validateIngressClass(ctx); err != nil {
logger.Warnf("error validating tailscale IngressClass: %v. In future this might be a terminal error.", err)
}
if !slices.Contains(ing.Finalizers, FinalizerName) {
// This log line is printed exactly once during initial provisioning,
// because once the finalizer is in place this block gets skipped. So,
@@ -217,15 +205,6 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
continue
}
for _, p := range rule.HTTP.Paths {
// Send a warning if folks use Exact path type - to make
// it easier for us to support Exact path type matching
// in the future if needed.
// https://kubernetes.io/docs/concepts/services-networking/ingress/#path-types
if *p.PathType == networkingv1.PathTypeExact {
msg := "Exact path type strict matching is currently not supported and requests will be routed as for Prefix path type. This behaviour might change in the future."
logger.Warnf(fmt.Sprintf("Unsupported Path type exact for path %s. %s", p.Path, msg))
a.recorder.Eventf(ing, corev1.EventTypeWarning, "UnsupportedPathTypeExact", msg)
}
addIngressBackend(&p.Backend, p.Path)
}
}
@@ -288,28 +267,5 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
func (a *IngressReconciler) shouldExpose(ing *networkingv1.Ingress) bool {
return ing != nil &&
ing.Spec.IngressClassName != nil &&
*ing.Spec.IngressClassName == tailscaleIngressClassName
}
// validateIngressClass attempts to validate that 'tailscale' IngressClass
// included in Tailscale installation manifests exists and has not been modified
// to attempt to enable features that we do not support.
func (a *IngressReconciler) validateIngressClass(ctx context.Context) error {
ic := &networkingv1.IngressClass{
ObjectMeta: metav1.ObjectMeta{
Name: tailscaleIngressClassName,
},
}
if err := a.Get(ctx, client.ObjectKeyFromObject(ic), ic); apierrors.IsNotFound(err) {
return errors.New("Tailscale IngressClass not found in cluster. Latest installation manifests include a tailscale IngressClass - please update")
} else if err != nil {
return fmt.Errorf("error retrieving 'tailscale' IngressClass: %w", err)
}
if ic.Spec.Controller != tailscaleIngressControllerName {
return fmt.Errorf("Tailscale Ingress class controller name %s does not match tailscale Ingress controller name %s. Ensure that you are using 'tailscale' IngressClass from latest Tailscale installation manifests", ic.Spec.Controller, tailscaleIngressControllerName)
}
if ic.GetAnnotations()[ingressClassDefaultAnnotation] != "" {
return fmt.Errorf("%s annotation is set on 'tailscale' IngressClass, but Tailscale Ingress controller does not support default Ingress class. Ensure that you are using 'tailscale' IngressClass from latest Tailscale installation manifests", ingressClassDefaultAnnotation)
}
return nil
*ing.Spec.IngressClassName == "tailscale"
}

View File

@@ -62,6 +62,7 @@ func main() {
priorityClassName = defaultEnv("PROXY_PRIORITY_CLASS_NAME", "")
tags = defaultEnv("PROXY_TAGS", "tag:k8s")
tsFirewallMode = defaultEnv("PROXY_FIREWALL_MODE", "")
tsEnableConnector = defaultBool("ENABLE_CONNECTOR", false)
)
var opts []kzap.Opts
@@ -92,7 +93,7 @@ func main() {
maybeLaunchAPIServerProxy(zlog, restConfig, s, mode)
// TODO (irbekrm): gather the reconciler options into an opts struct
// rather than passing a million of them in one by one.
runReconcilers(zlog, s, tsNamespace, restConfig, tsClient, image, priorityClassName, tags, tsFirewallMode)
runReconcilers(zlog, s, tsNamespace, restConfig, tsClient, image, priorityClassName, tags, tsFirewallMode, tsEnableConnector)
}
// initTSNet initializes the tsnet.Server and logs in to Tailscale. It uses the
@@ -200,7 +201,7 @@ waitOnline:
// runReconcilers starts the controller-runtime manager and registers the
// ServiceReconciler. It blocks forever.
func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string, restConfig *rest.Config, tsClient *tailscale.Client, image, priorityClassName, tags, tsFirewallMode string) {
func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string, restConfig *rest.Config, tsClient *tailscale.Client, image, priorityClassName, tags, tsFirewallMode string, enableConnector bool) {
var (
isDefaultLoadBalancer = defaultBool("OPERATOR_DEFAULT_LOAD_BALANCER", false)
)
@@ -215,16 +216,15 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
Field: client.InNamespace(tsNamespace).AsSelector(),
}
mgrOpts := manager.Options{
// TODO (irbekrm): stricter filtering what we watch/cache/call
// reconcilers on. c/r by default starts a watch on any
// resources that we GET via the controller manager's client.
Cache: cache.Options{
ByObject: map[client.Object]cache.ByObject{
&corev1.Secret{}: nsFilter,
&appsv1.StatefulSet{}: nsFilter,
},
},
Scheme: tsapi.GlobalScheme,
}
if enableConnector {
mgrOpts.Scheme = tsapi.GlobalScheme
}
mgr, err := manager.New(restConfig, mgrOpts)
if err != nil {
@@ -278,20 +278,22 @@ func runReconcilers(zlog *zap.SugaredLogger, s *tsnet.Server, tsNamespace string
startlog.Fatalf("could not create controller: %v", err)
}
connectorFilter := handler.EnqueueRequestsFromMapFunc(managedResourceHandlerForType("connector"))
err = builder.ControllerManagedBy(mgr).
For(&tsapi.Connector{}).
Watches(&appsv1.StatefulSet{}, connectorFilter).
Watches(&corev1.Secret{}, connectorFilter).
Complete(&ConnectorReconciler{
ssr: ssr,
recorder: eventRecorder,
Client: mgr.GetClient(),
logger: zlog.Named("connector-reconciler"),
clock: tstime.DefaultClock{},
})
if err != nil {
startlog.Fatal("could not create connector reconciler: %v", err)
if enableConnector {
connectorFilter := handler.EnqueueRequestsFromMapFunc(managedResourceHandlerForType("subnetrouter"))
err = builder.ControllerManagedBy(mgr).
For(&tsapi.Connector{}).
Watches(&appsv1.StatefulSet{}, connectorFilter).
Watches(&corev1.Secret{}, connectorFilter).
Complete(&ConnectorReconciler{
ssr: ssr,
recorder: eventRecorder,
Client: mgr.GetClient(),
logger: zlog.Named("connector-reconciler"),
clock: tstime.DefaultClock{},
})
if err != nil {
startlog.Fatal("could not create connector reconciler: %v", err)
}
}
startlog.Infof("Startup complete, operator running, version: %s", version.Long())
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {

View File

@@ -6,6 +6,7 @@
package main
import (
"context"
"crypto/tls"
"fmt"
"log"
@@ -23,11 +24,22 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/tsnet"
"tailscale.com/util/clientmetric"
"tailscale.com/util/ctxkey"
"tailscale.com/util/set"
)
var whoIsKey = ctxkey.New("", (*apitype.WhoIsResponse)(nil))
type whoIsKey struct{}
// whoIsFromRequest returns the WhoIsResponse previously stashed by a call to
// addWhoIsToRequest.
func whoIsFromRequest(r *http.Request) *apitype.WhoIsResponse {
return r.Context().Value(whoIsKey{}).(*apitype.WhoIsResponse)
}
// addWhoIsToRequest stashes who in r's context, retrievable by a call to
// whoIsFromRequest.
func addWhoIsToRequest(r *http.Request, who *apitype.WhoIsResponse) *http.Request {
return r.WithContext(context.WithValue(r.Context(), whoIsKey{}, who))
}
var counterNumRequestsProxied = clientmetric.NewCounter("k8s_auth_proxy_requests_proxied")
@@ -115,7 +127,7 @@ func (h *apiserverProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
counterNumRequestsProxied.Add(1)
h.rp.ServeHTTP(w, r.WithContext(whoIsKey.WithValue(r.Context(), who)))
h.rp.ServeHTTP(w, addWhoIsToRequest(r, who))
}
// runAPIServerProxy runs an HTTP server that authenticates requests using the
@@ -228,7 +240,7 @@ type impersonateRule struct {
// in the context by the apiserverProxy.
func addImpersonationHeaders(r *http.Request, log *zap.SugaredLogger) error {
log = log.With("remote", r.RemoteAddr)
who := whoIsKey.Value(r.Context())
who := whoIsFromRequest(r)
rules, err := tailcfg.UnmarshalCapJSON[capRule](who.CapMap, capabilityName)
if len(rules) == 0 && err == nil {
// Try the old capability name for backwards compatibility.

View File

@@ -95,7 +95,7 @@ func TestImpersonationHeaders(t *testing.T) {
for _, tc := range tests {
r := must.Get(http.NewRequest("GET", "https://op.ts.net/api/foo", nil))
r = r.WithContext(whoIsKey.WithValue(r.Context(), &apitype.WhoIsResponse{
r = addWhoIsToRequest(r, &apitype.WhoIsResponse{
Node: &tailcfg.Node{
Name: "node.ts.net",
Tags: tc.tags,
@@ -104,7 +104,7 @@ func TestImpersonationHeaders(t *testing.T) {
LoginName: tc.emailish,
},
CapMap: tc.capMap,
}))
})
addImpersonationHeaders(r, zl.Sugar())
if d := cmp.Diff(tc.wantHeaders, r.Header); d != "" {

View File

@@ -16,7 +16,6 @@ import (
"os"
"strings"
jsonpatch "github.com/evanphx/json-patch"
"go.uber.org/zap"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
@@ -28,7 +27,6 @@ import (
"sigs.k8s.io/yaml"
"tailscale.com/client/tailscale"
"tailscale.com/ipn"
tsapi "tailscale.com/k8s-operator/apis/v1alpha1"
"tailscale.com/net/netutil"
"tailscale.com/tailcfg"
"tailscale.com/tsnet"
@@ -54,9 +52,6 @@ const (
//MagicDNS name of tailnet node.
AnnotationTailnetTargetFQDN = "tailscale.com/tailnet-fqdn"
// Users can set this on a Service or Ingress. This prototype only looks at Services
AnnotationProxyClass = "tailscale.com/proxy-class"
// Annotations settable by users on ingresses.
AnnotationFunnel = "tailscale.com/funnel"
@@ -92,8 +87,6 @@ type tailscaleSTSConfig struct {
// Connector specifies a configuration of a Connector instance if that's
// what this StatefulSet should be created for.
Connector *connector
ProxyClass string
}
type connector struct {
@@ -221,17 +214,18 @@ const maxStatefulSetNameLength = 63 - 10 - 1
// generation will NOT result in a StatefulSet name longer than 52 chars.
// This is done because of https://github.com/kubernetes/kubernetes/issues/64023.
func statefulSetNameBase(parent string) string {
base := fmt.Sprintf("ts-%s-", parent)
// Calculate what length name GenerateName returns for this base.
generator := names.SimpleNameGenerator
for {
generatedName := generator.GenerateName(base)
excess := len(generatedName) - maxStatefulSetNameLength
if excess <= 0 {
return base
}
base = base[:len(base)-1-excess] // cut off the excess chars
base = base + "-" // re-instate the dash
generatedName := generator.GenerateName(base)
if excess := len(generatedName) - maxStatefulSetNameLength; excess > 0 {
base = base[:len(base)-excess-1] // take extra char off to make space for hyphen
base = base + "-" // re-instate hyphen
}
return base
}
func (a *tailscaleSTSReconciler) reconcileHeadlessService(ctx context.Context, logger *zap.SugaredLogger, sts *tailscaleSTSConfig) (*corev1.Service, error) {
@@ -404,102 +398,7 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
}
}
}
pod := &ss.Spec.Template
container := &pod.Spec.Containers[0]
// if proxyclass is set
// get the proxy class
// get the pod template thing from there
// how to merge?
if sts.ProxyClass != "" {
logger.Infof("looking at proxy class %s", sts.ProxyClass)
proxyClass := &tsapi.ProxyClass{
ObjectMeta: metav1.ObjectMeta{
Name: sts.ProxyClass,
},
}
if err := a.Get(ctx, client.ObjectKeyFromObject(proxyClass), proxyClass); err != nil {
return nil, fmt.Errorf("failed to get ProxyClass: %w", err)
}
logger.Infof("retrieved proxy class %#+v", proxyClass.Spec)
// only look at pod spec for this prototype
if ssOverlay := proxyClass.Spec.StatefulSet; ssOverlay != nil && ssOverlay.Pod != nil {
pod.Labels = ssOverlay.Pod.Labels
pod.Annotations = ssOverlay.Pod.Annotations
pod.Spec.NodeName = ssOverlay.Pod.NodeName
pod.Spec.NodeSelector = ssOverlay.Pod.NodeSelector
logger.Infof("Setting pod node selctor: %+#v", ssOverlay.Pod.NodeSelector)
pod.Spec.ImagePullSecrets = ssOverlay.Pod.ImagePullSecrets
pod.Spec.Tolerations = ssOverlay.Pod.Tolerations
if ssOverlay.Pod.PodSecurityContext != nil {
pod.Spec.SecurityContext = ssOverlay.Pod.PodSecurityContext
}
if contOverlay := proxyClass.Spec.StatefulSet.Pod.TailscaleContainer; contOverlay != nil {
if contOverlay.SecurityContext != nil {
// alternatively we could merge this with the existing security context
container.SecurityContext = contOverlay.SecurityContext
}
container.Resources = contOverlay.Resources
}
if initContOverlay := proxyClass.Spec.StatefulSet.Pod.TailscaleInitContainer; initContOverlay != nil {
if initContOverlay.SecurityContext != nil {
pod.Spec.InitContainers[0].SecurityContext = initContOverlay.SecurityContext
}
}
if len(ssOverlay.Pod.Patches) > 0 {
logger.Info("applying overlay patches")
// logger.Infof("before modifying pod's init containers are %#+v", pod.Spec.InitContainers)
// get all patches together
var patches []byte
for _, patch := range ssOverlay.Pod.Patches {
jsonBytes, err := json.Marshal(patch)
if err != nil {
return nil, fmt.Errorf("error marshaling JSON patch: %w", err)
}
// there is definitely a better way
jsonBytes = []byte("[" + string(jsonBytes) + "]")
// patch, err := jsonpatch.DecodePatch(jsonBytes)
// if err != nil {
// return nil, fmt.Errorf("error decoding JSON patch: %w", err)
// }
if len(patches) == 0 {
patches = jsonBytes
} else {
patches, err = jsonpatch.MergeMergePatches(patches, jsonBytes)
if err != nil {
return nil, fmt.Errorf("error merging patches: %w", err)
}
}
logger.Infof("patch before merging : %+v\n", string(jsonBytes))
}
// this can be done better
podBytes, err := json.Marshal(pod)
if err != nil {
return nil, fmt.Errorf("error marshaling Pod spec to JSON: %w", err)
}
logger.Infof("patches before unmarshal: %+#v\n", string(patches))
mergePatch, err := jsonpatch.DecodePatch(patches)
if err != nil {
return nil, fmt.Errorf("error decoding JSON patches: %w", err)
}
modifiedPodBytes, err := mergePatch.Apply(podBytes)
if err != nil {
return nil, fmt.Errorf("error applying patch: %w", err)
}
// modifiedPodBytes, err := jsonpatch.MergePatch(podBytes, patches)
// if err != nil {
// return nil, fmt.Errorf("error updating Pod spec using merge patch: %w", err)
// }
// if jsonpatch.Equal(podBytes, modifiedPodBytes) {
// logger.Info("no change was applied")
// }
logger.Infof("modified pod's init containers are %#+v", string(modifiedPodBytes))
if err = json.Unmarshal(modifiedPodBytes, pod); err != nil {
return nil, fmt.Errorf("error umarshaling pod bytes: %w", err)
}
}
}
}
container := &ss.Spec.Template.Spec.Containers[0]
container.Image = a.proxyImage
ss.ObjectMeta = metav1.ObjectMeta{
Name: headlessSvc.Name,
@@ -512,10 +411,7 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
"app": sts.ParentResourceUID,
},
}
mak.Set(&pod.Labels, "app", sts.ParentResourceUID)
for key, val := range sts.ChildResourceLabels {
pod.Labels[key] = val // sync StatefulSet labels to Pod to make it easier for users to select the Pod
}
mak.Set(&ss.Spec.Template.Labels, "app", sts.ParentResourceUID)
// Generic containerboot configuration options.
container.Env = append(container.Env,
@@ -533,12 +429,12 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
// it is passed via an environment variable. So we need to restart the
// container when the value changes. We do this by adding an annotation to
// the pod template that contains the last value we set.
mak.Set(&pod.Annotations, podAnnotationLastSetHostname, sts.Hostname)
mak.Set(&ss.Spec.Template.Annotations, podAnnotationLastSetHostname, sts.Hostname)
}
// Configure containeboot to run tailscaled with a configfile read from the state Secret.
if shouldDoTailscaledDeclarativeConfig(sts) {
mak.Set(&ss.Spec.Template.Annotations, podAnnotationLastSetConfigFileHash, tsConfigHash)
pod.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, corev1.Volume{
ss.Spec.Template.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, corev1.Volume{
Name: "tailscaledconfig",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
@@ -567,7 +463,7 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
Value: a.tsFirewallMode,
})
}
pod.Spec.PriorityClassName = a.proxyPriorityClassName
ss.Spec.Template.Spec.PriorityClassName = a.proxyPriorityClassName
// Ingress/egress proxy configuration options.
if sts.ClusterTargetIP != "" {
@@ -598,7 +494,7 @@ func (a *tailscaleSTSReconciler) reconcileSTS(ctx context.Context, logger *zap.S
ReadOnly: true,
MountPath: "/etc/tailscaled",
})
pod.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, corev1.Volume{
ss.Spec.Template.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, corev1.Volume{
Name: "serve-config",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{

View File

@@ -6,9 +6,6 @@
package main
import (
"fmt"
"regexp"
"strings"
"testing"
)
@@ -22,20 +19,32 @@ import (
// https://github.com/kubernetes/kubernetes/blob/v1.28.4/staging/src/k8s.io/apiserver/pkg/storage/names/generate.go#L45.
// https://github.com/kubernetes/kubernetes/pull/116430
func Test_statefulSetNameBase(t *testing.T) {
// Service name lengths can be 1 - 63 chars, be paranoid and test them all.
var b strings.Builder
for b.Len() < 63 {
if _, err := b.WriteString("a"); err != nil {
t.Fatalf("error writing to string builder: %v", err)
}
baseLength := b.Len()
if baseLength > 43 {
baseLength = 43 // currently 43 is the max base length
}
wantsNameR := regexp.MustCompile(`^ts-a{` + fmt.Sprint(baseLength) + `}-$`) // to match a string like ts-aaaa-
gotName := statefulSetNameBase(b.String())
if !wantsNameR.MatchString(gotName) {
t.Fatalf("expected string %s to match regex %s ", gotName, wantsNameR.String()) // fatal rather than error as this test is called 63 times
}
tests := []struct {
name string
in string
out string
}{
{
name: "43 chars",
in: "oidhexl9o832hcbhyg4uz6o0s7u9uae54h5k8ofs9xb",
out: "ts-oidhexl9o832hcbhyg4uz6o0s7u9uae54h5k8ofs9xb-",
},
{
name: "44 chars",
in: "oidhexl9o832hcbhyg4uz6o0s7u9uae54h5k8ofs9xbo",
out: "ts-oidhexl9o832hcbhyg4uz6o0s7u9uae54h5k8ofs9xb-",
},
{
name: "42 chars",
in: "oidhexl9o832hcbhyg4uz6o0s7u9uae54h5k8ofs9x",
out: "ts-oidhexl9o832hcbhyg4uz6o0s7u9uae54h5k8ofs9x-",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := statefulSetNameBase(tt.in); got != tt.out {
t.Errorf("stsNamePrefix(%s) = %q, want %s", tt.in, got, tt.out)
}
})
}
}

View File

@@ -183,7 +183,6 @@ func (a *ServiceReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
Hostname: hostname,
Tags: tags,
ChildResourceLabels: crl,
ProxyClass: svc.GetAnnotations()[AnnotationProxyClass], // nil?
}
a.mu.Lock()

View File

@@ -147,13 +147,7 @@ func expectedSTS(opts configOpts) *appsv1.StatefulSet {
ObjectMeta: metav1.ObjectMeta{
Annotations: annots,
DeletionGracePeriodSeconds: ptr.To[int64](10),
Labels: map[string]string{
"tailscale.com/managed": "true",
"tailscale.com/parent-resource": "test",
"tailscale.com/parent-resource-ns": opts.namespace,
"tailscale.com/parent-resource-type": opts.parentType,
"app": "1234-UID",
},
Labels: map[string]string{"app": "1234-UID"},
},
Spec: corev1.PodSpec{
ServiceAccountName: "proxies",

View File

@@ -66,7 +66,6 @@ tailscale.com/cmd/stund dependencies: (generated by github.com/tailscale/depawar
tailscale.com/types/tkatype from tailscale.com/tailcfg+
tailscale.com/types/views from tailscale.com/net/tsaddr+
tailscale.com/util/cmpx from tailscale.com/tailcfg+
tailscale.com/util/ctxkey from tailscale.com/tsweb+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/tailcfg
tailscale.com/util/lineread from tailscale.com/version/distro

View File

@@ -23,8 +23,6 @@ import (
var exitNodeCmd = &ffcli.Command{
Name: "exit-node",
ShortUsage: "exit-node [flags]",
ShortHelp: "Show machines on your tailnet configured as exit nodes",
LongHelp: "Show machines on your tailnet configured as exit nodes",
Subcommands: []*ffcli.Command{
{
Name: "list",

View File

@@ -143,7 +143,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/util/cloudenv from tailscale.com/net/dnscache+
tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy+
tailscale.com/util/cmpx from tailscale.com/cmd/tailscale/cli+
tailscale.com/util/ctxkey from tailscale.com/types/logger
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
tailscale.com/util/groupmember from tailscale.com/client/web
@@ -268,7 +267,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
image/png from github.com/skip2/go-qrcode
io from bufio+
io/fs from crypto/x509+
io/ioutil from github.com/godbus/dbus/v5+
io/ioutil from golang.org/x/sys/cpu+
log from expvar+
log/internal from log
maps from tailscale.com/types/views+

View File

@@ -344,7 +344,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/util/cloudenv from tailscale.com/net/dns/resolver+
tailscale.com/util/cmpver from tailscale.com/net/dns+
tailscale.com/util/cmpx from tailscale.com/derp/derphttp+
tailscale.com/util/ctxkey from tailscale.com/ipn/ipnlocal+
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics+
tailscale.com/util/dnsname from tailscale.com/hostinfo+

View File

@@ -159,16 +159,16 @@ func (c *Client) parseServerInfo(b []byte) (*serverInfo, error) {
}
type clientInfo struct {
// Version is the DERP protocol version that the client was built with.
// See the ProtocolVersion const.
Version int `json:"version,omitempty"`
// MeshKey optionally specifies a pre-shared key used by
// trusted clients. It's required to subscribe to the
// connection list & forward packets. It's empty for regular
// users.
MeshKey string `json:"meshKey,omitempty"`
// Version is the DERP protocol version that the client was built with.
// See the ProtocolVersion const.
Version int `json:"version,omitempty"`
// CanAckPings is whether the client declares it's able to ack
// pings.
CanAckPings bool

View File

@@ -712,6 +712,7 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
bw: bw,
logf: logger.WithPrefix(s.logf, fmt.Sprintf("derp client %v%s: ", remoteAddr, clientKey.ShortString())),
done: ctx.Done(),
remoteAddr: remoteAddr,
remoteIPPort: remoteIPPort,
connectedAt: s.clock.Now(),
sendQueue: make(chan pkt, perClientSendQueueDepth),
@@ -1316,6 +1317,7 @@ type sclient struct {
info clientInfo
logf logger.Logf
done <-chan struct{} // closed when connection closes
remoteAddr string // usually ip:port from net.Conn.RemoteAddr().String()
remoteIPPort netip.AddrPort // zero if remoteAddr is not ip:port.
sendQueue chan pkt // packets queued to this client; never closed
discoSendQueue chan pkt // important packets queued to this client; never closed
@@ -1352,13 +1354,16 @@ type sclient struct {
// peerConnState represents whether a peer is connected to the server
// or not.
type peerConnState struct {
ipPort netip.AddrPort // if present, the peer's IP:port
peer key.NodePublic
present bool
ipPort netip.AddrPort // if present, the peer's IP:port
}
// pkt is a request to write a data frame to an sclient.
type pkt struct {
// src is the who's the sender of the packet.
src key.NodePublic
// enqueuedAt is when a packet was put onto a queue before it was sent,
// and is used for reporting metrics on the duration of packets in the queue.
enqueuedAt time.Time
@@ -1366,9 +1371,6 @@ type pkt struct {
// bs is the data packet bytes.
// The memory is owned by pkt.
bs []byte
// src is the who's the sender of the packet.
src key.NodePublic
}
// peerGoneMsg is a request to write a peerGone frame to an sclient
@@ -1571,17 +1573,6 @@ func (c *sclient) sendMeshUpdates() error {
c.s.mu.Lock()
defer c.s.mu.Unlock()
// allow all happened-before mesh update request goroutines to complete, if
// we don't finish the task we'll queue another below.
drainUpdates:
for {
select {
case <-c.meshUpdate:
default:
break drainUpdates
}
}
writes := 0
for _, pcs := range c.peerStateChange {
if c.bw.Available() <= frameHeaderLen+keyLen {

2
go.mod
View File

@@ -68,7 +68,7 @@ require (
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a
github.com/tailscale/mkctr v0.0.0-20240102155253-bf50773ba734
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
github.com/tailscale/web-client-prebuilt v0.0.0-20240111230031-5ca22df9e6e7
github.com/tailscale/web-client-prebuilt v0.0.0-20240109232428-26bf65339dda
github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272
github.com/tc-hib/winres v0.2.1
github.com/tcnksm/go-httpstat v0.2.0

4
go.sum
View File

@@ -898,8 +898,8 @@ github.com/tailscale/mkctr v0.0.0-20240102155253-bf50773ba734 h1:93cvKHbvsPK3MKf
github.com/tailscale/mkctr v0.0.0-20240102155253-bf50773ba734/go.mod h1:6v53VHLmLKUaqWMpSGDeRWhltLSCEteMItYoiKLpdJk=
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk=
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
github.com/tailscale/web-client-prebuilt v0.0.0-20240111230031-5ca22df9e6e7 h1:xAgOVncJuuxkFZ2oXXDKFTH4HDdFYSZRYdA6oMrCewg=
github.com/tailscale/web-client-prebuilt v0.0.0-20240111230031-5ca22df9e6e7/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tailscale/web-client-prebuilt v0.0.0-20240109232428-26bf65339dda h1:S+2mKvqj3K84d7qCX7MEjMsCiNXbEzXQ+ZvGdHsvAyc=
github.com/tailscale/web-client-prebuilt v0.0.0-20240109232428-26bf65339dda/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272 h1:zwsem4CaamMdC3tFoTpzrsUSMDPV0K6rhnQdF7kXekQ=
github.com/tailscale/wireguard-go v0.0.0-20231121184858-cc193a0b3272/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4=
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=

View File

@@ -46,8 +46,6 @@ type WindowsToken interface {
// IsElevated reports whether the receiver is currently executing as an
// elevated administrative user.
IsElevated() bool
// IsLocalSystem reports whether the receiver is the built-in SYSTEM user.
IsLocalSystem() bool
// UserDir returns the special directory identified by folderID as associated
// with the receiver. folderID must be one of the KNOWNFOLDERID values from
// the x/sys/windows package, serialized as a stringified GUID.

View File

@@ -93,12 +93,6 @@ func (t *token) IsElevated() bool {
return t.t.IsElevated()
}
func (t *token) IsLocalSystem() bool {
// https://web.archive.org/web/2024/https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers
const systemUID = ipn.WindowsUserID("S-1-5-18")
return t.IsUID(systemUID)
}
func (t *token) UserDir(folderID string) (string, error) {
guid, err := windows.GUIDFromString(folderID)
if err != nil {

View File

@@ -2735,16 +2735,6 @@ func (b *LocalBackend) CheckIPNConnectionAllowed(ci *ipnauth.ConnIdentity) error
if !b.pm.CurrentPrefs().ForceDaemon() {
return nil
}
// Always allow Windows SYSTEM user to connect,
// even if Tailscale is currently being used by another user.
if tok, err := ci.WindowsToken(); err == nil {
defer tok.Close()
if tok.IsLocalSystem() {
return nil
}
}
uid := ci.WindowsUserID()
if uid == "" {
return errors.New("empty user uid in connection identity")

View File

@@ -34,7 +34,6 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/types/lazy"
"tailscale.com/types/logger"
"tailscale.com/util/ctxkey"
"tailscale.com/util/mak"
"tailscale.com/version"
)
@@ -49,7 +48,8 @@ const (
// current etag of a resource.
var ErrETagMismatch = errors.New("etag mismatch")
var serveHTTPContextKey ctxkey.Key[*serveHTTPContext]
// serveHTTPContextKey is the context.Value key for a *serveHTTPContext.
type serveHTTPContextKey struct{}
type serveHTTPContext struct {
SrcAddr netip.AddrPort
@@ -433,7 +433,7 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort)
hs := &http.Server{
Handler: http.HandlerFunc(b.serveWebHandler),
BaseContext: func(_ net.Listener) context.Context {
return serveHTTPContextKey.WithValue(context.Background(), &serveHTTPContext{
return context.WithValue(context.Background(), serveHTTPContextKey{}, &serveHTTPContext{
SrcAddr: srcAddr,
DestPort: dport,
})
@@ -500,6 +500,11 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort)
return nil
}
func getServeHTTPContext(r *http.Request) (c *serveHTTPContext, ok bool) {
c, ok = r.Context().Value(serveHTTPContextKey{}).(*serveHTTPContext)
return c, ok
}
func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView, at string, ok bool) {
var z ipn.HTTPHandlerView // zero value
@@ -516,7 +521,7 @@ func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView,
hostname = r.TLS.ServerName
}
sctx, ok := serveHTTPContextKey.ValueOk(r.Context())
sctx, ok := getServeHTTPContext(r)
if !ok {
b.logf("[unexpected] localbackend: no serveHTTPContext in request")
return z, "", false
@@ -679,7 +684,7 @@ func addProxyForwardedHeaders(r *httputil.ProxyRequest) {
if r.In.TLS != nil {
r.Out.Header.Set("X-Forwarded-Proto", "https")
}
if c, ok := serveHTTPContextKey.ValueOk(r.Out.Context()); ok {
if c, ok := getServeHTTPContext(r.Out); ok {
r.Out.Header.Set("X-Forwarded-For", c.SrcAddr.Addr().String())
}
}
@@ -691,7 +696,7 @@ func (b *LocalBackend) addTailscaleIdentityHeaders(r *httputil.ProxyRequest) {
r.Out.Header.Del("Tailscale-User-Profile-Pic")
r.Out.Header.Del("Tailscale-Headers-Info")
c, ok := serveHTTPContextKey.ValueOk(r.Out.Context())
c, ok := getServeHTTPContext(r.Out)
if !ok {
return
}

View File

@@ -158,7 +158,7 @@ func TestGetServeHandler(t *testing.T) {
TLS: &tls.ConnectionState{ServerName: serverName},
}
port := cmpx.Or(tt.port, 443)
req = req.WithContext(serveHTTPContextKey.WithValue(req.Context(), &serveHTTPContext{
req = req.WithContext(context.WithValue(req.Context(), serveHTTPContextKey{}, &serveHTTPContext{
DestPort: port,
}))
@@ -428,7 +428,7 @@ func TestServeHTTPProxy(t *testing.T) {
URL: &url.URL{Path: "/"},
TLS: &tls.ConnectionState{ServerName: "example.ts.net"},
}
req = req.WithContext(serveHTTPContextKey.WithValue(req.Context(), &serveHTTPContext{
req = req.WithContext(context.WithValue(req.Context(), serveHTTPContextKey{}, &serveHTTPContext{
DestPort: 443,
SrcAddr: netip.MustParseAddrPort(tt.srcIP + ":1234"), // random src port for tests
}))

View File

@@ -1,8 +1,6 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios && !android
package ipnlocal
import (

View File

@@ -1,30 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build ios || android
package ipnlocal
import (
"errors"
"net"
"tailscale.com/client/tailscale"
)
const webClientPort = 5252
type webClient struct{}
func (b *LocalBackend) ConfigureWebClient(lc *tailscale.LocalClient) {}
func (b *LocalBackend) webClientGetOrInit() error {
return errors.New("not implemented")
}
func (b *LocalBackend) webClientShutdown() {}
func (b *LocalBackend) handleWebClientConn(c net.Conn) error {
return errors.New("not implemented")
}
func (b *LocalBackend) updateWebClientListenersLocked() {}

View File

@@ -251,12 +251,6 @@ func (s *Server) checkConnIdentityLocked(ci *ipnauth.ConnIdentity) error {
return err
}
// Always allow Windows SYSTEM user to connect,
// even if Tailscale is currently being used by another user.
if chkTok != nil && chkTok.IsLocalSystem() {
return nil
}
activeTok, err := active.WindowsToken()
if err == nil {
defer activeTok.Close()
@@ -407,10 +401,8 @@ func (s *Server) addActiveHTTPRequest(req *http.Request, ci *ipnauth.ConnIdentit
if !errors.Is(err, ipnauth.ErrNotImplemented) {
s.logf("error obtaining access token: %v", err)
}
} else if !token.IsLocalSystem() {
// Tell the LocalBackend about the identity we're now running as,
// unless its the SYSTEM user. That user is not a real account and
// doesn't have a home directory.
} else {
// Tell the LocalBackend about the identity we're now running as.
uid, err := lb.SetCurrentUser(token)
if err != nil {
token.Close()

View File

@@ -49,7 +49,7 @@ func init() {
// Adds the list of known types to api.Scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(SchemeGroupVersion, &Connector{}, &ConnectorList{}, &ProxyClass{}, &ProxyClassList{})
scheme.AddKnownTypes(SchemeGroupVersion, &Connector{}, &ConnectorList{})
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil

View File

@@ -1,88 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !plan9
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
var ProxyClassKind = "ProxyClass"
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster,shortName=pc
type ProxyClass struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ProxyClassSpec `json:"spec"`
// This would need status if we do any validation in operator.
// +optional
// Status ProxyClassStatus `json:"status"`
}
// +kubebuilder:object:root=true
type ProxyClassList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata"`
Items []ProxyClass `json:"items"`
}
type ProxyClassSpec struct {
// +optional
Service `json:"service,omitempty"`
// +optional
StatefulSet *StatefulSet `json:"statefulSet,omitempty"`
}
// Configuration for the headless Service, not actually used in this prototype,
// but is here to better illustrate the API structure
type Service struct {
Labels map[string]string `json:"labels,omitempty"`
}
type StatefulSet struct {
// +optional
Labels map[string]string `json:"labels,omitempty"`
// +optional
Annotations map[string]string `json:"annotations,omitempty"`
// +optional
Pod *Pod `json:"pod,omitempty"`
}
type Pod struct {
// Or should we just sync statefulset.labels, statefulset.annotations?
// +optional
Labels map[string]string `json:"labels,omitempty"`
// +optional
Annotations map[string]string `json:"annotations,omitempty"`
TailscaleContainer *Container `json:"tailscaleContainer,omitempty"`
TailscaleInitContainer *Container `json:"tailscaleInitContainer,omitempty"`
PodSecurityContext *corev1.PodSecurityContext `json:"podSecurityContext,omitempty"`
ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"`
NodeName string `json:"nodeName,omitempty"`
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
Tolerations []corev1.Toleration `json:"tolerations,omitempty"`
Patches []Patch `json:"patches,omitempty"`
}
type Container struct {
SecurityContext *corev1.SecurityContext `json:"securityContext,omitempty"`
Resources corev1.ResourceRequirements `json:"resources,omitempty"`
}
// RFC 6902 JSON patch
type Patch struct {
Path string `json:"path"`
// +optional
Value string `json:"value,omitempty"`
Op string `json:"op"`
}

View File

@@ -8,7 +8,6 @@
package v1alpha1
import (
"k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
)
@@ -137,189 +136,6 @@ func (in *ConnectorStatus) DeepCopy() *ConnectorStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Container) DeepCopyInto(out *Container) {
*out = *in
if in.SecurityContext != nil {
in, out := &in.SecurityContext, &out.SecurityContext
*out = new(v1.SecurityContext)
(*in).DeepCopyInto(*out)
}
in.Resources.DeepCopyInto(&out.Resources)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Container.
func (in *Container) DeepCopy() *Container {
if in == nil {
return nil
}
out := new(Container)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Patch) DeepCopyInto(out *Patch) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Patch.
func (in *Patch) DeepCopy() *Patch {
if in == nil {
return nil
}
out := new(Patch)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Pod) DeepCopyInto(out *Pod) {
*out = *in
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Annotations != nil {
in, out := &in.Annotations, &out.Annotations
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.TailscaleContainer != nil {
in, out := &in.TailscaleContainer, &out.TailscaleContainer
*out = new(Container)
(*in).DeepCopyInto(*out)
}
if in.TailscaleInitContainer != nil {
in, out := &in.TailscaleInitContainer, &out.TailscaleInitContainer
*out = new(Container)
(*in).DeepCopyInto(*out)
}
if in.PodSecurityContext != nil {
in, out := &in.PodSecurityContext, &out.PodSecurityContext
*out = new(v1.PodSecurityContext)
(*in).DeepCopyInto(*out)
}
if in.ImagePullSecrets != nil {
in, out := &in.ImagePullSecrets, &out.ImagePullSecrets
*out = make([]v1.LocalObjectReference, len(*in))
copy(*out, *in)
}
if in.NodeSelector != nil {
in, out := &in.NodeSelector, &out.NodeSelector
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Tolerations != nil {
in, out := &in.Tolerations, &out.Tolerations
*out = make([]v1.Toleration, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.Patches != nil {
in, out := &in.Patches, &out.Patches
*out = make([]Patch, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Pod.
func (in *Pod) DeepCopy() *Pod {
if in == nil {
return nil
}
out := new(Pod)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProxyClass) DeepCopyInto(out *ProxyClass) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyClass.
func (in *ProxyClass) DeepCopy() *ProxyClass {
if in == nil {
return nil
}
out := new(ProxyClass)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ProxyClass) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProxyClassList) DeepCopyInto(out *ProxyClassList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]ProxyClass, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyClassList.
func (in *ProxyClassList) DeepCopy() *ProxyClassList {
if in == nil {
return nil
}
out := new(ProxyClassList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ProxyClassList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *ProxyClassSpec) DeepCopyInto(out *ProxyClassSpec) {
*out = *in
in.Service.DeepCopyInto(&out.Service)
if in.StatefulSet != nil {
in, out := &in.StatefulSet, &out.StatefulSet
*out = new(StatefulSet)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProxyClassSpec.
func (in *ProxyClassSpec) DeepCopy() *ProxyClassSpec {
if in == nil {
return nil
}
out := new(ProxyClassSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in Routes) DeepCopyInto(out *Routes) {
{
@@ -339,62 +155,6 @@ func (in Routes) DeepCopy() Routes {
return *out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Service) DeepCopyInto(out *Service) {
*out = *in
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Service.
func (in *Service) DeepCopy() *Service {
if in == nil {
return nil
}
out := new(Service)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *StatefulSet) DeepCopyInto(out *StatefulSet) {
*out = *in
if in.Labels != nil {
in, out := &in.Labels, &out.Labels
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Annotations != nil {
in, out := &in.Annotations, &out.Annotations
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
if in.Pod != nil {
in, out := &in.Pod, &out.Pod
*out = new(Pod)
(*in).DeepCopyInto(*out)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StatefulSet.
func (in *StatefulSet) DeepCopy() *StatefulSet {
if in == nil {
return nil
}
out := new(StatefulSet)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SubnetRouter) DeepCopyInto(out *SubnetRouter) {
*out = *in

View File

@@ -50,9 +50,9 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/jmespath/go-jmespath](https://pkg.go.dev/github.com/jmespath/go-jmespath) ([Apache-2.0](https://github.com/jmespath/go-jmespath/blob/v0.4.0/LICENSE))
- [github.com/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/5c7d0dd6ab86/license))
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/v1.3.5/LICENSE.md))
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.17.4/LICENSE))
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.17.4/internal/snapref/LICENSE))
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.17.4/zstd/internal/xxhash/LICENSE.txt))
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.17.0/LICENSE))
- [github.com/klauspost/compress/internal/snapref](https://pkg.go.dev/github.com/klauspost/compress/internal/snapref) ([BSD-3-Clause](https://github.com/klauspost/compress/blob/v1.17.0/internal/snapref/LICENSE))
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.17.0/zstd/internal/xxhash/LICENSE.txt))
- [github.com/kortschak/wol](https://pkg.go.dev/github.com/kortschak/wol) ([BSD-3-Clause](https://github.com/kortschak/wol/blob/da482cc4850a/LICENSE))
- [github.com/mdlayher/genetlink](https://pkg.go.dev/github.com/mdlayher/genetlink) ([MIT](https://github.com/mdlayher/genetlink/blob/v1.3.2/LICENSE.md))
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.2/LICENSE.md))
@@ -64,12 +64,12 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/pkg/errors](https://pkg.go.dev/github.com/pkg/errors) ([BSD-2-Clause](https://github.com/pkg/errors/blob/v0.9.1/LICENSE))
- [github.com/safchain/ethtool](https://pkg.go.dev/github.com/safchain/ethtool) ([Apache-2.0](https://github.com/safchain/ethtool/blob/v0.3.0/LICENSE))
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/7ce1f622c780/LICENSE))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/f0b76a10a08e/LICENSE))
- [github.com/tailscale/goupnp](https://pkg.go.dev/github.com/tailscale/goupnp) ([BSD-2-Clause](https://github.com/tailscale/goupnp/blob/c64d0f06ea05/LICENSE))
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
- [github.com/tailscale/tailscale-android](https://pkg.go.dev/github.com/tailscale/tailscale-android) ([BSD-3-Clause](https://github.com/tailscale/tailscale-android/blob/HEAD/LICENSE))
- [github.com/tailscale/web-client-prebuilt](https://pkg.go.dev/github.com/tailscale/web-client-prebuilt) ([BSD-3-Clause](https://github.com/tailscale/web-client-prebuilt/blob/26bf65339dda/LICENSE))
- [github.com/tailscale/web-client-prebuilt](https://pkg.go.dev/github.com/tailscale/web-client-prebuilt) ([BSD-3-Clause](https://github.com/tailscale/web-client-prebuilt/blob/9b3142ca6f79/LICENSE))
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/cc193a0b3272/LICENSE))
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
- [github.com/u-root/uio](https://pkg.go.dev/github.com/u-root/uio) ([BSD-3-Clause](https://github.com/u-root/uio/blob/3e8cd9d6bf63/LICENSE))
@@ -80,14 +80,14 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/6213f710f925/LICENSE))
- [go4.org/unsafe/assume-no-moving-gc](https://pkg.go.dev/go4.org/unsafe/assume-no-moving-gc) ([BSD-3-Clause](https://github.com/go4org/unsafe-assume-no-moving-gc/blob/e7c30c78aeb2/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/08396bb9:LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.15.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/92128663:LICENSE))
- [golang.org/x/exp/shiny](https://pkg.go.dev/golang.org/x/exp/shiny) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/334a2380:shiny/LICENSE))
- [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.12.0:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.18.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.5.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.15.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.15.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.14.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.14.0:LICENSE))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE))
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/4fe30062272c/LICENSE))

View File

@@ -1,10 +1,10 @@
# Tailscale for macOS/iOS/tvOS dependencies
# Tailscale for macOS/iOS dependencies
The following open source dependencies are used to build Tailscale on [macOS][], [iOS][] and [tvOS][]. See also the dependencies in the [Tailscale CLI][].
The following open source dependencies are used to build Tailscale on [macOS][]
and [iOS][]. See also the dependencies in the [Tailscale CLI][].
[macOS]: https://tailscale.com/kb/1016/install-mac/
[iOS]: https://tailscale.com/kb/1020/install-ios/
[tvOS]: https://tailscale.com/kb/1280/appletv/
[Tailscale CLI]: ./tailscale.md
## Go Packages
@@ -53,7 +53,7 @@ The following open source dependencies are used to build Tailscale on [macOS][],
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
- [github.com/pierrec/lz4/v4](https://pkg.go.dev/github.com/pierrec/lz4/v4) ([BSD-3-Clause](https://github.com/pierrec/lz4/blob/v4.1.18/LICENSE))
- [github.com/safchain/ethtool](https://pkg.go.dev/github.com/safchain/ethtool) ([Apache-2.0](https://github.com/safchain/ethtool/blob/v0.3.0/LICENSE))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/7ce1f622c780/LICENSE))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/f0b76a10a08e/LICENSE))
- [github.com/tailscale/goupnp](https://pkg.go.dev/github.com/tailscale/goupnp) ([BSD-2-Clause](https://github.com/tailscale/goupnp/blob/c64d0f06ea05/LICENSE))
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
@@ -65,12 +65,12 @@ The following open source dependencies are used to build Tailscale on [macOS][],
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/6213f710f925/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/08396bb9:LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.15.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/92128663:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.18.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.5.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.15.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.15.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.14.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.14.0:LICENSE))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE))
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/4fe30062272c/LICENSE))

View File

@@ -74,10 +74,10 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
- [github.com/tailscale/certstore](https://pkg.go.dev/github.com/tailscale/certstore) ([MIT](https://github.com/tailscale/certstore/blob/d3fa0460f47e/LICENSE.md))
- [github.com/tailscale/go-winio](https://pkg.go.dev/github.com/tailscale/go-winio) ([MIT](https://github.com/tailscale/go-winio/blob/c4f33415bf55/LICENSE))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/7ce1f622c780/LICENSE))
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/f0b76a10a08e/LICENSE))
- [github.com/tailscale/hujson](https://pkg.go.dev/github.com/tailscale/hujson) ([BSD-3-Clause](https://github.com/tailscale/hujson/blob/20486734a56a/LICENSE))
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
- [github.com/tailscale/web-client-prebuilt](https://pkg.go.dev/github.com/tailscale/web-client-prebuilt) ([BSD-3-Clause](https://github.com/tailscale/web-client-prebuilt/blob/5ca22df9e6e7/LICENSE))
- [github.com/tailscale/web-client-prebuilt](https://pkg.go.dev/github.com/tailscale/web-client-prebuilt) ([BSD-3-Clause](https://github.com/tailscale/web-client-prebuilt/blob/a4fa669015b2/LICENSE))
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/cc193a0b3272/LICENSE))
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
- [github.com/toqueteos/webbrowser](https://pkg.go.dev/github.com/toqueteos/webbrowser) ([MIT](https://github.com/toqueteos/webbrowser/blob/v1.2.0/LICENSE.md))
@@ -88,13 +88,13 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/6213f710f925/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/08396bb9:LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.15.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/92128663:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.18.0:LICENSE))
- [golang.org/x/oauth2](https://pkg.go.dev/golang.org/x/oauth2) ([BSD-3-Clause](https://cs.opensource.google/go/x/oauth2/+/v0.12.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.5.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.15.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.15.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.14.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.14.0:LICENSE))
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/v0.3.0:LICENSE))
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))

View File

@@ -52,7 +52,7 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
- [github.com/tailscale/go-winio](https://pkg.go.dev/github.com/tailscale/go-winio) ([MIT](https://github.com/tailscale/go-winio/blob/c4f33415bf55/LICENSE))
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/6a278000867c/LICENSE))
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/95b7e17614b9/LICENSE))
- [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/d2e5cdeed6dc/LICENSE))
- [github.com/tc-hib/winres](https://pkg.go.dev/github.com/tc-hib/winres) ([0BSD](https://github.com/tc-hib/winres/blob/v0.2.1/LICENSE))
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/v1.2.1-beta.2/LICENSE))
@@ -60,14 +60,14 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/6213f710f925/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/08396bb9:LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.15.0:LICENSE))
- [golang.org/x/exp/constraints](https://pkg.go.dev/golang.org/x/exp/constraints) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/92128663:LICENSE))
- [golang.org/x/image/bmp](https://pkg.go.dev/golang.org/x/image/bmp) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.14.0:LICENSE))
- [golang.org/x/image/bmp](https://pkg.go.dev/golang.org/x/image/bmp) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.12.0:LICENSE))
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.14.0:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.18.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.5.0:LICENSE))
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.15.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.15.0:LICENSE))
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.14.0:LICENSE))
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.14.0:LICENSE))
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))

View File

@@ -478,28 +478,6 @@ func (m *Monitor) IsMajorChangeFrom(s1, s2 *interfaces.State) bool {
return true
}
}
// Iterate over s2 in case there is a field in s2 that doesn't exist in s1
for iname, i := range s2.Interface {
if iname == m.tsIfName {
// Ignore changes in the Tailscale interface itself.
continue
}
ips := s2.InterfaceIPs[iname]
if !m.isInterestingInterface(i, ips) {
continue
}
i1, ok := s1.Interface[iname]
if !ok {
return true
}
ips1, ok := s1.InterfaceIPs[iname]
if !ok {
return true
}
if !i.Equal(i1) || !prefixesMajorEqual(ips, ips1) {
return true
}
}
return false
}

View File

@@ -1341,9 +1341,6 @@ const (
PeerCapabilityWakeOnLAN PeerCapability = "https://tailscale.com/cap/wake-on-lan"
// PeerCapabilityIngress grants the ability for a peer to send ingress traffic.
PeerCapabilityIngress PeerCapability = "https://tailscale.com/cap/ingress"
// PeerCapabilityWebUI grants the ability for a peer to edit features from the
// device Web UI.
PeerCapabilityWebUI PeerCapability = "tailscale.com/cap/webui"
)
// NodeCapMap is a map of capabilities to their optional values. It is valid for

View File

@@ -4,7 +4,6 @@
package rate
import (
"encoding/json"
"fmt"
"math"
"sync"
@@ -182,41 +181,3 @@ func (r *Value) rateNow(now mono.Time) float64 {
func (r *Value) normalizedIntegral() float64 {
return r.halfLife() / math.Ln2
}
type jsonValue struct {
// TODO: Use v2 "encoding/json" for native time.Duration formatting.
HalfLife string `json:"halfLife,omitempty,omitzero"`
Value float64 `json:"value,omitempty,omitzero"`
Updated mono.Time `json:"updated,omitempty,omitzero"`
}
func (r *Value) MarshalJSON() ([]byte, error) {
if r == nil {
return []byte("null"), nil
}
r.mu.Lock()
defer r.mu.Unlock()
v := jsonValue{Value: r.value, Updated: r.updated}
if r.HalfLife > 0 {
v.HalfLife = r.HalfLife.String()
}
return json.Marshal(v)
}
func (r *Value) UnmarshalJSON(b []byte) error {
var v jsonValue
if err := json.Unmarshal(b, &v); err != nil {
return err
}
halfLife, err := time.ParseDuration(v.HalfLife)
if err != nil && v.HalfLife != "" {
return fmt.Errorf("invalid halfLife: %w", err)
}
r.mu.Lock()
defer r.mu.Unlock()
r.HalfLife = halfLife
r.value = v.Value
r.updated = v.Updated
return nil
}

View File

@@ -6,14 +6,12 @@ package rate
import (
"flag"
"math"
"reflect"
"testing"
"time"
qt "github.com/frankban/quicktest"
"github.com/google/go-cmp/cmp/cmpopts"
"tailscale.com/tstime/mono"
"tailscale.com/util/must"
)
const (
@@ -236,26 +234,3 @@ func BenchmarkValue(b *testing.B) {
v.Add(1)
}
}
func TestValueMarshal(t *testing.T) {
now := mono.Now()
tests := []struct {
val *Value
str string
}{
{val: &Value{}, str: `{}`},
{val: &Value{HalfLife: 5 * time.Minute}, str: `{"halfLife":"` + (5 * time.Minute).String() + `"}`},
{val: &Value{value: 12345, updated: now}, str: `{"value":12345,"updated":` + string(must.Get(now.MarshalJSON())) + `}`},
}
for _, tt := range tests {
str := string(must.Get(tt.val.MarshalJSON()))
if str != tt.str {
t.Errorf("string mismatch: got %v, want %v", str, tt.str)
}
var val Value
must.Do(val.UnmarshalJSON([]byte(str)))
if !reflect.DeepEqual(&val, tt.val) {
t.Errorf("value mismatch: %+v, want %+v", &val, tt.val)
}
}
}

View File

@@ -8,7 +8,6 @@ import (
"net/http"
"github.com/google/uuid"
"tailscale.com/util/ctxkey"
)
// RequestID is an opaque identifier for a HTTP request, used to correlate
@@ -25,9 +24,6 @@ import (
// opaque string. The current implementation uses a UUID.
type RequestID string
// RequestIDKey stores and loads [RequestID] values within a [context.Context].
var RequestIDKey ctxkey.Key[RequestID]
// RequestIDHeader is a custom HTTP header that the WithRequestID middleware
// uses to determine whether to re-use a given request ID from the client
// or generate a new one.
@@ -46,16 +42,22 @@ func SetRequestID(h http.Handler) http.Handler {
// transitions if needed.
id = "REQ-1" + uuid.NewString()
}
ctx := RequestIDKey.WithValue(r.Context(), RequestID(id))
ctx := withRequestID(r.Context(), RequestID(id))
r = r.WithContext(ctx)
h.ServeHTTP(w, r)
})
}
type requestIDKey struct{}
// RequestIDFromContext retrieves the RequestID from context that can be set by
// the SetRequestID function.
//
// Deprecated: Use [RequestIDKey.Value] instead.
func RequestIDFromContext(ctx context.Context) RequestID {
return RequestIDKey.Value(ctx)
val, _ := ctx.Value(requestIDKey{}).(RequestID)
return val
}
// withRequestID sets the given request id value in the given context.
func withRequestID(ctx context.Context, rid RequestID) context.Context {
return context.WithValue(ctx, requestIDKey{}, rid)
}

View File

@@ -166,7 +166,7 @@ func TestStdHandler(t *testing.T) {
{
name: "handler returns 404 via HTTPError with request ID",
rh: handlerErr(0, Error(404, "not found", testErr)),
r: req(RequestIDKey.WithValue(bgCtx, exampleRequestID), "http://example.com/foo"),
r: req(withRequestID(bgCtx, exampleRequestID), "http://example.com/foo"),
wantCode: 404,
wantLog: AccessLogRecord{
When: startTime,
@@ -203,7 +203,7 @@ func TestStdHandler(t *testing.T) {
{
name: "handler returns 404 with request ID and nil child error",
rh: handlerErr(0, Error(404, "not found", nil)),
r: req(RequestIDKey.WithValue(bgCtx, exampleRequestID), "http://example.com/foo"),
r: req(withRequestID(bgCtx, exampleRequestID), "http://example.com/foo"),
wantCode: 404,
wantLog: AccessLogRecord{
When: startTime,
@@ -240,7 +240,7 @@ func TestStdHandler(t *testing.T) {
{
name: "handler returns user-visible error with request ID",
rh: handlerErr(0, vizerror.New("visible error")),
r: req(RequestIDKey.WithValue(bgCtx, exampleRequestID), "http://example.com/foo"),
r: req(withRequestID(bgCtx, exampleRequestID), "http://example.com/foo"),
wantCode: 500,
wantLog: AccessLogRecord{
When: startTime,
@@ -277,7 +277,7 @@ func TestStdHandler(t *testing.T) {
{
name: "handler returns user-visible error wrapped by private error with request ID",
rh: handlerErr(0, fmt.Errorf("private internal error: %w", vizerror.New("visible error"))),
r: req(RequestIDKey.WithValue(bgCtx, exampleRequestID), "http://example.com/foo"),
r: req(withRequestID(bgCtx, exampleRequestID), "http://example.com/foo"),
wantCode: 500,
wantLog: AccessLogRecord{
When: startTime,
@@ -314,7 +314,7 @@ func TestStdHandler(t *testing.T) {
{
name: "handler returns generic error with request ID",
rh: handlerErr(0, testErr),
r: req(RequestIDKey.WithValue(bgCtx, exampleRequestID), "http://example.com/foo"),
r: req(withRequestID(bgCtx, exampleRequestID), "http://example.com/foo"),
wantCode: 500,
wantLog: AccessLogRecord{
When: startTime,
@@ -350,7 +350,7 @@ func TestStdHandler(t *testing.T) {
{
name: "handler returns error after writing response with request ID",
rh: handlerErr(200, testErr),
r: req(RequestIDKey.WithValue(bgCtx, exampleRequestID), "http://example.com/foo"),
r: req(withRequestID(bgCtx, exampleRequestID), "http://example.com/foo"),
wantCode: 200,
wantLog: AccessLogRecord{
When: startTime,
@@ -446,7 +446,7 @@ func TestStdHandler(t *testing.T) {
{
name: "error handler gets run with request ID",
rh: handlerErr(0, Error(404, "not found", nil)), // status code changed in errHandler
r: req(RequestIDKey.WithValue(bgCtx, exampleRequestID), "http://example.com/"),
r: req(withRequestID(bgCtx, exampleRequestID), "http://example.com/"),
wantCode: 200,
errHandler: func(w http.ResponseWriter, r *http.Request, e HTTPError) {
requestID := RequestIDFromContext(r.Context())

View File

@@ -21,7 +21,6 @@ import (
"context"
"tailscale.com/envknob"
"tailscale.com/util/ctxkey"
)
// Logf is the basic Tailscale logger type: a printf-like func.
@@ -29,16 +28,13 @@ import (
// Logf functions must be safe for concurrent use.
type Logf func(format string, args ...any)
// LogfKey stores and loads [Logf] values within a [context.Context].
var LogfKey = ctxkey.New("", Logf(log.Printf))
// A Context is a context.Context that should contain a custom log function, obtainable from FromContext.
// If no log function is present, FromContext will return log.Printf.
// To construct a Context, use Add
//
// Deprecated: Do not use.
type Context context.Context
type logfKey struct{}
// jenc is a json.Encode + bytes.Buffer pair wired up to be reused in a pool.
type jenc struct {
buf bytes.Buffer
@@ -83,17 +79,17 @@ func (logf Logf) JSON(level int, recType string, v any) {
}
// FromContext extracts a log function from ctx.
//
// Deprecated: Use [LogfKey.Value] instead.
func FromContext(ctx Context) Logf {
return LogfKey.Value(ctx)
v := ctx.Value(logfKey{})
if v == nil {
return log.Printf
}
return v.(Logf)
}
// Ctx constructs a Context from ctx with fn as its custom log function.
//
// Deprecated: Use [LogfKey.WithValue] instead.
func Ctx(ctx context.Context, fn Logf) Context {
return LogfKey.WithValue(ctx, fn)
return context.WithValue(ctx, logfKey{}, fn)
}
// WithPrefix wraps f, prefixing each format with the provided prefix.

View File

@@ -1,140 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// ctxkey provides type-safe key-value pairs for use with [context.Context].
//
// Example usage:
//
// // Create a context key.
// var TimeoutKey = ctxkey.New("mapreduce.Timeout", 5*time.Second)
//
// // Store a context value.
// ctx = mapreduce.TimeoutKey.WithValue(ctx, 10*time.Second)
//
// // Load a context value.
// timeout := mapreduce.TimeoutKey.Value(ctx)
// ... // use timeout of type time.Duration
//
// This is inspired by https://go.dev/issue/49189.
package ctxkey
import (
"context"
"fmt"
"reflect"
)
// TODO(https://go.dev/issue/60088): Use reflect.TypeFor instead.
func reflectTypeFor[T any]() reflect.Type {
return reflect.TypeOf((*T)(nil)).Elem()
}
// Key is a generic key type associated with a specific value type.
//
// A zero Key is valid where the Value type itself is used as the context key.
// This pattern should only be used with locally declared Go types,
// otherwise different packages risk producing key conflicts.
//
// Example usage:
//
// type peerInfo struct { ... } // peerInfo is a locally declared type
// var peerInfoKey ctxkey.Key[peerInfo]
// ctx = peerInfoKey.WithValue(ctx, info) // store a context value
// info = peerInfoKey.Value(ctx) // load a context value
type Key[Value any] struct {
name *stringer[string]
defVal *Value
}
// New constructs a new context key with an associated value type
// where the default value for an unpopulated value is the provided value.
//
// The provided name is an arbitrary name only used for human debugging.
// As a convention, it is recommended that the name be the dot-delimited
// combination of the package name of the caller with the variable name.
// If the name is not provided, then the name of the Value type is used.
// Every key is unique, even if provided the same name.
//
// Example usage:
//
// package mapreduce
// var NumWorkersKey = ctxkey.New("mapreduce.NumWorkers", runtime.NumCPU())
func New[Value any](name string, defaultValue Value) Key[Value] {
// Allocate a new stringer to ensure that every invocation of New
// creates a universally unique context key even for the same name
// since newly allocated pointers are globally unique within a process.
key := Key[Value]{name: new(stringer[string])}
if name == "" {
name = reflectTypeFor[Value]().String()
}
key.name.v = name
if v := reflect.ValueOf(defaultValue); v.IsValid() && !v.IsZero() {
key.defVal = &defaultValue
}
return key
}
// contextKey returns the context key to use.
func (key Key[Value]) contextKey() any {
if key.name == nil {
// Use the reflect.Type of the Value (implies key not created by New).
return reflectTypeFor[Value]()
} else {
// Use the name pointer directly (implies key created by New).
return key.name
}
}
// WithValue returns a copy of parent in which the value associated with key is val.
//
// It is a type-safe equivalent of [context.WithValue].
func (key Key[Value]) WithValue(parent context.Context, val Value) context.Context {
return context.WithValue(parent, key.contextKey(), stringer[Value]{val})
}
// ValueOk returns the value in the context associated with this key
// and also reports whether it was present.
// If the value is not present, it returns the default value.
func (key Key[Value]) ValueOk(ctx context.Context) (v Value, ok bool) {
vv, ok := ctx.Value(key.contextKey()).(stringer[Value])
if !ok && key.defVal != nil {
vv.v = *key.defVal
}
return vv.v, ok
}
// Value returns the value in the context associated with this key.
// If the value is not present, it returns the default value.
func (key Key[Value]) Value(ctx context.Context) (v Value) {
v, _ = key.ValueOk(ctx)
return v
}
// Has reports whether the context has a value for this key.
func (key Key[Value]) Has(ctx context.Context) (ok bool) {
_, ok = key.ValueOk(ctx)
return ok
}
// String returns the name of the key.
func (key Key[Value]) String() string {
if key.name == nil {
return reflectTypeFor[Value]().String()
}
return key.name.String()
}
// stringer implements [fmt.Stringer] on a generic T.
//
// This assists in debugging such that printing a context prints key and value.
// Note that the [context] package lacks a dependency on [reflect],
// so it cannot print arbitrary values. By implementing [fmt.Stringer],
// we functionally teach a context how to print itself.
//
// Wrapping values within a struct has an added bonus that interface kinds
// are properly handled. Without wrapping, we would be unable to distinguish
// between a nil value that was explicitly set or not.
// However, the presence of a stringer indicates an explicit nil value.
type stringer[T any] struct{ v T }
func (v stringer[T]) String() string { return fmt.Sprint(v.v) }

View File

@@ -1,104 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package ctxkey
import (
"context"
"fmt"
"io"
"regexp"
"testing"
"time"
qt "github.com/frankban/quicktest"
)
func TestKey(t *testing.T) {
c := qt.New(t)
ctx := context.Background()
// Test keys with the same name as being distinct.
k1 := New("same.Name", "")
c.Assert(k1.String(), qt.Equals, "same.Name")
k2 := New("same.Name", "")
c.Assert(k2.String(), qt.Equals, "same.Name")
c.Assert(k1 == k2, qt.Equals, false)
ctx = k1.WithValue(ctx, "hello")
c.Assert(k1.Has(ctx), qt.Equals, true)
c.Assert(k1.Value(ctx), qt.Equals, "hello")
c.Assert(k2.Has(ctx), qt.Equals, false)
c.Assert(k2.Value(ctx), qt.Equals, "")
ctx = k2.WithValue(ctx, "goodbye")
c.Assert(k1.Has(ctx), qt.Equals, true)
c.Assert(k1.Value(ctx), qt.Equals, "hello")
c.Assert(k2.Has(ctx), qt.Equals, true)
c.Assert(k2.Value(ctx), qt.Equals, "goodbye")
// Test default value.
k3 := New("mapreduce.Timeout", time.Hour)
c.Assert(k3.Has(ctx), qt.Equals, false)
c.Assert(k3.Value(ctx), qt.Equals, time.Hour)
ctx = k3.WithValue(ctx, time.Minute)
c.Assert(k3.Has(ctx), qt.Equals, true)
c.Assert(k3.Value(ctx), qt.Equals, time.Minute)
// Test incomparable value.
k4 := New("slice", []int(nil))
c.Assert(k4.Has(ctx), qt.Equals, false)
c.Assert(k4.Value(ctx), qt.DeepEquals, []int(nil))
ctx = k4.WithValue(ctx, []int{1, 2, 3})
c.Assert(k4.Has(ctx), qt.Equals, true)
c.Assert(k4.Value(ctx), qt.DeepEquals, []int{1, 2, 3})
// Accessors should be allocation free.
c.Assert(testing.AllocsPerRun(100, func() {
k1.Value(ctx)
k1.Has(ctx)
k1.ValueOk(ctx)
}), qt.Equals, 0.0)
// Test keys that are created without New.
var k5 Key[string]
c.Assert(k5.String(), qt.Equals, "string")
c.Assert(k1 == k5, qt.Equals, false) // should be different from key created by New
c.Assert(k5.Has(ctx), qt.Equals, false)
ctx = k5.WithValue(ctx, "fizz")
c.Assert(k5.Value(ctx), qt.Equals, "fizz")
var k6 Key[string]
c.Assert(k6.String(), qt.Equals, "string")
c.Assert(k5 == k6, qt.Equals, true)
c.Assert(k6.Has(ctx), qt.Equals, true)
ctx = k6.WithValue(ctx, "fizz")
// Test interface value types.
var k7 Key[any]
c.Assert(k7.Has(ctx), qt.Equals, false)
ctx = k7.WithValue(ctx, "whatever")
c.Assert(k7.Value(ctx), qt.DeepEquals, "whatever")
ctx = k7.WithValue(ctx, []int{1, 2, 3})
c.Assert(k7.Value(ctx), qt.DeepEquals, []int{1, 2, 3})
ctx = k7.WithValue(ctx, nil)
c.Assert(k7.Has(ctx), qt.Equals, true)
c.Assert(k7.Value(ctx), qt.DeepEquals, nil)
k8 := New[error]("error", io.EOF)
c.Assert(k8.Has(ctx), qt.Equals, false)
c.Assert(k8.Value(ctx), qt.Equals, io.EOF)
ctx = k8.WithValue(ctx, nil)
c.Assert(k8.Value(ctx), qt.Equals, nil)
c.Assert(k8.Has(ctx), qt.Equals, true)
err := fmt.Errorf("read error: %w", io.ErrUnexpectedEOF)
ctx = k8.WithValue(ctx, err)
c.Assert(k8.Value(ctx), qt.Equals, err)
c.Assert(k8.Has(ctx), qt.Equals, true)
}
func TestStringer(t *testing.T) {
t.SkipNow() // TODO(https://go.dev/cl/555697): Enable this after fix is merged upstream.
c := qt.New(t)
ctx := context.Background()
c.Assert(fmt.Sprint(New("foo.Bar", "").WithValue(ctx, "baz")), qt.Matches, regexp.MustCompile("foo.Bar.*baz"))
c.Assert(fmt.Sprint(New("", []int{}).WithValue(ctx, []int{1, 2, 3})), qt.Matches, regexp.MustCompile(fmt.Sprintf("%[1]T.*%[1]v", []int{1, 2, 3})))
c.Assert(fmt.Sprint(New("", 0).WithValue(ctx, 5)), qt.Matches, regexp.MustCompile("int.*5"))
c.Assert(fmt.Sprint(Key[time.Duration]{}.WithValue(ctx, time.Hour)), qt.Matches, regexp.MustCompile(fmt.Sprintf("%[1]T.*%[1]v", time.Hour)))
}

View File

@@ -12,7 +12,6 @@ import (
"net/netip"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"sync"
@@ -156,13 +155,6 @@ type firewallTweaker struct {
// stop makes fwProc exit when closed.
fwProcWriter io.WriteCloser
fwProcEncoder *json.Encoder
// The path to the 'netsh.exe' binary, populated during the first call
// to runFirewall.
//
// not protected by mu; netshPath is only mutated inside netshPathOnce
netshPathOnce sync.Once
netshPath string
}
func (ft *firewallTweaker) clear() { ft.set(nil, nil, nil) }
@@ -193,43 +185,10 @@ func (ft *firewallTweaker) set(cidrs []string, routes, localRoutes []netip.Prefi
go ft.doAsyncSet()
}
// getNetshPath returns the path that should be used to execute netsh.
//
// We've seen a report from a customer that we're triggering the "cannot run
// executable found relative to current directory" protection that was added to
// prevent running possibly attacker-controlled binaries. To mitigate this,
// first try looking up the path to netsh.exe in the System32 directory
// explicitly, and then fall back to the prior behaviour of passing "netsh" to
// os/exec.Command.
func (ft *firewallTweaker) getNetshPath() string {
ft.netshPathOnce.Do(func() {
// The default value is the old approach: just run "netsh" and
// let os/exec resolve that into a full path.
ft.netshPath = "netsh"
path, err := windows.KnownFolderPath(windows.FOLDERID_System, 0)
if err != nil {
ft.logf("getNetshPath: error getting FOLDERID_System: %v", err)
return
}
expath := filepath.Join(path, "netsh.exe")
if _, err := os.Stat(expath); err == nil {
ft.netshPath = expath
return
} else if !os.IsNotExist(err) {
ft.logf("getNetshPath: error checking for existence of %q: %v", expath, err)
}
// Keep default
})
return ft.netshPath
}
func (ft *firewallTweaker) runFirewall(args ...string) (time.Duration, error) {
t0 := time.Now()
args = append([]string{"advfirewall", "firewall"}, args...)
cmd := exec.Command(ft.getNetshPath(), args...)
cmd := exec.Command("netsh", args...)
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
b, err := cmd.CombinedOutput()
if err != nil {

View File

@@ -1,19 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package router
import (
"path/filepath"
"testing"
)
func TestGetNetshPath(t *testing.T) {
ft := &firewallTweaker{
logf: t.Logf,
}
path := ft.getNetshPath()
if !filepath.IsAbs(path) {
t.Errorf("expected absolute path for netsh.exe: %q", path)
}
}