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

tailapi

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: 22 Imported by: 0

Documentation

Index

Constants

View Source
const (
	PolicyWriteNotAttempted = "not_attempted"
	PolicyWriteUnchanged    = "unchanged"
	PolicyWriteChanged      = "changed"
	PolicyWriteRejected     = "rejected"
	PolicyWriteUnknown      = "unknown"
)
View Source
const (
	InviteKindUser   = "user"
	InviteKindDevice = "device"

	InviteRoleMember       = "member"
	InviteRoleAdmin        = "admin"
	InviteRoleITAdmin      = "it-admin"
	InviteRoleNetworkAdmin = "network-admin"
	InviteRoleBillingAdmin = "billing-admin"
	InviteRoleAuditor      = "auditor"
)
View Source
const APIBaseURLEnv = "TSLINK_API_BASE_URL"

APIBaseURLEnv redirects the Tailscale REST API client to a loopback-only endpoint. It exists so command-layer tests can exercise the real tailapi implementation without contacting Tailscale.

View Source
const DefaultTag = "tag:tsmain"

DefaultTag is the default ACL tag applied to tslink services.

View Source
const FunnelNodeAttr = "funnel"

FunnelNodeAttr is the policy-file attribute that authorizes Funnel nodes.

Variables

View Source
var ErrNoAPIClient = errors.New("no API client available")

ErrNoAPIClient means cleanup cannot use the Tailscale API because no API key is configured.

View Source
var ErrPolicyAccessDenied = errors.New("tailnet policy access forbidden")

ErrPolicyAccessDenied reports that the configured API credential can reach the tailnet policy endpoint but lacks permission to read or update it.

View Source
var ErrPolicyConflict = errors.New("tailnet policy changed concurrently; retry the operation")

ErrPolicyConflict reports that the policy ETag changed between read and write. Callers may retry the entire read-modify-write operation.

View Source
var ErrTailnetHTTPSDisabled = errors.New("tailnet HTTPS is disabled")

ErrTailnetHTTPSDisabled reports the non-policy Funnel prerequisite without issuing an ACL write. Enabling it requires the tailnet settings API and the networking_settings OAuth scope; nodeAttrs cannot express this setting.

View Source
var ErrTailnetSettingsUnavailable = errors.New("tailnet settings unavailable")

ErrTailnetSettingsUnavailable means TSLink refused to guess whether HTTPS is enabled. An ACL POST is unsafe until the non-policy prerequisite can be read with the networking_settings OAuth scope.

Functions

func CleanupStaleNodes

func CleanupStaleNodes(ctx context.Context, targets []CleanupTarget) error

CleanupStaleNodes reports stale nodes that conflict with the given service targets.

func DeleteTag

func DeleteTag(ctx context.Context, tag string) error

DeleteTag removes a tag from tagOwners using the lossless HuJSON path. A referenced tag is refused, except for TSLink's exact canonical Funnel grant, which is removed atomically with the plumbing tag so teardown cannot leave a dangling nodeAttrs reference. Returns ErrNoAPIClient if no API client is available.

func EnsureTags

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

EnsureTags checks that the given tags exist in the ACL tagOwners. Missing tags are created with owner ["autogroup:admin"]. The Raw/Set path and surgical HuJSON AST edit preserve comments, formatting, and unknown keys. Returns ErrNoAPIClient for non-empty tag input if no API client is available.

func FindExactDeviceNodeID

func FindExactDeviceNodeID(ctx context.Context, hostname string) (string, int, error)

FindExactDeviceNodeID performs the read-only lookup used by cleanup adoption. It deliberately uses literal hostname equality and requires a TSLink tag: suffix, prefix, wildcard, regex, foreign-tagged, and first-match selection are never accepted for migration proof.

func HostnameMatchesServiceName

func HostnameMatchesServiceName(hostname, service string) bool

HostnameMatchesServiceName reports whether a tailnet hostname is either the literal service name or one of the numeric collision variants tsnet appends when the plain hostname is already taken (<name>-1, <name>-2, ...). It is the same discovery predicate cleanup uses to decide which hostname-only matches to report as Protected, exported so read-only views can label a device's local family without reimplementing the rule.

Discovery only. A true result never authorizes deletion; DELETE remains gated on exact NodeID ownership proof.

func InviteRoles

func InviteRoles() []string

InviteRoles returns the exact role enum accepted by the archived first-party OpenAPI contract.

func NewTailscaleClient

func NewTailscaleClient() (*tailscale.Client, error)

NewTailscaleClient exposes the same fail-closed loopback override gate to higher-level callers that must pass a client factory into credentials.

func ReadTags

func ReadTags(ctx context.Context) ([]string, error)

ReadTags returns user-managed tag names from the tailnet ACL tagOwners. The shared Funnel plumbing tag is deliberately hidden from `tslink tags pull` so it is not presented as a user tag. Returns ErrNoAPIClient if no API client is available.

