Help us shape the future of Go! Take the 2026 Go Developer Survey

server

package
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2026 License: Apache-2.0 Imports: 59 Imported by: 0

Documentation

Index

Constants

View Source
const (
	MCPEventSnapshot  = "snapshot"
	MCPEventUpdate    = "update"
	MCPEventKeepalive = "keepalive"
)

Event stream frame types. Every frame says what it is in two places: the SSE `event:` field and the payload's own "type". A client is never asked to infer whether it is holding a full state or a delta.

View Source
const (
	// DefaultMCPEventsKeepalive is the heartbeat period when configuration
	// does not set one.
	//
	// The heartbeat is not optional. A tailnet long connection can go silent
	// without the socket reporting an error, and in that state "no events" and
	// "the connection is dead" produce byte-identical observations at the
	// client: nothing arrives either way. The heartbeat is what makes those two
	// distinguishable, so a client can reconnect on silence instead of showing
	// stale state forever.
	DefaultMCPEventsKeepalive = 20 * time.Second
	// MinMCPEventsKeepalive and MaxMCPEventsKeepalive bound the configured
	// value. Below the minimum the heartbeat becomes traffic rather than
	// liveness; above the maximum a dead connection outlives any UI that cares.
	MinMCPEventsKeepalive = 5 * time.Second
	MaxMCPEventsKeepalive = 5 * time.Minute
)
View Source
const DefaultMCPNodeName = "tslink-mcp"

DefaultMCPNodeName is the hostname of the dedicated tsnet node that carries the remote MCP control plane.

The control plane deliberately does not travel as a registry service. A registry entry would appear in `tslink list`, would be a candidate for `tslink cleanup`, and could be given --funnel — which would publish the control plane to the public internet through an existing, already-audited code path. Keeping it off the registry means no code path in this program can make it publishable: the only listener it ever opens is ListenTLS on its own tsnet node.

View Source
const MCPControlPlanePath = "/mcp"

MCPControlPlanePath is the single HTTP endpoint the control plane serves.

View Source
const MCPEventsPath = "/events"

MCPEventsPath is the control plane's server-sent event stream.

It is a separate path from MCPControlPlanePath on purpose. The Streamable HTTP transport mounted there runs stateless, which makes the SDK answer GET with 405; sharing one path would mean deciding between the two by method, and the MCP transport — not this file — owns what GET means on /mcp.

View Source
const PortalRequestPath = "/access-requests"

PortalRequestPath accepts CSRF-protected requests from human tailnet members.

Variables

View Source
var ErrMCPNoPrincipal = errors.New("mcp control plane is enabled but no principal is authorized; set mcp.allow (owner) or mcp.bindings (scoped) in config.json to login emails or tag:… entries, or disable the control plane")

ErrMCPNoPrincipal is returned when the control plane is enabled without an authorization principal. It is a refusal to start, not a warning: an endpoint that answers every tailnet peer is the failure this feature exists to avoid.

Functions

func ACLMiddleware

func ACLMiddleware(allowedUsers []string, localClient *LocalClient) func(http.Handler) http.Handler

ACLMiddleware returns an HTTP middleware that enforces access control. allowedUsers is a list of email addresses and/or "tag:xxx" entries. If allowedUsers is empty, all requests are allowed. It uses the Tailscale LocalClient WhoIs to identify the caller. A refusal is answered like any other app refusal (writeAccessDenial); the legacy list has no gateway counterpart, so it names no app and links to no dashboard.

func AccessEventMiddleware

func AccessEventMiddleware(svc registry.Service, opts accesslog.Options, writer accesslog.Writer, identity *IdentityResolver, now func() time.Time, next http.Handler) http.Handler

AccessEventMiddleware wraps authorization, limits and the app. Only actual request-body reads and response writes are counted, excluding HTTP framing.

func AccessLogMiddleware

func AccessLogMiddleware(serviceName string, identity *IdentityResolver, next http.Handler) http.Handler

AccessLogMiddleware logs every HTTP request with structured fields via slog.

identity may be nil, and is nil for every node that cannot resolve a caller. The login and node fields are then empty, which is the same shape the record has for a caller whose identity could not be established -- one schema, so a reader parsing these lines never has to handle a missing key.

The two identity fields are the only ones sourced from outside the request itself, and they carry exactly what the local Tailscale daemon attested for the source address. Nothing here reads an X-Tailscale-* request header: those are attacker-controlled on the way in (the proxy strips them for that reason), and a log that repeated them would record a claim as a fact.