func RevokePendingDeviceInvite

func RevokePendingDeviceInvite(ctx context.Context, target DeviceTarget, id string) (string, error)

RevokePendingDeviceInvite only deletes the recorded ID on its proven node. Accepted shares are reported separately. Absence is idempotent success.

func ValidateInviteID

func ValidateInviteID(id string) error

ValidateInviteID accepts the bare decimal strings used by both first-party invite schemas. Rejecting every non-ASCII digit also keeps path separators, percent escapes, and dot segments away from the HTTP boundary.

func ValidateInviteKind

func ValidateInviteKind(kind string) error

ValidateInviteKind requires callers to select a namespace explicitly so a colliding numeric ID can never make TSLink mutate the other invite kind.

func ValidateInviteRole

func ValidateInviteRole(role string) error

ValidateInviteRole rejects unsupported roles before any API client or network request is created.

Types

type CleanupResult

type CleanupResult struct {
	Matched              []string
	WouldDelete          []string
	Deleted              []string
	Protected            []string
	Skipped              bool
	SkipReason           string
	ResolvedOwnershipIDs []string
}

CleanupResult describes a stale-node cleanup attempt.

func CleanupStaleNodesResult

func CleanupStaleNodesResult(ctx context.Context, targets []CleanupTarget) (CleanupResult, error)

CleanupStaleNodesResult reconciles exact ownership and may issue DELETE only for a device whose NodeID appears in a CleanupTarget.

func CleanupStaleNodesResultWithDryRun

func CleanupStaleNodesResultWithDryRun(ctx context.Context, targets []CleanupTarget, dryRun bool) (CleanupResult, error)

CleanupStaleNodesResultWithDryRun reconciles devices using exact NodeID ownership. Hostname matches without exact proof remain Protected.

func DeleteDevicesForService

func DeleteDevicesForService(ctx context.Context, target CleanupTarget) (CleanupResult, error)

DeleteDevicesForService deletes exact recorded NodeIDs and reports every hostname-only match as protected.

type CleanupTarget

type CleanupTarget struct {
	Hostname string
	Tags     []string
	NodeIDs  []string
}

CleanupTarget identifies the expected TSLink-owned device identity.

func CleanupTargetForOwnedService

func CleanupTargetForOwnedService(svc registry.Service, nodeIDs []string) CleanupTarget

CleanupTargetForOwnedService attaches durable exact NodeID proof. Hostname remains discovery-only and can never authorize DELETE.

func CleanupTargetForService

func CleanupTargetForService(svc registry.Service) CleanupTarget

CleanupTargetForService builds the remote cleanup ownership target for a service.

func CleanupTargetsForServices

func CleanupTargetsForServices(services []registry.Service) []CleanupTarget

CleanupTargetsForServices builds remote cleanup ownership targets for services.

type DeviceInviteOutcomeUnknown

type DeviceInviteOutcomeUnknown struct{ Err error }

DeviceInviteOutcomeUnknown means POST was attempted; even an HTTP refusal or unreadable response does not prove the remote side effect absent.

func (*DeviceInviteOutcomeUnknown) Error

func (*DeviceInviteOutcomeUnknown) Unwrap

func (e *DeviceInviteOutcomeUnknown) Unwrap() error

type DeviceTarget

type DeviceTarget struct {
	Kind     string
	Service  string
	Hostname string
	NodeID   string
}

DeviceTarget combines the collision-aware hostname with the exact stable node ID recorded by the running TSLink node. Hostname matching finds candidates; NodeID is the ownership proof.

type FunnelPolicyRequest

type FunnelPolicyRequest struct {
	Tags   []string
	Target string
	Owners []string
}

FunnelPolicyRequest describes one fused Funnel policy transaction. Tags are ordinary tagOwners entries that may be created with the legacy admin owner; Target is the shared Funnel tag and Owners are existing tag identities held by the OAuth clients that must be able to mint keys for Target.

type Invite

type Invite struct {
	Grant           *registry.Person `json:"grant,omitempty"`
	AccessState     string           `json:"access_state,omitempty"`
	Message         string           `json:"message,omitempty"`
	TargetKind      string           `json:"target_kind,omitempty"`
	Kind            string           `json:"kind"`
	ID              string           `json:"id,omitempty"`
	Recipient       string           `json:"recipient,omitempty"`
	Email           string           `json:"email,omitempty"`
	Emailed         bool             `json:"emailed"`
	InviteURL       string           `json:"invite_url,omitempty"`
	Role            string           `json:"role,omitempty"`
	Service         string           `json:"service,omitempty"`
	DeviceID        int64            `json:"device_id,omitempty"`
	MultiUse        bool             `json:"multi_use,omitempty"`
	AllowExitNode   bool             `json:"allow_exit_node,omitempty"`
	Accepted        bool             `json:"accepted,omitempty"`
	Created         string           `json:"created,omitempty"`
	LastEmailSentAt string           `json:"last_email_sent_at,omitempty"`
}

Invite is the stable TSLink representation of either first-party invite schema. InviteURL is copied verbatim from the API and is never constructed.

func CreateDeviceInvite

func CreateDeviceInvite(ctx context.Context, target DeviceTarget, email string, printLink, multiUse, allowExitNode bool) (Invite, error)

func CreateUserInvite

func CreateUserInvite(ctx context.Context, email, role string, printLink bool) (Invite, error)

CreateUserInvite creates exactly one invite. email is always the named recipient; printLink controls whether it is omitted from the API body.

func ListDeviceInvitesForTarget

func ListDeviceInvitesForTarget(ctx context.Context, target DeviceTarget) ([]Invite, error)

ListDeviceInvitesForTarget needs no running snapshot: the durable node ID and hostname must still match a currently owned remote device.

func ResendInvite

func ResendInvite(ctx context.Context, kind, id string, targets []DeviceTarget) (Invite, error)

func RevokeInvite

func RevokeInvite(ctx context.Context, kind, id string, targets []DeviceTarget) (Invite, error)

type InviteList

type InviteList struct {
	Complete      bool                 `json:"complete"`
	UserInvites   []Invite             `json:"user_invites"`
	DeviceInvites []Invite             `json:"device_invites"`
	DeviceTargets []InviteTargetStatus `json:"device_targets"`
	Count         int                  `json:"count"`
}

func ListInvites

func ListInvites(ctx context.Context, targets []DeviceTarget) (InviteList, error)

type InviteTargetError

type InviteTargetError struct {
	Code    string   `json:"code"`
	Message string   `json:"message"`
	Next    []string `json:"next,omitempty"`
}

type InviteTargetStatus

type InviteTargetStatus struct {
	TargetKind  string             `json:"target_kind,omitempty"`
	Service     string             `json:"service"`
	Checked     bool               `json:"checked"`
	InviteCount int                `json:"invite_count"`
	Error       *InviteTargetError `json:"error,omitempty"`
}

type Membership added in v0.3.0

type Membership string
const (
	MembershipMember Membership = "member"
	MembershipGuest  Membership = "non_member"
)

func ResolveMembership added in v0.3.0

func ResolveMembership(ctx context.Context, login, controlURL string) (Membership, error)

ResolveMembership uses the complete user directory, never online devices, email domains or a persisted guest bit. Unknown is an error, not a guest.

type PeopleInviteTargetGone

type PeopleInviteTargetGone struct{ Target DeviceTarget }

PeopleInviteTargetGone is evidence from a successful device listing that the recorded node and hostname are absent. Lookup failures and ownership mismatches never produce this terminal evidence.

func (*PeopleInviteTargetGone) Error

func (e *PeopleInviteTargetGone) Error() string

type PolicyMutationResult

type PolicyMutationResult struct {
	Changed      bool
	WriteOutcome string
}

PolicyMutationResult distinguishes a confirmed change from an idempotent no-op and from a write whose server-side outcome could not be observed.

func EnsureFunnelAttr

func EnsureFunnelAttr(ctx context.Context, request FunnelPolicyRequest) (PolicyMutationResult, error)

EnsureFunnelAttr performs the complete Funnel policy provisioning in one Raw -> surgical patch -> Set transaction: ordinary requested tags, the shared tagOwners rule with caller-derived owner tags, and the exact-target nodeAttrs grant. It never modifies a grant whose target is not exactly the requested target. Existing wildcard or broader Funnel grants are recognized as coverage and left byte-for-byte untouched.

type TailnetDevice

type TailnetDevice struct {
	Hostname           string
	Name               string
	Tags               []string
	OS                 string
	CreatedAt          *time.Time
	LastSeenAt         *time.Time
	ConnectedToControl bool
	Authorized         bool
}

TailnetDevice is the read-only projection of one TSLink-tagged tailnet device. It deliberately omits NodeID and every other node identity value: deletion authorization lives only in tailapi.CleanupStaleNodesResultWithDryRun, which matches owned[d.NodeID] exactly, and a NodeID that never leaves this package cannot be copied into a caller that widens that predicate.

func ListTSLinkDevices

func ListTSLinkDevices(ctx context.Context) ([]TailnetDevice, error)

ListTSLinkDevices lists every TSLink-tagged device in the tailnet using the same one-GET device listing and the same TSLink tag predicate that cleanup adoption uses. It is strictly read-only: it issues no DELETE, records no ownership, and grants no deletion authority.

The tailnet is shared across machines, so the returned set spans every host running TSLink. Deciding which rows belong to the calling machine is the caller's job; this function has no access to any local registry.

Jump to

Keyboard shortcuts

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