func AppAccessAt

func AppAccessAt(reg *registry.Registry, svc registry.Service, login string, tags []string, now time.Time) (bool, *time.Time)

AppAccessAt exposes the listener decision from the same model as the portal.

func CloseTSNetServer

func CloseTSNetServer(name string, srv io.Closer)

CloseTSNetServer releases a tsnet node whose start may not have completed; every Close that can follow an incomplete Start or Up goes through it (service nodes, the MCP control-plane node, the CLI's client-secret validation node), while stopNodeLocked closes only nodes whose start completed. tsnet's Close dereferences state its start builds part-way through, so closing a node whose start failed early panics upstream (tailscale.com v1.102.4, tsnet.Server.close; reached for example when os.Executable fails because /proc/self/exe is unreadable). That panic must not take down the daemon, whose supervisor would restart it into the same failure, nor turn a CLI command's start error into a panic trace: it is logged, and the start error stays the reported outcome. The log line holds name and the panic value only, so name must never be a credential.

func MCPAuthMiddleware

func MCPAuthMiddleware(allowedUsers []string, localClient *LocalClient, next http.Handler) http.Handler

MCPAuthMiddleware authorizes a caller against an explicit principal list, using the same Tailscale LocalClient WhoIs path the service proxy uses.

It is fail-closed in every direction. An empty principal list denies every caller rather than allowing every caller, which is the opposite of ACLMiddleware's per-service contract and is deliberate: a service with no --allow is a public page inside the tailnet, while a control plane with no principal is a mistake.

func MCPOriginMiddleware

func MCPOriginMiddleware(next http.Handler) http.Handler

MCPOriginMiddleware implements the transport specification's Origin requirement. The 2026-07-28 Streamable HTTP transport states, under "Security Warning":

"Servers MUST validate the Origin header on all incoming connections to
prevent DNS rebinding attacks. If the Origin header is present and
invalid, servers MUST respond with HTTP 403 Forbidden. The HTTP response
body MAY comprise a JSON-RPC error response that has no id."

A request with no Origin header is not a browser request and is allowed through; that is what every command-line MCP client sends. A request that does carry an Origin must have come from this endpoint's own https origin. Anything else — another host, a plain http origin, the opaque "null" origin a sandboxed document sends, or an unparseable value — is refused before the request reaches authorization or any tool.

func NewMCPControlPlaneHandler

func NewMCPControlPlaneHandler(cp *MCPControlPlane, localClient *LocalClient) http.Handler

NewMCPControlPlaneHandler assembles the control plane's HTTP surface. It is the single assembly used in production and asserted by tests, so a test cannot pass against a chain the daemon does not actually serve.

Order is load-bearing. Origin validation is outermost after logging because the specification decides it before anything else and it must not be able to probe authorization. Authorization comes next, so an unauthorized request never reaches the routing mux and therefore never reaches a tool. Only the mounted MCP path exists on this node.

func NewProxyHandler

func NewProxyHandler(target string, identity *IdentityResolver) (http.Handler, error)

NewProxyHandler returns the reverse proxy for one proxy service. identity may be nil, in which case no X-Tailscale-* identity is injected; it is shared with the node's access log so one caller costs one WhoIs.

func NewProxyHandlerWithOptions

func NewProxyHandlerWithOptions(target string, identity *IdentityResolver, options ProxyOptions) (http.Handler, error)

NewProxyHandlerWithOptions configures per-service HTTP forwarding. The default constructor retains target Host rewriting for existing callers.

func RequestLimitsAfterAccess added in v0.3.0

func RequestLimitsAfterAccess(service registry.Service, report func(inspect.WarningView), access func(http.Handler) http.Handler, backend http.Handler) http.Handler

RequestLimitsAfterAccess is RequestLimitsMiddleware for a backend that sits behind an access check, so the caller is told about the limit only once the check lets them through. A caller who is refused gets the refusal, never the 413 and its app name and configured limit, whatever the size of the request: the gateway decides access before limits, and an app's own node must answer the same request the same way.

Everything that protects the connection is unchanged and still comes first: the connection and header budgets, full duplex, the read-idle budget and the bounded disposal of a body nobody read (which a refusal, like a 413, leaves unread). Only the answer to a declared oversize waits for the access check.

func RequestLimitsMiddleware

func RequestLimitsMiddleware(service registry.Service, report func(inspect.WarningView), next http.Handler) http.Handler

RequestLimitsMiddleware streams the original body. Deadlines are renewed before each read, so backend backpressure is not counted as client idleness. No response writer wrapper is introduced: Flush, Hijack and 1xx remain intact. A request that declares a body over the limit is refused with 413 at once.

func ResourceBudgetMiddleware

func ResourceBudgetMiddleware(next http.Handler) http.Handler

func ValidateServiceForStartup

func ValidateServiceForStartup(svc registry.Service) error

ValidateServiceForStartup validates a service definition before starting its node. It delegates to the canonical registry validator and adds startup context.

Types

type AppAccessDecision

type AppAccessDecision struct {
	Allowed          bool
	IdentityEnforced bool
	DirectoryVisible bool
	ExpiresAt        *time.Time
}

AppAccessDecision separates listener enforcement from directory disclosure. Raw TCP and Funnel have no TSLink per-identity gate; their cards are an owner/admin inventory and do not assert anything about visitor reachability.

func AppAccessDecisionAt

func AppAccessDecisionAt(reg *registry.Registry, svc registry.Service, login string, tags []string, now time.Time) AppAccessDecision

type AuthHandoff

type AuthHandoff struct {
	Service string
	AuthURL string
	// State is pending, complete or cancelled. Empty means pending for older callers.
	State string
}

AuthHandoff is emitted when a credential-free tsnet node needs the user to authorize it. AuthURL comes from local.Client.Status, the documented stable status API.

type AuthHandoffFunc

type AuthHandoffFunc func(context.Context, AuthHandoff) error

AuthHandoffFunc receives credential-free interactive enrollment events.

type AuthKeyProvider

type AuthKeyProvider func(ctx context.Context, svc registry.Service) (string, error)

AuthKeyProvider resolves auth material for a service immediately before its tsnet node starts.

type CleanupStaleNodesFunc

type CleanupStaleNodesFunc func(ctx context.Context, targets []tailapi.CleanupTarget) (tailapi.CleanupResult, error)

CleanupStaleNodesFunc removes stale tailnet nodes for service targets before forced reauth.

type EnsureFunnelAttrFunc

type EnsureFunnelAttrFunc func(ctx context.Context, request tailapi.FunnelPolicyRequest) (tailapi.PolicyMutationResult, error)

EnsureFunnelAttrFunc is the signature for the fused, lossless Funnel policy transaction. The request includes ordinary tags only when --manage-acl is enabled; the shared Funnel owner and nodeAttrs edits remain default-on.

type EnsureTagsFunc

type EnsureTagsFunc func(ctx context.Context, tags []string) error

EnsureTagsFunc is the signature for ensuring ACL tags exist.

type FileHandler

type FileHandler struct {
	// contains filtered or unexported fields
}

FileHandler pins the directory object resolved at construction time. An initial symlink root is allowed and its then-current referent is pinned; replacing the path later cannot retarget requests. Close releases the one root descriptor held for this file node's lifetime.

func NewFileHandler

func NewFileHandler(dir string) (*FileHandler, error)

NewFileHandler returns an HTTP handler that serves files from dir, confined to the directory subtree. Path opens are anchored at the OS root handle for dir, so a concurrent symlink swap cannot turn a prior path check into an open outside the served tree.

dir must be a non-empty absolute path. Empty or relative roots are rejected with an error rather than silently resolving to the process working directory (which for a Unix daemon is `/`). This is defense-in-depth: the canonical pre-side-effect validation already rejects empty/relative file paths before a node is constructed, but a direct caller of this constructor must not be able to serve cwd by mistake.

func NewSingleFileHandler

func NewSingleFileHandler(dir, file string) (*FileHandler, error)

NewSingleFileHandler returns an HTTP handler that serves exactly one file out of dir: GET /<file> is the file, GET / redirects to it, and every other path is 404 with no directory listing and no access to any sibling.

It exists because a file share created from a regular file still has to anchor its opens somewhere, and the only anchor a file has is its parent directory. Serving that directory is what made `tslink share ./report.html` publish every sibling of report.html; the root here is still the directory, but the routing above is the whole reachable surface, so the sibling is not addressable rather than merely unlisted.

file must be a bare file name. The caller has already enforced that at the registry boundary; repeating the check here is defense-in-depth for a direct caller of this constructor, the same reason NewFileHandler re-checks its own root. It calls the registry's own predicate rather than restating it, because a restated copy is a copy that can drift -- and the first version of this function did drift, accepting and rejecting different sets of whitespace.

func (*FileHandler) Close

func (h *FileHandler) Close() error

func (*FileHandler) ServeHTTP

func (h *FileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

type GatewayProxyOptions added in v0.2.0

type GatewayProxyOptions struct {
	Remote    bool
	Authority func() string
	App       string
	HostLabel string
	Observe   func(bool)
}

type IdentityResolver

type IdentityResolver struct {
	// contains filtered or unexported fields
}

IdentityResolver answers "who is behind this request address" for every consumer on one service node, through a single shared cache.

One instance per node is the point. The proxy's X-Tailscale-* injection and the access log's login field both want the same answer for the same address in the same request, and before this type each consumer that wanted identity carried its own cache or none at all. Sharing one means adding a consumer costs no additional WhoIs calls.

Every method is fail-open and nil-safe: a nil resolver, an unavailable local client and a WhoIs error are all reported as "not known" rather than as an error, because the callers are observability and header enrichment, not authorization. Authorization lives in ACLMiddleware and MCPAuthMiddleware, which resolve identity themselves and fail closed. Routing those through this cache would let a revoked principal keep its access for the rest of a TTL, so they deliberately do not use it.

func NewIdentityResolver

func NewIdentityResolver(provide func() (*LocalClient, error)) *IdentityResolver

NewIdentityResolver returns a resolver that acquires its local client on first use by calling provide. provide may be called more than once: an early failure is transient often enough that giving up forever would silently cost identity for the life of the process.

func NewStaticIdentityResolver

func NewStaticIdentityResolver(client *LocalClient) *IdentityResolver

NewStaticIdentityResolver returns a resolver over an already-open local client. A nil client yields a nil resolver, so "no identity is available" has one representation rather than two.

func (*IdentityResolver) Principal

func (r *IdentityResolver) Principal(ctx context.Context, remoteAddr string) (login, node string)

Principal returns the verified login and node name behind remoteAddr. Both are empty when identity cannot be established.

A tagged node reports an empty login rather than the pseudo-user Tailscale attributes tagged traffic to. The field exists to answer "which account did this", and a tag is a machine: filling it with a tag name would make a fleet of tagged nodes read as one very busy person. The node name still identifies the caller, so nothing is lost -- it is reported in the field that means "machine" instead of the one that means "account".

type LifecycleReconcileFunc

type LifecycleReconcileFunc func(context.Context, time.Time) (bool, error)

LifecycleReconcileFunc persists wall-clock expiration and reconciles remote resources. The bool reports whether registry.json changed.

type LocalClient

type LocalClient = local.Client

type MCPCaller

type MCPCaller struct {
	Login string
	Tags  []string
}

func MCPCallerFromContext

func MCPCallerFromContext(ctx context.Context) (MCPCaller, bool)

type MCPControlPlane

type MCPControlPlane struct {
	// NodeName is the tsnet hostname. Empty means DefaultMCPNodeName.
	NodeName string
	// Tags are advertised when the daemon runs with a stored credential.
	Tags []string
	// AllowedUsers is the authorization principal list. Empty is a start-time
	// refusal, never "allow everyone".
	AllowedUsers []string
	Bindings     []mcpscope.Binding
	// Handler is the MCP transport handler mounted at MCPControlPlanePath.
	Handler http.Handler
	// EventsSnapshot builds the body of one event frame. A nil function
	// disables the event stream entirely: MCPEventsPath is then not mounted,
	// and the node answers it with the mux's own 404. That is the default for
	// any caller that does not opt in, so the stream cannot appear by
	// accident.
	//
	// This package deliberately does not know the payload's shape. The views a
	// client needs are the CLI's own list and status results, which live in
	// package cmd; building them here would mean a second copy of them, and
	// the copy would be the one that rots.
	EventsSnapshot func(context.Context) (any, error)
	// EventsKeepalive is the heartbeat period. Zero means
	// DefaultMCPEventsKeepalive; out-of-range values are clamped to
	// [MinMCPEventsKeepalive, MaxMCPEventsKeepalive].
	EventsKeepalive time.Duration
}

MCPControlPlane is the daemon-side configuration of the remote MCP transport. Handler carries the tool surface; this package never builds it, so the stdio and HTTP transports cannot drift into two tool registries.

func (*MCPControlPlane) Validate

func (c *MCPControlPlane) Validate() error

Validate reports whether the control plane may start.

type MCPEventPayload

type MCPEventPayload struct {
	Type string `json:"type"`
	// Instance is this daemon process's identity; see the contract above.
	Instance string `json:"instance"`
	// EventID mirrors the SSE id: field: monotonic across this instance,
	// shared by every stream.
	EventID uint64 `json:"event_id"`
	// Sequence is the per-connection frame ordinal, starting at 1.
	Sequence   uint64 `json:"sequence"`
	EmittedAt  string `json:"emitted_at"`
	Keepalive  string `json:"keepalive_interval,omitempty"`
	State      any    `json:"state,omitempty"`
	StateError string `json:"state_error,omitempty"`
}

MCPEventPayload is the envelope every frame carries. State is the transport-independent body built by MCPControlPlane.EventsSnapshot; this package never inspects it.

Three numbering fields, three different scopes. They are not redundant, and reading one as the other is the mistake this comment exists to prevent:

  • Instance names the daemon process. It is generated once at start and is the same on every frame of every stream until the daemon restarts. It is the only thing that lets a client tell "the server restarted, so EventID legitimately starts low again" from "I lost frames".
  • EventID is monotonic over the whole instance and is what the SSE id: field carries. It is shared by every stream, so one stream sees gaps wherever another stream consumed a value: it orders frames and detects staleness across a reconnect, and it must not be used for per-stream gap detection.
  • Sequence is this connection's own frame counter, starting at 1 for the snapshot. It is contiguous within one connection and resets on the next, which is what makes per-stream gap detection possible at all.

There is no Last-Event-ID replay. A reconnect is answered with a full snapshot, so a client that missed frames is made current by the first frame it receives rather than by asking for the ones it lost.

type PortalApp

type PortalApp struct {
	HostLabel    string     `json:"host_label,omitempty"`
	OfflineSince *time.Time `json:"offline_since,omitempty"`
	Name         string     `json:"name"`
	URL          string     `json:"url"`
	Health       string     `json:"health"`
	ExpiresAt    *time.Time `json:"expires_at,omitempty"`
	ExpiryText   string     `json:"expiry"`
	AccessNote   string     `json:"access_note,omitempty"`
}
func (app PortalApp) BrowserLink() bool

BrowserLink keeps TCP connection addresses as text, matching tslink url.

type PortalHandler

type PortalHandler struct {
	RegistryPath  string
	LocalClient   *LocalClient
	CanonicalHost func() string
	Apps          func(*registry.Registry, time.Time) map[string]PortalApp
	Now           func() time.Time
	Requests      *PortalRequests
}

PortalHandler uses live, uncached WhoIs and the same decision as app nodes. Apps must return runtime observations only; authorization stays here.

func (PortalHandler) ServeHTTP

func (h PortalHandler) ServeHTTP(w http.ResponseWriter, r *http.Request)

type PortalRequests

type PortalRequests struct {
	Notifier      health.NotifierConfig
	NotifierError string
	Publish       func()
	Record        func(registry.RequestEvent)
	// contains filtered or unexported fields
}

type ProxyOptions

type ProxyOptions struct {
	Gateway *GatewayProxyOptions
	// App names the app in the response a visitor gets when its backend fails
	// on a direct app node. Through the gateway, Gateway.App does.
	App          string
	Dialer       UpstreamDialer
	PreserveHost bool
	// CanonicalHost reads the receiving node's trusted runtime name. It must
	// never derive a name from a request, TLS SNI, or registry service name.
	CanonicalHost func() string
}

type Server

type Server struct {
	// contains filtered or unexported fields
}

Server manages multiple tsnet nodes, one per registered service.

func New

func New(authKey, controlURL string) (*Server, error)

New creates a new multi-node server.

func (*Server) AccessLogWriter

func (s *Server) AccessLogWriter() accesslog.Writer

AccessLogWriter returns the owning daemon's typed nonblocking writer. It is nil before Run has initialized storage or when local storage is unavailable.

func (*Server) HoldsNodeState

func (s *Server) HoldsNodeState(name string) bool

HoldsNodeState reports whether this server runs a node for name, or is starting one, and therefore has a tsnet server holding that name's state directory open or about to write into it.

It exists for the lifecycle reconciliation, which decides whether the local state of an orphan service is safe to delete. That decision needs a fact only this process has, and the honest form of the fact is "I am holding it right now" rather than "nothing is holding it anywhere": a separate `tslink cleanup` process gets no answer from here and does not delete. The reconciler asks under the ownership ledger's lock, the lock a startup takes to reserve the directory, so a "no" stays true until its removal is done.

func (*Server) MCPControlPlaneEnabled

func (s *Server) MCPControlPlaneEnabled() bool

MCPControlPlaneEnabled reports whether a control plane is configured.

func (*Server) Run

func (s *Server) Run(ctx context.Context) error

Run starts all registered service nodes and watches for registry changes.

func (*Server) SetAuthHandoffFunc

func (s *Server) SetAuthHandoffFunc(fn AuthHandoffFunc)

SetAuthHandoffFunc sets the callback used to publish interactive login URLs.

func (*Server) SetAuthKeyProvider

func (s *Server) SetAuthKeyProvider(fn AuthKeyProvider)

SetAuthKeyProvider sets the function used to resolve auth material per service. A provider is taken to mint its keys through the Tailscale API until SetUserSuppliedAuthKey says otherwise, so call that after this.

func (*Server) SetAutoProvisionFunnel

func (s *Server) SetAutoProvisionFunnel(enabled bool)

SetAutoProvisionFunnel applies the daemon-wide kill switch. False wins over every service's default-on setting without mutating registry.json.

func (*Server) SetCleanupStaleNodesFn

func (s *Server) SetCleanupStaleNodesFn(fn CleanupStaleNodesFunc)

SetCleanupStaleNodesFn sets the function used to remove stale tailnet nodes before forced reauth.

func (*Server) SetControlURLUnverified

func (s *Server) SetControlURLUnverified(unverified bool)

SetControlURLUnverified marks the server's control URL as a default the caller fell back to (config.json failed to load) rather than configuration. A service without its own control URL then has an unknown control server for identity comparison, so a control URL difference alone never resets its node.

func (*Server) SetCredentialed

func (s *Server) SetCredentialed(credentialed bool)

SetCredentialed records whether this process is running the stored- credential tier. The command layer resolves this independently from the per-service auth keys supplied by AuthKeyProvider.

func (*Server) SetEnsureFunnelAttrFn

func (s *Server) SetEnsureFunnelAttrFn(fn EnsureFunnelAttrFunc)

SetEnsureFunnelAttrFn sets the function used for automatic Funnel policy provisioning.

func (*Server) SetEnsureTagsFn

func (s *Server) SetEnsureTagsFn(fn EnsureTagsFunc)

SetEnsureTagsFn sets the function called to ensure ACL tags before starting nodes.

func (*Server) SetLifecycleReconcileFn

func (s *Server) SetLifecycleReconcileFn(fn LifecycleReconcileFunc)

SetLifecycleReconcileFn configures the shared cleanup/expiration reconciler.

func (*Server) SetMCPControlPlane

func (s *Server) SetMCPControlPlane(cp *MCPControlPlane)

SetMCPControlPlane installs the remote MCP control plane configuration. A nil argument leaves the daemon in its default state, where no control-plane listener is opened and no control-plane tsnet node is created.

func (*Server) SetReadyFunc

func (s *Server) SetReadyFunc(fn func() error)

SetReadyFunc sets a callback invoked after watcher setup and initial sync succeed.

func (*Server) SetUserSuppliedAuthKey

func (s *Server) SetUserSuppliedAuthKey(userSupplied bool)

SetUserSuppliedAuthKey records whether the auth-key provider returns a key the user supplied (the legacy authkey file) rather than one minted through the Tailscale API with a stored credential. Only a minted key is tied to Tailscale's control server; see mintedKeyControlURLError.

func (*Server) TryWithNodeStateLock

func (s *Server) TryWithNodeStateLock(ctx context.Context, fn func() error) (bool, error)

TryWithNodeStateLock acquires the same startup gate without waiting. Optional local cleanup may skip a tick when startup is active or shutdown has begun.

func (*Server) WithNodeStateLock

func (s *Server) WithNodeStateLock(fn func() error) error

WithNodeStateLock excludes an in-progress sync from the final, local part of orphan cleanup. In particular, a node being started is not yet in nodes.

type ServiceNode

type ServiceNode struct {
	// contains filtered or unexported fields
}

ServiceNode represents a single tsnet node serving one service.

type UpstreamDialer added in v0.2.0

type UpstreamDialer interface {
	DialContext(context.Context, string, string) (net.Conn, error)
}

UpstreamDialer is shared by local gateway transports and the future tsnet adapter. Only the registry target is handed to the transport; request data never selects the destination. TLS remains verified by http.Transport.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL